3性能之巅:系统、企业与云可观测性(第2版)

操作系统

内核、系统调用与调度

系统调用上下文切换中断内核同步/proc

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第3章 操作系统

内核、系统调用与调度

章节导读

操作系统是应用与硬件之间的翻译官、调度员与守门人。性能工程师可以不懂内核源码,但必须懂内核的行为模型——因为应用感受到的每一次"莫名的慢",几乎都要穿过操作系统的某个机制:一次系统调用的陷入开销、一次上下文切换的缓存污染、一次缺页异常的磁盘往返、一次锁等待的调度让出。本章建立操作系统层的性能地图,它是后续 CPU(第6章)、内存(第7章)、存储(第8章)、网络(第9章)各章的地基。

先从内核架构说起。现代操作系统内核主要分两派:宏内核(monolithic kernel)把调度、内存管理、文件系统、网络栈、驱动全部跑在一个地址空间的内核态,Linux、Unix 属此派;微内核(microkernel)只在内核态保留最小的 IPC、调度与内存管理,驱动和文件系统作为用户态服务运行,QNX、seL4、L4 属此派。微内核的卖点是隔离性与可靠性(驱动崩溃不拖垮内核),代价是组件间通信要走 IPC,路径更长。1992 年 Tanenbaum 与 Linus Torvalds 关于宏内核 vs 微内核的著名论战,以 Linux 在服务器市场的胜利告终——性能与开发效率压倒了理论优雅。但历史有讽刺意味:今天 Linux 用户态的 eBPF、FUSE、io_uring、DPDK 都是在"把内核功能搬到用户态",某种意义上是微内核思想的回归。对性能分析者,关键认知是:Linux 内核是一个约 3000 万行代码的宏内核,应用与它的全部交互界面是约 400 个系统调用、/proc 与 /sys 伪文件系统、以及信号。

系统调用是用户态与内核态的边界。每次系统调用经历:用户程序执行 syscall 指令(x86-64)→ CPU 切换到内核态、保存用户寄存器上下文 → 内核根据系统调用号查表执行服务函数 → 返回用户态、恢复上下文。这条路径的固定开销在现代 CPU 上约 100-500ns(Spectre/Meltdown 缓解措施 KPTI 开启后,涉及页表切换,开销可达数百 ns 到 1μs)。单次看微不足道,但对系统调用密集的程序就是灾难:一个每秒做 100 万次 4 字节 write() 的程序,光陷入开销就消耗 0.1-1 个 CPU 核,还不含数据拷贝。这就是为什么高性能程序都追求"减少系统调用次数":缓冲 I/O(一次写 64KB 而不是一字节一次)、批量提交(io_uring 一次提交数百个 I/O 请求)、零拷贝(sendfile/splice 避免用户态-内核态数据搬运)。观测系统调用的工具谱系:strace(ptrace 机制,开销巨大)、perf trace(基于 tracepoint,开销中等)、bpftrace/BCC 的 syscount、sys-slower(eBPF,开销极低),第 4 章详述。

进程与线程是内核调度的基本实体。Linux 内核不区分进程与线程——两者都是 task_struct,线程只是共享地址空间的轻量任务(clone 系统调用创建)。调度器以任务为单位分配 CPU 时间。上下文切换分两类:进程间切换需要切换页表(TLB 刷新,除非有 PCID/ASID 优化)、切换内核栈,直接成本约 1-3μs,但真正的代价是间接的——新任务的缓存是冷的,L1/L2/L3 与 TLB 需要重新预热,对缓存敏感的程序,一次切换的"隐性成本"可达数十 μs。vmstat 的 cs 列显示每秒上下文切换总数,但要区分自愿切换(voluntary,任务等资源主动让出,/proc/PID/status 的 nonvoluntary_ctxt_switches 与 voluntary_ctxt_switches 分别计数)与非自愿切换(时间片耗尽被抢占)。自愿切换率高通常意味着锁争用或 I/O 等待;非自愿切换率高意味着 CPU 过载、 runnable 任务多于核数。

