Appearance
操作系统与硬件
- 写作时间:
2026-03-04 首次提交,2026-07-13 最近修改 - 当前字符:
7536
前言已经交代了全书路线:我们会对照 Linux 内核源码理解操作系统原理,也会观察真实系统的运行结果。现在进入基础与概览,第一步不是记住某个内核函数,而是先确定操作系统位于应用程序和硬件之间的什么位置。
来看一个最小场景。just linux 会启动课程使用的 Linux 容器;容器隔离了进程看到的部分用户空间环境,却仍共享 Docker 虚拟机提供的 Linux 内核。课程镜像默认跟随宿主的原生架构,因此 Apple Silicon 上通常是 AArch64,x86 主机上通常是 x86-64;涉及特定体系结构的正文会明确标注。后续命令都可以在其中重复执行:
console
$ just linux
root@hands-on-os:/project# ps aux | head
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 4168 3492 pts/0 Ss 06:56 0:00 bash
root 7 0.0 0.0 6008 3272 pts/0 R+ 06:56 0:00 ps aux
root 8 0.0 0.0 2316 1204 pts/0 S+ 06:56 0:00 headps 显示的是进程(process),也就是程序的一次执行实例;进程标识符(Process ID, PID)是内核分配给进程的数字编号。进程在采样时可以正在运行,也可以处于等待等其他状态。这个环境虽然只出现了 bash、ps 和 head,它们仍然要共享 CPU、内存和 I/O 设备,而且一个进程不能随意读写另一个进程的状态。这些要求由谁实现?
答案先落在 操作系统 上。操作系统一方面通过 资源管理 分配 CPU、内存与设备,另一方面通过 系统抽象 给程序提供稳定接口。继续观察硬件时,处理器拓扑 决定系统有多少可执行指令的逻辑 CPU,以及它们怎样共享资源;存储层次 解释不同存储部件为何存在巨大的速度差异;I/O 设备 则让数据能够在内存与外部世界之间传输。
基础与概览会沿着这条边界继续推进:操作系统与硬件建立整体关系;内核设计比较操作系统核心的组织方式;特权边界解释应用为什么不能直接控制硬件;系统调用再展示用户程序怎样向内核提出受控请求。
操作系统
操作系统(Operating System, OS)是管理计算机硬件资源,并为应用程序提供运行环境与服务接口的系统软件。
这里的“操作系统”可以包含多个层次。内核(kernel)是以硬件特权运行、直接管理 CPU、内存和设备的核心程序;启动程序、系统服务和命令行工具构成用户空间中的系统环境。不同语境可能把整个环境称为操作系统,也可能只讨论内核。本书研究机制时会明确区分二者。
一个程序若独占机器,确实可以直接控制硬件。问题出现在多个程序同时存在以后:它们都可能希望运行指令、申请内存、保存文件或接收网络数据。操作系统位于这些程序与硬件之间,把每次请求纳入统一管理,并在一个程序出错时尽量限制影响范围。
操作系统并不替应用完成业务计算。文本编辑器仍要决定怎样修改文档,数据库仍要实现索引与查询;操作系统负责的是这些程序共同依赖、且必须由全局可信组件协调的基础能力。
资源管理
资源管理(resource management)是操作系统记录、分配、复用并回收有限硬件资源的职责。
处理器只提供有限数量的逻辑执行上下文,操作系统要决定每个逻辑 CPU 接下来运行哪个任务;物理内存容量有限,操作系统要记录哪些区域属于哪个进程;磁盘、网卡和终端可能被多个进程同时使用,操作系统要把请求排队并在完成后通知正确的请求者。一个支持多个硬件线程的物理核心可以暴露多个逻辑 CPU,内部还可能乱序或并行执行同一指令流中的多条指令,因此不能把“一个核心”简单等同于“同一时刻只处理一条机器指令”。
资源管理同时包含共享与保护。共享回答“多个进程怎样使用同一硬件”,保护回答“一个进程能访问到什么范围”。如果只做共享而没有保护,某个错误指针就可能覆盖其他进程内存;如果只做保护而不做复用,许多程序又无法方便地同时运行。
开篇的 %CPU、%MEM、PID 和状态字段来自内核对进程的持续记账。ps 自己没有直接读取其他进程内核对象的能力,它读取内核通过 /proc 提供的进程状态;/proc 是一个由内核动态生成内容的特殊文件系统,进程生命周期会继续使用它观察进程。这里先注意结果:资源分配不是应用自行约定,而是内核掌握并通过接口报告。
系统抽象
系统抽象(system abstraction)是操作系统把不同硬件和内核对象表示成一组稳定、可组合的程序接口。应用程序编程接口(Application Programming Interface, API)就是应用能够调用的一组操作、参数和返回规则。
应用若直接操作每种磁盘和网卡,就必须了解设备寄存器、命令格式与完成方式。设备驱动(device driver)是把内核的通用 I/O 请求转换成具体设备命令的系统软件。操作系统在设备之上提供文件、用于网络通信的套接字(socket)和进程等对象,应用针对这些对象编程,具体硬件差异由内核和驱动处理。抽象没有消除差异,而是把差异限制在接口背后的实现中。
Unix 系统常把不同对象接入文件描述符(file descriptor, fd)接口。fd 是进程用来引用一个已打开内核对象的小整数。普通文件、管道、终端和 socket 都可以通过 fd 使用 read()、write()、close() 等操作,但它们的具体语义仍然不同。例如普通文件支持偏移,传输控制协议(Transmission Control Protocol, TCP)套接字表示一条可靠的连续字节流。统一的是操作入口,不是所有对象的行为。
应用向内核请求服务的受控入口叫作系统调用(system call)。系统调用一课会解释它怎样跨越权限边界,这里先观察这类请求确实存在。strace 是 Linux 的系统调用追踪工具,下面只保留 ls 最关键的几类调用:
console
$ strace -e trace=execve,openat,getdents64,write ls course/basics/code 2>&1 >/dev/null | grep -E 'execve|/etc/ld.so.cache|/proc/filesystems|course/basics/code|getdents64|write|exited'
execve("/usr/bin/ls", ["ls", "course/basics/code"], 0xffffcd1fec58 /* 10 vars */) = 0
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/proc/filesystems", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "course/basics/code", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
getdents64(3, 0xaaaacc2e3640 /* 3 entries */, 32768) = 80
getdents64(3, 0xaaaacc2e3640 /* 0 entries */, 32768) = 0
write(1, "hello.c\n", 8) = 8
+++ exited with 0 +++execve 启动 ls,openat 打开运行所需文件和目标目录,getdents64 读取目录项,write 把结果送到标准输出。一个看似简单的目录列表,已经经过了进程、文件系统和输出设备的共同配合。
再把行为缩小到一个最小 C 程序:
c
#include <unistd.h>
int main(void) {
write(1, "hello\n", 6);
return 0;
}console
$ gcc course/basics/code/hello.c -o /tmp/hello && /tmp/hello
hellowrite(1, "hello\n", 6) 请求内核向 fd 1 写入 6 个字节。fd 1 按约定是标准输出,但它可能连接终端、文件或管道;程序不必为每种去向改写打印逻辑。这就是系统抽象带来的可组合性。
如果没有操作系统会怎样?
没有操作系统时,单个程序需要自己初始化硬件、处理中断、管理内存并实现设备驱动。若还要同时运行两个程序,就必须另外实现 CPU 时间分配、内存保护和设备请求仲裁。
这些能力并不会因为没有“操作系统”这个名称而消失。只要多个程序要安全共享机器,就必须有一段可信软件统一实现它们;这段软件实际上已经承担了操作系统的核心职责。
处理器拓扑
处理器拓扑(processor topology)是处理器插槽、物理核心和硬件线程之间的层级与共享关系。
一个 socket 表示主板上的处理器插槽,一个 core 表示能够执行指令的物理核心,一个 hardware thread 表示核心向操作系统暴露的逻辑执行上下文。这里的 hardware thread 也叫逻辑 CPU,不是线程中的软件线程。一个核心若支持同时多线程(Simultaneous Multithreading, SMT),就可以暴露多个逻辑 CPU。
lscpu 可以显示容器当前看到的拓扑:
console
$ lscpu | grep -E '^Architecture|^CPU\(s\):|^Thread\(s\) per core|^Core\(s\) per socket|^Socket\(s\):'
Architecture: aarch64
CPU(s): 10
Thread(s) per core: 1
Socket(s): -这组 AArch64 Docker 虚拟机(Virtual Machine, VM)向容器提供了 10 个逻辑 CPU,却没有在汇总信息中报告 socket 与 core 数量,- 表示未知而不是 0。物理机或信息完整的虚拟机可能满足“socket 数量 × 每 socket 核心数 × 每核心线程数 = 逻辑 CPU 数量”,但离线 CPU、容器 CPU 集合限制、虚拟拓扑或异构处理器都可能使简单乘法不再适用。
对称多处理(Symmetric Multiprocessing, SMP)表示各处理器以对等地位运行同一操作系统,并共享主存与 I/O 系统。它没有要求所有内存访问延迟完全相同。统一内存访问(Uniform Memory Access, UMA)机器中,各核心访问主存的代价近似一致;非一致性内存访问(Non-Uniform Memory Access, NUMA)机器则把 CPU 与部分内存组成节点,访问本节点内存通常快于远端节点。现代多插槽 NUMA 服务器仍然可以是 SMP 系统。
这个区别会直接影响后续机制。Linux 调度器不仅要选择可运行 CPU,还会考虑缓存和 NUMA 亲和性;内核内存分配也会根据内存策略和节点可用性选择页面。这里先建立拓扑含义,具体策略分别由对应课程展开。
存储层次
存储层次(memory hierarchy)是按照容量、延迟、带宽和持久性组织寄存器、缓存、主存与外部存储的结构。
CPU 执行指令的速度远快于主存提供数据的速度,而主存又远快于固态硬盘(Solid-State Drive, SSD)与机械硬盘(Hard Disk Drive, HDD)。系统因此使用多级结构,让频繁访问的数据尽量位于更快但容量更小的层级。下面的数字只是常见数量级,不同 CPU、内存和设备会显著变化:
| 层级 | 常见容量 | 常见访问延迟 | 是否掉电保留 |
|---|---|---|---|
| 寄存器 | 数百字节 | 小于 1 ns | 否 |
| L1 cache | 每核心数十 KiB | 约 1 ns | 否 |
| L2 cache | 每核心数百 KiB 到数 MiB | 数 ns | 否 |
| Last-level cache | 数 MiB 到数十 MiB | 十几到数十 ns | 否 |
| 主存 RAM | 数 GiB 到数 TiB | 数十到数百 ns | 否 |
| SSD | 数百 GiB 到数 TiB | 数十到数百 µs | 是 |
| HDD | 数 TiB | 数 ms | 是 |
Cache 是否私有、由哪些核心共享取决于具体处理器,不能把“L2 一定私有、L3 一定全 socket 共享”当成架构保证。操作系统会读取固件与 CPU 提供的拓扑信息,再据此安排调度和内存策略。
缓存之所以能提高命中率,是因为程序访问常具有局部性(locality)。时间局部性表示刚访问的数据很可能很快再次访问;空间局部性表示访问一个地址后,附近地址很可能随后被访问。硬件通常按固定大小的缓存行(cache line)搬运相邻数据,而不是只搬一个字节。
操作系统也利用同一原理。页缓存(page cache)把近期使用的文件数据保留在主存中,后续读取可以不访问慢速设备;配置了交换空间(swap)时,内存管理还可以把暂时不活跃、且不直接对应文件的内存页写入该磁盘区域,为当前活跃数据释放物理内存。这里先说明它们与存储层次的关系,虚拟内存会完整推导具体机制。
I/O 设备
输入/输出设备(input/output device, I/O device)是与 CPU 和主存交换外部数据或控制信号的硬件,例如磁盘、网卡、键盘和显示控制器。
设备通常通过控制器暴露寄存器和缓冲区,CPU 通过总线向控制器提交命令。控制器寄存器可以位于独立的 I/O 地址空间,也可以映射到处理器物理地址空间;后一种方式叫内存映射 I/O(Memory-Mapped I/O, MMIO)。少量数据可以由 CPU 直接读写设备寄存器;大量数据若仍逐字节经过 CPU,会消耗大量指令。因此现代设备常使用直接内存访问(Direct Memory Access, DMA):内核驱动准备包含 DMA 地址、长度和方向的描述符并通知控制器,控制器随后直接在设备与主存之间传输数据。
DMA 完成后,设备还要让内核发现结果。中断(interrupt)是硬件异步请求 CPU 执行处理程序的机制;高速设备也可能由内核轮询完成队列,以减少高频中断。于是一次 I/O 可以先形成最小模型:CPU 提交请求,设备通过 DMA 传输,内核再通过中断或轮询发现完成。内核设计会从软件组织继续推进,特权边界再解释为什么用户程序不能自行配置这条路径。
小结
| 概念 | 说明 |
|---|---|
| 操作系统 | 管理硬件资源,并为应用提供运行环境与服务接口的系统软件 |
| 资源管理 | 分配、复用、保护并回收 CPU、内存和设备等有限资源 |
| 系统抽象 | 用进程、文件、Socket 与 fd 等稳定接口隐藏实现差异 |
| 处理器拓扑 | socket、core 与 hardware thread 的层级和共享关系 |
| 存储层次 | 按容量、速度和持久性组织寄存器、缓存、主存与外部存储 |
| I/O 设备 | 通过控制器、DMA 和中断与 CPU、主存交换数据的硬件 |
操作系统的接口设计和内部机制都受硬件约束:资源必须被复用和保护,而硬件在并行度、访问延迟与完成方式上的差异决定了具体实现成本。
Linux 与硬件入口:
include/linux/sched.h:进程与调度相关核心结构- DMA API HOWTO:DMA 地址、映射与同步的内核视角
内核设计将继续观察这个直接管理硬件的核心程序怎样划分职责,以及不同组织方式为什么会产生不同取舍。