icon

안동민 개발노트

2장 : 컴퓨터 시스템 구조

부팅 과정과 운영체제 적재


컴퓨터에 전원을 넣으면 화면에 로고가 뜨고, 잠시 후 로그인 화면이 나타납니다. 이 짧은 시간 동안 아무것도 없는 상태에서 운영체제라는 거대한 소프트웨어가 메모리에 적재됩니다. 전원이 들어온 순간 RAM은 비어 있고, CPU는 어떤 프로그램도 실행하지 않고 있습니다. 이 공백 상태에서 어떻게 OS가 시작될 수 있을까요?

이 과정을 이해하면 서버 관리, 컨테이너 운영, 임베디드 개발에서 만나는 부팅 관련 문제를 훨씬 쉽게 파악할 수 있습니다.


전원 ON에서 커널까지

부팅은 크게 세 단계로 나뉩니다. 각 단계는 이전 단계가 다음 단계를 불러오는 체인 로딩(Chain Loading) 방식입니다. 마치 릴레이 달리기처럼, 각 주자가 다음 주자에게 바통을 넘기는 구조입니다.

1단계 — 펌웨어 실행 (BIOS/UEFI)

전원이 들어오면 CPU는 하드웨어적으로 정해진 주소(Reset Vector)의 코드를 실행합니다. x86 CPU의 경우 이 주소는 0xFFFFFFF0(물리 메모리의 최상단 근처)입니다. 이 주소에는 메인보드의 플래시 메모리(ROM)에 저장된 펌웨어 코드가 매핑되어 있습니다.

왜 RAM이 아니라 ROM인가? RAM은 전원이 꺼지면 내용이 사라지는 휘발성 메모리입니다. 전원을 방금 켰으므로 RAM은 비어 있습니다. 반면 ROM(정확히는 플래시 메모리)은 비휘발성이므로 전원이 꺼져도 코드가 유지됩니다. CPU가 최초로 실행하는 코드는 반드시 비휘발성 저장 장치에 있어야 합니다.

전통적인 BIOS(Basic Input/Output System)는 먼저 POST(Power-On Self-Test)를 수행합니다. CPU가 정상인지, RAM이 올바르게 동작하는지, 키보드가 연결되어 있는지, 디스크가 인식되는지를 점검합니다. 문제가 있으면 비프음(beep code)으로 알려줍니다. 비프음 패턴으로 어떤 하드웨어가 문제인지 구분할 수 있습니다.

POST가 끝나면 BIOS는 부팅 장치를 찾습니다. BIOS 설정(CMOS)에 저장된 부팅 순서(예: SSD → USB → DVD)에 따라, 각 장치의 첫 번째 섹터(512바이트)를 읽어봅니다. 이 섹터의 마지막 2바이트가 매직 넘버 0x55AA이면 유효한 MBR(Master Boot Record)입니다. MBR에는 부트로더의 첫 번째 단계 코드(최대 446바이트)와 파티션 테이블이 들어 있습니다.

현대 시스템은 BIOS 대신 UEFI(Unified Extensible Firmware Interface)를 사용합니다. UEFI는 BIOS의 근본적인 한계를 해결합니다.

  • MBR의 한계 극복: MBR은 2TB 이상의 디스크를 지원하지 못합니다. UEFI는 GPT(GUID Partition Table)를 사용하여 최대 9.4 ZB(제타바이트)까지 지원합니다.
  • 보안 부팅(Secure Boot): 부트로더와 커널의 디지털 서명을 검증하여, 악성 코드가 부팅 과정에 개입하는 부트킷(Bootkit) 공격을 방지합니다.
  • 빠른 부팅: 병렬 초기화와 드라이버 최적화로 BIOS보다 부팅이 빠릅니다.
  • 네트워크 부팅: 네트워크에서 직접 OS를 불러오는 PXE 부팅을 내장 지원합니다. 서버라 데이터센터에서 수백 대의 서버를 동시에 프로비저닝할 때 사용합니다.

UEFI 환경에서는 디스크에 ESP(EFI System Partition)라는 FAT32 파일 시스템의 특별한 파티션이 있습니다. 여기에 부트로더의 EFI 바이너리가 저장됩니다. UEFI 펌웨어가 직접 FAT32를 읽을 수 있으므로, MBR처럼 512바이트 제한에 구애받지 않습니다.

2단계 — 부트로더 실행

펌웨어가 부팅 장치에서 부트로더(Boot Loader)를 읽어 메모리에 적재하고 실행합니다. 부트로더의 역할은 커널 이미지를 찾아서 메모리에 올리고, 필요한 초기 설정을 한 뒤 커널에 제어를 넘기는 것입니다.

GRUB(GRand Unified Bootloader)은 Linux에서 가장 널리 쓰이는 부트로더입니다.

  • 여러 OS가 설치된 경우 어떤 OS를 부팅할지 선택 메뉴를 보여줍니다(듀얼 부팅).
  • 커널에 전달할 파라미터를 설정할 수 있습니다. 예를 들어 root=/dev/sda2는 루트 파일 시스템의 위치를, quiet는 부팅 시 커널 메시지를 숨기라는 뜻입니다.
  • initramfs(Initial RAM File System)도 함께 메모리에 적재합니다.