中断机制是硬件与内核的异步接口。网卡收到包、磁盘完成 I/O、时钟 tick 到点,都触发硬件中断(hardirq)。中断处理的原则是"快进快出":顶半部(top half)在内关中断状态下只做最紧急的事(确认硬件、把数据从设备搬到内存),其余工作推给底半部(bottom half)——softirq(软中断,如网络收发的 NET_RX/NET_TX、块设备的 BLOCK)、tasklet(基于 softirq 的串行化机制)、workqueue(可睡眠的内核工作线程)。性能视角的关键点:softirq 在哪个 CPU 上执行取决于中断亲和(/proc/interrupts 与 irqbalance),如果所有网卡队列的中断都落在 CPU0,你会看到 CPU0 的 %soft 100% 而其他核空闲——单核软中断饱和是网络性能的常见瓶颈(mpstat 的 %soft 列观测)。/proc/softirqs 给出每类软中断在各核上的计数。

内核同步原语决定了并发代码的性能上限。自旋锁(spinlock)让等待者忙等,适合临界区极短的场景,但持锁者被抢占时会造成其他核空转;互斥锁(mutex)让等待者睡眠,适合临界区较长的场景,代价是两次上下文切换;RCU(Read-Copy-Update)是 Linux 内核最精妙的发明之一:读者零开销(无锁无原子操作),写者复制-修改-替换并等待宽限期(grace period)后释放旧版本,读多写少的场景(路由表、文件描述符表)性能极佳。用户态对应 pthread_mutex、futex(快速路径用户态原子操作,争用时才陷入内核)。观测锁争用:perf lock、bpftrace 追踪 futex 系统调用、BCC 的 funclatency 测持锁函数延迟。锁争用的性能特征是:CPU 利用率不高但吞吐上不去,自愿上下文切换率高,off-CPU 时间集中在 futex_wait。

