본문 바로가기
TYLER SONGBlog
블로그 목록
CS학습

Kernel: 사용자 모드와 커널 모드를 가르는 특권 소프트웨어

커널은 CPU, 메모리, 디스크 같은 하드웨어 자원을 관리하며 응용 프로그램에게 안전한 추상화를 제공하는 운영체제의 핵심 소프트웨어다. 사용자 프로세스는 시스템 콜을 통해서만 커널 기능에 접근할 수 있고, 이 경계가 보안과 안정성의 기반이 된다. 컨테이너, 비동기 I/O 같은 최신 인프라 기술도 결국 커널이 제공하는 기능 위에서 동작한다.

송민성6분 읽기

1. 개념

커널(Kernel)은 운영체제에서 하드웨어 자원을 직접 제어하고, 그 위에서 실행되는 모든 프로세스에게 CPU, 메모리, 파일, 네트워크 같은 자원을 안전하게 나누어주는 소프트웨어다.

CPU는 실행 권한 수준을 하드웨어 차원에서 구분한다. x86 계열은 이를 링(Ring 0~3)으로 표현하는데, 커널은 가장 높은 권한인 Ring 0(커널 모드)에서 실행되고, 우리가 작성하는 애플리케이션 코드는 Ring 3(사용자 모드)에서 실행된다. 사용자 모드 코드는 메모리 접근이나 I/O 명령 같은 특권 연산을 직접 실행할 수 없고, 반드시 커널에게 요청해야 한다.

2. 왜 사용하는가

커널이 존재하는 이유는 세 가지로 요약된다.

  • 보호(Protection): 프로세스 A가 프로세스 B의 메모리를 마음대로 읽거나, 디스크를 직접 조작해서 다른 프로세스의 파일을 망가뜨리면 안 된다. 커널이 중간에서 접근을 통제한다.
  • 추상화(Abstraction): 개발자는 디스크 컨트롤러의 스펙을 몰라도 open(), read() 같은 통일된 인터페이스로 파일을 다룰 수 있다. 커널이 하드웨어 차이를 감춘다.
  • 다중화(Multiplexing): CPU 코어는 몇 개 없는데 프로세스는 수백 개가 동시에 떠 있다. 커널의 스케줄러가 시분할로 CPU를 나누어준다.

3. 동작 원리

사용자 모드 → 커널 모드 전환

사용자 프로세스가 파일을 읽고 싶으면 read() 시스템 콜을 호출한다. 이 호출은 내부적으로 syscall 같은 특수 CPU 명령을 실행해서 인터럽트를 발생시키고, CPU는 실행 모드를 커널 모드로 전환한 뒤 커널의 시스템 콜 핸들러로 점프한다. 커널은 요청을 처리하고 결과를 사용자 모드로 되돌려준다.

이 전환에는 비용이 든다. 레지스터 저장, 권한 검사, 캐시/TLB 영향 등으로 순수 함수 호출보다 훨씬 느리다.

인터럽트와 예외

키보드 입력, 타이머, 디스크 완료 신호 같은 하드웨어 이벤트는 인터럽트(Interrupt)로 커널에 전달된다. 0으로 나누기 같은 프로그램 오류는 예외(Exception, Trap)로 전달된다. 커널은 인터럽트 벡터 테이블을 통해 해당 이벤트에 맞는 핸들러를 실행한다.

컨텍스트 스위칭

커널 스케줄러가 실행 중인 프로세스를 바꾸기로 결정하면, 현재 프로세스의 레지스터 상태를 PCB(Process Control Block)에 저장하고 다음 프로세스의 상태를 복원한다. 이 과정을 컨텍스트 스위칭(Context Switching)이라 부르며, 프로세스 간 전환은 스레드 간 전환보다 비용이 크다. 프로세스는 별도의 가상 메모리 주소 공간을 갖기 때문에 페이지 테이블 교체와 TLB 플러시가 추가로 필요하기 때문이다.

모놀리식 커널과 마이크로커널

리눅스는 모놀리식 커널(Monolithic Kernel) 구조를 따른다. 파일 시스템, 네트워크 스택, 디바이스 드라이버가 모두 커널 공간에서 실행되어 서로 함수 호출로 직접 통신하므로 빠르다. 반면 마이크로커널(Microkernel)은 메모리 관리, 스케줄링 같은 최소 기능만 커널에 두고 나머지는 사용자 공간 서버로 분리한다. 통신에 메시지 패싱이 필요해 상대적으로 느리지만 장애 격리와 안정성이 좋다. QNX, seL4가 대표적이다.

4. 코드 예제

