# 第 6 章实验:一次系统调用、一次上下文切换,到底要多久? 原书第 6 章的作业(Homework (Measurement)):测量系统调用和上下文切换的开销。 ```bash cd labs/06-lde make # 编译 timer syscall ctxsw(-O2) make run # 依次运行全部测量 ``` ## 1. `./timer` —— 先认识你的计时器 连续两次调用 `gettimeofday()`,绝大多数时候读数**完全相同** —— 它只精确到微秒,而一次系统调用只要几百纳秒。 所以原书说:要测一个很短的操作,必须**重复成千上万次**,再用总时间除以次数。 `clock_gettime(CLOCK_MONOTONIC)` 精确到纳秒,x86 上还可以用 `rdtsc` 直接读 CPU 周期计数器。 ## 2. `./syscall [次数]` —— 系统调用的开销 - **普通函数调用**:几纳秒。 - **`clock_gettime`**:几十纳秒。它**不是真的系统调用**:Linux 把它放在映射到用户空间的 vDSO 里完成,不需要陷入内核。 - **`getppid()`、`read(fd, buf, 0)`**:几百纳秒。这才是真正的 trap → 内核 → return-from-trap 往返, 比普通函数调用贵上百倍:要保存/恢复寄存器、切换特权级、查系统调用表、检查参数,还有现代 CPU 的安全缓解措施(如页表隔离)带来的额外成本。 ## 3. `./ctxsw [来回次数] [CPU编号]` —— 上下文切换的开销 仿照 lmbench:两个进程用两根管道打乒乓。一方 `write` 后立刻 `read` 另一根管道而阻塞, 操作系统只好把它从 CPU 上换下、换上另一方 —— 每个来回恰好 2 次上下文切换。 - 第二个参数是 CPU 编号,程序用 `sched_setaffinity()` 把父子进程都绑到这个 CPU 上(默认 0);传 `-1` 表示不绑定。 多核机器上如果不绑定,两个进程可能在两个 CPU 上各跑各的,测到的就不是"在同一个 CPU 上停下一个、恢复另一个"的成本了。 - 程序还在单个进程里测了 4 次管道读写(不会阻塞、没有切换)作为基线,`(来回 - 基线) / 2` 即单次切换的估计值。 这个估计里还包含了调度器选下一个进程、唤醒等开销,以及切换后缓存/TLB 变"冷"的间接代价。 ## 示例输出(WSL2 上的一次运行;你的机器上数字会不同,同一台机器多跑几次也会波动) ``` $ ./syscall 普通函数调用 : 3.9 ns/次 clock_gettime (vDSO) : 24.1 ns/次 ← 看起来像系统调用,其实没进内核 getppid() 系统调用 : 298.8 ns/次 read(fd,buf,0) 系统调用 : 456.8 ns/次 (重复 2000000 次) $ ./ctxsw 200000 0 绑定 CPU : 父子进程都绑在 CPU 0 上 来回次数 : 200000 每个来回 : 22.70 us(2 次切换 + 4 次管道读写) 管道读写基线 : 4.63 us(同一进程内 4 次管道读写,无切换) 估算单次上下文切换: 9.03 us = (来回 - 基线) / 2 ``` 原书(1996 年,200 MHz P6,Linux 1.3.37):系统调用约 4 µs,上下文切换约 6 µs;书中说现代系统可以做到亚微秒级。 WSL2 运行在 Hyper-V 虚拟机里,系统调用和切换都要额外付出虚拟化与安全缓解措施的代价,所以数字比裸机 Linux 大。