可观测性接口是本章的落脚点。/proc 是进程与系统信息的伪文件系统:/proc/PID/stat(进程 CPU 时间)、/proc/PID/status(内存、切换计数)、/proc/PID/wchan(当前阻塞在哪个内核函数)、/proc/stat、/proc/meminfo、/proc/interrupts、/proc/softirqs。/sys 暴露设备与内核对象模型:/sys/block/*/queue/(I/O 调度器、队列深度)、/sys/devices/system/cpu/(频率、拓扑)。perf_events 是统一的性能观测系统调用接口,聚合硬件计数器、软件事件、tracepoint、kprobe/uprobe。tracepoint 是内核开发者埋的静态追踪点(/sys/kernel/debug/tracing/events/ 下可浏览),稳定且有文档;kprobe 允许在几乎任意内核指令处动态插桩,灵活但无稳定性保证。这些接口构成了第 4 章全部工具的地基。理解了操作系统的工作模型,你就拥有了"穿透表象"的能力:应用说"慢",你能问出"是 on-CPU 还是 off-CPU?off-CPU 在哪个内核路径?是等锁、等 I/O 还是被调度延迟?"——这正是性能工程师与普通人看问题的分水岭。

核心概念

  • 系统调用
  • 上下文切换
  • 中断
  • 内核同步
  • /proc

要点精析

系统调用的真实成本需要拆解:陷入/返回的固定开销(模式切换、寄存器保存、KPTI 页表切换)约 100ns-1μs;参数检查与内核逻辑执行;数据拷贝(用户缓冲区到内核缓冲区,如 write() 的 copy_from_user,带宽约 10GB/s 量级,拷贝 1MB 需 100μs);以及可能的阻塞(read 无数据时睡眠)。Spectre/Meltdown 之后,KPTI(内核页表隔离)使每次系统调用增加 CR3 切换与 TLB 压力,某些场景系统调用开销翻倍以上,这也是为什么内核 5.x 时代 io_uring 等"减少陷入次数"的接口迅速流行。优化启示:批量系统调用(readv/writev、recvmmsg、io_uring)、增大缓冲(应用层 buffer 后一次 write)、零拷贝(sendfile、splice、MSG_ZEROCOPY)。测量方法:bpftrace 统计 sys_enter/sys_exit 差值可得每类系统调用的延迟分布,BCC 的 syscount -L 还能显示延迟排行。

上下文切换的成本模型:直接成本(保存/恢复寄存器、切换内核栈、进程切换时的页表切换与 TLB 影响)约 1-3μs;间接成本(缓存污染)可达 10-100μs,取决于工作集大小与切换频率。诊断时区分自愿/非自愿切换至关重要:/proc/PID/status 中 voluntary_ctxt_switches 高说明任务频繁等待资源(锁、I/O、sleep),nonvoluntary_ctxt_switches 高说明 CPU 竞争激烈、时间片被抢占。pidstat -w 1 按进程显示两类切换速率。经验阈值:单进程每秒自愿切换数千次以上值得警惕(可能是锁争用或过小的 I/O 粒度);系统总 cs(vmstat)在数万到十几万属正常繁忙,百万级则必有异常。上下文切换风暴的常见根因:线程数远超核数(线程池配置错误)、自旋锁退化为睡眠锁、频繁的短 sleep、cgroup CPU 配额限流(第 10 章)。

中断与软中断的性能分析要点。/proc/interrupts 显示每个硬件中断在各 CPU 上的累计次数,两列相减可得中断速率;如果某设备中断 99% 集中在一个核,检查中断亲和设置(/proc/irq/N/smp_affinity)与 irqbalance 服务。多队列网卡(RSS)应把不同队列的中断分散到不同核。软中断方面,mpstat 的 %soft 列显示每核软中断 CPU 占比,ksoftirqd 内核线程出现在 top 中且 CPU 高,说明软中断负载重到被推迟到内核线程处理——网络包速率(pps)过高是常见原因,sar -n DEV 的 rxpck/s 超过单核处理能力(约 50-100 万 pps,视包大小)时就会出现。NET_RX 软中断的预算由 net.core.netdev_budget 与 netdev_budget_usecs 控制,超出预算的包推迟到下轮,造成延迟。观测工具:/proc/softirqs 差分、bpftrace 追踪 softirq_entry。

内核同步原语的选择与观测。自旋锁:临界区内不能睡眠,等待者忙等消耗 CPU——特征是 %system 高但有效工作少,perf 中 native_queued_spin_lock_slowpath 成为热点(这是内核自旋锁争用的标志性符号)。互斥锁与信号量:等待者睡眠,特征是自愿上下文切换高、off-CPU 时间集中在 mutex_lock。RCU:读者完全无开销,写者承担复制与宽限期等待,适合读多写少;rcu_sched 相关的 stall 警告(dmesg 中 rcu_sched detected stalls)说明某 CPU 长时间未经历静止状态,通常是内核 bug 或长时间关中断。futex 是用户态锁的内核支撑:无争用时 pthread_mutex_lock 纯用户态完成(原子 CAS),有争用时陷入 futex 系统调用睡眠。观测用户态锁争用:perf lock(内核锁)、bpftrace 统计 sys_enter_futex 到 sys_exit_futex 的延迟、BCC 的 offcputime 看阻塞栈。锁争用的量化指标是"锁等待时间占总延迟的比例"。

/proc 与 /sys 的关键文件速查。/proc/PID/stat:第 14-17 字段是 utime/stime/cutime/cstime(用户态/内核态 CPU 时间),第 3 字段是状态(R 运行、S 可中断睡眠、D 不可中断睡眠——D 状态进程是 I/O 或内核锁等待的信号,大量 D 状态进程 + 高负载 = 存储或 NFS 问题);/proc/PID/wchan 显示进程当前阻塞的内核函数名,快速判断"在等什么"(futex_wait 等锁、io_schedule 等 I/O、ep_poll 等事件);/proc/PID/limits 显示资源限制;/proc/PID/io 显示该进程累计读写字节(read_bytes 是真实到达块层的量,rchar 是系统调用层面的量,两者差值反映页缓存命中);/proc/sys/ 即 sysctl 树,网络与 VM 调优参数全在这里;/sys/block/sda/queue/scheduler 显示与切换 I/O 调度器;/sys/kernel/debug/tracing/ 是 ftrace 的接口目录。熟练使用这些文件,你可以在没有任何额外工具的情况下完成大部分观测。

perf_events 子系统是现代 Linux 观测的统一底座,值得单独理解。它把四类事件源统一到一个系统调用(perf_event_open)下:硬件 PMU 事件(cycles、instructions、cache-misses、branch-misses,由 CPU 的性能监控单元计数,多路复用时有采样误差)、软件事件(context-switches、page-faults、cpu-clock,内核自己计数)、tracepoint(内核静态追踪点,如 sched:sched_switch、syscalls:sys_enter_*)、动态追踪(kprobe/uprobe,perf 通过 debugfs 接口挂载)。perf 命令行工具(stat/record/trace)与 BCC/bpftrace 底层都使用 perf_events 或其事件定义。理解这个架构的实际意义:当某工具报"event not supported"时,你能判断是虚拟机未暴露 PMU、内核未编译某选项(CONFIG_FTRACE)、还是权限问题(perf_event_paranoid 设置,/proc/sys/kernel/perf_event_paranoid 值为 2 时普通用户只能测用户态,-1 最宽松)。

tracepoint 与 kprobe 的对比是选择追踪工具的依据。tracepoint 是内核开发者在源码中预埋的静态点(约 2000 个),有 ABI 稳定性承诺(跨版本字段基本不变)、有文档(format 文件描述字段)、开销可控(未激活时近乎零,激活时每个事件约 0.5-1μs),代表点:sched:sched_switch(每次上下文切换)、sched:sched_wakeup(唤醒)、block:block_rq_issue/complete(块 I/O 发出/完成)、tcp:tcp_retransmit_skb(重传)、syscalls:sys_enter_*(全部系统调用)。kprobe 基于断点指令动态插桩,可挂在几乎任意内核函数入口(/proc/kallsyms 里的符号),灵活无比(能追踪没有 tracepoint 的内部函数),但无稳定性保证(函数名与参数随内核版本变化)、单事件开销略高(约 1-2μs,int3 断点陷入)。方法论:优先 tracepoint,无合适点时再用 kprobe;生产环境长期开启的观测只用 tracepoint。

进程调度器的行为模型(CFS 为主)。Linux 默认调度器 CFS(完全公平调度器,2.6.23 引入,6.6 起被 EEVDF 取代)的核心思想是追踪每个任务的"虚拟运行时间"(vruntime),总是挑选 vruntime 最小的任务运行,实现按权重(nice 值)比例分配 CPU。性能相关的可调参数:sched_min_granularity_ns(最小调度粒度,防止切换过频,默认约 2-4ms)与 sched_latency_ns(调度延迟目标,即所有 runnable 任务至少运行一次的周期)。当 runnable 任务数 × 最小粒度 > 调度延迟时,实际调度延迟会被拉长——线程数远超核数的应用会感受到明显的调度等待。观测调度延迟:BCC 的 runqlat(基于 sched_wakeup 到 sched_switch 的追踪,给出任务在运行队列中等待时间的直方图)、runqlen(队列长度采样)。调度延迟高但 CPU 利用率不高,是容器 CPU 限流或 cgroup 权重问题的信号(第 10 章)。实时调度类(SCHED_FIFO/RR)优先级高于所有普通任务,滥用会导致系统卡死(RT throttling 机制默认限制 RT 任务占 95%)。

用户态与内核态的内存与执行边界带来的性能含义。CPU 特权级(ring 0 内核态 / ring 3 用户态)决定了:用户程序不能直接访问硬件与内核内存,必须通过系统调用请求服务;内核代码的错误会拖垮整机(宏内核的代价)。性能上,模式切换本身(syscall/sysret 指令)只需几十 ns,但完整的系统调用路径包括栈切换、参数校验、KPTI 页表切换,实测 100ns-1μs。另一个常被忽视的边界成本是拷贝语义:传统 read/write 必须在用户缓冲区与内核缓冲区之间拷贝数据(页缓存),网络发送路径是"应用 buffer → 内核 socket buffer → 网卡 DMA"两次搬运。零拷贝技术(sendfile 让数据全程留在内核、splice 用管道在两个 fd 间转移页引用、io_uring 的注册缓冲区)消除这些拷贝,对大流量服务(视频、文件分发)可节省 30-50% 的 CPU。观测拷贝开销:perf 中 copy_user_enhanced_fast_string 等符号的占比。

内核模块、eBPF 与安全边界。传统内核模块(LKM)可以扩展内核功能但运行在 ring 0,一个指针错误就是整机 panic——这也是生产环境不敢随意加载模块的原因。eBPF 提供了革命性的替代:用户编写的程序经验证器(verifier)静态检查(无循环、内存访问受界、必然终止)后加载进内核,在 JIT 编译后以接近原生的速度执行,且崩溃风险被验证器排除。eBPF 程序可挂载到 kprobe、tracepoint、perf 事件、XDP 钩子、cgroup 等位置,能读内核数据结构、维护 map 状态、向用户态输出。BCC(Python + C 前端)与 bpftrace(awk 风格 DSL)是两大工具链。性能意义:过去需要写内核模块才能做的自定义观测(如"统计某个内核函数的参数分布"),现在一条 bpftrace 命令搞定,且生产环境安全。第 4 章将全面展开 eBPF 工具,本章先建立"内核可编程性"的概念:现代 Linux 内核正在变成一个可编程平台。

D 状态(不可中断睡眠)与负载均值的 Linux 特色。Linux 的负载均值(load average)统计的是 R 状态 + D 状态的任务数的指数移动平均,这与 Unix 传统(只统计 runnable)不同。后果:NFS 服务器挂了,客户端上所有访问 NFS 的进程进入 D 状态,负载均值飙到几百,但 CPU 完全空闲——"高负载低 CPU"的经典组合,指向 I/O 等待而非计算饱和。D 状态进程不可被信号打断(kill -9 也杀不掉,必须等内核路径返回),长时间 D 状态通常意味着存储故障、内核锁死锁或驱动 bug。诊断:ps -eo pid,stat,wchan,cmd | awk '$2 ~ /D/' 列出 D 状态进程及其阻塞的内核函数(wchan),如果 wchan 都指向同一函数(如 nfs_wait 或某个 mutex),根因即明。这个知识点串联了本章多个概念:进程状态机、wchan 观测、负载均值语义。

把操作系统知识转化为排障问题的清单。收到"应用慢"的报告,OS 层的标准问题序列:(1)CPU:mpstat 看 %usr/%sys/%soft/%steal 分布——%sys 高指向系统调用/锁/中断开销,%soft 高指向网络包处理,%steal 高指向虚拟化争抢;(2)调度:runqlat 看调度延迟分布,是否被 cgroup 限流(/sys/fs/cgroup/.../cpu.stat 的 nr_throttled);(3)系统调用:syscount 看调用频率与延迟排行,是否有异常高频调用;(4)上下文切换:pidstat -w 区分自愿/非自愿;(5)中断:/proc/interrupts 差分看中断分布是否均衡;(6)内存路径:/proc/PID/stat 的 minflt/majflt 看缺页速率,majflt 非零说明在从磁盘换入;(7)阻塞分析:/proc/PID/wchan 或 offcputime 看 off-CPU 的内核栈。这七步覆盖了 OS 层 90% 的性能问题入口,每一步都有明确的工具与判读标准,是第 2 章 USE 方法在操作系统层的细化。

代码示例

cat /proc/interrupts | head -5
/* 间隔 1 秒再看一次,差分得中断速率 */
watch -d -n 1 "grep -E 'eth0|nvme' /proc/interrupts"
cat /proc/softirqs
mpstat -P ALL 1 5
/* 关注 %irq 与 %soft 列 */
cat /proc/irq/24/smp_affinity_list
echo 2 > /proc/irq/24/smp_affinity_list
/* 把 24 号中断绑定到 CPU2 */
这组命令用于诊断中断分布问题。/proc/interrupts 每行是一个中断源,各列对应 CPU 核上的累计计数,第一列是中断号(IRQ),最后一列是设备名(如 eth0-TxRx-0 表示网卡第 0 个队列)。间隔采样做差分即得每秒中断数(irate 思路)。mpstat -P ALL 的 %irq 是硬中断 CPU 占比、%soft 是软中断占比——健康的多队列网卡应看到中断与软中断均匀分布在多个核上;如果 CPU0 的 %soft 高达 80% 而其他核接近 0,说明所有网络处理挤在一个核上,单核软中断饱和会成为吞吐瓶颈(典型症状:网卡带宽远未跑满但 pps 上不去、延迟抖动大)。修复方法是设置中断亲和:先找到设备对应的 IRQ 号(grep eth0 /proc/interrupts),把 smp_affinity_list 写成目标核号,多队列网卡应逐队列绑到不同核(并关闭 irqbalance 或配置其策略,避免它把你的设置改回去)。/proc/softirqs 显示各类软中断(HI、TIMER、NET_TX、NET_RX、BLOCK 等)在各核的计数,NET_RX 高是收包压力、BLOCK 高是块 I/O 完成处理压力。绑核后重跑 mpstat 验证分布改善,并用 sar -n DEV 确认吞吐是否提升。
pidstat -w -p $(pgrep -f myapp) 1
/* 自愿与非自愿上下文切换速率 */
cat /proc/$(pgrep -f myapp)/status | grep ctxt
vmstat 1 5
/* cs 列:系统总上下文切换 */
perf stat -e context-switches,cpu-migrations -p $(pgrep -f myapp) sleep 10
这组命令聚焦上下文切换分析。pidstat -w 输出 cswch/s(自愿切换,任务主动等待资源)与 nvcswch/s(非自愿切换,时间片耗尽被抢占),两者的比例直接提示问题方向:自愿切换占绝对多数且数值高(每秒数千以上),说明任务频繁阻塞——去查锁争用(futex)或 I/O 等待;非自愿切换高说明 runnable 任务多于 CPU 供给——去查 CPU 饱和、线程数配置或 cgroup 限流。/proc/PID/status 的 voluntary_ctxt_switches 是累计值,适合脚本差分。vmstat 的 cs 列是系统级总切换(含内核线程),配合 r 列(运行队列)判断:cs 高 + r 高 = CPU 竞争激烈;cs 高 + r 低 = 阻塞型切换为主。cpu-migrations 是任务被调度器迁移到其他核的次数,过高伤害缓存局部性(L1/L2 内容作废),对延迟敏感服务可用 taskset 或 sched_setaffinity 绑核减少迁移。经验案例:某 Java 服务 P99 抖动,pidstat -w 显示自愿切换每秒 2 万次,bpftrace 追踪发现集中在 futex——最终定位到日志库的同步锁,改异步日志后切换率降 90%、P99 稳定。
strace -c -f -p $(pgrep -f myapp)
/* 10 秒后 Ctrl-C,看系统调用统计表 */
perf trace -p $(pgrep -f myapp) -s
/* perf 版本的调用统计,开销远低于 strace */
/usr/share/bcc/tools/syscount -P -p $(pgrep -f myapp)
/* eBPF 版:按进程统计调用次数与延迟 */
/usr/share/bcc/tools/sys-slower 10
/* 显示耗时超过 10ms 的系统调用 */
这四个工具构成系统调用观测的"开销梯度",展示方法论中"用最低开销工具回答问题"的原则。strace -c 统计调用次数、总耗时、错误数,信息全面但 ptrace 机制开销巨大(可使目标慢 10-100 倍),只适合开发环境或短时诊断。perf trace -s 基于 syscalls tracepoint,输出类似的汇总统计,开销低一个数量级,是生产环境的替代品。BCC syscount 用 eBPF 挂载 tracepoint,-P 按进程聚合,还能加 -L 按延迟排序,开销与 perf trace 相当但聚合更灵活。sys-slower 是"异常猎手":只输出超过阈值的慢调用(含进程名、调用类型、延迟、时间戳),适合长时间挂着等待偶发问题——比如你怀疑"偶发的 500ms 延迟来自某个慢系统调用",sys-slower 100(100ms 阈值)挂一小时,抓到就是铁证。典型发现模式:read 慢 → 存储或页缓存问题;futex 慢 → 锁争用;connect 慢 → 网络或对端 backlog;write 慢 → 缓冲区满或磁盘饱和。注意 syscount/sys-slower 需要内核 headers 与 BCC 环境,新系统可用 libbpf-tools 的等价物。
ps -eo pid,stat,wchan:32,comm | awk '$2 ~ /D/'
cat /proc/PID/stack
/* 需要 root,显示进程的内核栈 */
/usr/share/bcc/tools/offcputime -p PID 10
/* off-CPU 时间按阻塞栈聚合,10 秒 */
这组命令是"任务在等什么"的钻取工具链。第一步 ps 过滤 STAT 含 D 的进程(不可中断睡眠),wchan 列直接给出阻塞点的内核函数名:io_schedule 表示等块 I/O、nfs_wait_* 等 NFS、rwsem/mutex 相关函数等内核锁、wait_on_page_bit 等页回写或读入。大量进程 wchan 相同即锁定共同瓶颈。/proc/PID/stack 给出完整内核调用栈(比 wchan 单函数更精确),需要 root,对 R 状态进程显示的是当前执行点。offcputime 是终极武器:它用 eBPF 追踪 sched_switch,把任务每次离开 CPU 到重新上 CPU 之间的"off-CPU 时间"按阻塞时的用户栈+内核栈聚合,输出火焰图式的报告——宽度代表等待时间。适用场景:CPU 利用率低但延迟高(时间都花在等待上)、锁争用分析(栈顶是 futex_wait 及其调用者)、I/O 等待归因(栈中含 io_schedule 与文件系统路径)。方法论提示:on-CPU 分析(perf record)回答"CPU 时间花在哪",off-CPU 分析回答"等待时间花在哪",两者合起来才是完整的延迟分解,Gregg 称之为"全时间分析"。
sysctl -a 2>/dev/null | grep -E "somaxconn|tcp_max_syn_backlog|swappiness|dirty_ratio|file-max"
ulimit -n
cat /proc/sys/kernel/perf_event_paranoid
cat /sys/kernel/debug/tracing/available_events | wc -l
cat /sys/kernel/debug/tracing/events/sched/sched_switch/format
这组命令检查内核观测与调优的"静态配置面"。sysctl 部分列出性能排障最常涉及的参数:net.core.somaxconn(accept 队列上限,默认 128/4096 视版本,高并发服务常需调大且应用 listen() 的 backlog 参数要同步调大,取两者较小值生效)、net.ipv4.tcp_max_syn_backlog(半连接队列)、vm.swappiness(0-200,控制回收时匿名页与文件页的倾向,数据库服务器常设 1-10)、vm.dirty_ratio/dirty_background_ratio(脏页占内存比例阈值,触发同步/异步回写,大内存机器默认值会导致脏页堆积数 GB、回写风暴,常按绝对字节数改用 dirty_bytes)、fs.file-max(系统级 fd 上限)。ulimit -n 是进程级 fd 限制(软限制),"Too many open files"错误的根源,systemd 服务要用 LimitNOFILE 配置。perf_event_paranoid 决定非 root 用户能用 perf/eBPF 做什么:2(多数发行版默认)只允许用户态测量,生产排障常临时调为 1 或 -1。最后两行展示 tracepoint 基础设施:available_events 列出内核全部静态追踪点(通常上千个),format 文件描述某事件的字段——写 bpftrace/perf 脚本前查 format 是标准动作。

