Skip to content
Published on

操作系统的现代理解 — io_uring、cgroups/namespaces、eBPF、NUMA、GPU UVM、EEVDF、Zero-Copy 完全指南(2025)

分享
Authors

为什么现在要重新学 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 演进的历史

  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 提交多个请求
// ... 稍后
io_uring_wait_cqe(&ring, &cqe);

优点:

  • syscall 开销大幅下降 — SQPOLL 模式下为零。
  • Batching — 一次提交多个 I/O。
  • 文件 + 网络 + 定时器 + accept的统一。
  • sendmsg、recvmsg、splice 等大部分都支持。

采用情况:

  • Rust tokio(部分)、glommiomonoio
  • 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内核函数追踪
bpftraceeBPF 单行脚本
bcceBPF 工具集(execsnoop、opensnoop 等)
stracesyscall 追踪(慢)
ltrace库调用追踪
pmap进程内存映射
iotop / biolatencyI/O 分析
perf topCPU 采样
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 绑定 — 两个 socket 以上的系统。
  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 — 十大反模式

  1. 把容器当成 VM — 忘了"共享内核",安全和性能上就会产生误解。
  2. 一个进程里开几万个线程 — 上下文切换地狱。协程才是答案。
  3. 用阻塞 I/O 做大量并发 — 事件循环 / 协程 / Virtual Thread。
  4. 保持 swappiness=60(默认值) — DB 必须调低。
  5. 在大内存机器上无视 THP — 可能成为延迟毛刺的原因。
  6. 无视 NUMA — 机器大就只跑一个进程,结果只有一半性能。
  7. 对 syscall 频繁的应用常开 strace — 慢 100 倍。请用 eBPF。
  8. 以 root 运行容器 — 用 rootless 或 drop capabilities。
  9. 把 Firecracker 当成"普通 K8s 容器"对待 — 存储和网络都不一样。
  10. 把 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 的比较

"我的代码在被执行之前,到底发生了什么?"下一篇见。