1 内核机制课程要覆盖什么
在 shell 中执行 cat /etc/hostname,你看到的是几行文本;在机器繁忙时执行同样的命令,却可能要等更久。命令没有改变,差异可能来自文件数据是否已在内存、进程是否获得 CPU、底层设备是否及时完成工作。本课要建立的直觉是:用户程序提出请求,内核检查请求、组织资源访问,并在条件满足后交还结果。理解这些连接点,才能把“机器卡了”变成可以检验的判断。
Linux 内核负责管理进程、内存、文件访问和网络通信等公共资源。用户进程通常不能随意访问其他进程的内存,也不能直接控制硬件;它通过受约束的接口提出要求。shell 本身也是用户进程,执行外部命令时会创建或启动其他进程,重定向则把文件描述符接到文件或管道上。命令行里的简短操作,往往已经穿过多个内核机制。

本课围绕系统调用、执行上下文、延后工作和观测证据展开。系统调用回答“程序怎样请求内核服务”;中断回答“外部事件怎样引起处理”;软中断与工作队列回答“哪些工作稍后做、在哪里做”。这些概念彼此关联,但不能互相代替。进程正在系统调用中等待数据,并不意味着 CPU 正在不停执行这个请求;设备产生中断,也不意味着某个应用已经拿到了数据。
学习时可以沿着 3 个问题观察每段行为:谁触发了它,当前在哪里执行,它能否等待。再补上接口返回了什么、失败时证据在哪里,就能形成适合日常运维的解释框架。这里不展开协议栈内部算法、设备驱动实现或复杂调度策略。目标是读懂接口和故障现象,为后续 I/O 模型准备语言,而不是凭几条命令修改生产内核。
例如,本机文件读取慢和远端连接读取慢,都可能表现为进程停在 read,但等待对象不同。前者可能涉及存储访问,后者可能在等对端发送数据。只看命令名称或 CPU 利用率无法确定原因。合理起点是记录命令、输入、时间范围和返回结果,再逐层补充证据。机制知识的价值,在于帮助选择接下来要观察什么。
2 系统调用与用户/内核边界
系统调用是用户程序请求内核服务的受控入口。程序调用 openat 打开路径,调用 read 读取字节,调用 write 写出内容,调用 close 释放描述符。源代码里看到的函数通常先经过用户空间的库封装;函数名与实际系统调用不总是完全对应,也不是每个库函数都会进入内核。例如字符串长度计算通常只访问用户内存。分析时应区分程序接口和实际发生的内核入口。
跨过边界后,内核需要检查参数及对象状态。文件描述符只是进程用于引用已打开对象的整数,不是可任意操作设备的权限凭证。无效描述符可能导致 EBADF,路径访问受限可能导致 EACCES,不存在的路径可能导致 ENOENT。这些错误代表不同的失败阶段。把所有失败统一解释成“内核异常”,会丢掉最有用的线索。
读取成功也不保证返回请求的全部长度。请求读取 4096 字节,实际可能只得到 200 字节,调用者应使用实际返回值继续处理。普通文件读到末尾返回 0;管道在数据读尽且所有写端关闭后也可返回 0。对于写端仍打开、当前无数据的管道,阻塞读取可能等待,非阻塞读取则可能返回 EAGAIN。解释返回值必须带上对象类型和状态。