实验笔记

实验三:透视操作系统——亲手测量系统调用、上下文切换与调度的成本。

实验目标:(1)用实验数据建立对"边界成本"的量化直觉(系统调用多少 ns、上下文切换多少 μs);(2)掌握中断/软中断分布的诊断与调整;(3)用 off-CPU 分析定位一个阻塞型性能问题。

环境准备:Linux 虚拟机(2 核以上),安装 gcc、perf、bpftrace、BCC tools、sysstat。需要 root 权限(部分实验涉及 /proc/irq 与 perf_event_paranoid)。先把 sysctl kernel.perf_event_paranoid 设为 -1 以便非 root 使用 perf。

步骤一(测量系统调用开销):编写 C 程序 syscall_bench.c:循环 100 万次调用 getpid()(最简单的系统调用),用 clock_gettime(CLOCK_MONOTONIC) 计时,输出每次调用的平均纳秒数。再写一个对照循环:100 万次调用一个空的普通函数。两者差值即系统调用的陷入开销。预期结果:空函数约 1-2ns,getpid 约 100-500ns(取决于 CPU 与 KPTI 状态)。进阶:用 syscall(SYS_getpid) 绕过 glibc 缓存直接陷入;对比 echo 0/1 > /sys/devices/system/cpu/vulnerabilities/meltdown 相关缓解开关前后的差异(虚拟机中可能不可控,记录即可)。思考:为什么高性能库都强调批量系统调用?用你的实测数字计算:每秒 100 万次 4KB write 的陷入开销占一个核的百分之几?

