内核态与用户态:特权级与系统调用机制
CPU 通过特权级把指令分成"危险的"与"安全的"两档:应用跑在非特权的用户态(user mode),需要执行特权操作时经系统调用(system call)陷入内核态(kernel mode),由内核代理完成。
一句话定义
CPU 通过特权级把指令分成"危险的"与"安全的"两档:应用跑在非特权的用户态(user mode),需要执行特权操作时经系统调用(system call)陷入内核态(kernel mode),由内核代理完成。
为什么重要
系统调用是用户程序与内核之间的全部契约,是理解任何系统行为的入口:进程创建(kp-007)、文件读写(kp-021)、I/O 模型(kp-028)的语义都以它定义。同时每次系统调用都有特权级切换的固定开销,是性能观测(kp-031)中"系统时间"的主要来源。
前置知识
kp-001 的分层图;kp-002 的中断机制(系统调用的实现复用了中断基础设施)。
核心概念
- 特权级(privilege level):x86-64 用 Ring 0/3 区分内核态与用户态;用户态执行特权指令(如操作页表、关中断)会触发异常。
- 陷入(trap):从用户态进入内核态的动作,系统调用、缺页、除零都走这条通道。
- 系统调用号与系统调用表:每个系统调用一个编号,内核查表分发。
- vDSO:内核把个别只读调用(如 gettimeofday)映射进用户空间,免于真正的陷入。
- libc 包装:
glibc把寄存器级的 syscall 指令包装成 C 函数。
原理与机制
以 x86-64 的 syscall 指令为例,一次系统调用的完整流程:
用户程序 内核
放置系统调用号到 rax
放置参数到 rdi/rsi/rdx...
执行 syscall 指令
-------------------------> 切换到内核态(硬件完成)
保存用户寄存器现场
查系统调用表[rax]
执行对应服务例程
恢复用户现场
<------------------------ sysret 返回用户态
取得返回值 (rax)
关键点:特权级切换由硬件一次完成(比老式软中断快),但保存/恢复现场、参数校验、切换后的缓存与分支预测扰动,使一次系统调用仍是微秒以下的"重"操作——通常几十到几百纳秒,比函数调用慢 2~3 个数量级。这也是内核引入 vDSO、io_uring(kp-028)等"减少陷入次数"设计的动机。
直观类比
用户态像银行的客户,内核态像柜台内部。客户不能自己翻账本(特权指令),必须填单子(系统调用号+参数)递进窗口,柜员核对后代办并递回结果。单子本身不贵,但排队和交接有固定成本。
实例或案例
可复现实验(Linux,需安装 strace;macOS 用 dtruss 需关闭 SIP,建议在虚拟机中练习):
strace -c ls # 统计 ls 的系统调用分布:openat/read/write/mmap...
strace -e trace=openat,read,write cat /etc/hostname # 过滤关注的三类调用
strace -T echo hello # 显示每次调用的耗时
strace -c ls 输出里 mmap、mprotect 出现次数远多于直觉,正好引出 kp-019 的惰性分配主题;write 的次数说明缓冲策略(kp-027)。
常见误区
- 把库函数当系统调用:
printf、malloc都不是,真正进入内核的是write、mmap(brk)。 - 认为系统调用一定比普通函数"只慢一点点":相差两个数量级,高频小 I/O 的性能问题常源于此。
- 认为 getcwd、gettimeofday 必然陷入内核:vDSO 使部分调用完全不发生特权级切换。
与其他知识点的关系
kp-007 的 fork/exec/wait 是一组具体系统调用;kp-031 用 strace 作为观测第一工具;kp-028 的 io_uring 本质是"绕过每次调用都陷入"的架构升级。
延伸阅读
《Linux 内核设计与实现》第 5 章"系统调用";man 2 syscall。
自测题
- 系统调用与函数调用的本质区别是什么?
答:函数调用在用户态同一特权级内完成;系统调用引发硬件特权级切换进入内核,由内核代为执行特权操作。
- 为什么 gettimeofday 可以不陷入内核?
答:内核通过 vDSO 把时钟数据以只读方式映射进用户空间,用户态直接读取即可,无需特权操作。
- 观察
strace -c ls,为什么 mmap 出现很多次?
答:动态链接器与 libc 用 mmap 映射共享库,内核也可能用它分配匿名内存,这与 kp-019 的惰性分配一致。