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。"几乎不动现有的线程代码,就能处理数百万并发请求。"

传统线程: 每个线程 1MB+ 的 OS 栈 → 几万个就是极限。 虚拟线程: 在 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 提交多个请求
// ... 稍后
io_uring_wait_cqe(&ring, &cqe);

优点:

采用情况:

安全问题: 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 是性能的隐形杀手

解决办法:

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 在没有 root 的情况下运行容器。由 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. 大型服务器有多个 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

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

绕开内核网络栈,在用户空间直接操作网卡。云上的虚拟交换机、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 绑定 — 两个 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 上面是运行时。接下来讲语言是怎么被执行的

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

评论

还没有评论。

登录后即可发表评论