等待与执行应分开理解。线程进入阻塞读取后,内核可以把它放入等待状态,让其他可运行线程使用 CPU。条件变化后,该线程被唤醒,接下来仍需等待运行机会,并重新检查条件。唤醒并不是数据永久归它所有的承诺,也不是调用已经完成的通知。多个等待者、信号和对象状态变化,都可能影响后续结果。
可以用 strace -e trace=openat,read,write,close cat /etc/hostname 初步观察边界。输出中除了目标文件,还可能出现库或运行环境使用的文件;应按路径和描述符关联记录。strace 展示的是跟踪到的系统调用及结果,不会自动解释内核内部发生了什么。它会带来额外开销,因此被跟踪程序的运行时间不能直接当作无观测时的性能基线。
同理,网络程序的 read 返回慢,不足以证明网络栈处理慢。经过时间可能包含等待对端、等待调度和实际处理。排查时先确认是否收到错误、是否为正常等待,再决定是否深入内核证据。把返回值、对象状态与时间放在同张记录里,比只截取“耗时很长”的行更有解释力。
3 中断、软中断与工作队列
系统调用通常由正在运行的线程主动发起,中断则可以由设备事件触发。设备报告工作完成或新数据到达时,CPU 需要及时响应。传统硬中断处理路径通常只完成必要确认、保存状态和安排后续工作,避免长时间占用处理机会。这里的“及时”不是要求把全部任务立即做完,而是要求把紧迫部分和可延后部分分开。
延后工作常被统称为“下半部”,但这个称呼不说明具体执行上下文。普通软中断用于处理某些需要延后的工作,例如网络相关处理;它仍受到不能随意睡眠的约束。负载较高时可能看到 ksoftirqd/N 线程参与处理,不能因此把软中断回调理解成可以任意阻塞的普通线程函数。机制的语义比进程列表中的名称更可靠。
普通工作队列把工作交给内核工作线程执行,通常具有允许睡眠的进程上下文,适合某些可阻塞的后续任务。不过“可以睡眠”仍取决于当前持锁状态和所调用接口的约束。持有自旋锁等原子约束时,即使身处线程上下文,也不能随便调用可能睡眠的操作。本课中的工作队列均指普通工作队列,不把其他变体混入入门规则。

“睡眠”在这里指等待条件或资源时让出执行机会,不是特指执行 sleep 命令。等待互斥锁、某些内存分配和某些 I/O 操作,都可能发生睡眠。判断依据应是接口契约及调用时上下文,不能因为测试中操作很快,就认定它永远不会阻塞。低负载只让错误不容易暴露,并没有改变约束。
可以用 cat /proc/interrupts 观察中断计数,用 cat /proc/softirqs 观察各 CPU 的软中断累计计数。观察同样时间间隔内的增量,才适合讨论活动分布;开机以来累计值更大,不等于此刻压力更大。容器里的视图可能受限制,也可能反映宿主机范围,使用前要确认观测边界。这些计数都不能单独给出某个请求的延迟。
批量处理还存在取舍:每轮做得更多,可以减少反复安排工作的成本,却可能让其他任务等得更久;每轮做得更少,有利于更频繁地交还执行机会,但可能增加调度和排队成本。下面的滑块是简化模型,帮助区分总完成时间与最长连续处理时间;它不是建议修改某个内核参数。
4 动手:用跟踪点观察一次 syscall 路径
本实验在可恢复的 Linux 虚拟机上进行,使用内核跟踪点记录 read 的进入与退出。跟踪点是内核在特定位置提供的观测事件。我们只观察系统调用边界,不把事件对当作完整的函数调用链。实验需要系统允许性能事件记录,且安装了匹配环境的 perf。遇到权限或功能缺失时,应记录限制,不要为了完成实验随意降低整机安全配置。
先执行 uname -r 和 perf --version,保存内核与工具版本。随后检查常见 tracefs 挂载位置:mount | grep tracefs。常见路径是 /sys/kernel/tracing,也可能通过 /sys/kernel/debug/tracing 访问。只读检查不要求你改写全局跟踪开关;若系统没有挂载或没有开放访问,请使用预先配置好的实验环境。
在已确认的 tracefs 路径下检查 events/syscalls/sys_enter_read/format 与 events/syscalls/sys_exit_read/format。例如 sudo cat /sys/kernel/tracing/events/syscalls/sys_enter_read/format 可以显示该事件实际提供的字段。事件名称和可用字段取决于内核配置与架构;文件不存在就停止沿用下面的命令,先确认当前环境支持哪些事件。不要根据其他机器的截图猜测本机格式。