步骤二(测量上下文切换成本):编写两个进程用管道(pipe)互相 ping-pong:A 写 1 字节到管道,B 读到后写回,往返一次含两次上下文切换。测量 10 万次往返的总时间,除以 20 万次切换得单次切换成本。预期:1-5μs 量级。同时用 vmstat 1 观察 cs 列验证切换速率与你的计算一致。进阶:用 taskset 把两个进程绑到同一核与不同核,对比结果并解释(同核切换无需跨核缓存同步但无法并行;跨核切换有缓存行迁移成本)。

步骤三(中断亲和实验):cat /proc/interrupts 记录初始分布;用 ping -f 或 iperf3 制造网络流量(虚拟机可用宿主机对打,或对 loopback 压测);再次差分 /proc/interrupts 与 mpstat -P ALL 1,观察中断与 %soft 集中在哪个核。如果集中,手动把该 IRQ 的 smp_affinity_list 改到另一个核,验证流量处理随之迁移。记录修改前后相同流量下的 CPU 分布与吞吐(iperf3 带宽或 ping 延迟分布)。思考:为什么多队列网卡 + RSS + 每队列绑核是高网络性能的标准配置?

步骤四(off-CPU 分析实战):编写一个"有问题"的 Python 脚本 slow_app.py:起 4 个线程,每个线程循环执行"查询一个被全局 threading.Lock 保护的字典 + time.sleep(0.001) 持锁",人为制造锁争用。运行它,先观察:pidstat -w 显示自愿切换极高、CPU 利用率却不高。然后用 offcputime -p PID 10 采集,输出中应看到 futex_wait 相关的栈占据绝大部分 off-CPU 时间,且用户态栈指向你的 lock 获取代码。把锁的粒度改小(每线程独立字典)后重测,对比 off-CPU 报告的变化。这个练习完整走了一遍"现象(低 CPU 高延迟)→ 指标(自愿切换)→ 归因(off-CPU 栈)→ 修复 → 验证"的闭环。

