필사 모드: 운영체제의 현대적 이해 — io_uring, cgroups/namespaces, eBPF, NUMA, GPU UVM, EEVDF, Zero-Copy 완벽 가이드 (2025)
한국어왜 지금 OS를 다시 배우는가
"OS는 학부 때 배운 거 아냐?"라고 묻는다면, 대답:
- **io_uring (2019)** — epoll/kqueue의 후계자. Node.js, Rust tokio, PostgreSQL 17이 도입 중.
- **cgroups v2 (2016+, 2022년 주류)** — Docker·K8s 리소스 제어의 기반.
- **eBPF** — 이전 글(Observability, Network)에서 등장한 그 기술. 2020년대 커널의 혁명.
- **EEVDF (2024 Linux 6.6)** — CFS 스케줄러를 대체.
- **NUMA** — 32 vCPU 이상 쓰는 순간 성능 영향을 크게 받기 시작.
- **GPU UVM (Unified Virtual Memory)** — LLM 추론·훈련의 기반.
- **io_uring 기반 네트워킹** — AF_XDP, DPDK와 경쟁.
**2025년 엔지니어가 OS를 모르면**, 왜 자기 앱이 느린지, 왜 컨테이너가 OOM을 맞는지, 왜 CPU 100%인데 처리량이 안 오르는지 설명할 수 없다.
Part 1 — 프로세스, 스레드, 코루틴 — 현대의 비교
프로세스
- 별도 주소 공간. 격리 강력.
- 생성 비용 크다 (fork + exec).
- IPC 필요.
- 예: 유닉스 쉘 파이프, Chrome 탭.
스레드
- 같은 주소 공간 공유.
- 생성 비용 작다 (pthread_create).
- 공유 메모리로 통신 쉬움 + 위험(데이터 레이스).
- 커널 스케줄링 → 컨텍스트 스위치 오버헤드.
코루틴 / Fiber / Green Thread
- 유저 스페이스 스케줄링.
- 생성 비용 거의 없음(수천만 개 가능).
- 협력적 스케줄링 — await 지점에서 양보.
- 예: Go goroutine, Rust async, Python asyncio, Java Virtual Thread(2023).
Java Virtual Threads — Project Loom (2023)
JDK 21 LTS. "기존 스레드 코드를 거의 그대로 두고 수백만 동시 요청 처리."
**전통 스레드:** 스레드당 OS 스택 1MB+ → 수만 개가 한계.
**가상 스레드:** JVM 내부 스케줄링, 필요할 때만 스택 할당 → 수백만 가능.
블로킹 I/O를 쓰면서도 논블로킹의 동시성을 얻는다. **2024-2025년 Spring Boot 채택**으로 Java 백엔드 지형이 바뀌는 중.
Go goroutine
- M:N 스케줄링 (M 커널 스레드 : N 고루틴).
- 채널로 통신.
- 스택이 초기 2KB에서 동적 확장.
- GOMAXPROCS가 P(Processor) 수 결정.
언제 무엇을?
| 상황 | 선택 |
|---|---|
| CPU 바운드 병렬 | 스레드 + 공유 메모리 |
| I/O 바운드 대량 동시 | 코루틴/async |
| 보안 격리 강력 필요 | 프로세스 + IPC |
| JVM에서 수백만 동시 | Virtual Threads |
Part 2 — io_uring — epoll을 넘어서
I/O 진화의 역사
1. **블로킹 I/O** — `read()`가 데이터 올 때까지 블록.
2. **select/poll** — FD 세트 전부 스캔. O(n).
3. **epoll (Linux, 2002) / kqueue (BSD)** — 이벤트 기반, O(1).
4. **aio_read(POSIX AIO)** — 한계 많음. 거의 안 씀.
5. **io_uring (2019, Jens Axboe)** — 진정한 비동기.
io_uring의 구조
**Submission Queue (SQ)** + **Completion Queue (CQ)**. 둘 다 mmap된 공유 메모리. **syscall 없이** 작업을 제출하고 완료를 확인.
io_uring_prep_read(sqe, fd, buf, len, offset);
io_uring_submit(&ring); // syscall 1회로 여러 요청 제출
// ... 나중에
io_uring_wait_cqe(&ring, &cqe);
**이점:**
- **Syscall 오버헤드 대폭 감소** — SQPOLL 모드는 0.
- **Batching** — 여러 I/O를 한 번에 제출.
- **파일 + 네트워크 + 타이머 + accept** 통합.
- **sendmsg, recvmsg, splice 등** 대부분 지원.
**채택:**
- **Rust tokio**(부분), **glommio**, **monoio**.
- **Node.js** — 실험적.
- **PostgreSQL 17(2024)** — async I/O 기반으로 io_uring 도입 검토.
- **QEMU, RocksDB, ScyllaDB**.
**보안 이슈:** 2023년 구글이 Chrome 샌드박스에서 io_uring 차단. 커널 공격면 넓히기. 그래서 **신뢰 환경 한정**으로 쓰라는 권고.
Part 3 — 가상 메모리의 현대
4단계 페이지 테이블 (x86_64)
`CR3 → PML4 → PDPT → PD → PT → Physical Page`
48비트 가상 주소 = 9+9+9+9+12 비트.
5단계 — 57비트 주소 공간 (Ice Lake 2021+)
대형 서버에 필요. 2024년 Linux 기본 설정으로 이동 중.
TLB (Translation Lookaside Buffer)
가상→물리 변환 캐시. 크기 수백 엔트리. **TLB miss가 성능의 숨은 살인자**.
**해결:**
- **Huge Pages (2MB, 1GB)** — 한 TLB 엔트리로 큰 영역 커버.
- **Transparent Huge Pages (THP)** — 리눅스가 자동 통합. 단, 지연 스파이크 원인이 되기도.
Memory Overcommit
Linux 기본: "요청한 메모리를 다 주는 척" → 실제 쓸 때 페이지 할당. 나중에 메모리 부족 → **OOM Killer** 발동.
echo 2 > /proc/sys/vm/overcommit_memory # strict accounting
Part 4 — cgroups + namespaces = 컨테이너
cgroups v2
리소스 그룹 계층. CPU, 메모리, I/O, PID 수 제한.
/sys/fs/cgroup/
my-app/
cpu.max # "50000 100000" → 50% CPU
memory.max # "1G"
io.max # "8:0 rbps=10485760" → 10MB/s read
namespaces
프로세스의 "자기만의 뷰".
- **mnt**: 파일시스템.
- **pid**: 프로세스 ID.
- **net**: 네트워크 인터페이스·라우팅.
- **ipc**: IPC.
- **uts**: hostname.
- **user**: UID/GID 매핑.
- **cgroup**: cgroup 뷰.
- **time**: 2020년 추가, 부팅 시간 가상화.
Docker의 본질
container = chroot + namespaces + cgroups + capabilities + seccomp + AppArmor/SELinux
**가상머신이 아니다.** 한 커널을 공유하며 "보여지는 영역"만 다르다.
rootless 컨테이너 (2020+)
**user namespace**로 루트 없이 컨테이너 실행. Podman, Buildah가 주도.
Part 5 — eBPF — 커널에 코드를 주입하기
eBPF란
**Extended BPF.** 커널 공간에서 실행되는 작은 VM. 샌드박스·검증기로 안전성 보장.
**왜 혁명인가:**
- 커널 수정 없이 기능 추가.
- 전통적으론 모듈 개발 필요, 재부팅 필요.
- eBPF는 실시간 로드, 언로드.
쓰이는 곳
- **관측성**: bpftrace, Parca, Pixie.
- **보안**: Tetragon, Falco.
- **네트워킹**: Cilium, Katran, XDP로 초고속 L4 LB.
- **프로파일링**: 연속 프로파일링(Parca, Polar Signals).
XDP (eXpress Data Path)
네트워크 카드에서 **NIC 드라이버 레벨**에서 eBPF 실행. 패킷이 커널 스택에 들어오기 전 처리 → DDoS 방어 등에 사용. **초당 수천만 패킷 처리 가능**.
개발 스택
- **bpftrace** — 한 줄 스크립트(awk 스타일).
- **libbpf + CO-RE(Compile Once Run Everywhere)** — C/Rust.
- **Aya** (Rust) — 2024년 성숙도 높아짐.
// bpftrace 예: open() syscall 추적
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s -> %s\n", comm, str(args->filename)); }'
Part 6 — NUMA — 32코어 이상에서의 숨은 비용
NUMA란
**Non-Uniform Memory Access.** 큰 서버는 여러 소켓(물리 CPU), 각 소켓이 자기 메모리 뱅크를 가짐. **다른 소켓의 메모리 접근은 1.5-3배 느리다.**
확인
numactl --hardware
node 0 cpus: 0-23
node 0 size: 96GB
node 1 cpus: 24-47
node 1 size: 96GB
node distances: 10 (local), 21 (remote)
NUMA 바인딩
프로세스를 NUMA 노드 0에만 실행
numactl --cpunodebind=0 --membind=0 ./my-app
**DB 서버, LLM 추론, 고성능 프록시**에서 필수. 기본값에 맡기면 스케줄러가 최적이 아닌 결정을 한다.
Kubernetes + NUMA
- **Topology Manager** (알파→베타) — Pod를 NUMA 경계에 맞춰 배치.
- **CPU Manager static policy** — 전용 코어 할당.
**LLM 추론 K8s 클러스터**에선 이 설정이 처리량 30%+ 차이.
Part 7 — Linux 스케줄러의 진화
CFS (Completely Fair Scheduler, 2007-2024)
- **가상 런타임(vruntime)** 으로 공정성 추구.
- **nice 값**으로 우선순위.
- **red-black tree**로 관리.
EEVDF (Earliest Eligible Virtual Deadline First, 2024)
**Linux 6.6**부터 기본. Peter Zijlstra 주도.
- CFS의 단점(지연 민감 태스크에 불리) 해결.
- **지연 허용치(slice)** 파라미터 도입.
- 미디어·게임·대화형 워크로드에 더 친화적.
CPU 격리 기법
- **isolcpus** — 지정 코어를 일반 스케줄링에서 제외.
- **nohz_full** — 타이머 인터럽트 제거.
- **RCU 콜백 오프로드** — 지정 코어를 방해 X.
**저지연 트레이딩·HFT 인프라**에선 표준.
Part 8 — GPU 드라이버와 LLM의 관계
GPU 컨테이너의 복잡성
Docker로 GPU 쓰려면:
- **NVIDIA Container Toolkit** — 호스트 드라이버를 컨테이너에 마운트.
- **NVIDIA MIG (Multi-Instance GPU)** — H100을 7개 인스턴스로 분할.
- **MPS (Multi-Process Service)** — GPU 공유.
UVM (Unified Virtual Memory, CUDA 6+)
CPU와 GPU가 **같은 주소 공간**을 공유. 페이지 폴트 시 필요한 페이지만 전송. LLM 훈련 시 **GPU 메모리보다 큰 모델**을 다룰 때 필수.
CUDA Graph
반복되는 커널 호출 패턴을 그래프로 캡처, 오버헤드 제거. **LLM 추론의 디코딩 루프**에서 20%+ 속도 개선 사례.
2024-2025 GPU OS 이슈
- **Fractional GPU sharing** (Run.ai, Aptakube) — 프로덕션 이슈 잦음.
- **NVIDIA DCGM + K8s 메트릭** — GPU 관측 표준.
- **AMD ROCm, Intel oneAPI** — CUDA 대체 성숙도 서서히 증가.
Part 9 — I/O 성능의 끝판왕
Zero-Copy
전통: `read() → buffer → write()` 각 단계 복사.
Zero-copy: `sendfile(), splice(), io_uring, MSG_ZEROCOPY` — 커널이 직접 DMA.
**실제 사례:** Kafka가 consumer에 데이터 보낼 때 `sendfile()` 사용 → CPU 사용률 절반으로.
DMA (Direct Memory Access)
CPU 관여 없이 디바이스가 메모리 직접 접근. 네트워크 카드, SSD, GPU 모두 DMA 엔진 내장.
RDMA (Remote DMA)
**다른 머신의 메모리에 CPU 관여 없이 직접 접근**. Infiniband, RoCE(RDMA over Converged Ethernet).
- 지연 시간 1μs 대.
- **HFT, HPC, 대형 데이터 웨어하우스**.
- **2024년 AI 훈련 클러스터의 기본** — NVIDIA GPUDirect RDMA.
DPDK / AF_XDP
커널 네트워크 스택을 **우회**해 사용자 공간에서 직접 NIC 다룸. 클라우드 가상 스위치, 5G UPF, 고성능 프록시(Cloudflare, Fastly).
Part 10 — WSL2, 컨테이너, 가상화의 교차점
WSL2 (Windows Subsystem for Linux 2)
- 실제로 **경량 Hyper-V VM** 안에서 리눅스 커널 실행.
- Windows 커널과 리눅스 커널 공존.
- 파일 성능: **WSL 네이티브 FS는 빠름, Windows /mnt/c는 느림**.
- 2024년 systemd 기본 지원 추가.
Firecracker (AWS Lambda 기반)
- 2018년 오픈소스화. microVM.
- 부팅 125ms 이하.
- KVM 기반, 120줄 미만의 VMM.
- **Fly.io, Kata Containers**도 사용.
gVisor (Google)
- 사용자 공간 커널 재구현.
- syscall을 사용자 공간 Sentry가 처리 → 호스트 커널 공격면 축소.
- 성능 오버헤드 있지만 격리 강력.
Kata Containers
- OCI 호환 컨테이너 런타임 + microVM.
- 컨테이너의 편리함 + VM의 격리.
- 멀티테넌트 K8s에서 유용.
Part 11 — 관측의 도구들
| 도구 | 용도 |
|---|---|
| **perf** | 하드웨어 + 소프트웨어 이벤트 |
| **ftrace** | 커널 함수 추적 |
| **bpftrace** | eBPF 한 줄 스크립트 |
| **bcc** | eBPF 도구 모음 (execsnoop, opensnoop 등) |
| **strace** | syscall 추적 (느림) |
| **ltrace** | 라이브러리 호출 추적 |
| **pmap** | 프로세스 메모리 맵 |
| **iotop / biolatency** | I/O 분석 |
| **perf top** | CPU 샘플링 |
| **flame graph** | 스택 시각화 (Brendan Gregg) |
Continuous Profiling
- **Parca, Pyroscope, Polar Signals**.
- eBPF 기반 상시 프로파일링 오버헤드 1% 미만.
- "P99이 느린데 CPU는 한가" 같은 수수께끼를 푼다.
Part 12 — OS 체크리스트 (12항목)
1. **ulimit 확인** — 프로덕션 서버의 FD 제한, nproc 제한.
2. **Swap 정책** — swappiness=1(DB) 또는 0(레이턴시 민감).
3. **Transparent Huge Pages** — DB는 대부분 끄는 게 낫다.
4. **NUMA 바인딩** — 소켓 2개 이상 시스템.
5. **io_uring 지원 확인** — 최신 커널에서 fd 한도 조정.
6. **cgroups v2 사용** — v1은 기능 제한.
7. **seccomp 프로필** — 컨테이너 syscall 제한.
8. **기본 TCP 파라미터 튜닝** — somaxconn, tcp_max_syn_backlog.
9. **커널 버전 확인** — 최신 LTS(6.6+)가 EEVDF, io_uring 성숙.
10. **eBPF 관측 인프라** — 프로파일링 도구 하나는 상시 가동.
11. **OOM Killer 로그 모니터링** — dmesg의 단서.
12. **CPU governor** — `performance` 모드 설정(서버).
Part 13 — 10대 안티패턴
1. **컨테이너를 VM으로 취급** — "커널 공유"를 잊으면 보안·성능 오해 발생.
2. **한 프로세스에 수만 스레드** — 컨텍스트 스위치 지옥. 코루틴이 답.
3. **블로킹 I/O로 대량 동시성** — 이벤트 루프 / 코루틴 / Virtual Thread.
4. **swappiness=60 (기본) 유지** — DB는 낮춰야 한다.
5. **대용량 RAM에서 THP 무시** — 지연 스파이크의 원인이 될 수 있다.
6. **NUMA 무시** — 큰 머신이라고 그냥 프로세스 하나 돌리면 절반 성능.
7. **syscall 잦은 앱에 strace 상시 가동** — 100배 느려짐. eBPF 쓰라.
8. **컨테이너에 루트로 실행** — rootless 또는 drop capabilities.
9. **Firecracker를 '일반 K8s 컨테이너'와 같이 취급** — 스토리지·네트워크 차이.
10. **GPU를 docker run에 그냥 연결** — Toolkit + 드라이버 버전 매트릭스 확인.
마치며 — OS는 여전히 '성능의 경계'
2025년 앱의 성능 상한은 종종 OS가 정한다. io_uring을 아느냐 모르느냐가 네트워크 서버의 처리량 2배 차이를 만들고, cgroups v2 설정이 컨테이너 OOM을 결정하고, NUMA 바인딩이 LLM 추론 비용을 좌우한다.
OS는 "내 앱보다 아래"가 아니라 **"내 앱의 일부"**다. 좋은 엔지니어는 필요한 순간에 이 경계를 넘을 수 있다. `/proc/`를 탐험하고, `perf top`을 돌리고, `bpftrace` 한 줄로 답을 얻는다.
학부 OS 수업에서 배운 것들(페이지 테이블, 스케줄러, 세마포어)은 여전히 살아있다. 다만 형태가 진화했다. 2025년의 OS는 단일 커널 + 수천 컨테이너 + GPU + RDMA가 공존하는 세계. 이 세계를 이해하는 것이 엔지니어의 새로운 기초다.
다음 글 예고 — "컴파일러와 현대 언어 런타임" — LLVM, JIT, GC, Inline Caching, Escape Analysis, WASM 런타임까지
OS 아래는 하드웨어, OS 위에는 런타임. 다음은 **언어가 어떻게 실행되는가**.
- **LLVM의 지배** — Rust, Swift, Julia, Zig, Crystal이 공유하는 뼈대
- **JIT 컴파일러 내부** — V8의 TurboFan, JVM의 C2, LuaJIT
- **Hidden Class와 Inline Caching** — V8이 JS를 빠르게 만드는 비밀
- **Escape Analysis** — 스택에 남길까 힙에 올릴까
- **Garbage Collector 계보** — Mark & Sweep부터 ZGC, Shenandoah, G1, Go의 3색 동시
- **Tiered Compilation** — V8 Ignition/Sparkplug/Maglev/TurboFan
- **Rust의 Monomorphization** — 제네릭이 왜 빠른가
- **Go의 work-stealing** — goroutine 스케줄러 내부
- **Python의 Specializing Adaptive Interpreter** — 3.13의 도약
- **WASM 런타임들** — Wasmtime, wasmer, WasmEdge 비교
"내 코드가 실행되기까지 무슨 일이 벌어지는가?" 다음 글에서.
현재 단락 (1/200)
"OS는 학부 때 배운 거 아냐?"라고 묻는다면, 대답: