컴퓨터공학 300 주제 시리즈의 101번째 글이다. 전체 지도는 여기.

한 줄 요약

운영체제는 하드웨어를 나눠 쓰게(가상화), 함께 쓰게(동시성), 오래 남게(영속성) 만드는 소프트웨어이고, 그 중심에서 특권 모드로 돌며 자원을 직접 다루는 부분이 커널이다.

왜 필요한가

CPU 는 한 번에 한 명령어 흐름만 실행한다. 메모리는 주소가 매겨진 바이트 배열일 뿐이다. 디스크는 블록 번호로만 읽고 쓴다. 이 맨 하드웨어 위에서 브라우저, 데이터베이스, 웹 서버가 동시에 돌아가려면 누군가 다음 일을 해야 한다.

  • 여러 프로그램이 CPU 를 번갈아 쓰게 한다.
  • 한 프로그램이 다른 프로그램의 메모리를 덮어쓰지 못하게 막는다.
  • “블록 4812번” 대신 /var/log/app.log 같은 이름으로 데이터를 다루게 한다.
  • 버그 있는 프로그램 하나가 기계 전체를 멈추지 못하게 한다.

이 일을 각 프로그램이 알아서 하면 서로 믿을 수 없다. 그래서 모두가 믿는 하나의 심판을 두고, 그 심판에게만 하드웨어를 직접 만질 권한을 준다. 그것이 커널이다.

서버 장애를 읽는 언어도 대부분 여기서 나온다. OOM killed, load average, iowait, too many open files, D 상태 프로세스 — 전부 운영체제가 자원을 나누는 방식에서 생기는 단어다.

핵심 개념

운영체제의 세 기둥

OSTEP(Arpaci-Dusseau 부부의 교과서)은 운영체제를 세 주제로 나눈다. 이 시리즈의 Part 6 도 이 틀을 따른다.

기둥 하는 일 대표 주제
가상화(Virtualization) 물리 자원 하나를 여러 개처럼 보이게 한다 프로세스, 스케줄링, 페이징
동시성(Concurrency) 동시에 도는 흐름이 서로를 망치지 않게 한다 락, 세마포어, 교착 상태
영속성(Persistence) 전원이 꺼져도 데이터가 남게 한다 파일 시스템, 저널링

가상화가 핵심이다. 각 프로세스는 CPU 를 혼자 쓰는 것처럼, 0번지부터 시작하는 메모리를 혼자 가진 것처럼 느낀다. 실제로는 커널이 아주 짧은 시간 단위로 CPU 를 돌려주고, 주소 변환으로 메모리를 나눈다.

커널과 사용자 공간

+--------------------------------------------------+
|  사용자 공간 (user mode, 비특권)                   |
|   nginx   postgres   python   bash   kubelet     |
|   libc / 런타임 라이브러리                         |
+--------------------- 시스템 콜 경계 ---------------+
|  커널 공간 (kernel mode, 특권)                     |
|   스케줄러  메모리 관리  VFS/파일시스템  네트워크   |
|   디바이스 드라이버                                |
+--------------------------------------------------+
|  하드웨어: CPU, RAM, 디스크, NIC                    |
+--------------------------------------------------+

CPU 는 최소 두 가지 실행 모드를 하드웨어로 지원한다. x86 에서는 링 0(커널)과 링 3(사용자), ARM 에서는 EL1 과 EL0 이 대표적이다. 사용자 모드에서는 입출력 포트 접근, 페이지 테이블 교체, 인터럽트 끄기 같은 특권 명령을 실행하면 CPU 가 예외를 일으킨다. 그래서 일반 프로그램은 하드웨어를 직접 만질 수 없고, 시스템 콜이라는 정해진 문으로 커널에게 부탁해야 한다.

커널이 다시 CPU 를 가져오는 경로는 세 가지다.

  1. 시스템 콜: 프로그램이 스스로 커널을 부른다(read, write, fork).
  2. 예외: 0으로 나누기, 없는 페이지 접근(page fault) 같은 사건.
  3. 인터럽트: 타이머, 디스크 완료, 패킷 도착 같은 외부 신호.

특히 타이머 인터럽트가 중요하다. 프로그램이 무한 루프를 돌아도 일정 시간마다 타이머가 울려 커널이 CPU 를 되찾는다. 이것이 선점형(preemptive) 멀티태스킹의 기반이다.

커널 구조: 모놀리식과 마이크로커널

구조 설명 예
모놀리식 스케줄러, 파일시스템, 드라이버가 한 주소 공간의 커널 안에 있다 Linux, 전통적 BSD
마이크로커널 커널은 IPC·스케줄링·주소 공간만 맡고 나머지는 사용자 공간 서버로 뺀다 seL4, QNX, MINIX 3
하이브리드 둘을 섞는다 Windows NT 계열, macOS(XNU)

Linux 는 모놀리식이지만 적재 가능 모듈(.ko)로 드라이버를 실행 중에 넣고 뺄 수 있다. 모놀리식은 내부 호출이 함수 호출이라 빠르고, 마이크로커널은 드라이버가 죽어도 커널이 살아남아 견고하다. 어느 쪽이 정답이라기보다 성능과 격리 사이의 선택이다.

정책과 메커니즘의 분리

운영체제 설계에서 자주 쓰는 원칙이다. 메커니즘은 “어떻게” (컨텍스트 스위칭 코드, 페이지 테이블 교체), 정책은 “무엇을” (다음에 어느 프로세스를 돌릴지, 어느 페이지를 내보낼지)이다. 둘을 나누면 정책만 바꿔 끼울 수 있다. Linux 의 스케줄링 클래스, I/O 스케줄러 선택, cgroup 설정이 모두 이 원칙을 따른다.