initramfs는 왜 필요할까요? 커널이 루트 파일 시스템을 마운트하려면 디스크 드라이버가 필요합니다. 그런데 디스크 드라이버는 파일 시스템 안에 있습니다. 닭이 먼저냐 달걀이 먼저냐의 문제입니다. initramfs는 이 순환을 깨는 해법입니다. 커널이 부팅에 필요한 최소한의 드라이버와 도구를 담은 임시 파일 시스템을 RAM에 올리고, 이 드라이버로 실제 디스크를 접근하여 진짜 루트 파일 시스템을 마운트합니다.

Windows는 자체 부트로더인 Windows Boot Manager(bootmgr)를 사용합니다. BCD(Boot Configuration Data)에 부팅 설정이 저장되며, bcdedit 명령어로 편집할 수 있습니다.

3단계 — 커널 적재와 초기화

부트로더가 커널 이미지(Linux의 경우 vmlinuz)를 메모리에 적재하고 제어권을 커널에 넘깁니다. vmlinuz는 압축된 커널 이미지이며, 맨 앞에 자체 압축 해제 코드가 있어서 스스로 압축을 풀고 실행됩니다.

커널은 다음 작업을 순서대로 수행합니다.

  • CPU 초기화: 보호 모드 또는 롱 모드 설정, 페이지 테이블 설정, GDT/IDT(전역 디스크립터 테이블/인터럽트 디스크립터 테이블) 설정.
  • 메모리 관리 초기화: 물리 메모리 맵을 확인하고, 페이지 프레임 할당자를 초기화합니다. BIOS/UEFI로부터 전달받은 메모리 맵에서 사용 가능한 물리 메모리 영역을 파악합니다.
  • 인터럽트 핸들러 등록: IDT에 각 인터럽트 번호별 핸들러를 등록합니다. 이후 발생하는 모든 인터럽트와 예외를 처리할 준비를 합니다.
  • 장치 감지 및 드라이버 초기화: PCI 버스를 스캔하여 연결된 장치를 감지하고, 해당 드라이버를 로드합니다. dmesg 명령으로 이 과정을 확인할 수 있습니다.
  • 루트 파일 시스템 마운트: initramfs의 임시 루트를 실제 디스크의 파일 시스템으로 교체합니다.
  • 첫 번째 사용자 프로세스 생성: 커널의 마지막 초기화 단계로, PID 1의 프로세스를 생성합니다.

init 프로세스와 systemd

커널 초기화가 완료되면 첫 번째 사용자 공간 프로세스를 생성합니다. 이 프로세스의 PID(Process ID)는 항상 1입니다. 시스템의 모든 다른 프로세스는 이 PID 1 프로세스의 자손입니다. PID 1이 종료되면 시스템이 종료됩니다.

전통적 init

전통적인 Unix 시스템에서는 SysV init이 PID 1이었습니다. /etc/inittab 파일에서 런레벨(Run Level)을 확인하고, 해당 런레벨의 초기화 스크립트(/etc/rc.d/ 또는 /etc/init.d/)를 순차적으로 실행했습니다.

런레벨은 시스템의 동작 모드를 정의합니다. 0은 시스템 종료, 1은 싱글 유저 모드(복구 모드), 3은 멀티 유저 텍스트 모드, 5는 멀티 유저 GUI 모드, 6은 재부팅입니다.

SysV init의 가장 큰 단점은 순차 실행입니다. 서비스 A를 시작하고 완료될 때까지 기다린 뒤에야 서비스 B를 시작합니다. 서비스가 30개면 30번의 순차 대기가 발생하여 부팅이 느립니다.

systemd

현대 Linux 배포판의 대부분은 systemd를 사용합니다. Lennart Poettering이 2010년에 시작한 프로젝트로, init 시스템을 근본적으로 재설계했습니다.

병렬 시작: 독립적인 서비스를 동시에 시작합니다. 네트워크 서비스와 로그 서비스가 서로 의존하지 않으면, 동시에 시작되어 부팅 시간이 단축됩니다.

소켓 활성화(Socket Activation): 서비스를 실제로 시작하지 않고, 소켓만 미리 열어둡니다. 누군가 그 소켓에 접속하면 그때 서비스를 시작합니다. 이 방식으로 의존성 문제를 우아하게 해결합니다. 서비스 B가 서비스 A의 소켓에 연결하려 하면, systemd가 A를 자동으로 시작시킵니다.

유닛(Unit) 기반 관리: 서비스, 마운트 포인트, 타이머, 소켓 등을 모두 "유닛"이라는 일관된 단위로 관리합니다. 유닛 파일은 선언적인 INI 형식으로, 셸 스크립트보다 파싱이 쉽고 오류가 적습니다.

cgroup 통합: 각 서비스를 별도의 cgroup에 넣어 리소스 사용을 추적하고 제한합니다. 특정 서비스가 메모리를 과도하게 사용하면 자동으로 제한할 수 있습니다.