Python의 os 모듈은 시스템 콜을 얇게 감싼 래퍼다. fork()를 호출하면 실제로 커널에게 새 프로세스 생성을 요청한다.

python
import os pid = os.fork() if pid == 0: # 자식 프로세스: 커널이 새로 만든 프로세스 print(f"자식 프로세스 실행, PID={os.getpid()}, 부모 PID={os.getppid()}") os._exit(0) else: # 부모 프로세스 print(f"부모 프로세스, 자식 PID={pid} 생성됨") os.waitpid(pid, 0) # 자식 종료를 커널에게 대기 요청

strace로 실제 어떤 시스템 콜이 커널에 전달되는지 관찰할 수 있다.

shell
# python 스크립트가 호출하는 시스템 콜 통계를 요약해서 보여준다 strace -c python3 -c "import os; os.getpid()"
shell
# 결과 예시 (환경에 따라 수치는 다르게 나온다) % time seconds usecs/call calls syscall ------ ----------- ----------- --------- ---------------- 22.31 0.000123 12 10 mmap 18.45 0.000102 8 12 openat 15.02 0.000083 20 4 read ...

5. 성능 특성

  • 시스템 콜 한 번 호출은 일반 함수 호출보다 느리다. 모드 전환과 권한 검사 오버헤드 때문이며, 정확한 수치는 CPU 세대, 커널 버전, Spectre/Meltdown 완화 패치 적용 여부에 따라 크게 달라진다.
  • Meltdown 취약점 대응으로 도입된 KPTI(Kernel Page Table Isolation) 패치는 시스템 콜과 인터럽트 처리 비용을 늘렸다. 사용자/커널 페이지 테이블을 분리하면서 TLB 플러시가 추가되었기 때문이다.
  • 프로세스 컨텍스트 스위칭은 스레드 컨텍스트 스위칭보다 비용이 크다. 스레드는 같은 프로세스 내에서 주소 공간을 공유하므로 페이지 테이블 교체가 필요 없다.
  • 이런 오버헤드 때문에 고성능 네트워크 장비나 트레이딩 시스템에서는 아예 커널을 우회하는 DPDK, io_uring 같은 기술을 쓴다.

6. 실무 사용 사례

  • 컨테이너: Docker 컨테이너는 별도의 VM이 아니라 리눅스 커널이 제공하는 네임스페이스(Namespace, PID·네트워크·마운트 격리)와 cgroups(자원 제한)를 활용한 격리 기법이다. 커널은 하나만 존재하고 공유된다.
  • 비동기 I/O: epoll, io_uring은 커널이 제공하는 이벤트 통지 인터페이스로, Node.js의 libuv나 Nginx의 이벤트 루프가 내부적으로 이 시스템 콜을 사용한다.
  • 관측 도구: eBPF는 커널을 재컴파일하지 않고도 커널 내부에서 안전하게 코드를 실행시켜 성능 모니터링, 보안 감시를 수행하는 기술이다. Cilium, Falco 같은 도구가 이를 기반으로 한다.

7. 주의할 점

  • 시스템 콜을 반복 호출하는 코드는 사용자/커널 모드 전환 비용이 누적되어 느려진다. 파일을 한 바이트씩 read() 하지 않고 버퍼링해서 읽는 이유다.
  • 커널 모드에서 실행되는 코드의 버그는 커널 패닉(Kernel Panic)이나 블루스크린처럼 시스템 전체를 멈출 수 있다. 사용자 모드 프로그램 버그는 보통 해당 프로세스만 죽인다.
  • 커널 취약점은 권한 상승(Privilege Escalation) 공격으로 이어질 수 있다. 사용자 권한으로 침투한 공격자가 커널 버그를 이용해 root 권한을 얻는 사례가 실제로 존재한다.
  • strace, perf 같은 도구 자체도 시스템 콜 오버헤드를 유발하므로, 프로파일링 결과를 해석할 때 관측 도구가 성능에 준 영향을 감안해야 한다.

8. 핵심 정리

커널은 사용자 모드와 커널 모드라는 하드웨어 수준의 권한 경계를 이용해 프로세스를 보호하고, 하드웨어를 추상화하고, 자원을 다중화한다. 시스템 콜과 인터럽트가 이 경계를 넘나드는 유일한 합법적 통로이며, 이 전환에는 실질적인 성능 비용이 따른다. 컨테이너, 비동기 I/O, eBPF 같은 현대적인 인프라 기술은 모두 커널이 제공하는 기본 메커니즘 위에 쌓아 올린 결과물이다.

© 2026 Tyler Song