步骤五(调度延迟观测):用 stress-ng --cpu 0 制造 CPU 饱和,同时运行 /usr/share/bcc/tools/runqlat 10 1 采集调度延迟直方图。空载时延迟应在微秒级,饱和时出现毫秒级长尾。再运行 runqlen 观察队列长度。思考:为什么"CPU 利用率 80%"的系统可能已经出现明显的调度延迟?(提示:排队论的非线性,以及利用率是时间平均而调度延迟是任务视角。)

预期结果:一份实验报告,包含系统调用与上下文切换的实测数字、中断分布调整前后的对比、off-CPU 火焰图两张(修复前后)、调度延迟直方图两张(空载/饱和)。

进阶挑战:(1)用 bpftrace 写一个统计 read 系统调用延迟分布的脚本(tracepoint:syscalls:sys_exit_read,@ = hist(args->ret)),对页缓存命中与磁盘读分别测量;(2)阅读 /proc/PID/stack 与内核文档 Documentation/trace/kprobes.rst,用 bpftrace 的 kprobe 追踪一个你感兴趣的内部内核函数;(3)制造 D 状态:挂载一个 NFS 到虚拟机后杀掉 NFS 服务端进程,访问挂载点使进程进入 D 状态,练习用 ps + wchan 诊断,并解释为什么 kill -9 无效。

