from - zero · operating-systems
00. 从 0 开始理解操作系统
0. 本章先解决什么问题
应用程序看起来是在“自己运行”:
读文件
申请内存
创建线程
发网络请求
等待输入
写日志
但这些动作几乎都绕不开操作系统。
操作系统要解决的根本问题是:
有限、复杂、危险的硬件资源
如何被多个程序安全、稳定、相对公平地共享?
本章先建立 OS 的全局地图:
- OS 是资源管理者,也是抽象提供者。
- 用户态和内核态为什么存在。
- 进程、线程、虚拟内存、文件、Socket 都是什么抽象。
- 系统调用为什么是应用进入内核的入口。
- 程序卡住、慢、OOM、IO 阻塞、上下文切换为什么都和 OS 有关。
先看 OS 的位置:应用通过系统调用进入内核,内核再管理 CPU、内存、文件、网络和设备。
这张图怎么读
从应用往下看:程序不能直接随意占用 CPU、物理内存、磁盘块、网卡和设备寄存器,而是通过进程、线程、虚拟内存、文件、Socket、设备文件等抽象向 OS 提出请求。
再从内核往下看:OS 把这些请求翻译成调度、页表、缓存、驱动、中断和硬件操作。读 OS 时要一直抓住这条主线:抽象给应用稳定接口,资源管理决定真实成本和失败方式。
1. 操作系统到底是什么
操作系统是夹在应用程序和硬件之间的一层系统软件。
它做两类事:
| 角色 | 说明 |
|---|---|
| 资源管理者 | 管 CPU、内存、磁盘、网卡、设备 |
| 抽象提供者 | 提供进程、线程、文件、虚拟内存、Socket、锁等接口 |
如果没有 OS,每个程序都要自己管理硬件:
- 自己决定 CPU 给谁用。
- 自己避免读写别的程序内存。
- 自己控制磁盘和网卡。
- 自己处理设备中断。
- 自己处理权限和错误。
这几乎不可维护,也不安全。
2. OS 解决的核心矛盾
| 矛盾 | OS 的解决方式 |
|---|---|
| 一个或少数 CPU,很多任务想运行 | 进程、线程、调度 |
| 一块内存,多个程序都要用 | 虚拟内存、页表、地址空间隔离 |
| 磁盘和设备慢,程序想方便读写 | 文件系统、页缓存、IO 调度 |
| 硬件操作危险 | 用户态/内核态、系统调用、权限 |
| 程序可能互相干扰 | 进程隔离、权限、资源限制 |
| 多个执行流共享数据 | 锁、信号量、条件变量、原子操作 |
| 设备随时产生事件 | 中断、驱动、DMA |
所以 OS 的核心不是“提供很多 API”,而是把硬件资源变成可管理、可隔离、可复用的抽象。
3. 三层结构:应用、内核、硬件
应用程序通常不能直接操作硬件。它必须通过系统调用请求内核。
内核再根据权限、状态、资源情况决定是否执行。
4. 用户态和内核态
CPU 支持不同权限级别。
| 状态 | 能做什么 | 风险 |
|---|---|---|
| 用户态 | 运行普通程序代码 | 权限受限,不能随便操作硬件 |
| 内核态 | 运行 OS 内核代码 | 权限高,错误会影响整个系统 |
为什么要分?
- 防止应用修改内核内存。
- 防止一个程序破坏另一个程序。
- 防止普通程序随意操作磁盘、网卡、页表。
- 让系统资源有统一管理者。
代价:
用户态 <-> 内核态切换有成本
频繁系统调用、小 IO、频繁锁阻塞,都可能让切换成本变明显。
5. 系统调用:应用请求内核服务
系统调用是应用进入内核服务的正式入口。
用户态代码
-> 发起系统调用
-> CPU 切到内核态
-> 内核检查参数和权限
-> 内核执行服务
-> 返回用户态
常见服务:
- 创建进程或线程。
- 打开文件。
- 读写文件。
- 建立网络连接。
- 申请或映射内存。
- 等待事件。
- 获取时间。
高级 API 往往只是包装,底层仍然会落到系统调用和内核对象。
手推:一次 read 不只是“从文件拿数据”
把文件读取拆开看,可以看到 OS 为什么不是背景板。
应用调用 read(fd, buffer, n)
-> CPU 从用户态切到内核态
-> 内核检查 fd 是否有效、权限是否允许
-> 查打开文件表,找到文件对象和当前位置
-> 查看页缓存里有没有目标数据
-> 命中: 从页缓存复制到用户缓冲区
-> 未命中: 发起磁盘 IO,线程可能阻塞
-> 设备完成后通过中断/DMA 通知内核
-> 内核把数据放入页缓存,再复制给应用
-> 更新文件偏移,返回实际读取字节数
这个流程解释了几个常见现象:
| 现象 | OS 层解释 |
|---|---|
| 第一次读慢,第二次读快 | 第二次可能命中页缓存 |
read 返回值小于请求大小 | 文件剩余不足、管道/socket 暂时只有部分数据、被信号打断等 |
| 程序看起来卡住 | 线程可能在等磁盘、网络、锁或调度 |
| CPU 不高但请求慢 | 时间花在等待 IO,不是计算 |
| 读文件影响内存占用 | 页缓存也占用系统内存 |
所以 OS 视角下,读文件不是单个动作,而是一条资源链:
fd -> 文件对象 -> 页缓存 -> 块设备 -> 驱动/中断/DMA -> 用户缓冲区
排查读慢时,必须判断慢在链条哪一段,而不是只看应用函数名。
6. 进程和线程的第一层直觉
| 概念 | 直觉 |
|---|---|
| 程序 | 磁盘上的代码和资源 |
| 进程 | 程序运行后的资源容器 |
| 线程 | 进程里的执行流,真正被 CPU 调度 |
进程拥有:
- 地址空间。
- 打开的文件。
- 权限。
- 环境变量。
- 资源限制。
- 一个或多个线程。
线程共享进程资源,但每个线程有自己的执行上下文,比如栈、寄存器状态、程序计数器。
共享带来通信方便,也带来竞态和同步问题。
7. 虚拟内存的第一层直觉
程序看到的是虚拟地址,不是物理内存地址。
程序访问虚拟地址
-> MMU 根据页表翻译
-> 得到物理地址
虚拟内存带来:
| 能力 | 说明 |
|---|---|
| 隔离 | 进程不能随便访问别的进程内存 |
| 简化 | 每个进程都像拥有独立连续地址空间 |
| 按需加载 | 用到某页再加载 |
| 权限 | 页可以设为只读、可写、可执行 |
| 共享 | 多进程可以映射同一文件或共享内存 |
| 换页 | 内存不足时部分页可换出 |
虚拟内存是 OS 和硬件协作的典型抽象。
8. 文件和 Socket 也是抽象
磁盘底层是块设备,但程序看到的是文件:
open -> read/write -> close
网络底层是网卡、协议栈、包和缓冲区,但程序看到的是 Socket:
socket -> connect/accept -> send/recv -> close
这些抽象隐藏了大量细节:
- 权限检查。
- 路径解析。
- 页缓存。
- 内核缓冲区。
- 设备驱动。
- 中断和 DMA。
- 阻塞和唤醒。
抽象让编程简单,但当性能或错误出现时,抽象会泄漏,你必须知道下面发生了什么。
9. OS 如何连接其他三大件
| 课程 | OS 连接点 |
|---|---|
| 组成原理 | 中断、DMA、页表、CPU 调度、Cache、设备 |
| 数据结构 | 调度队列、页表、文件系统索引、缓冲区、锁等待图 |
| 计算机网络 | Socket、内核网络栈、端口、缓冲区、IO 多路复用 |
例如一次网络读取:
网卡收到包
-> 中断或轮询
-> DMA 到内存
-> 内核网络栈处理
-> 放入 socket buffer
-> 应用 read/recv
-> 用户态拿到数据
这同时涉及组成原理、OS 和网络。
10. 联系实际:程序问题如何落到 OS
| 现象 | OS 方向 |
|---|---|
| 程序卡住 | IO 阻塞、锁等待、线程池耗尽、死锁 |
| CPU 飙高 | 忙等、频繁上下文切换、系统调用、调度竞争 |
| 内存不够 | 堆、栈、映射区、页缓存、外部内存、资源限制 |
| 文件读写慢 | 页缓存、磁盘 IO、同步刷盘、目录/元数据 |
| 网络连接多 | Socket、端口、内核缓冲区、epoll/多路复用 |
| 请求超时 | 队列堆积、IO 等待、调度延迟、对端慢 |
| 偶发延迟尖刺 | 缺页、swap、GC、上下文切换、锁竞争、刷盘 |
排查时不要只看应用代码。很多问题要问:
线程现在在 CPU 上跑,还是在等锁/IO/调度?
内存是在堆里,还是页缓存/映射区/外部内存?
数据是在用户态,还是内核缓冲区,还是设备队列?
机制深挖:OS 不是“帮程序运行”,而是在分配稀缺资源
把 OS 理解成“程序启动器”会太浅。更准确的模型是:
很多程序同时提出资源请求
OS 决定谁现在得到资源,谁等待,谁失败
这些资源包括:
| 资源 | OS 要做的决定 |
|---|---|
| CPU | 哪个线程现在运行,运行多久 |
| 内存 | 哪些页在物理内存,哪些换出或回收 |
| 文件 | 谁能打开、读写到哪里、何时落盘 |
| 网络 | 哪个 socket 收到哪些包,缓冲区满了怎么办 |
| 设备 | 请求如何排队,中断如何处理 |
因此很多应用层症状都是资源决策的外部表现:
慢 = 可能在等待 CPU / IO / 锁 / 队列
失败 = 可能权限不够 / 资源耗尽 / 状态不合法
卡住 = 可能没有被唤醒 / 等待条件永远不满足
抖动 = 可能被调度、缺页、刷盘、中断影响
学习 OS 的第一性目标不是背概念,而是学会把现象翻译成资源问题。
反例:用户态看到“函数很慢”,根因可能在内核队列
假设应用里有一行:
read(socket)
它变慢时,表面上像是这个函数慢。实际上可能有很多完全不同的原因:
| 位置 | 可能原因 |
|---|---|
| 应用线程 | 线程没被调度到 CPU |
| socket buffer | 缓冲区没有数据,线程阻塞等待 |
| 协议栈 | 包到了但还在内核处理队列 |
| 网卡/驱动 | 中断或 DMA 处理积压 |
| 对端 | 对端没有及时发送 |
| 网络路径 | 丢包、重传、拥塞 |
这些原因在应用层都可能表现为:
read 花了很久才返回
所以 OS 排障要反过来追问:
线程是在运行还是睡眠?
如果睡眠,等待对象是什么?
数据是否已经到达内核?
内核缓冲区、设备队列、调度队列哪一个在排队?
这就是 OS 课程和真实调试的连接点:把一个“慢函数”拆成资源等待链。
建模卡:把一次程序运行映射到 OS 资源
任意选择一个程序,从启动到退出,写出它使用过哪些 OS 资源:
| 阶段 | OS 资源/抽象 | 可能失败点 |
|---|---|---|
| 启动 | 进程、地址空间、可执行文件 | 权限、文件不存在、内存不足 |
| 运行 | 线程、CPU 时间片、虚拟内存 | 调度等待、缺页、同步阻塞 |
| 读写 | 文件描述符、页缓存、设备队列 | 阻塞、部分读写、刷盘失败 |
| 通信 | Socket、缓冲区、端口 | 连接失败、队列满、对端关闭 |
| 退出 | 资源释放、父子进程关系 | 僵尸进程、句柄泄漏 |
练习重点:不要把 OS 看成背景。程序每一步都在借用 OS 的资源抽象;一旦抽象背后的资源耗尽或等待,应用层就会表现成慢、卡、失败或泄漏。
11. 学完本章你能解决什么问题
学完这一章,你应该能解决或开始分析这些问题:
- OS 为什么必须存在?
- OS 管理哪些硬件资源?
- 用户态和内核态为什么要分开?
- 系统调用为什么有成本?
- 进程和线程的第一层区别是什么?
- 虚拟内存为什么让进程互相隔离?
- 文件和 Socket 为什么是 OS 提供的抽象?
- 程序卡住、慢、OOM、IO 阻塞时,如何先从 OS 角度提出问题?
操作系统不是远离应用的理论课。只要程序跑在真实机器上,OS 就在决定它如何拿到 CPU、内存、文件、网络和设备。