在可写的实验目录中,先确认 s26-read.data 不是需要保留的已有记录,再运行下面的命令。它将事件范围限制在指定命令及其相关执行范围;本例命令本身不创建额外子进程。不要改成无筛选的全系统长时间采集。
sudo perf record -o s26-read.data \
-e syscalls:sys_enter_read \
-e syscalls:sys_exit_read \
-- cat /etc/hostname
sudo perf script -i s26-read.data
读取输出时先找同个线程的进入与退出事件,再核对时间和返回值。进入事件通常包含描述符、缓冲区地址和请求长度,退出事件包含返回值;以本机格式文件为准。正返回值通常表示实际字节数,0 的意义需要结合对象状态,负返回值可能对应错误。程序启动阶段也可能读取其他文件,不能把每次 read 都认作对 /etc/hostname 的读取。
若要确定描述符对应哪个对象,可以另做受限的 strace 观察 openat 与 close,但应把两次运行明确区分。不同运行中的描述符编号与时间不一定相同,不能拼成同次请求的证据。需要严格关联时,应在同次采集中补充适当事件;这超出本次最小实验的要求。先学会承认观测范围,比画出未经支持的完整路径更重要。
进入与退出的时间差是这次系统调用的经过时间,可能包含睡眠、抢占和等待运行,并非纯 CPU 执行时间。若事件缺失、采集报告丢失或无法配对,就不能据此计算可靠耗时。结束后保存命令、版本、事件范围和限制,停止记录。小文件实验适合认识事件结构,不足以推断繁忙网络服务的性能。
5 内核模块与稳定接口边界
内核模块是可以按系统支持方式加载的内核代码,用于提供某些可选功能。它运行在内核环境中,其错误可能影响整台机器。模块与普通命令不同:普通进程退出往往只结束自身,而模块的并发访问、对象寿命和卸载条件可能影响其他进程。因此,认识模块首先是认识变更边界,而不是急着执行加载和卸载。
可以用 uname -r 确认正在运行的内核,用 lsmod 查看已加载模块,用 modinfo 模块名 查看模块文件提供的信息。这些是观察入口。modinfo 显示模块存在,并不代表它已加载;模块出现在列表中,也不证明目标请求实际经过它。某项功能还可能直接编入内核,不以独立模块出现在 lsmod 中。应把构建方式、加载状态和运行证据分开记录。
Linux 对用户空间可见接口保持兼容性有长期承诺,常用系统调用属于重要边界,但新功能是否可用仍取决于内核版本和配置。内核内部函数、结构和模块接口则不能假设跨版本保持不变。针对某个版本构建的外部模块,不应仅凭文件名相似就复制到另一台机器使用。内核版本、配置、架构和模块构建信息都可能影响兼容性。
/proc、/sys、系统调用和跟踪点也不是同种接口。某些文件提供状态快照,某些文件允许修改运行参数,跟踪点则用于事件观测。写入参数可能立即影响整机或特定命名空间的行为;读取成功不代表可以放心写入。使用时查清该接口的文档、单位、作用范围与版本要求,不把某次实验中的字段或数值当成永久规范。
模块实验应使用可恢复环境,并先明确依赖、使用者和回退方法。正在使用的文件系统、网络功能或设备相关模块,不适合为了“验证是否有用”直接卸载。即使模块引用计数看起来很低,也不能替代对依赖关系的理解。入门阶段能够解释模块提供什么功能、依赖哪个版本、从哪里取得错误证据,就已经具备了有用的操作边界。
实际报告可以写成:“内核版本为已记录版本,模块构建与之匹配,加载日志无报错,目标功能测试通过。”不要只写“模块加载成功”。前者把兼容性、运行状态和功能结果分别交代,便于后续故障回溯。至于内部符号怎样导出、驱动怎样注册,本课暂不展开。
6 常见坑:在错误上下文睡眠、滥用全局锁
第 1 类常见错误,是把可能阻塞的操作放进不允许睡眠的路径。开发者可能复用某个辅助函数,因为它平时完成得很快,却没有注意它内部可能等待锁或资源。在普通进程路径测试通过后,再把它放到软中断处理路径,就可能触发上下文违规。判断正确性需要检查最坏情况下允许发生什么,不能只看平均耗时。
某些调试配置会报告类似原子上下文睡眠的警告,但没有警告并不等于安全。警告是否出现受内核配置、代码路径是否触发等因素影响。遇到这类信息时,应保存完整日志、内核版本、触发负载和调用位置,确认当时的上下文及持锁状态。仅截取最后几行,往往会丢失识别问题所需的前置线索。
第 2 类错误,是用全局锁包住过多工作。锁保护共享状态,不能凭空消除等待成本。当多个 CPU 或线程都要访问同个锁时,原本可并行的路径可能排队。如果持锁期间还做慢操作,等待会进一步放大。吞吐下降可能来自竞争,尾延迟增长也可能来自少数特别长的临界区;平均 CPU 利用率无法区分这些情况。
互斥锁与自旋锁不能只按“哪个更快”选择。互斥锁的等待可能睡眠,自旋锁的使用带来不能睡眠等约束;选择取决于执行上下文和保护对象。本课不讲锁实现,先记住:需要保护哪些状态、谁会并发访问、临界区是否可能阻塞、获取多个锁的顺序是否一致。这些问题比凭经验替换锁类型更接近正确性。
修正时通常先缩小临界区,只在必要范围内更新共享状态,再把可独立完成的工作移开。但移动工作也会改变执行时间和对象寿命:原调用者返回后,后续工作引用的对象是否仍有效,取消与关闭是否等待清理完成,都需要明确。把问题转移到工作队列,不会自动解决资源释放与并发访问。
排查报告应区分事实与假设。事实可以是某段时间锁等待增多、某类工作积压、日志出现上下文警告;假设才是这些现象导致请求变慢。保持负载条件,改变明确因素,再观察结果,才能增强因果判断。反复更换锁、线程数和预算,只得到“似乎快了”的结果,通常无法形成可维护的修复。
7 案例:错误的下半部导致延迟尖刺
设想某台实验主机接收持续网络流量,团队加入了接收后的统计功能。低流量时业务正常,突发流量出现后,接收处理的 P99 延迟升高,并偶尔出现相关内核警告。团队最初怀疑链路不稳定。这里需要先确认测量区间、接收负载和丢包情况,避免把所有超时都归入内核,也避免仅凭 CPU 平均值排除主机问题。
检查改动后发现,新增辅助操作从软中断路径调用,内部存在可能睡眠的步骤;它还把共享统计更新与后续处理包在同个全局锁内。前者违反执行上下文要求,后者可能扩大竞争范围。即使延迟尖刺与改动时间相邻,也需要分别证明违规路径可达、竞争实际发生,不能把两个猜测直接合并成结论。

