Week 2:System Call / Trap(高密度版)
先统一术语
- Trap:用户态执行触发受控转移到内核态(syscall、异常、中断统称入口机制)。
- System Call:有意触发的 trap,用于请求内核服务。
- Exception/Fault:执行异常(如 page fault)导致的 trap,语义不同于主动 syscall。
核心不是“怎么进内核”,而是:跨 boundary 的固定成本 + 可变成本分别是什么。
一次 syscall 的成本分解
可抽象为:
- 用户态准备参数(寄存器/栈)
- 特权级切换(保存必要上下文)
- 进入内核入口路径(校验、分发)
- 执行内核逻辑(可能阻塞、可能唤醒)
- 返回用户态(恢复上下文,继续执行)
固定成本来源:
- 模式切换和控制流重定向
- 安全检查(参数、权限、地址合法性)
- pipeline/BTB 预测状态扰动(微架构相关)
可变成本来源:
- 内核路径长度(VFS、网络栈、锁竞争)
- copy_to/from_user
- 睡眠唤醒与调度
为什么“少 syscall”常常成立
不是因为 syscall “慢一点”,而是它破坏了热路径连续性:
- 用户态计算被频繁切断,指令局部性下降。
- 频繁 copy 和边界检查拉高 cycles/req。
- 高并发下,进入内核后更容易遇到共享资源竞争。
因此经验法则:
batch > 单次调用mmap/共享内存 > 频繁 read/writeio_uring/async(当适配场景)> 同步小调用风暴
Trap 与页故障的关系
page fault 也是 trap,但语义是“执行无法继续,需内核修复现场”:
- 缺页分配(anonymous demand paging)
- COW 复制
- 权限错误(非法访问 -> 信号/终止)
所以 week1 与 week2 不是两章独立知识,而是同一条边界的两种触发方式:
- syscall:主动跨边界
- fault:被动跨边界
接口设计视角:如何“少而稳”
批处理(Batching)
- 合并多个小请求,摊薄固定切换成本。
- 代价:更复杂的错误处理与重试语义。
零拷贝路径
- 避免 user<->kernel 多次拷贝,降低带宽与 cache 污染。
- 代价:生命周期管理复杂(buffer pinning、引用计数、回收时机)。
共享内存 + 门铃机制
- 数据面在共享区,控制面用少量 syscall 唤醒。
- 常见于低延迟撮合、行情分发、日志管线。
观测框架(先证据后优化)
你要能把“慢”拆成三段:
- 边界切换是否过密(calls/sec)
- 单次内核路径是否过长(avg cycles / tail latency)
- 是否有阻塞扩散(run queue 增长、上下文切换激增)
实践中常见指标组合:
- syscall 频率分布(而非总量)
- p50/p99 syscall latency
- voluntary/involuntary context switch
- 内核热点函数火焰图
高频误判
- “把函数内逻辑优化 20%,系统就会快很多。”
-> 如果瓶颈在边界切换,用户态微优化收益很有限。 - “syscall 都一样慢。”
-> 错。路径深度、锁、拷贝、阻塞行为差异巨大。 - “异步一定更快。”
-> 错。队列化、回调/状态机复杂度和 cache 行为可能反噬。
低延迟系统的落地 checklist
- 合并可合并的调用,控制 syscall burst。
- 热路径避免动态分配和可阻塞系统调用。
- 明确区分数据面与控制面接口。
- 把边界成本预算写进 SLA(不是事后调参)。