1. 操作系统是什么
1.1 三个核心职责
操作系统(Operating System,OS)本质上是运行在硬件之上、应用程序之下的一层系统软件,它承担三件事:
- 资源管理者:CPU 时间、内存空间、磁盘容量、网络带宽、外设——所有硬件资源都由内核统一分配与回收,避免多个程序直接争抢硬件。
- 抽象提供者:把裸硬件抽象成易用的接口。进程抽象了 CPU、虚拟内存抽象了物理内存、文件抽象了磁盘块、Socket 抽象了网卡。程序员因此不必面对寄存器和扇区。
- 控制程序:负责加载并调度程序的执行,处理错误与中断,维护系统安全边界(谁能访问什么)。
一句话记忆:操作系统 = 硬件资源的管家 + 硬件能力的翻译官 + 程序执行的裁判。
1.2 层次结构
+---------------------------+
| 应用程序 App |
+---------------------------+
| 库函数 / 运行时 (libc) |
+---------------------------+
| 系统调用接口 (syscall) |
+===========================+ ← 特权级分界线
| 内核 Kernel |
| 进程 | 内存 | 文件 | 网络 |
+---------------------------+
| 设备驱动 / HAL |
+---------------------------+
| 硬件 CPU / 内存 / IO |
+---------------------------+
理解这张图的关键:系统调用接口就是用户态与内核态的唯一正规通道。应用程序不能直接操作硬件,必须「请求」内核代为执行。
2. 内核体系结构
2.1 宏内核 Monolithic Kernel
所有核心子系统(进程调度、内存管理、文件系统、网络协议栈、驱动)都运行在同一个内核地址空间,以函数调用的方式互相协作。Linux、FreeBSD 属于这一类。
- 优点:性能高,模块间是普通函数调用,没有跨进程通信开销;共享数据结构可直接访问。
- 缺点:耦合度高,任一驱动崩溃会拖垮整个内核;内核体积大。
2.2 微内核 Microkernel
内核只保留最基础的能力(地址空间管理、线程调度、进程间通信 IPC),文件系统、驱动、网络栈都搬到用户态服务进程中。Mach、QNX、seL4 属于这一类。
- 优点:可靠性高、隔离性好,服务崩溃可重启而不影响内核;更易形式化验证。
- 缺点:IPC 开销大,一次文件读可能要多次跨进程消息传递,性能敏感场景吃亏。
2.3 混合内核与外核
| 类型 | 代表 | 核心特征 |
|---|---|---|
| 宏内核 | Linux | 全功能同地址空间,函数调用 |
| 微内核 | QNX、seL4 | 最小内核 + 用户态服务 |
| 混合内核 | Windows NT、macOS XNU | 微内核骨架但服务在内核态 |
| 外核 Exokernel | MIT Exokernel | 内核只做资源保护,库操作系统实现抽象 |
工程现实:纯粹的宏内核与微内核都少见,主流是「宏内核 + 可加载模块」或「混合内核」,兼顾性能与可维护性。
3. 特权级与用户态内核态
3.1 Ring 模型
x86 提供 4 个特权级 Ring 0~3。实际只用到两个:Ring 0 内核态、Ring 3 用户态。CPU 通过段寄存器/页表标记当前处于哪个特权级,某些指令(如 lgdt、in/out、修改页表基址)只在 Ring 0 可用。
Ring 0 内核态 —— 可执行全部指令,访问全部内存
Ring 3 用户态 —— 受限指令集,只能访问自己的地址空间
3.2 为什么需要隔离
- 保护:用户程序写错指针不会破坏内核数据结构。
- 公平:防止用户程序长时间占用 CPU(必须经内核调度)。
- 安全:防止程序直接读写其他进程内存或设备寄存器。
3.3 切换开销
用户态到内核态的切换(trap)不是免费的:保存现场(寄存器)、切换栈、刷新部分 TLB/Cache 状态。一次系统调用约几十到几百纳秒,因此高性能程序倾向于批量处理(如 readv/writev、io_uring)来摊薄切换成本。
4. 系统调用
4.1 调用链路
用户程序调用 read()
↓ libc 封装:把参数放入寄存器,设置系统调用号
↓ 执行 syscall / int 0x80 指令(trap)
↓ CPU 切换到 Ring 0,跳到内核 sys_call_table[号]
↓ 内核执行真正的 read 逻辑
↓ 返回用户态,恢复现场
以 x86-64 Linux 为例:系统调用号放 rax,参数依次放 rdi、rsi、rdx、r10、r8、r9,返回值放 rax。
4.2 代码示例
// 直接发起系统调用(绕过 libc 封装,便于观察)
#include <unistd.h>
#include <sys/syscall.h>
long n = syscall(SYS_write, 1, "hello\n", 6);
// 等价于 write(1, "hello\n", 6),1 是 stdout 的文件描述符
# 观察一个进程调用了哪些系统调用
strace -f -e trace=read,write ./a.out
# 统计系统调用耗时分布
strace -c ./a.out
4.3 三组易混概念
- 系统调用 vs 库函数:
read()是库函数封装,底层走syscall;printf()是纯库函数,内部可能调用write(),也可能先写缓冲区。 - 系统调用 vs 用户态函数:
strlen()完全在用户态完成,不触发切换。 - 系统调用 vs 中断:系统调用是同步的、由程序主动发起;中断是异步的、由硬件触发。
5. 中断、异常与陷入
CPU 处理「意外事件」有三种入口,统称**陷入(trap)**到内核:
| 类型 | 来源 | 同步性 | 例子 |
|---|---|---|---|
| 中断 Interrupt | 外部硬件 | 异步 | 时钟中断、网卡收包 |
| 异常 Exception | 指令执行 | 同步 | 缺页、除零、非法指令 |
| 系统调用 Syscall | 程序主动 | 同步 | read、fork、mmap |
处理流程(简化):
硬件事件 → 保存现场 → 查中断向量表 → 执行处理程序
→ 恢复现场 → iret 返回被中断的指令
面试常问:中断处理为什么要「上半部 + 下半部」拆分?因为中断上下文要求极短、不可睡眠,把耗时工作(如网络包协议处理)下移到软中断/工作队列中延后执行。
6. 内核模块与设备驱动
Linux 支持可加载内核模块(LKM):驱动以 .ko 形式在运行时插入内核,无需重启,这是宏内核保持灵活性的关键设计。
lsmod # 列出已加载模块
modprobe e1000e # 加载网卡驱动(自动处理依赖)
modinfo e1000e # 查看模块信息与参数
rmmod e1000e # 卸载
设备在 Linux 中通过三类节点抽象:
- 字符设备:按字节流访问(键盘、串口),
/dev/ttyS0。 - 块设备:按块随机访问,可挂载文件系统(磁盘),
/dev/sda。 - 网络设备:没有设备文件,通过 Socket 接口访问,
eth0。
驱动模型的核心思想:内核定义统一的接口(file_operations),驱动只负责实现具体操作函数,从而让上层文件系统与下层硬件解耦。
7. 启动流程
7.1 从加电到内核
① 加电 → CPU 复位,跳到固件入口(BIOS/UEFI)
② 固件自检 POST,枚举设备,读取引导设备 MBR/ESP
③ 引导加载器(GRUB/LILO)加载内核镜像与 initrd
④ 内核解压并初始化:内存管理、调度器、中断、驱动
⑤ 挂载根文件系统,启动 1 号进程 init/systemd
⑥ init 拉起服务,进入多用户目标,等待登录
7.2 关键细节
- 实模式到保护模式:内核启动早期要自己搭建分页、切换到 64 位长模式。
- initrd/initramfs:临时根文件系统,内含加载真实根分区所需的驱动(如 RAID、LVM、加密)。
- init 的两种角色:作为用户态第一个进程(PID 1),负责收养孤儿进程;作为服务管理器,按依赖拉起服务。
- systemd 的并行化:用 socket 激活与依赖图替代 SysV 的串行脚本,显著缩短启动时间。
systemd-analyze # 查看总启动耗时
systemd-analyze blame # 各服务耗时排序
systemd-analyze critical-chain # 关键启动链
8. 主流系统对比与工程取舍
| 系统 | 内核类型 | 特点 |
|---|---|---|
| Linux | 宏内核 + 模块 | 开源、驱动生态最全、服务器主导 |
| Windows NT | 混合内核 | 图形子系统入内核、商业支持强 |
| macOS XNU | 混合内核 | Mach + BSD,Darwin 开源内核 |
| FreeRTOS | 微内核风格 | 实时、极小、嵌入式 |
选型思路:服务器/通用场景看生态与性能(Linux),实时确定性场景看调度可预测性(RTOS),安全关键场景看可验证性(seL4)。
9. 常见陷阱
- 把系统调用当普通函数:一次
read走一次 trap,循环里逐字节read性能灾难,应批量读。 - 混淆「用户态线程」与「内核态线程」:协程在用户态切换,不经过内核,
strace看不到。 - 忽略中断上下文限制:中断处理函数里不能睡眠、不能拿会阻塞的锁。
- 以为 fork 会立刻复制内存:现代系统用写时复制(COW),
fork后共享物理页直到写入。 - 认为微内核一定更好:隔离性好但 IPC 开销大,多数生产系统选了宏内核路线。
- 启动顺序搞错:initramfs 缺驱动会导致「找不到根分区」而 panic,排查时优先怀疑此项。
参考文章
- 进程与线程 — 内核如何抽象 CPU 与执行流
- 内存管理 — 内核的另一半核心职责
- 文件系统与 IO — 内核如何抽象存储
- IO 模型 — 系统调用之上的 IO 编程范式
- CPU 调度 — 内核调度器的具体策略
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。