커널이 관리하는 자원 한눈에

자원 추상화 사용자가 보는 것
CPU 프로세스·스레드 PID, 우선순위
메모리 가상 주소 공간 malloc, mmap
저장장치 파일·디렉터리 경로, 파일 디스크립터
네트워크 소켓 IP:포트
장치 장치 파일 /dev/sda, /dev/null

유닉스 계열은 많은 것을 파일 디스크립터라는 정수 하나로 통일한다. 파일, 파이프, 소켓, 터미널이 모두 read/write 로 다뤄진다.

직접 해 보기

커널이 실제로 시간을 쓰는지 눈으로 확인해 보자. os.times() 는 현재 프로세스가 사용자 모드(user)와 커널 모드(system)에서 쓴 CPU 시간을 따로 돌려준다.

import os, time

def report(label, fn):
    t0 = os.times(); w0 = time.perf_counter()
    fn()
    t1 = os.times(); w1 = time.perf_counter()
    print(f"{label:12s} user={t1.user-t0.user:5.2f}s  sys={t1.system-t0.system:5.2f}s  wall={w1-w0:5.2f}s")

def compute():          # 순수 계산: 커널을 거의 부르지 않는다
    s = 0
    for i in range(3_000_000):
        s += i * i

def syscalls():         # 매 반복마다 stat(2) 시스템 콜
    for _ in range(300_000):
        os.stat(".")

def sleeping():         # CPU 를 내려놓고 기다린다
    time.sleep(1)

report("compute", compute)
report("syscalls", syscalls)
report("sleep", sleeping)

어느 리눅스 노트북에서 돌린 결과다(숫자는 기계마다 다르다).

compute      user= 0.46s  sys= 0.00s  wall= 0.46s
syscalls     user= 0.87s  sys= 0.88s  wall= 1.89s
sleep        user= 0.00s  sys= 0.00s  wall= 1.00s

세 줄이 운영체제의 세 얼굴을 보여 준다. 순수 계산은 커널 시간이 0에 가깝다. stat 을 반복하면 시간의 절반 가까이가 커널 안에서 쓰인다. sleep 은 벽시계 시간만 흐르고 CPU 시간은 거의 0이다 — 그동안 커널이 CPU 를 다른 일에 줬다는 뜻이다. 셸에서 time 명령이 보여 주는 user/sys 도 같은 값이다.

현업에서는

  • top 의 us, sy, wa: CPU 사용률 줄의 sy 가 높으면 시스템 콜이나 커널 작업이 많다는 뜻이다. 작은 write 를 수없이 하는 로깅, 과도한 컨텍스트 스위칭이 흔한 원인이다. wa(iowait)는 CPU 가 놀면서 디스크를 기다린 시간이다.
  • 컨테이너는 커널을 공유한다: k3s 노드 위의 모든 파드는 노드의 커널 하나를 함께 쓴다. 컨테이너 이미지에 무엇이 들었든 커널 버전은 호스트 것이다. 그래서 커널 버그·커널 파라미터(sysctl)는 노드 전체에 영향을 준다.
  • 커널 업그레이드는 재부팅이다: 사용자 공간 패키지는 프로세스만 재시작하면 되지만 커널은 기계를 다시 켜야 한다. 클러스터에서 노드를 한 대씩 drain 하고 재부팅하는 이유다.
  • 장애 메시지의 출처: dmesg 에 찍히는 OOM killer, 디스크 I/O 에러, 소프트 락업은 모두 커널이 남긴 기록이다. 애플리케이션 로그에 흔적이 없을 때 가장 먼저 볼 곳이다.

확인 문제

  1. 운영체제의 세 기둥을 들고, 각각 한 가지 예를 적어라.
  2. 사용자 프로그램이 디스크 컨트롤러 레지스터에 직접 쓰려고 하면 무슨 일이 일어나는가?
  3. 무한 루프를 도는 프로그램이 있어도 다른 프로그램이 계속 실행되는 이유는 무엇인가?
  4. 모놀리식 커널과 마이크로커널의 장단점을 한 문장씩 비교하라.
  5. 위 실험에서 sleep 의 user·sys 시간이 거의 0인데 wall 시간은 1초인 이유는?

풀이

  1. 가상화(프로세스마다 독립된 주소 공간), 동시성(뮤텍스로 공유 자료 보호), 영속성(파일 시스템 저널링).
  2. 사용자 모드에서는 특권 연산이 금지되어 있으므로 CPU 가 예외를 일으키고, 커널이 보통 그 프로세스에 시그널을 보내 종료시킨다. 장치 접근은 시스템 콜로만 가능하다.
  3. 하드웨어 타이머 인터럽트가 주기적으로 커널에게 제어권을 돌려주고, 커널 스케줄러가 다른 프로세스로 전환하기 때문이다(선점).
  4. 모놀리식은 커널 내부 호출이 빨라 성능이 좋지만 드라이버 버그가 커널 전체를 죽일 수 있다. 마이크로커널은 서비스가 격리되어 견고하지만 IPC 비용이 든다.
  5. 잠든 동안 프로세스는 대기 상태라 CPU 를 쓰지 않는다. 커널은 그 시간에 CPU 를 다른 프로세스에게 주거나 쉬게 한다.

더 읽을거리 (References)