团队在隔离环境重放相同流量,保存警告上下文,记录软中断计数增量、CPU 分布和相关等待证据。软中断计数只能说明活动变化,不能单独证明哪个辅助函数执行过;代码审查和针对性跟踪补充了这部分证据。若观测工具本身引入明显负担,应缩小采集范围,并保留无采集负载结果作为对照。
修正方案把紧迫且不能阻塞的处理留在原路径,把确实需要等待的后续工作交给合适的普通工作队列。共享锁只覆盖必要的统计更新。同时明确队列容量、对象引用和停止清理过程,避免突发流量把等待变成无限积压。工作队列使执行上下文适合可阻塞操作,但其处理能力仍有限,不能承担没有边界的输入。
验收时保持输入速率、突发形态、CPU 配置和测试时长一致,比较修正前后的延迟分位数、吞吐、丢包或错误、队列积压与资源使用。若 P99 降低却丢弃了更多任务,不能直接宣布成功。若警告消失但工作队列持续增长,也只是把风险移到了后面。正确结果必须同时满足功能与性能要求。
这个案例提供的是排查方法,不是“网络慢就换工作队列”的处方。实际尖刺还可能来自用户进程未及时读取、对端停顿或其他资源竞争。先建立时间关联,再检查上下文与等待对象,最后通过受控对照验收改动,才能把机制知识变成可信结论。
8 把内核改动的验收写进清单
验收应在改动前确定。先固定内核版本、配置、硬件或虚拟资源、相关模块和负载生成方式,再记录基线。基线至少包括完成数量、错误类型、延迟分位数和资源使用;涉及队列时还要记录积压是否随时间增长。只有“改完以后很快”而没有可比基线,无法证明收益,也难以发现被牺牲的能力。
延迟数据需要说明起止点。用户请求发出到收到结果、系统调用进入到退出、某个工作项入队到完成,是不同指标。测量窗口也要覆盖稳定负载与突发阶段。P99 需要足够样本,短时间内只有少量请求时,分位数很容易受偶发值影响;应报告样本量和测试条件,而不是只留下漂亮数字。