systemd 필수 명령어
systemctl status nginx         # 서비스 상태(활성, PID, 메모리, 로그) 확인
systemctl start nginx          # 서비스 즉시 시작
systemctl stop nginx           # 서비스 즉시 중지
systemctl restart nginx        # 서비스 재시작
systemctl enable nginx         # 부팅 시 자동 시작 등록
systemctl disable nginx        # 부팅 시 자동 시작 해제
systemctl list-units --failed  # 실패한 서비스 목록 확인
journalctl -u nginx -f         # 서비스 로그 실시간 추적
systemd-analyze blame          # 부팅 시 각 서비스별 소요 시간 확인

systemd-analyze blame은 부팅 최적화에 유용합니다. 어떤 서비스가 부팅 시간을 가장 많이 잡아먹는지 한눈에 보여줍니다.

systemd 유닛 파일 구조

서비스 유닛 파일은 보통 /etc/systemd/system/ 또는 /usr/lib/systemd/system/에 위치합니다.

유닛 파일 예시: /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=network.target postgresql.service
Requires=postgresql.service

[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/start.sh
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
  • After: 이 서비스는 network.target과 postgresql.service 이후에 시작됩니다.
  • Requires: postgresql.service가 실패하면 이 서비스도 실행되지 않습니다.
  • Restart=on-failure: 프로세스가 비정상 종료(0이 아닌 exit code)되면 자동 재시작합니다.
  • RestartSec=5: 재시작 전 5초 대기합니다. 즉시 재시작하면 같은 오류가 반복될 수 있으므로.
  • WantedBy=multi-user.target: systemctl enable 시 멀티 유저 모드에서 자동 시작됩니다.

부팅 과정과 실무

부팅 과정을 이해하면 실무에서 만나는 여러 상황에 즉각 대응할 수 있습니다.

서버 재부팅 후 서비스 미시작: 가장 흔한 실수입니다. systemctl start는 지금 한 번 시작할 뿐이고, systemctl enable을 해야 재부팅 후에도 자동 시작됩니다. 이 차이를 모르면 서버 재부팅 때마다 수동으로 서비스를 시작해야 합니다.

서비스 시작 순서 문제: 웹 서버가 데이터베이스보다 먼저 시작되어 연결 실패하는 경우, 유닛 파일에 After=Requires=를 설정하여 의존성을 명시합니다.

부팅이 느린 경우: systemd-analyze blame으로 병목을 식별합니다. 불필요한 서비스(예: Bluetooth on server)를 systemctl disable로 비활성화하여 부팅 시간을 단축합니다.

커널 패닉(Kernel Panic): 커널이 복구할 수 없는 오류를 감지하면 시스템이 멈춥니다. 메모리 하드웨어 오류, 손상된 커널 모듈, 드라이버 버그 등이 원인입니다. dmesg/var/log/kern.log에서 마지막 커널 메시지를 확인하여 원인을 추적합니다.

Docker 컨테이너와 PID 1: 컨테이너 안에서는 systemd 대신 애플리케이션 프로세스가 직접 PID 1이 됩니다(DockerfileCMDENTRYPOINT). PID 1 프로세스가 종료되면 컨테이너도 종료됩니다. 또한 PID 1은 고아 프로세스(부모가 종료된 프로세스)를 거두는(reaping) 책임이 있는데, 일반 애플리케이션은 이 역할을 수행하지 않으므로 좀비 프로세스 문제가 발생할 수 있습니다. 이를 해결하기 위해 tini 같은 경량 init을 PID 1으로 사용하기도 합니다.

GRUB 복구: 디스크 파티션을 변경하거나 OS를 재설치한 후 GRUB가 깨지면, 부팅이 되지 않습니다. Live USB로 부팅한 뒤 grub-installupdate-grub으로 복구합니다. 이 때 chroot로 설치된 시스템의 루트를 임시로 교체하여 작업합니다.


부팅 과정 요약

단계실행 주체위치역할
Reset VectorCPU하드와이어펌웨어 코드의 시작 주소
BIOS/UEFI펌웨어ROM/플래시POST, 부팅 장치 탐색
부트로더 (GRUB)부트로더디스크 ESP/MBR커널 이미지 메모리 적재
커널 초기화커널RAM하드웨어 초기화, 드라이버 로드
PID 1 (systemd)사용자 공간RAM서비스 시작, 시스템 준비
로그인 화면Display ManagerRAM사용자 인증

전원 버튼을 누른 순간부터 로그인 화면이 나타나기까지, 이 모든 단계가 수 초 내에 일어납니다. 각 단계가 다음 단계를 불러오는 체인 로딩 구조이므로, 어느 한 단계가 실패하면 부팅이 중단됩니다. 부팅 문제를 해결할 때는 어느 단계까지 성공했는가?를 먼저 파악하는 것이 핵심입니다.

다음 장에서는 부팅 후 실행되는 프로그램들이 OS에서 어떻게 관리되는지, 프로세스의 개념을 다루겠습니다.

목차