- 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 shell 管道、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。"几乎不动现有的线程代码,就能处理数百万并发请求。"
传统线程: 每个线程 1MB+ 的 OS 栈 → 几万个就是极限。 虚拟线程: 在 JVM 内部调度,只在需要时分配栈 → 数百万也可以。
写着阻塞 I/O,拿到的却是非阻塞的并发能力。随着 2024-2025 年 Spring Boot 的采用,Java 后端的地貌正在改变。
Go goroutine
- M:N 调度(M 个内核线程 : N 个协程)。
- 用 channel 通信。
- 栈从初始的 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 提交多个请求
// ... 稍后
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 — 虚拟内存的现代
四级页表 (x86_64)
CR3 → PML4 → PDPT → PD → PT → Physical Page
48 位虚拟地址 = 9+9+9+9+12 位。
五级 — 57 位地址空间 (Ice Lake 2021+)
大型服务器需要。2024 年正在往 Linux 默认配置迁移。
TLB (Translation Lookaside Buffer)
虚拟→物理转换的缓存。规模是数百个条目。TLB miss 是性能的隐形杀手。
解决办法:
- Huge Pages (2MB, 1GB) — 一个 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
它不是虚拟机。 它共享同一个内核,只是"能看见的范围"不同。
rootless 容器 (2020+)
用 user namespace 在没有 root 的情况下运行容器。由 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. 大型服务器有多个 socket(物理 CPU),每个 socket 都有自己的内存 bank。访问另一个 socket 的内存要慢 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(alpha→beta) — 让 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
绕开内核网络栈,在用户空间直接操作网卡。云上的虚拟交换机、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 绑定 — 两个 socket 以上的系统。
- 确认 io_uring 支持 — 在新内核上调整 fd 上限。
- 使用 cgroups v2 — v1 功能受限。
- seccomp 配置 — 限制容器的 syscall。
- 基础 TCP 参数调优 — somaxconn、tcp_max_syn_backlog。
- 确认内核版本 — 最新 LTS(6.6+)的 EEVDF、io_uring 更成熟。
- eBPF 观测基础设施 — 至少保持一个剖析工具常态运行。
- 监控 OOM Killer 日志 — 线索在 dmesg 里。
- CPU governor — 设成
performance模式(服务器)。
Part 13 — 十大反模式
- 把容器当成 VM — 忘了"共享内核",安全和性能上就会产生误解。
- 一个进程里开几万个线程 — 上下文切换地狱。协程才是答案。
- 用阻塞 I/O 做大量并发 — 事件循环 / 协程 / Virtual Thread。
- 保持 swappiness=60(默认值) — DB 必须调低。
- 在大内存机器上无视 THP — 可能成为延迟毛刺的原因。
- 无视 NUMA — 机器大就只跑一个进程,结果只有一半性能。
- 对 syscall 频繁的应用常开 strace — 慢 100 倍。请用 eBPF。
- 以 root 运行容器 — 用 rootless 或 drop capabilities。
- 把 Firecracker 当成"普通 K8s 容器"对待 — 存储和网络都不一样。
- 把 GPU 直接挂到 docker run 上 — 要核对 Toolkit + 驱动的版本矩阵。
结语 — OS 依然是"性能的边界"
2025 年应用的性能上限,往往是由 OS 决定的。懂不懂 io_uring 会让网络服务器的吞吐差两倍,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 的三色并发
- Tiered Compilation — V8 Ignition/Sparkplug/Maglev/TurboFan
- Rust 的 Monomorphization — 泛型为什么快
- Go 的 work-stealing — goroutine 调度器内部
- Python 的 Specializing Adaptive Interpreter — 3.13 的飞跃
- 各种 WASM 运行时 — Wasmtime、wasmer、WasmEdge 的比较
"我的代码在被执行之前,到底发生了什么?"下一篇见。