- Published on
オペレーティングシステムの現代的理解 — io_uring、cgroups/namespaces、eBPF、NUMA、GPU UVM、EEVDF、Zero-Copy完全ガイド(2025)
- Authors

- Name
- Youngju Kim
- @fjvbn20031
なぜ今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が必要。
- 例: UNIXシェルのパイプ、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進化の歴史
- ブロッキングI/O —
read()がデータが来るまでブロック。 - select/poll — FDセットを全部スキャン。O(n)。
- epoll (Linux, 2002) / kqueue (BSD) — イベントベース、O(1)。
- aio_read(POSIX AIO) — 限界が多い。ほとんど使われません。
- 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モードならゼロ。
- Batching — 複数のI/Oを一度に提出。
- ファイル + ネットワーク + タイマー + acceptの統合。
- sendmsg、recvmsg、spliceなど大部分をサポート。
採用:
- Rust tokio(一部)、glommio、monoio。
- Node.js — 実験的。
- PostgreSQL 17(2024) — 非同期I/Oを基盤としてio_uringの導入を検討中。
- QEMU、RocksDB、ScyllaDB。
セキュリティの問題: 2023年にGoogleが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ミスは性能の隠れた殺し屋です。
解決策:
- Huge Pages (2MB, 1GB) — 1つのTLBエントリで広い領域をカバー。
- Transparent Huge Pages (THP) — Linuxが自動で統合します。ただし、遅延スパイクの原因になることもあります。
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
仮想マシンではありません。 1つのカーネルを共有し、「見える領域」だけが違います。
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コールバックのオフロード — 指定コアを妨げません。
低遅延トレーディング・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の中でLinuxカーネルを実行しています。
- WindowsカーネルとLinuxカーネルが共存します。
- ファイル性能: 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項目)
- ulimitの確認 — プロダクションサーバーのFD制限、nproc制限。
- Swapポリシー — swappiness=1(DB)または0(レイテンシに敏感)。
- Transparent Huge Pages — DBはほとんどの場合オフにしたほうが良いです。
- NUMAバインディング — ソケットが2つ以上のシステムで。
- io_uringのサポート確認 — 最新カーネルでfdの上限を調整。
- cgroups v2の使用 — v1は機能が限られます。
- seccompプロファイル — コンテナのsyscallを制限。
- 基本TCPパラメータのチューニング — somaxconn、tcp_max_syn_backlog。
- カーネルバージョンの確認 — 最新LTS(6.6+)はEEVDF、io_uringが成熟。
- eBPF観測インフラ — プロファイリングツールを1つは常時稼働。
- OOM Killerログの監視 — dmesgの手がかり。
- CPU governor —
performanceモードを設定(サーバー)。
Part 13 — 10大アンチパターン
- コンテナをVMとして扱う — 「カーネル共有」を忘れると、セキュリティ・性能の誤解が生じます。
- 1プロセスに数万スレッド — コンテキストスイッチ地獄。コルーチンが答えです。
- ブロッキングI/Oで大量の同時実行 — イベントループ / コルーチン / Virtual Thread。
- swappiness=60(デフォルト)を維持 — DBは下げるべきです。
- 大容量RAMでTHPを無視 — 遅延スパイクの原因になり得ます。
- NUMAを無視 — 大きなマシンだからとプロセス1つを回すだけでは性能が半分です。
- syscallの多いアプリでstraceを常時稼働 — 100倍遅くなります。eBPFを使ってください。
- コンテナをルートで実行 — rootlessまたはdrop capabilities。
- Firecrackerを「一般的なK8sコンテナ」と同じように扱う — ストレージ・ネットワークが違います。
- 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の比較
「自分のコードが実行されるまでに何が起きているのか?」次回の記事で。