延伸阅读

  • 《Systems Performance》第 2 版第 3 章 Operating Systems 原文。原书以更学术的严谨度覆盖内核架构、系统调用、中断、进程/线程模型、虚拟内存与文件系统的 OS 层机制,并给出 Solaris 与 Linux 的对照。特别推荐其中"翻译问题"(lost in translation)一节:应用语义如何在内核层丢失(内核看到的是 fd 与页,不知道那是"一次用户登录"),这是理解观测局限性的关键。
  • Daniel P. Bovet & Marco Cesati《Understanding the Linux Kernel》或 Robert Love《Linux Kernel Development》(《Linux 内核设计与实现》)。前者是百科全书式的内核机制参考(进程调度、中断、同步、内存管理章节与本章对应),后者更薄更易读,适合建立整体框架。读内核书的目的不是写内核,而是让 wchan、/proc/PID/stack、offcputime 输出中的每个符号都有意义。
  • 《深入理解计算机系统》(CSAPP)第 8 章异常控制流与第 10 章系统级 I/O。CSAPP 从程序员视角讲解进程、线程、系统调用与 I/O 的"用户侧契约",与本章的"内核侧实现"互为表里。特别是 8.1 节异常分类(中断/陷阱/故障/终止)精确对应本章的 hardirq、syscall、page fault、signal 机制。
  • Linux 内核文档:Documentation/admin-guide/sysctl/(全部 sysctl 参数语义)、Documentation/trace/(ftrace、kprobes、tracepoint 官方文档)、Documentation/accounting/psi.rst。这些一手文档是判读指标的权威依据,比如 dirty_ratio 的精确语义、perf_event_paranoid 各级别允许的操作,网上博客常有讹传,以内核文档为准。
  • Brendan Gregg《Linux Performance》讲义与 linuxperf.html 全景图(https://www.brendangregg.com/linuxperf.html)。其中按"内核子系统 × 工具"组织的图表与本章知识结构完全对应,可作为本章的可视化复习提纲。另推荐他的演讲《Performance Checklists for Linux》(YouTube),把方法论与 OS 工具串成实战流程。
  • man 手册:man 2 syscalls(系统调用总表)、man 5 proc(/proc 文件系统的权威文档,字段级说明)、man 7 sched(调度策略)、man perf_event_open。man 5 proc 尤其值得通读——/proc/PID/stat 的 52 个字段、/proc/meminfo 的每个计数器的精确含义都在这里,是写采集脚本时避免误读的最后防线。
  • LWN.net 的调度器与 eBPF 专题文章,特别是关于 EEVDF 调度器(6.6 内核取代 CFS)的系列报道,以及 Ingo Molnar 的 CFS 设计文档( Documentation/scheduler/sched-design-CFS.rst)。理解调度器演进(O(1) → CFS → EEVDF)有助于判读不同内核版本上的调度行为差异,sched_min_granularity 等参数在 EEVDF 下已不存在,排障时要先确认内核版本。
3/13操作系统
0%