OS
목차
Part 1: 운영체제
1. 운영체제와 커널
1.1 운영체제의 역할
운영체제(Operating System)는 하드웨어 자원을 효율적으로 관리하고, 애플리케이션에게 안정적인 실행 환경을 제공하는 시스템 소프트웨어이다.
주요 역할:
- 자원 관리: CPU, 메모리, 디스크, 네트워크 등의 하드웨어 자원을 여러 프로세스가 공유할 수 있도록 관리
- 추상화 제공: 복잡한 하드웨어를 간단한 인터페이스로 추상화하여 애플리케이션 개발 용이성 제공
- 보안과 격리: 프로세스 간 격리를 통해 시스템 안정성과 보안 보장
- 효율성 극대화: 자원의 효율적 사용을 통한 시스템 성능 최적화
1.2 Kernel Space vs User Space
리눅스는 보안과 안정성을 위해 CPU 실행 모드를 두 가지로 분리한다.
User Space (사용자 공간)
사용자 공간은 일반 애플리케이션이 실행되는 영역이다:
- 제한된 명령어 실행: CPU의 일부 명령어만 사용 가능하며, 특권 명령어는 실행할 수 없다
- 메모리 보호: 각 프로세스는 독립된 가상 메모리 공간을 가지며, 다른 프로세스의 메모리에 접근할 수 없다
- 하드웨어 접근 제한: 디스크, 네트워크 카드 등의 하드웨어에 직접 접근할 수 없다
- 시스템 콜 의존성: 커널의 기능을 사용하려면 반드시 시스템 콜(System Call)을 통해야 한다
Kernel Space (커널 공간)
커널 공간은 운영체제의 주요 기능이 실행되는 영역으로, 최고 권한을 가진다:
- 모든 명령어 실행: CPU의 모든 명령어를 제한 없이 실행할 수 있다
- 전체 메모리 접근: 물리 메모리의 모든 영역에 접근 가능하다
- 하드웨어 직접 제어: 모든 하드웨어 장치를 직접 제어할 수 있다
- 시스템 자원 관리: 프로세스 스케줄링, 메모리 관리, 파일 시스템 등 주요 기능 수행
모드 전환 (Mode Switching)
사용자 프로그램이 파일을 읽거나 네트워크 통신을 하려면 커널의 도움이 필요한다. 이때 CPU는 사용자 모드에서 커널 모드로 전환된다:
- 사용자 프로그램이 시스템 콜 요청
- CPU가 사용자 모드에서 커널 모드로 전환
- 커널이 요청된 작업 수행
- CPU가 다시 사용자 모드로 전환
- 결과를 사용자 프로그램에 반환
이러한 모드 전환은 성능 오버헤드를 발생시키지만, 시스템의 안정성과 보안을 위해 필수적이다.
1.3 Monolithic Kernel 아키텍처
리눅스는 Monolithic Kernel(단일형 커널) 구조를 채택하고 있다. 이는 운영체제의 모든 주요 기능이 하나의 큰 프로그램으로 커널 공간에서 실행되는 방식이다.
Monolithic Kernel의 구성 요소
리눅스 커널은 다음과 같은 주요 서브시스템으로 구성된다:
- 프로세스 관리: 프로세스 생성, 종료, 스케줄링 담당
- 메모리 관리: 가상 메모리, 페이징, 스왑 관리
- 파일 시스템: VFS(Virtual File System) 계층과 실제 파일 시스템(ext4, xfs 등) 구현
- 네트워크 스택: TCP/IP 프로토콜 스택, 소켓 인터페이스
- 디바이스 드라이버: 하드웨어 장치 제어 코드
장점
- 고성능: 모든 컴포넌트가 같은 메모리 공간에서 실행되어 함수 호출로 직접 통신할 수 있다
- 효율적 자원 공유: 커널의 모든 부분이 메모리와 CPU 캐시를 효율적으로 공유한다
- 통합 최적화: 전체 시스템을 하나의 단위로 보고 최적화할 수 있다
단점
- 큰 코드베이스: 전체 커널 이미지 크기가 크고 복잡한다
- 안정성 위험: 디바이스 드라이버 하나의 버그가 전체 시스템을 다운시킬 수 있다
- 개발 난이도: 새로운 기능 추가 시 커널 전체에 대한 이해가 필요한다
커널 모듈 시스템
리눅스는 Monolithic 구조의 단점을 보완하기 위해 동적 모듈 로딩(Dynamic Module Loading) 메커니즘을 제공한다:
- 필요한 드라이버만 런타임에 로드하여 메모리 절약
- 시스템 재부팅 없이 드라이버 업데이트 가능
- 커널 재컴파일 없이 새로운 기능 추가 가능
2. 시스템 콜과 인터럽트
2.1 System Call의 이해
시스템 콜(System Call)은 사용자 프로그램이 커널 기능을 요청하는 유일한 인터페이스이다. 파일 I/O, 네트워크 통신, 프로세스 생성 등 모든 특권 작업은 시스템 콜을 통해서만 가능하다.
시스템 콜의 필요성
사용자 프로그램은 보안과 안정성을 위해 하드웨어에 직접 접근할 수 없다:
- 파일을 읽으려면 디스크 컨트롤러에 직접 명령을 내릴 수 없다
- 네트워크 패킷을 보내려면 네트워크 카드를 직접 제어할 수 없다
- 다른 프로세스의 메모리를 읽거나 쓸 수 없다
따라서 이러한 작업은 커널에 요청해야 하며, 이 요청 메커니즘이 바로 시스템 콜이다.
시스템 콜 실행 과정
1단계: 사용자 프로그램의 요청
사용자 프로그램이 open() 함수를 호출하여 파일 열기를 요청한다.
2단계: C 라이브러리 래퍼 함수
open() 함수는 실제로는 glibc가 제공하는 래퍼 함수이다. 이 함수는:
- 시스템 콜 번호를 레지스터에 설정
- 함수 인자들을 정해진 레지스터에 배치
- 특수 CPU 명령어 실행
3단계: 트랩 발생 및 모드 전환
특수 CPU 명령어가 실행되면:
- CPU는 즉시 사용자 모드에서 커널 모드로 전환
- 현재 프로그램의 실행 상태를 저장
- 커널의 시스템 콜 핸들러로 제어권 이동
4단계: 커널의 시스템 콜 처리
커널은 다음 작업을 수행한다:
- 사용자 공간의 파일명을 커널 공간으로 복사
- 권한 검사 (파일 접근 권한이 있는가?)
- 파일 시스템에서 파일 찾기
- 파일 디스크립터 할당
- 파일 열기 작업 수행
- 파일 디스크립터 번호 반환
5단계: 사용자 모드로 복귀
- 커널이 작업 완료 후 결과를 레지스터에 저장
- CPU가 커널 모드에서 사용자 모드로 다시 전환
- 저장했던 프로그램 상태 복원
- 사용자 프로그램의 다음 명령어부터 계속 실행
주요 시스템 콜 카테고리
파일 및 I/O 시스템 콜
open(): 파일 열기read(): 파일 읽기write(): 파일 쓰기close(): 파일 닫기lseek(): 파일 오프셋 이동ioctl(): 디바이스 제어
프로세스 관리 시스템 콜
fork(): 새 프로세스 생성exec(): 프로그램 실행exit(): 프로세스 종료wait(): 자식 프로세스 대기getpid(): 프로세스 ID 조회
메모리 관리 시스템 콜
mmap(): 메모리 매핑munmap(): 메모리 매핑 해제brk(): 힙 메모리 크기 조정mprotect(): 메모리 보호 속성 변경
네트워크 시스템 콜
socket(): 소켓 생성bind(): 소켓에 주소 바인딩listen(): 연결 대기accept(): 연결 수락connect(): 서버에 연결send()/recv(): 데이터 송수신
2.2 Interrupt와 Exception
시스템 콜 외에도 CPU의 정상적인 실행 흐름을 중단시키는 메커니즘이 두 가지 더 있다.
Interrupt (인터럽트)
인터럽트는 하드웨어 장치가 CPU에게 보내는 비동기적 신호이다:
- 타이머 인터럽트: 일정 시간마다 발생하여 프로세스 스케줄링에 사용
- 키보드 인터럽트: 키보드 입력 발생 시
- 네트워크 인터럽트: 네트워크 패킷 수신 시
- 디스크 인터럽트: 디스크 I/O 완료 시
인터럽트가 발생하면 CPU는 현재 작업을 중단하고 해당 인터럽트를 처리하는 인터럽트 핸들러(Interrupt Handler)를 실행한다.
Exception (예외)
예외는 소프트웨어 실행 중 발생하는 동기적 이벤트이다:
- Page Fault: 접근하려는 메모리 페이지가 물리 메모리에 없을 때
- Division by Zero: 0으로 나누기 시도
- Invalid Opcode: 잘못된 기계어 명령 실행
- Protection Fault: 권한 없는 메모리 접근 시도
- System Call: 사용자 프로그램이 의도적으로 발생시키는 예외
인터럽트 처리의 중요성
인터럽트 처리는 시스템 성능에 직접적인 영향을 미친다:
- Top Half (상위 절반): 긴급하고 중요한 작업만 빠르게 처리
- Bottom Half (하위 절반): 나머지 작업은 나중에 천천히 처리
이러한 분리는 인터럽트 처리 시간을 최소화하여 시스템 응답성을 향상시킨다.
2.3 C 표준 라이브러리 (glibc)
glibc(GNU C Library)는 리눅스 시스템에서 가장 널리 사용되는 C 표준 라이브러리이다.
glibc의 역할
glibc는 시스템 콜과 사용자 프로그램 사이의 중간 계층으로 작동한다:
시스템 콜 래핑
사용자가 read() 함수를 호출하면, glibc 내부에서는 레지스터 설정, syscall 명령 실행 등의 복잡한 과정을 추상화한다. 사용자는 간단한 함수 호출만으로 시스템 콜을 사용할 수 있다.
표준 C 함수 제공
glibc는 POSIX 및 C 표준에서 정의한 수백 개의 함수를 제공한다:
- 문자열 처리:
strlen(),strcpy(),strcmp()등 - 메모리 관리:
malloc(),free(),calloc()등 - 파일 I/O:
fopen(),fread(),fwrite()등 - 수학 함수:
sin(),cos(),sqrt()등
버퍼링 최적화
glibc는 성능 향상을 위해 I/O 버퍼링을 수행한다. fwrite()는 내부 버퍼에 데이터를 모았다가 버퍼가 가득 차면 한 번에 시스템 콜을 호출한다. 이를 통해 시스템 콜 호출 횟수를 줄여 성능을 크게 향상시킨다.
Thread-Local Storage
glibc는 멀티스레드 환경에서 각 스레드가 독립적인 변수를 가질 수 있도록 지원한다. 예를 들어 errno는 스레드별로 독립적인 값을 가진다.
동적 링킹
대부분의 프로그램은 glibc를 동적으로 링크한다. 이는 메모리 효율성과 업데이트 용이성을 제공한다.
3. 메모리 관리
3.1 Virtual Memory (가상 메모리)
가상 메모리는 현대 운영체제의 가장 중요한 추상화 중 하나이다. 이를 통해 각 프로세스는 마치 전체 메모리를 독점하는 것처럼 동작할 수 있다.
가상 메모리의 주요 개념
프로세스별 독립 주소 공간
각 프로세스는 자신만의 가상 주소 공간을 갖는다. 예를 들어, 프로세스 A와 프로세스 B가 모두 가상 주소 0x1000을 사용할 수 있지만, 이들은 서로 다른 물리 메모리를 가리킨다.
이를 통해:
- 프로세스 간 메모리 격리 보장
- 프로세스가 다른 프로세스의 메모리를 침범할 수 없음
- 시스템 안정성과 보안성 향상
주소 공간의 구조
64비트 시스템에서 각 프로세스는 이론적으로 2^64 바이트의 주소 공간을 가진다. 실제로는 커널이 일부 영역을 예약하고, 나머지를 다음과 같이 나눈다:
Text 영역 (코드 세그먼트)
- 실행 가능한 기계어 코드 저장
- 읽기 전용으로 보호되어 실수로 코드를 덮어쓰는 것 방지
- 여러 프로세스가 같은 프로그램을 실행할 때 공유 가능
Data 영역
- 초기화된 전역 변수와 정적 변수
- 읽기/쓰기 가능
- 프로그램 시작 시 크기 고정
BSS 영역
- 초기화되지 않은 전역 변수와 정적 변수
- 프로그램 로딩 시 0으로 초기화
- 디스크 공간 절약을 위해 실행 파일에는 크기만 기록
Heap 영역
- 동적 메모리 할당 영역 (
malloc(),new등) - 프로그램 실행 중 크기가 동적으로 변함
- 낮은 주소에서 높은 주소로 성장
Stack 영역
- 함수 호출 정보, 지역 변수, 함수 매개변수 저장
- 높은 주소에서 낮은 주소로 성장
- 함수 호출 시 증가, 반환 시 감소
3.2 Paging과 Page Table
페이징 메커니즘
가상 메모리를 실제 물리 메모리로 변환하는 주요 메커니즘이 페이징(Paging)이다.
페이지와 페이지 프레임
- 페이지(Page): 가상 메모리를 고정 크기(보통 4KB)로 나눈 단위
- 페이지 프레임(Page Frame): 물리 메모리를 같은 크기로 나눈 단위
Page Table (페이지 테이블)
페이지 테이블은 가상 페이지 번호를 물리 페이지 프레임 번호로 변환하는 자료구조이다. 가상 주소는 페이지 번호와 페이지 내 오프셋으로 구성되고, Page Table을 조회하여 물리 주소의 프레임 번호와 오프셋으로 변환된다.
Multi-level Page Table
64비트 시스템에서는 주소 공간이 너무 커서 단일 페이지 테이블로는 메모리 낭비가 심한다. 따라서 다단계 페이지 테이블을 사용한다:
- x86-64는 4단계 페이지 테이블 사용
- 실제로 사용하는 메모리 영역만 페이지 테이블 생성
- 메모리 효율성 크게 향상
TLB (Translation Lookaside Buffer)
페이지 테이블 조회는 메모리 접근이 필요하므로 느리다. 이를 해결하기 위해 CPU에 TLB라는 캐시를 둔다:
- 최근 사용한 페이지 변환 정보를 저장
- CPU 내부 캐시로 매우 빠른 접근
- TLB hit rate가 90% 이상이면 성능 크게 향상
3.3 Page Fault
Page Fault의 개념
프로세스가 접근하려는 가상 페이지가 물리 메모리에 없을 때 Page Fault가 발생한다. 이는 예외(Exception)의 일종으로, 커널이 처리한다.
Page Fault의 종류
Minor Page Fault
페이지가 물리 메모리에는 있지만 프로세스의 페이지 테이블에 매핑되지 않은 경우:
- 공유 라이브러리 페이지가 이미 다른 프로세스에 의해 로드된 경우
- Copy-on-Write로 인해 읽기 전용으로 공유 중인 페이지
처리가 빠르며 디스크 I/O가 필요 없다.
Major Page Fault
페이지가 물리 메모리에 없어서 디스크에서 읽어와야 하는 경우:
- 처음 파일을 메모리에 매핑할 때
- 스왑 아웃된 페이지를 다시 읽어올 때
디스크 I/O가 필요하므로 매우 느리다.
Demand Paging
리눅스는 Demand Paging 전략을 사용한다:
- 프로그램 실행 시 모든 코드를 메모리에 로드하지 않음
- 실제로 접근할 때만 페이지를 로드
- 메모리 효율성 향상
Copy-on-Write (CoW)
fork() 시스템 콜로 자식 프로세스를 생성할 때, 부모의 모든 메모리를 복사하지 않고:
- 처음에는 부모와 자식이 같은 물리 페이지를 공유 (읽기 전용)
- 둘 중 하나가 메모리를 수정하려고 할 때 Page Fault 발생
- 그때 비로소 해당 페이지만 복사
이를 통해 fork() 성능이 크게 향상된다.
3.4 Page Cache
Page Cache의 역할
Page Cache는 파일 시스템 I/O 성능을 극적으로 향상시키는 커널의 메모리 캐시 영역이다.
작동 원리
파일을 읽을 때:
- 커널은 먼저 Page Cache를 확인
- 캐시에 데이터가 있으면 즉시 반환 (디스크 I/O 없음)
- 캐시에 없으면 디스크에서 읽고 Page Cache에 저장
파일을 쓸 때:
- 데이터를 Page Cache에 먼저 저장 (Write-back)
- 나중에 백그라운드에서 실제 디스크에 기록
- 쓰기 작업이 즉시 완료된 것처럼 보임
컨테이너 환경에서의 Page Cache
중요한 점은 Page Cache는 호스트 전체가 공유한다는 것이다:
- 컨테이너 A가 파일을 읽어 Page Cache에 저장
- 컨테이너 B가 같은 파일을 읽으면 캐시 히트
- 이는 컨테이너 간 성능 간섭의 원인이 될 수 있음
메모리 회수
Page Cache는 “사용 가능한” 메모리로 간주된다:
- 시스템에 메모리가 부족하면 Page Cache를 회수
- 더티 페이지(수정된 페이지)는 먼저 디스크에 기록 후 회수
- 깨끗한 페이지는 즉시 회수 가능
3.5 Swap과 OOM Killer
Swap 메모리
Swap은 물리 메모리가 부족할 때 디스크 공간을 메모리처럼 사용하는 메커니즘이다.
Swap의 작동
- 물리 메모리가 부족해지면 커널이 메모리 회수 시작
- 자주 사용하지 않는 페이지를 스왑 공간으로 이동
- 나중에 해당 페이지 접근 시 Major Page Fault 발생
- 디스크에서 다시 메모리로 읽어옴
Swappiness 설정
Swappiness 값은 시스템이 얼마나 적극적으로 스왑을 사용할지 결정한다:
- 값이 높을수록 적극적으로 스왑 사용
- 값이 낮을수록 메모리를 더 오래 유지
- 컨테이너 환경에서는 보통 낮은 값 권장 (0-10)
OOM Killer (Out Of Memory Killer)
물리 메모리와 스왑이 모두 고갈되면 OOM Killer가 활성화된다.
OOM Killer의 작동
- 커널이 메모리 부족 상황 감지
- 각 프로세스의 OOM Score 계산
- 점수가 가장 높은 프로세스 강제 종료
- 메모리 확보 후 시스템 계속 동작
OOM Score 계산
OOM Score는 다음 요소를 고려한다:
- 메모리 사용량 (많이 사용할수록 점수 높음)
- 프로세스 실행 시간 (짧을수록 점수 높음)
- nice 값 (우선순위가 낮을수록 점수 높음)
- root 프로세스인지 여부 (일반 프로세스가 점수 높음)
컨테이너와 OOM
컨테이너에 메모리 제한을 설정하면:
- 컨테이너가 제한을 초과할 때 OOM Killer가 컨테이너 내에서만 작동
- 호스트 시스템 전체에 영향을 주지 않음
- 이것이 Cgroup의 중요한 역할 중 하나
3.6 NUMA (Non-Uniform Memory Access)
NUMA의 개념
현대의 멀티소켓 서버에서는 각 CPU 소켓이 자신만의 메모리를 가지며, 다른 소켓의 메모리에 접근할 때 성능 저하가 발생한다.
NUMA 구조
- 로컬 메모리: 같은 소켓의 메모리 - 빠른 접근
- 원격 메모리: 다른 소켓의 메모리 - 느린 접근 (2-3배 지연)
NUMA 정책
리눅스는 여러 NUMA 정책을 제공한다:
- NUMA_LOCAL: 현재 노드의 메모리 우선 할당
- NUMA_INTERLEAVE: 여러 노드에 균등 분산
- NUMA_BIND: 특정 노드에만 할당
컨테이너와 NUMA
컨테이너 오케스트레이터는 NUMA 인식 스케줄링을 통해:
- 같은 NUMA 노드의 CPU와 메모리를 함께 할당
- 크로스 노드 메모리 접근 최소화
- 성능 최적화
4. 프로세스 관리
4.1 프로세스와 스레드
프로세스의 개념
프로세스(Process)는 실행 중인 프로그램의 인스턴스이다. 운영체제가 자원을 할당하는 기본 단위이며, 다음을 포함한다:
- 독립적인 가상 메모리 주소 공간
- 프로그램 코드 (Text 영역)
- 데이터 영역 (전역 변수, 힙, 스택)
- 파일 디스크립터 테이블
- 시그널 핸들러 정보
- 환경 변수
프로세스 제어 블록 (PCB)
커널은 각 프로세스의 정보를 PCB(Process Control Block)에 저장한다:
- PID (Process ID)
- 프로세스 상태 (실행, 대기, 중단 등)
- CPU 레지스터 값
- 메모리 관리 정보 (페이지 테이블 포인터)
- I/O 상태 정보
- 스케줄링 정보
스레드의 개념
스레드(Thread)는 프로세스 내의 실행 흐름 단위이다.
스레드의 특징
- 같은 프로세스의 스레드들은 메모리 공간(코드, 데이터, 힙)을 공유
- 각 스레드는 독립적인 스택을 가짐
- 각 스레드는 독립적인 레지스터 세트를 가짐
- 프로세스보다 생성/전환 비용이 훨씬 저렴
멀티스레딩의 장점
- 자원 공유: 메모리를 공유하여 프로세스 간 통신(IPC)보다 효율적
- 응답성: 한 스레드가 블록되어도 다른 스레드는 계속 실행
- 경제성: 컨텍스트 스위칭 비용이 프로세스보다 훨씬 낮음
- 확장성: 멀티코어 CPU를 효과적으로 활용
멀티스레딩의 단점
- 동기화 문제: Race condition, Deadlock 등의 복잡한 문제
- 디버깅 어려움: 비결정적 동작으로 버그 재현이 어려움
- 보안 위험: 한 스레드의 버그가 전체 프로세스에 영향
4.2 프로세스 생성: Fork와 Exec
Fork 시스템 콜
fork()는 현재 프로세스의 복사본을 생성하는 시스템 콜이다.
Fork의 작동 원리
- 커널이 새로운 PCB 생성
- 부모 프로세스의 메모리 공간을 자식에게 복사 (실제로는 CoW)
- 파일 디스크립터 테이블 복사
- 부모와 자식 모두 fork() 다음 코드부터 실행
- 부모는 자식의 PID를 반환받고, 자식은 0을 반환받음
Copy-on-Write (CoW) 최적화
실제로 fork()는 메모리를 즉시 복사하지 않는다:
- 부모와 자식이 처음에는 같은 물리 메모리를 읽기 전용으로 공유
- 둘 중 하나가 메모리를 수정하려 할 때 Page Fault 발생
- 그때 해당 페이지만 복사
이를 통해 fork() 성능이 극적으로 향상된다.
Exec 시스템 콜 패밀리
exec 계열 함수들은 현재 프로세스를 새로운 프로그램으로 교체한다.
Exec의 작동 원리
- 현재 프로세스의 메모리 영역을 정리
- 새 프로그램을 메모리에 로드
- 새 프로그램의 main()부터 실행 시작
- PID는 그대로 유지
- 열려있던 파일 디스크립터는 대부분 유지
Fork-Exec 패턴
대부분의 유닉스 프로그램은 이 두 시스템 콜을 조합하여 새 프로세스를 생성한다:
fork()로 자식 프로세스 생성- 자식에서
exec()로 새 프로그램 실행 - 부모는
wait()로 자식의 종료 대기
이 패턴은 셸(shell)이 명령어를 실행하는 기본 방식이다.
4.3 PID와 프로세스 계층
PID (Process ID)
각 프로세스는 고유한 PID를 가진다:
- PID는 양의 정수 (일반적으로 1부터 시작)
- 시스템 부팅 시 첫 번째 프로세스는 PID 1 (init 또는 systemd)
- 프로세스 종료 시 PID는 재사용될 수 있음
PPID (Parent Process ID)
모든 프로세스는 자신을 생성한 부모 프로세스의 PID인 PPID를 가진다.
프로세스 트리 구조
리눅스의 모든 프로세스는 트리 구조를 형성한다. systemd (PID 1)가 루트에 있고, 그 아래로 sshd, dockerd, kubelet 등의 프로세스가 계층적으로 연결된다.
init 프로세스 (PID 1)
PID 1은 특별한 역할을 한다:
- 시스템 부팅 시 커널이 직접 실행하는 첫 사용자 공간 프로세스
- 모든 프로세스의 조상
- 고아 프로세스(부모가 먼저 종료된 프로세스)를 입양
- 시스템 종료 시 마지막까지 살아있는 프로세스
전통적으로 init 프로세스였지만, 현대 리눅스는 systemd를 사용한다.
좀비 프로세스와 고아 프로세스
좀비 프로세스 (Zombie Process)
자식 프로세스가 종료되었지만 부모가 아직 wait()를 호출하지 않은 상태:
- 프로세스는 종료되었지만 PCB는 남아있음
- 리소스는 해제되었지만 exit status는 보존
- 부모가
wait()를 호출하면 완전히 제거됨
고아 프로세스 (Orphan Process)
부모 프로세스가 자식보다 먼저 종료된 경우:
- init (PID 1)이 자동으로 새로운 부모가 됨
- init은 주기적으로
wait()를 호출하여 좀비 프로세스 정리 - 시스템 안정성 유지
프로세스 그룹과 세션
프로세스 그룹
관련된 프로세스들을 하나로 묶은 단위:
- 각 그룹은 PGID(Process Group ID)를 가짐
- 시그널을 그룹 전체에 보낼 수 있음
- 파이프라인의 모든 프로세스가 하나의 그룹을 형성
세션
하나 이상의 프로세스 그룹을 포함하는 단위:
- 각 세션은 SID(Session ID)를 가짐
- 터미널과 연결된 프로세스들의 집합
- 세션 리더는 제어 터미널을 가질 수 있음
4.4 프로세스 상태
프로세스 상태 전이
리눅스 프로세스는 다음과 같은 상태를 가진다:
실행 상태 (Running - R)
- CPU에서 현재 실행 중이거나 실행 큐에서 대기 중
- ps 명령에서 ‘R’로 표시
대기 상태 (Sleeping)
인터럽터블 슬립 (Interruptible Sleep - S)
- I/O 완료나 이벤트를 기다리는 중
- 시그널에 의해 깨어날 수 있음
- 대부분의 대기 상태가 이에 해당
언인터럽터블 슬립 (Uninterruptible Sleep - D)
- 디스크 I/O 같은 중요한 작업 대기 중
- 시그널로도 중단할 수 없음
- 이 상태가 오래 지속되면 시스템 문제의 신호
중단 상태 (Stopped - T)
- SIGSTOP 또는 SIGTSTP 시그널에 의해 일시 정지
- SIGCONT 시그널로 재개 가능
- 디버거가 프로세스를 제어할 때 사용
좀비 상태 (Zombie - Z)
- 프로세스는 종료되었지만 부모가 wait()를 호출하지 않음
- 리소스는 해제되었지만 PCB는 남아있음
5. 프로세스 스케줄링
5.1 스케줄러의 역할
CPU 스케줄러는 제한된 CPU 자원을 여러 프로세스에 효율적으로 분배하는 커널의 주요 컴포넌트이다.
스케줄러의 목표
- 공정성: 모든 프로세스에 공평한 CPU 시간 제공
- 응답성: 사용자 상호작용이 빠르게 느껴지도록
- 처리량: 단위 시간당 최대한 많은 작업 완료
- 효율성: 스케줄링 오버헤드 최소화
5.2 CFS (Completely Fair Scheduler)
CFS의 기본 개념
리눅스 2.6.23부터 사용된 CFS는 모든 프로세스에 공평하게 CPU 시간을 분배하는 것을 목표로 한다.
vruntime (Virtual Runtime)
CFS의 주요 개념은 vruntime이다:
- 각 프로세스가 지금까지 CPU를 사용한 시간을 기록
- 실제 실행 시간을 프로세스의 우선순위로 가중치 조정
- 가장 작은 vruntime을 가진 프로세스가 다음에 실행될 자격이 있음
Red-Black Tree
CFS는 실행 큐를 Red-Black Tree로 관리한다:
- vruntime을 키로 사용하는 자가 균형 이진 탐색 트리
- 가장 작은 vruntime(가장 왼쪽 노드)을 O(1)에 찾음
- 프로세스 삽입/삭제도 O(log n)으로 효율적
스케줄링 과정
- 타이머 인터럽트가 주기적으로 발생 (일반적으로 1ms마다)
- 현재 실행 중인 프로세스의 vruntime 증가
- vruntime이 일정 값을 초과하면 스케줄러 호출
- Red-Black Tree에서 가장 작은 vruntime의 프로세스 선택
- 컨텍스트 스위칭으로 새 프로세스 실행
nice 값과 우선순위
nice 값(-20 ~ 19)으로 프로세스 우선순위를 조정할 수 있다. nice 값이 높을수록 우선순위가 낮다.
nice 값이 다른 프로세스의 vruntime 증가율이 달라진다:
- nice 0 프로세스: vruntime이 실시간으로 증가
- nice 19 프로세스: vruntime이 더 빠르게 증가 (CPU를 덜 받음)
- nice -20 프로세스: vruntime이 더 천천히 증가 (CPU를 더 받음)
5.3 실시간 스케줄링
실시간 스케줄링 클래스
CFS는 일반 프로세스용이고, 실시간 작업을 위한 별도의 스케줄러가 있다:
SCHED_FIFO (First-In-First-Out)
- 우선순위 기반의 선점형 스케줄링
- 같은 우선순위 내에서는 FIFO 순서
- 자발적으로 양보하거나 더 높은 우선순위 프로세스가 나타날 때까지 실행
SCHED_RR (Round-Robin)
- SCHED_FIFO와 유사하지만 타임 슬라이스 제한 있음
- 같은 우선순위 내에서 순환하며 실행
주의사항
실시간 스케줄링은 강력하지만 위험한다:
- 잘못 사용하면 시스템 전체가 응답 불가능해질 수 있음
- root 권한 필요
- 임베디드 시스템이나 특수한 경우에만 사용 권장
5.4 CPU Affinity
CPU Affinity의 개념
CPU Affinity는 프로세스가 실행될 수 있는 CPU 코어를 제한하는 기능이다.
왜 필요한가?
캐시 지역성 (Cache Locality)
- CPU 코어는 자주 사용하는 데이터를 L1/L2 캐시에 저장
- 프로세스가 같은 코어에서 계속 실행되면 캐시 히트율 증가
- 다른 코어로 이동하면 캐시 미스 발생
NUMA 최적화
- 특정 메모리 영역에 가까운 CPU에서 실행하도록 설정
- 원격 메모리 접근 최소화
간섭 방지
- 중요한 프로세스를 특정 코어에 고정하여 다른 프로세스의 간섭 방지
컨테이너 환경에서의 활용
Kubernetes에서도 CPU Affinity를 활용할 수 있다. CPU Manager 정책을 통해 전용 CPU를 할당하여 성능 최적화가 가능하다.
5.5 Context Switching
컨텍스트 스위칭의 개념
컨텍스트 스위칭은 CPU가 한 프로세스의 실행을 중단하고 다른 프로세스를 실행하는 과정이다.
컨텍스트 스위칭 과정
- 현재 프로세스 상태 저장
- CPU 레지스터 값을 PCB에 저장
- 프로그램 카운터(PC) 저장
- 스택 포인터 저장
- 새 프로세스 선택
- 스케줄러가 다음 실행할 프로세스 결정
- 새 프로세스 상태 복원
- 새 프로세스의 PCB에서 레지스터 값 복원
- 페이지 테이블 포인터 변경 (메모리 컨텍스트 전환)
- TLB 플러시
- 실행 재개
- 새 프로세스의 다음 명령어부터 실행
성능 영향
컨텍스트 스위칭은 비용이 많이 든다:
- 직접 비용: 레지스터 저장/복원에 수십 마이크로초
- 간접 비용: 캐시/TLB 무효화로 인한 성능 저하
- 너무 잦은 컨텍스트 스위칭은 시스템 성능 저하
스레드 vs 프로세스 전환
스레드 간 컨텍스트 스위칭이 프로세스 간보다 빠른 이유:
- 같은 주소 공간을 사용하므로 페이지 테이블 변경 불필요
- TLB 플러시 불필요
- 캐시 무효화가 적음
6. 시그널과 IPC
6.1 Signal (시그널)
시그널의 개념
시그널은 프로세스에게 비동기적으로 이벤트를 알리는 소프트웨어 인터럽트이다.
자주 사용되는 시그널
SIGINT (2)
- Ctrl+C로 발생
- 프로그램 종료 요청 (잡을 수 있음)
- 정상적인 종료 처리 가능
SIGTERM (15)
- 기본 kill 명령의 시그널
- 우아한 종료(graceful shutdown) 요청
- 프로세스가 정리 작업 후 종료 가능
SIGKILL (9)
- 강제 종료
- 잡을 수 없고 무시할 수 없음
- 즉시 프로세스 종료
SIGSTOP (19)
- 프로세스 일시 정지
- 잡을 수 없음
SIGCONT (18)
- 정지된 프로세스 재개
SIGCHLD (17)
- 자식 프로세스가 종료되거나 상태 변경 시 부모에게 전송
- wait() 시스템 콜과 함께 사용
시그널 처리
프로세스는 시그널을 세 가지 방식으로 처리할 수 있다:
기본 동작 (Default Action)
- 시그널 핸들러를 설정하지 않으면 기본 동작 수행
- SIGTERM의 기본 동작은 프로세스 종료
무시 (Ignore)
- 특정 시그널을 무시하도록 설정 가능
사용자 정의 핸들러 (Custom Handler)
- 개발자가 직접 시그널 처리 로직 정의
- 정리 작업이나 상태 저장 등 수행 가능
6.2 IPC (Inter-Process Communication)
IPC의 필요성
프로세스는 기본적으로 독립된 메모리 공간을 가지므로, 데이터를 교환하려면 특별한 메커니즘이 필요한다.
Pipe (파이프)
가장 간단한 IPC 메커니즘으로, 한 방향 데이터 스트림을 제공한다. 주로 부모-자식 프로세스 간 통신에 사용된다.
Named Pipe (FIFO)
일반 파이프와 달리 파일 시스템에 이름을 가지며, 관련 없는 프로세스 간에도 통신 가능하다.
Message Queue (메시지 큐)
구조화된 메시지를 주고받을 수 있는 IPC 메커니즘이다. 메시지 타입별로 선택적 수신이 가능하다.
Shared Memory (공유 메모리)
가장 빠른 IPC 방법으로, 여러 프로세스가 같은 물리 메모리 영역을 공유한다. 주의사항으로 동기화 메커니즘(세마포어, 뮤텍스 등)과 함께 사용해야 한다.
Semaphore (세마포어)
프로세스 간 동기화를 위한 메커니즘이다. 공유 자원에 대한 접근을 제어한다.
Socket (소켓)
네트워크 통신에 사용되지만, 같은 시스템 내 프로세스 간 통신에도 사용 가능하다 (Unix Domain Socket).
IPC 메커니즘 비교
- Pipe: 중간 속도, 사용 쉬움, 부모-자식 간 단순 데이터 전송
- FIFO: 중간 속도, 사용 쉬움, 무관한 프로세스 간 스트림 전송
- Message Queue: 중간 속도, 구조화된 메시지 전송
- Shared Memory: 가장 빠름, 사용 어려움, 동기화 필요, 대용량 데이터 공유
- Semaphore: 동기화 전용
- Socket: 가장 느림, 네트워크/로컬 통신
7. 파일 시스템
7.1 VFS (Virtual File System)
VFS의 역할
VFS는 다양한 파일 시스템에 대한 통합된 추상화 계층을 제공한다. 이를 통해 사용자 프로그램은 파일 시스템의 종류와 관계없이 동일한 API를 사용할 수 있다.
VFS의 구조
VFS는 다음과 같은 주요 객체를 정의한다:
superblock
- 파일 시스템 전체의 메타데이터
- 파일 시스템 타입, 크기, 상태 등
inode (Index Node)
- 파일의 메타데이터를 저장하는 자료구조
- 파일 크기, 권한, 소유자, 타임스탬프
- 데이터 블록 위치 정보
- 파일 이름은 inode에 저장되지 않음 (dentry에 저장)
dentry (Directory Entry)
- 파일 이름과 inode 번호의 매핑
- 디렉토리 구조를 표현
- 파일 경로 탐색에 사용
file
- 열린 파일을 나타내는 커널 자료구조
- 파일 오프셋 (현재 읽기/쓰기 위치)
- 파일 디스크립터와 연결
VFS의 장점
- 애플리케이션은 파일 시스템 타입을 신경 쓸 필요 없음
- 새로운 파일 시스템 추가가 용이
- 다양한 저장 장치를 동일한 방식으로 접근
7.2 inode와 Hard Link / Symbolic Link
inode의 역할
inode는 파일의 실제 메타데이터를 저장하는 주요 자료구조이다:
- 파일 타입 (일반 파일, 디렉토리, 심볼릭 링크 등)
- 파일 권한 (rwxrwxrwx)
- 소유자 UID, 그룹 GID
- 파일 크기
- 타임스탬프 (생성, 수정, 접근 시간)
- 하드 링크 카운트
- 데이터 블록 포인터
Hard Link (하드 링크)
하드 링크는 같은 inode를 가리키는 여러 개의 파일 이름이다:
- 원본 파일과 하드 링크는 완전히 동등
- 하나를 삭제해도 다른 링크는 유효
- 모든 하드 링크가 삭제되어야 inode와 데이터 블록 삭제
- 같은 파일 시스템 내에서만 생성 가능
- 디렉토리에 대해서는 생성 불가 (루프 방지)
Symbolic Link (심볼릭 링크, Soft Link)
심볼릭 링크는 다른 파일의 경로를 저장하는 특수 파일이다:
- 원본 파일의 경로를 문자열로 저장
- 원본 파일이 삭제되면 심볼릭 링크는 깨짐 (dangling link)
- 다른 파일 시스템의 파일도 가리킬 수 있음
- 디렉토리에 대해서도 생성 가능
7.3 파일 디스크립터 (File Descriptor)
파일 디스크립터의 개념
파일 디스크립터는 열린 파일을 식별하는 정수이다. 프로세스가 파일을 열면 커널이 파일 디스크립터를 할당한다.
표준 파일 디스크립터
모든 프로세스는 기본적으로 3개의 파일 디스크립터를 가진다:
- 0 (STDIN_FILENO): 표준 입력
- 1 (STDOUT_FILENO): 표준 출력
- 2 (STDERR_FILENO): 표준 에러
파일 디스크립터 테이블
각 프로세스는 자신만의 파일 디스크립터 테이블을 가진다:
- 파일 디스크립터 → 파일 객체 포인터 매핑
- fork() 시 부모의 파일 디스크립터 테이블 복사
- exec() 후에도 기본적으로 유지 (close-on-exec 플래그 제외)
파일 디스크립터와 inode의 관계
파일 디스크립터 → 파일 객체 → inode 의 3단계 간접 참조 구조이다:
- 여러 파일 디스크립터가 같은 파일 객체를 가리킬 수 있음
- 여러 파일 객체가 같은 inode를 가리킬 수 있음
7.4 Block Device와 Journaling
Block Device (블록 디바이스)
블록 디바이스는 고정 크기 블록 단위로 데이터를 읽고 쓰는 장치이다:
- HDD, SSD, USB 드라이브 등
- 블록 크기는 보통 512 바이트 또는 4KB
- 랜덤 액세스 가능 (특정 블록 직접 접근)
Character Device와의 차이
- Block Device: 블록 단위, 랜덤 액세스, 캐싱 가능
- Character Device: 바이트 스트림, 순차 액세스, 캐싱 불가 (키보드, 마우스 등)
Journaling File System
저널링은 파일 시스템의 일관성을 보장하기 위한 메커니즘이다.
저널링의 작동 원리
- 파일 시스템 변경 작업을 저널(로그)에 먼저 기록
- 저널에 변경 내용이 안전하게 기록되면 실제 파일 시스템에 적용
- 작업 완료 후 저널 엔트리 삭제
장점
- 시스템 크래시나 전원 차단 시에도 파일 시스템 복구 가능
- fsck(파일 시스템 검사) 시간 대폭 단축
- 데이터 무결성 향상
저널링 모드
Journal (가장 안전)
- 메타데이터와 데이터 모두 저널에 기록
- 가장 느리지만 가장 안전
Ordered (기본값)
- 메타데이터만 저널에 기록
- 데이터는 메타데이터 이전에 먼저 기록
- 성능과 안정성의 균형
Writeback (가장 빠름)
- 메타데이터만 저널에 기록
- 데이터 쓰기 순서 보장 안 됨
- 가장 빠르지만 일관성 낮음
주요 저널링 파일 시스템
- ext4: 리눅스의 기본 파일 시스템
- XFS: 대용량 파일과 병렬 I/O에 최적화
- Btrfs: 스냅샷, 압축 등 고급 기능 제공
- F2FS: SSD/플래시 메모리 최적화
7.5 파일 시스템 마운트
마운트의 개념
마운트는 파일 시스템을 디렉토리 트리의 특정 위치에 연결하는 작업이다.
마운트 과정
- 블록 디바이스 확인
- 파일 시스템 타입 식별
- 슈퍼블록 읽기
- 마운트 포인트에 파일 시스템 연결
마운트 포인트
- 기존 디렉토리를 마운트 포인트로 사용
- 마운트 전 해당 디렉토리의 내용은 숨겨짐
- 언마운트하면 원래 내용이 다시 나타남
바인드 마운트 (Bind Mount)
이미 마운트된 디렉토리를 다른 위치에 연결:
- 같은 파일 시스템을 여러 위치에서 접근 가능
- 컨테이너 기술에서 호스트 디렉토리를 컨테이너에 공유할 때 사용
8. 디바이스와 I/O
8.1 디바이스 파일
디바이스 파일의 개념
리눅스에서는 “Everything is a file” 철학에 따라 하드웨어 장치도 파일로 표현된다. 디바이스 파일은 주로 /dev 디렉토리에 위치한다.
디바이스 파일 타입
Character Device (문자 디바이스)
- 바이트 스트림 방식으로 데이터 전송
- 버퍼링 없이 직접 접근
- 예: 키보드, 마우스, 시리얼 포트
ls -l에서 ‘c’로 표시
Block Device (블록 디바이스)
- 고정 크기 블록 단위로 데이터 전송
- 버퍼링과 캐싱 가능
- 랜덤 액세스 지원
- 예: 하드 디스크, SSD, USB 드라이브
ls -l에서 ‘b’로 표시
주요 디바이스 파일
/dev/null: 쓰기는 버려지고, 읽기는 EOF 반환/dev/zero: 읽으면 무한한 0 바이트 스트림 반환/dev/random,/dev/urandom: 난수 생성/dev/sda,/dev/sdb: SCSI/SATA 디스크/dev/nvme0n1: NVMe SSD/dev/tty: 현재 터미널
8.2 Major/Minor Number
디바이스 번호
각 디바이스 파일은 Major Number와 Minor Number를 가진다:
Major Number
- 디바이스 드라이버를 식별
- 커널이 어떤 드라이버로 요청을 라우팅할지 결정
Minor Number
- 같은 드라이버가 관리하는 여러 장치를 구분
- 예:
/dev/sda1,/dev/sda2는 같은 Major, 다른 Minor
8.3 I/O 모델
Blocking I/O (블로킹 I/O)
가장 일반적인 I/O 모델로, I/O 작업이 완료될 때까지 프로세스가 대기한다:
- 간단하고 직관적
- 하지만 대기 중에는 다른 작업 불가
- 한 번에 하나의 I/O만 처리
Non-blocking I/O (논블로킹 I/O)
I/O 작업을 즉시 반환하고, 데이터가 준비되지 않았으면 에러를 반환한다:
- 프로세스가 대기하지 않고 다른 작업 수행 가능
- 하지만 폴링이 필요하여 CPU 낭비 가능
I/O Multiplexing (다중 I/O)
여러 파일 디스크립터를 동시에 모니터링:
- select(): 가장 오래된 방식, FD 개수 제한 있음
- poll(): select()의 개선 버전, FD 개수 제한 없음
- epoll(): 리눅스 전용, 대량 연결에 최적화
Asynchronous I/O (비동기 I/O)
I/O 작업을 요청하고 즉시 반환하며, 작업 완료 시 콜백이나 시그널로 통지:
- 가장 효율적이지만 구현이 복잡
- 리눅스의 AIO (Asynchronous I/O) API
8.4 Direct I/O와 Buffer I/O
Buffer I/O (버퍼 I/O)
기본 I/O 방식으로, Page Cache를 통해 수행된다:
- 커널이 자동으로 캐싱 및 선행 읽기(readahead) 수행
- 대부분의 애플리케이션에 적합
- 쓰기는 즉시 반환 (write-back)
Direct I/O
Page Cache를 우회하고 직접 디스크와 통신:
- 데이터베이스 같은 애플리케이션이 자체 캐시 관리 시 유용
- 커널의 캐싱 오버헤드 제거
- 하지만 애플리케이션이 모든 최적화를 책임져야 함
- O_DIRECT 플래그로 활성화
8.5 Zero-Copy
전통적인 데이터 복사
네트워크로 파일을 전송할 때 전통적 방식:
- 디스크 → 커널 버퍼 (DMA)
- 커널 버퍼 → 사용자 공간 버퍼 (CPU 복사)
- 사용자 공간 버퍼 → 소켓 버퍼 (CPU 복사)
- 소켓 버퍼 → 네트워크 카드 (DMA)
총 4번의 컨텍스트 스위칭과 2번의 CPU 복사가 발생한다.
Zero-Copy 최적화
sendfile() 시스템 콜 사용 시:
- 디스크 → 커널 버퍼 (DMA)
- 커널 버퍼 → 네트워크 카드 (DMA, CPU 복사 없음)
사용자 공간을 거치지 않아:
- 컨텍스트 스위칭 2회로 감소
- CPU 복사 0회 (DMA만 사용)
- 대폭적인 성능 향상
웹 서버나 파일 서버에서 매우 중요한 최적화 기법이다.
9. 보안 메커니즘
9.1 UID/GID와 Permission Model
UID와 GID
리눅스는 사용자와 그룹을 숫자로 식별한다:
UID (User ID)
- 각 사용자는 고유한 UID를 가짐
- UID 0은 root 사용자 (슈퍼유저)
- 일반 사용자는 보통 1000부터 시작
GID (Group ID)
- 각 그룹은 고유한 GID를 가짐
- 사용자는 여러 그룹에 속할 수 있음
- Primary Group과 Supplementary Groups
파일 권한 모델
리눅스의 전통적인 권한 모델은 rwx 비트로 구성된다:
권한 비트
- r (read): 파일 읽기, 디렉토리 목록 보기
- w (write): 파일 쓰기, 디렉토리에 파일 생성/삭제
- x (execute): 파일 실행, 디렉토리 진입
세 가지 권한 그룹
- User (Owner): 파일 소유자의 권한
- Group: 파일 그룹의 권한
- Others: 그 외 모든 사용자의 권한
예: rwxr-xr--
- 소유자: 읽기, 쓰기, 실행 가능
- 그룹: 읽기, 실행 가능
- 기타: 읽기만 가능
특수 권한 비트
SUID (Set User ID)
- 파일 실행 시 소유자의 권한으로 실행
/usr/bin/passwd가 대표적 예
SGID (Set Group ID)
- 파일 실행 시 그룹의 권한으로 실행
- 디렉토리에 설정 시 새 파일이 디렉토리 그룹을 상속
Sticky Bit
- 디렉토리에 설정 시 소유자만 파일 삭제 가능
/tmp디렉토리가 대표적 예
9.2 DAC vs MAC
DAC (Discretionary Access Control)
전통적인 리눅스 권한 모델로, 파일 소유자가 권한을 결정한다:
특징
- 유연하고 사용하기 쉬움
- 사용자가 자신의 파일에 대한 전체 제어권 보유
- 하지만 보안 정책 강제가 어려움
한계
- 악의적인 프로그램이 사용자 권한으로 모든 파일 접근 가능
- 권한 관리가 분산되어 일관된 보안 정책 적용 어려움
MAC (Mandatory Access Control)
시스템 관리자가 정의한 강제 보안 정책을 적용한다:
특징
- 중앙화된 보안 정책
- 사용자도 정책을 우회할 수 없음
- 더 강력한 보안
리눅스의 MAC 구현
SELinux (Security-Enhanced Linux)
- NSA에서 개발
- 레이블 기반 접근 제어
- Red Hat, CentOS, Fedora의 기본값
- 복잡하지만 매우 강력
AppArmor
- Novell에서 개발
- 경로 기반 접근 제어
- Ubuntu, SUSE의 기본값
- SELinux보다 사용하기 쉬움
9.3 Linux Capabilities
Capabilities의 개념
전통적으로 root는 모든 권한을 가지는 “전능한” 사용자였다. Capabilities는 root의 권한을 세분화된 단위로 분할한 것이다.
주요 Capabilities
CAP_NET_ADMIN
- 네트워크 인터페이스 설정
- 방화벽 규칙 수정
- 라우팅 테이블 변경
CAP_SYS_ADMIN
- 가장 광범위한 권한
- 마운트, 스왑 설정, 호스트명 변경 등
- 보안상 가장 위험한 capability
CAP_KILL
- 임의의 프로세스에 시그널 전송
CAP_NET_BIND_SERVICE
- 1024 이하의 특권 포트 바인딩 (HTTP 80, HTTPS 443 등)
CAP_CHOWN
- 파일 소유자 변경
CAP_DAC_OVERRIDE
- 파일 권한 검사 우회
Capabilities의 장점
- 프로세스에 필요한 최소 권한만 부여 (최소 권한 원칙)
- root 권한 없이도 특정 작업 수행 가능
- 컨테이너 보안의 주요 메커니즘
컨테이너와 Capabilities
Docker 컨테이너는 기본적으로 제한된 capabilities만 가진다:
- 불필요한 capabilities 제거하여 공격 표면 축소
- 필요 시
--cap-add로 추가 --cap-drop으로 기본 capabilities도 제거 가능
9.4 Seccomp (Secure Computing Mode)
Seccomp의 개념
Seccomp는 프로세스가 사용할 수 있는 시스템 콜을 제한하는 보안 메커니즘이다.
Seccomp 모드
Strict Mode
- 가장 제한적인 모드
- read(), write(), exit(), sigreturn()만 허용
- 거의 사용되지 않음
Filter Mode (Seccomp-BPF)
- BPF(Berkeley Packet Filter) 프로그램으로 시스템 콜 필터링
- 세밀한 제어 가능
- 현대 컨테이너 보안의 주요
작동 방식
- BPF 프로그램으로 허용/거부 규칙 정의
- 프로세스에 seccomp 필터 적용
- 금지된 시스템 콜 호출 시 SIGSYS 시그널 또는 프로세스 종료
컨테이너와 Seccomp
Docker는 기본 seccomp 프로파일을 적용한다:
- 약 300개 중 44개의 위험한 시스템 콜을 차단
- 컨테이너 탈출 방지
- 커널 취약점 악용 방지
차단되는 시스템 콜 예시:
reboot: 시스템 재부팅swapon/swapoff: 스왑 제어mount: 파일 시스템 마운트keyctl: 커널 키 관리
9.5 Namespace와 보안
Namespace의 보안 측면
Namespace는 단순한 격리 메커니즘이 아니라 중요한 보안 기능이다:
User Namespace의 특별한 역할
User Namespace는 보안에서 매우 중요한다:
- 컨테이너 내부의 root(UID 0)를 호스트의 일반 사용자로 매핑
- 컨테이너 탈출 시에도 제한된 권한만 보유
- Rootless 컨테이너의 주요 기술
예시
- 컨테이너 내부: UID 0 (root)
- 호스트: UID 1000 (일반 사용자)
- 컨테이너가 탈출해도 호스트에서는 일반 사용자 권한만 가짐