LabHub

블로그

운영체제의 현대적 이해 — io_uring, cgroups/namespaces, eBPF, NUMA, GPU UVM, EEVDF, Zero-Copy 완벽 가이드 (2025)

한국어English日本語中文

왜 지금 OS를 다시 배우는가

"OS는 학부 때 배운 거 아냐?"라고 묻는다면, 대답:

2025년 엔지니어가 OS를 모르면, 왜 자기 앱이 느린지, 왜 컨테이너가 OOM을 맞는지, 왜 CPU 100%인데 처리량이 안 오르는지 설명할 수 없다.

Part 1 — 프로세스, 스레드, 코루틴 — 현대의 비교

프로세스

스레드

코루틴 / Fiber / Green Thread

Java Virtual Threads — Project Loom (2023)

JDK 21 LTS. "기존 스레드 코드를 거의 그대로 두고 수백만 동시 요청 처리."

전통 스레드: 스레드당 OS 스택 1MB+ → 수만 개가 한계. 가상 스레드: JVM 내부 스케줄링, 필요할 때만 스택 할당 → 수백만 가능.

블로킹 I/O를 쓰면서도 논블로킹의 동시성을 얻는다. 2024-2025년 Spring Boot 채택으로 Java 백엔드 지형이 바뀌는 중.

Go goroutine

언제 무엇을?

상황선택
CPU 바운드 병렬스레드 + 공유 메모리
I/O 바운드 대량 동시코루틴/async
보안 격리 강력 필요프로세스 + IPC
JVM에서 수백만 동시Virtual Threads

Part 2 — io_uring — epoll을 넘어서

I/O 진화의 역사

  1. 블로킹 I/Oread()가 데이터 올 때까지 블록.
  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);

이점:

채택:

보안 이슈: 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가 성능의 숨은 살인자.

해결:

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

프로세스의 "자기만의 뷰".

Docker의 본질

container = chroot + namespaces + cgroups + capabilities + seccomp + AppArmor/SELinux

가상머신이 아니다. 한 커널을 공유하며 "보여지는 영역"만 다르다.

rootless 컨테이너 (2020+)

user namespace로 루트 없이 컨테이너 실행. Podman, Buildah가 주도.

Part 5 — eBPF — 커널에 코드를 주입하기

eBPF란

Extended BPF. 커널 공간에서 실행되는 작은 VM. 샌드박스·검증기로 안전성 보장.

왜 혁명인가:

쓰이는 곳

XDP (eXpress Data Path)

네트워크 카드에서 NIC 드라이버 레벨에서 eBPF 실행. 패킷이 커널 스택에 들어오기 전 처리 → DDoS 방어 등에 사용. 초당 수천만 패킷 처리 가능.

개발 스택

// 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

LLM 추론 K8s 클러스터에선 이 설정이 처리량 30%+ 차이.

Part 7 — Linux 스케줄러의 진화

CFS (Completely Fair Scheduler, 2007-2024)

EEVDF (Earliest Eligible Virtual Deadline First, 2024)

Linux 6.6부터 기본. Peter Zijlstra 주도.

CPU 격리 기법

저지연 트레이딩·HFT 인프라에선 표준.

Part 8 — GPU 드라이버와 LLM의 관계

GPU 컨테이너의 복잡성

Docker로 GPU 쓰려면:

UVM (Unified Virtual Memory, CUDA 6+)

CPU와 GPU가 같은 주소 공간을 공유. 페이지 폴트 시 필요한 페이지만 전송. LLM 훈련 시 GPU 메모리보다 큰 모델을 다룰 때 필수.

CUDA Graph

반복되는 커널 호출 패턴을 그래프로 캡처, 오버헤드 제거. LLM 추론의 디코딩 루프에서 20%+ 속도 개선 사례.

2024-2025 GPU OS 이슈

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).

DPDK / AF_XDP

커널 네트워크 스택을 우회해 사용자 공간에서 직접 NIC 다룸. 클라우드 가상 스위치, 5G UPF, 고성능 프록시(Cloudflare, Fastly).

Part 10 — WSL2, 컨테이너, 가상화의 교차점

WSL2 (Windows Subsystem for Linux 2)

Firecracker (AWS Lambda 기반)

gVisor (Google)

Kata Containers

Part 11 — 관측의 도구들

도구용도
perf하드웨어 + 소프트웨어 이벤트
ftrace커널 함수 추적
bpftraceeBPF 한 줄 스크립트
bcceBPF 도구 모음 (execsnoop, opensnoop 등)
stracesyscall 추적 (느림)
ltrace라이브러리 호출 추적
pmap프로세스 메모리 맵
iotop / biolatencyI/O 분석
perf topCPU 샘플링
flame graph스택 시각화 (Brendan Gregg)

Continuous Profiling

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 governorperformance 모드 설정(서버).

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 위에는 런타임. 다음은 언어가 어떻게 실행되는가.

"내 코드가 실행되기까지 무슨 일이 벌어지는가?" 다음 글에서.

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다