操作系统结构¶
本章问题:程序调用一次“读文件”,怎样跨过权限边界,让内核安全地完成请求?
学习顺序:
- 区分应用接口、语言库和真正的系统调用。
- 追踪用户态进入内核再返回的完整路径。
- 理解参数检查、内核结构与策略/机制。
- 分清编译、链接、装载、整机启动和虚拟化。
一次读取怎样进内核¶
- 先补三个词。
- 寄存器是CPU内少量、快速的存储位置,执行指令时保存地址、操作数等;
- 程序计数器保存下一条要执行的指令位置;
- 陷入是通过受控入口把执行转交给内核的一类方式。
- 用户态与内核态是CPU权限状态,同一个线程也能在两种权限下先后执行。
flowchart TB
A["用户态:应用调用读取API"] --> B{"语言库已有所需数据?"}
B -->|"有"| C["直接返回库缓冲内容"]
B -->|"无,需要内核服务"| D["准备系统调用编号和参数"]
D --> E["执行受控入口指令,进入内核态"]
E --> F["检查参数、权限,执行读取"]
F --> G{"需要等待设备?"}
G -->|"无需等待"| H["原线程返回用户态"]
G -->|"需要等待"| I["原线程阻塞,CPU可运行其他线程"]
I --> J["设备完成:原线程就绪"]
J -->|"再次得到CPU"| H
图中“进入内核态”改变权限;“阻塞后运行另一线程”才改变执行身份。这两个动作可能接连发生,也可能只发生前者。
应用要读文件,先调用语言库提供的接口(API)。API是程序员看到的函数约定,系统调用是进入内核请求服务的边界。
例如库里已有缓冲数据时,读取可以直接在用户态返回;缓冲为空时,才可能触发内核读取。
一次API调用与一次系统调用没有固定的一对一关系,printf也可能先格式化并缓冲文字。
- 真正进入内核时,应用准备调用编号和参数,执行体系结构规定的陷入指令。
- CPU保存返回信息并转到内核入口;
- 内核识别请求、检查参数与权限,完成操作后返回结果和执行位置。
- 数据已就绪时,同一个线程可以立即返回;
- 等待设备时才可能阻塞并让出CPU。
- 跨权限边界不等于换运行任务。
练习 1¶
题目
自编题:读取API原先直接返回库缓冲数据。 修改后缓冲为空,触发内核读取, 内核数据已就绪,原线程直接返回。 关于这次修改,哪项正确?
- A. 增加了模式转换,未必发生任务切换
- B. 增加了任务切换,未必发生模式转换
- C. 仍只在用户态运行,因为入口是API
- D. 必然等待磁盘,因为调用了读取接口
参考解答
- 答案:A。 API原先可完全在用户态完成。
- 修改后系统调用跨权限边界;
- 题设数据已就绪,同一线程可直接返回。
参数为什么要检查¶
参数可放在寄存器、内存参数块中,或通过栈传递。这只决定“怎样取参数”,不保证参数可信。内核必须检查地址范围、读写权限、长度以及运算是否溢出。
例子与推演
例如允许写的用户地址为 \([1000,2000)\),应用要求从1996开始写8字节。首地址合法,但范围是 \([1996,2004)\),越界4字节,应拒绝。半开区间中的2000不属于合法区域。
仅检查首地址,或因为参数来自用户栈就跳过检查,都会让保护失效。
- 常见系统调用可以按用途记:创建或等待进程;
- 打开、读写文件;
- 控制设备;
- 查询时间和系统信息;
- 进程通信;
- 设置访问权限。
- 命令行解释器(shell)读取并解释命令,通常通过创建进程和执行程序完成工作;
- 它本身一般也是用户态程序。
- 系统工具、库、内核和硬件因此组成不同服务层。
内核内部怎样组织¶
决定“下一个运行谁”是策略;提供保存现场、恢复现场的方法是机制。把两者分开,替换调度策略时就不必重写整套切换代码。
| 结构 | 怎么安排服务 | 主要取舍 |
|---|---|---|
| 单体内核 | 多数服务在同一内核空间直接调用 | 调用路径短,错误影响范围可能较大 |
| 分层结构 | 上层依赖下层服务 | 边界易理解,划层和跨层可能有成本 |
| 微内核 | 内核保留少量基础机制,其他服务放用户进程 | 更易隔离,消息传递和切换有开销 |
| 模块化 | 通过约定接口装入或卸载模块 | 易扩展;模块仍可能在内核态 |
| 混合结构 | 综合多种安排 | 应分析实际服务位置与调用路径 |
把文件服务移到用户态后,一条读路径可能变成“应用→内核消息机制→文件服务→内核驱动→设备→回复应用”。服务出错更难直接破坏其他地址空间,但依赖它的应用仍可能读失败或等待。
结构名称不能单独证明性能或可靠性。
练习 2¶
题目
自编题:原文件服务位于单体内核内部。改造后位于独立用户态服务进程,应用用消息请求它,必要I/O仍经过内核。开发者声称“通信变快且服务错误不会影响别人”。
- ① 写出改造后的读取路径。
- ② 找出该断言中两个没有保证的部分。
参考解答
解答
- 路径:应用→内核消息机制→文件服务;
- 服务需要设备时再经内核驱动访问设备,结果经回复消息送回应用。
- 跨服务通信可能增加传递和切换开销,没有足够测量不能断言更快。
- 独立地址空间可限制直接内存破坏,但服务失效仍可能使依赖者读取失败或等待。
- 隔离收益不等于故障对外完全无影响。
判分要点
- 须体现服务在用户态且仍需内核支持。
- 须把通信成本与隔离收益分别解释。
- 不能把结构标签当成绝对性能或可靠性保证。
程序与系统怎样启动¶
- 编译把源代码转成机器代码;
- 链接把多个目标文件拼接起来,并解决“调用的函数在哪”等符号引用;
- 装载给程序建立运行时地址空间和初始状态。
- 三者分别回答“指令是什么、依赖在哪里、运行时放在哪”。
flowchart LR
S["源代码"] -->|"编译"| O["目标文件"]
O -->|"链接库并解析符号"| E["可执行文件"]
E -->|"装载和建立映射"| P["进程开始执行"]
动态链接允许部分库在装载或运行时解析;它仍需完成相应符号与地址处理。整机引导则在操作系统尚未运行时把内核带入执行,和普通应用装载处于不同阶段。
源文件先编译成目标文件,再由链接器组合代码与数据,解决符号引用,得到可执行文件。装载器建立运行需要的地址映射与初始状态,再转入入口。
动态链接还会装入依赖库;ELF中的程序入口 e_entry 与指定解释器的 PT_INTERP 有不同用途,不能把所有入口都称为动态加载器。
整机启动要先有固件或引导程序初始化必要硬件、装入内核,再转入内核初始化,建立内存管理、驱动和初始进程。
构建配置决定哪些功能被编入、作为模块或禁用;Makefile描述目标、依赖和生成规则,改变依赖后才知道应重建什么。
今年Lab 0把这些角色放在同一条链上:Docker提供工具运行环境,交叉编译器在宿主环境产生RISC-V代码,QEMU模拟目标机器,内核在其中启动。
GDB利用带符号的 vmlinux 调试;实验用的 Image 与它承担的文件用途不同。Lab 1则接着观察引导和时钟中断。
这里先认清角色,具体运行证据来自实际实验。
虚拟机增加了哪一层¶
虚拟机监控器(hypervisor)把CPU、内存与设备呈现为虚拟硬件,客户操作系统在这套硬件视图上运行。监控器可直接位于硬件之上,也可依托宿主操作系统。
容器通常共享宿主内核,隔离进程环境,与每台都运行客户内核的虚拟机不同。
虚拟CPU仍要被安排到物理CPU上。忽略同时多线程硬件,2个物理核心最多同时执行2条指令流;两台虚拟机各宣称2个vCPU,也不能让4条计算任务同时占有4个物理核心。
虚拟化提供资源视图和隔离,实际吞吐仍受硬件与调度约束。
练习 3¶
题目
自编题:只允许访问用户字节区间[1000,2000)。读系统调用要写入从1996开始的8字节,整数运算无溢出,其他权限均满足。
- ① 仅检查起始地址合法吗?应如何判断?
- ② 参数先放用户栈,再跨入内核,能否因此省掉内核对指针和长度的检查?
- ③ 2个物理核心,2个虚拟机各有2个vCPU;忽略SMT,四条CPU密集任务能否同时执行?
参考解答
解答
- 不能只检查1996;
- 完整范围是[1996,2004),末4字节越过2000,应拒绝这个越界缓冲区。
- 传参位置只决定怎样取参数,不保证参数可信;
- 内核仍须验证完整范围、权限及相应访问条件。
- 共有4个vCPU,但只有2条物理核心执行流,最多同时执行2条任务,其余需等待或交替运行。
- 虚拟机提供隔离与虚拟资源,不凭空增加核心。
判分要点
- 检查完整半开区间而非只验首地址。
- 说明传参机制与安全验证是不同步骤。
- 给2条物理并行上限并说明vCPU的调度含义。
记忆要点¶
本章记忆要点
- API是程序看到的调用约定;系统调用是请求内核服务的受控边界。
- 用户态/内核态说明权限;线程身份说明谁在执行,二者分开记录。
- 检查指针时看完整范围、权限与长度运算,不能只看首地址。
- 策略决定选择,机制落实选择;结构评价需要看实际调用路径。
- 编译产机器代码,链接解依赖,装载建运行环境;vCPU仍受物理执行位置限制。