功能清单应覆盖成功与失败路径。例如读取可能短读,资源不足可能失败,进程可能提前退出,队列可能在关闭时仍有工作。涉及模块或延后工作时,还要确认启动、停止和重复操作能正确完成。测试关注接口承诺与资源回收,不必为了“深入”而覆盖尚未理解的所有内核细节,但不能只验证最顺利的路径。
可以为实验预先约定门槛:在固定负载下 P99 不超过基线的 1.1 倍,目标吞吐不下降,错误率不增加,积压能在突发结束后回落。这里的 1.1 是教学示例,真实门槛由业务约束和测量波动决定。判定必须使用同套方法,也应注明跟踪是否开启,避免把观测开销误当成改动代价。
回退清单要能执行:保留已验证内核与模块的获取方式,确认启动入口,说明谁负责恢复、需要多长时间,以及回退后检查哪些指标。生产变更还需要按组织流程推进。出现新增警告、持续积压或错误率升高时,应触发预设处置,而不是在现场无限延长观察。内核相关变更影响面较大,清楚的停止条件本身就是验收的一部分。
9 从内核机制走向 I/O 模型(S27)
到这里,可以把“请求在哪里等”作为连接下一课的主线。系统调用提供进入内核的边界,阻塞调用可能使线程等待;非阻塞调用在条件不满足时可以立即返回特定错误,由程序决定何时再试。两者都需要内核检查和维护对象状态,区别并不在于是否经过内核。
继续考虑 1 个进程同时处理多个连接:如果它在某个连接上阻塞读取,当前线程就可能暂时无法处理其他任务。把描述符设为非阻塞后,程序也不能无休止地循环读取,否则可能消耗 CPU 却没有推进工作。于是需要讨论如何等待就绪、如何组织任务,以及通知到达后怎样处理实际返回值。相关接口留到 S27 展开。
中断通知、I/O 就绪与操作完成也应保持区分。设备事件可能推动内核更新状态;应用得知某个对象可以尝试读写,并不保证读写满足全部请求。并发访问和状态变化仍可能使操作返回较少数据或再次提示稍后重试。只有明确接口语义,才能判断应用应该继续处理、等待还是报告错误。
下一课还会比较同步与异步的组织方式。本课暂不把“异步”简单等同于“后台有线程”,也不提前展开实现细节。带走 3 个问题即可:谁在等待,等待什么条件,由谁通知继续。再把执行上下文、返回值和测量边界补上,I/O 模型就会从术语比较变成可观察的程序行为。
放进方案里
在主机接收处理优化方案中,先写清问题边界:固定内核版本和网络负载下,接收完成延迟出现尖刺,目标是在保持吞吐与错误率要求的同时降低尾延迟。测量起点与终点、负载来源、样本量和突发持续时间都应列明。把用户等待、系统调用经过时间和内核工作处理时间分别命名,避免用不同口径比较结果。
机制说明应交代执行上下文:原路径从哪里触发,哪些步骤不能睡眠,哪些后续操作可能阻塞。候选方案把可阻塞工作放到合适的普通工作队列,并说明队列容量、过载处理、共享锁范围和对象寿命。若依据仍是代码审查假设,应明确标注,安排最小观测补充证据,不把推测写成已确认根因。
观测计划限定事件、目标进程或相关范围以及采集时长,保存内核与工具版本、返回值、警告日志和积压变化。先确认事件可用、权限合规与记录是否丢失,再解释数据。对照测试保留相同输入与资源配置,同时报告延迟分位数、吞吐、错误和 CPU 使用;跟踪开启与关闭的结果分开记录。
验收条款使用预设门槛,并覆盖短读、资源不足、突发结束、停止清理等路径。若延迟降低却错误增加,或工作积压无法回落,候选方案不通过。发布顺序从隔离实验到小范围验证,负责人和观察窗口写进执行安排,避免改动完成后才临时寻找判断标准。
最后写明回退触发条件、已验证版本的恢复方式及恢复后的复核指标。归档基线、候选结果和未解决限制,让后续人员知道结论适用于什么环境。进入 S27 的设计讨论时,再据此选择应用侧等待方式,确保内核处理、应用重试与容量约束能够共同满足目标。
10 小结:下一步 I/O 模型(S27)
系统调用连接用户请求与内核服务,中断及延后机制组织事件处理,执行上下文决定哪些操作允许等待。排查需要把返回值、经过时间与等待对象连起来,再用受控对照确认改动效果。
自测时尝试解释:为什么 syscall 时间长不等于 CPU 执行久?为什么看到 ksoftirqd 不能推断软中断允许睡眠?为什么把任务移到工作队列后仍要限制积压?如果能回答这些问题,就可以带着“谁等待、等什么、谁通知”的框架进入 S27,比较阻塞、非阻塞与异步 I/O 的程序组织方式。