一、引子:系统调用到底是怎么"调进去"的
- 操作系统在软件设计上只提供两样东西:
- 系统调用号:每个系统调用有一个唯一下标(如
__NR_open = 5) - 陷入机制:
int 0x80(32 位)/syscall(64 位),并为 0x80 号中断注册了固定的处理方法
- 系统调用号:每个系统调用有一个唯一下标(如
- 我们调的
open/fork/read其实是glibc 的封装函数:- 追踪 glibc 2.9 源码:
open→_IO_new_file_open→ … →INLINE_SYSCALL宏 - 宏里做的事:把系统调用号放进 EAX 寄存器 → 执行
int 0x80/syscall
- 追踪 glibc 2.9 源码:
- 陷入内核后,0x80 的中断处理方法拿着 EAX 里的系统调用号,去查系统调用表(函数指针数组),执行对应内核函数
💡 结论:OS 只提供"系统调用号 + 陷入机制",上层接口全是 C 库封装的。所以才有"C 生万物"——几乎所有语言 runtime 最终都站在 glibc 之上。
二、重谈地址空间:0-3GB 用户区,3-4GB 内核区
进程的整个地址空间(4GB)被切成两段:
| 区间 | 名称 | 里面装的是什么 |
|---|---|---|
| 0 ~ 3GB | 用户区 | 本进程自己的代码、数据、堆、栈等(个性化内容) |
| 3 ~ 4GB | 内核区 | 操作系统的代码和数据(全系统共享的内容) |
对应的页表也分两套:
- 用户级页表:每个进程一份
- 每个进程都有自己独立的 0-3GB,各自映射到自己的代码和数据
- 进程之间互不干扰,随便你怎么折腾
- 内核页表:全系统只有一份
- 所有进程的 3-4GB 都通过同一份内核页表映射到同一个操作系统
- 无论系统里有 10 个还是 1000 个进程在调度,每一个进程都能在自己的地址空间里看到完整的 OS
这套设计意味着什么
✔任何一个进程都能找到操作系统。只要从自己的代码区跳转到 3-4GB 区域,就能访问到内核的所有代码和数据——与"当前是哪个进程在跑"无关。
✔CPU 再也不怕中断了。上节课说"进程跑着跑着来个中断,CPU 就跑过去执行中断向量表里的方法"——为什么能做到?因为不管 CPU 在执行哪个进程,中断来了之后,都可以通过当前进程地址空间里的 3-4GB 内核映射找到操作系统的一切。
三、矛盾出现了:用户能不能直接访问 3-4GB?
既然 OS 就映射在每个进程的 3-4GB,那我在代码里捏造一个 3-4GB 的地址,解引用、跳转过去,不就能随便读写内核了?
✖不行。操作系统不相信任何人。
- OS 虽然把内核映射进了你的地址空间,但禁止你以"直接访问地址"的方式碰它
- 想使用操作系统,必须走系统调用:系统调用号入 EAX →
int 0x80/syscall陷入内核 → 查系统调用表 - 这是唯一的固定路径,其他一切动作都会被操作系统拦截
💡 所以问题的关键变成:CPU 怎么知道"当前执行的是用户代码还是内核代码"?—— 这就引出了用户态和内核态。
四、正式定义:用户态与内核态
- 用户态(User Mode):CPU 当前特权级别为3,进程以"用户身份"运行,只允许访问 0-3GB的用户区,无权访问 3-4GB 的内核区
- 内核态(Kernel Mode):CPU 当前特权级别为0,进程以"内核身份"运行,允许通过系统调用访问 3-4GB的内核区
用一句话说:
用户态 = 我只能跑我自己的代码;内核态 = 我被允许(以系统调用的固定方式)执行操作系统的代码。
五、硬件实现:CS 段寄存器的低 2 位 —— CPL
"当前是用户态还是内核态"不是软件约定,而是CPU 硬件层面的状态:
- CPU 的CS 段寄存器低 2 位表示当前特权级别,称为CPL(Current Privilege Level)
CPL = 0→ 内核态CPL = 3→ 用户态
- 平时跑用户代码时,CPL = 3
陷入内核时发生了什么
执行int 0x80/syscall的瞬间,CPU 硬件自动完成两件事:
- CS 转而指向操作系统对应的代码段
- CPL 由 3 改为 0—— 这就叫"陷入内核"
陷入之后就能为所欲为了吗
✖ 不能。即使 CPL 已经是 0,OS 依旧禁止你以地址方式直接访问内核的任意代码和数据:
- 陷入后执行的是 0x80 绑定的固定处理历程
- 你必须提供系统调用号,走"系统调用号 + 查系统调用表"这一条路
- 其他路径全被封死,一切越权动作都会被拦截
💡 这就是 OS 的自保逻辑:让你进来,但只让你走我给你铺好的那一条道。
六、总结
| 对比项 | 用户态 | 内核态 |
|---|---|---|
| CPL(CS 低 2 位) | 3 | 0 |
| 可访问范围 | 0-3GB 用户区 | 3-4GB 内核区(仅限系统调用路径) |
| 页表 | 进程私有的用户级页表 | 全系统唯一的内核页表 |
| 典型场景 | 执行自己的 main、库函数 | 执行系统调用的内核实现、中断处理 |
核心链条回顾:
- 所有进程的 3-4GB 都映射同一个 OS →任何进程都能找到内核
- 但 OS 不信任任何人 →直接访问 3-4GB 会被拦截
- 想调内核 → 系统调用号入 EAX +
int 0x80/syscall - CPU 硬件配合:CS 指向内核代码段 + CPL 3→0 →陷入内核
- 陷入后只能走固定历程:查系统调用表执行,做完返回,CPL 0→3 回用户态
💡 三个概念的对应关系(为下一篇"信号捕捉"铺垫):
- 陷阱(trap):
int 0x80/syscall—— CPU 主动执行指令陷入内核 - 异常(exception):除零、野指针、缺页 —— CPU 执行出错被动陷入
- 硬件中断(interrupt):键盘、时钟等外设触发