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. 学完本章你能解决什么问题

学完这一章,你应该能解决或开始分析这些问题:

  1. OS 为什么必须存在?
  2. OS 管理哪些硬件资源?
  3. 用户态和内核态为什么要分开?
  4. 系统调用为什么有成本?
  5. 进程和线程的第一层区别是什么?
  6. 虚拟内存为什么让进程互相隔离?
  7. 文件和 Socket 为什么是 OS 提供的抽象?
  8. 程序卡住、慢、OOM、IO 阻塞时,如何先从 OS 角度提出问题?

操作系统不是远离应用的理论课。只要程序跑在真实机器上,OS 就在决定它如何拿到 CPU、内存、文件、网络和设备。

延伸阅读