29. 操作系统概述与体系结构

从整体视角认识操作系统:三大核心职责(资源管理、抽象提供、程序控制)、宏内核与微内核的体系结构之争、特权级与用户态内核态切换、系统调用的完整调用链路、中断与异常的处理时机、设备驱动模型,以及从加电自检到用户空间 init 的完整启动流程,并给出面试高频考点与工程取舍。

1. 操作系统是什么

1.1 三个核心职责

操作系统(Operating System,OS)本质上是运行在硬件之上、应用程序之下的一层系统软件,它承担三件事:

  1. 资源管理者:CPU 时间、内存空间、磁盘容量、网络带宽、外设——所有硬件资源都由内核统一分配与回收,避免多个程序直接争抢硬件。
  2. 抽象提供者:把裸硬件抽象成易用的接口。进程抽象了 CPU、虚拟内存抽象了物理内存、文件抽象了磁盘块、Socket 抽象了网卡。程序员因此不必面对寄存器和扇区。
  3. 控制程序:负责加载并调度程序的执行,处理错误与中断,维护系统安全边界(谁能访问什么)。

一句话记忆:操作系统 = 硬件资源的管家 + 硬件能力的翻译官 + 程序执行的裁判。

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微内核骨架但服务在内核态
外核 ExokernelMIT 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,排查时优先怀疑此项。

参考文章

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「计算机基础」更多文章

  1. 34. 编程范式与类型系统
  2. 33. 分布式系统基础
  3. 32. 加密与安全基础