Skip to main content

操作系统速查

··5850 words·30 mins· loading · loading · ·
GaleInk
Author
GaleInk
A Breezing Gale ~
Table of Contents
Operating Systems::Notes - This article is part of a series.
Part 11: This Article

内存管理的目标
#

  1. 抽象:给每个进程独立、连续的虚拟地址空间(“整块内存归我一个人用"的幻觉)
  2. 保护:进程 A 踩不到进程 B,用户态碰不到内核的关键数据
  3. 共享:需要共享的页(如共享库、mmap 文件)能被多个进程同时映射
  4. 透明:缺页换入换出全部由 OS 自动完成,进程无感知
  5. 效率:地址翻译要快(TLB)、缺页次数要少(合理的置换算法)、内存利用率要高(稀疏分配)

程序何时进入内存
#

  1. 编译时:编译器生成可执行文件,逻辑地址(虚拟地址)已固定,代码和数据在磁盘上
  2. 载入时(load time):加载器从磁盘读入可执行文件,建立虚拟地址空间(VMA),页表结构建立但物理页框尚未分配
  3. 运行时(run time):CPU 首次访问某页 → 页表项为空 → 缺页异常 → OS 分配物理页框、从磁盘读数据、建立 PTE

这叫 惰性加载(demand paging)——进程不是一次性全部进入内存,而是用到的页面才调入。

实际 Linux 中 execve 时建立 VMA、映射代码段和数据段,物理内存直到进程被调度运行、触发缺页时才分配。


进程空间与物理内存的对应(重定位)
#

动态重定位(运行时):每个进程有基址寄存器(或页表),CPU 发出的每个 VA 自动加基址 → PA。VA: 0x1000 → [基址: 0x50000] → PA: 0x51000

页式重定位(现代系统):VA 拆为 VPN + 页内偏移 → MMU 查页表 → 得 PFN → 拼接 PA。同一 VA 0x1000,进程 A 的页表指向物理页 5,进程 B 指向物理页 12,互不干扰——这就是"地址空间是对内存的抽象"的硬件实现。


x86-64 Linux 的虚拟地址空间
#

48 位(256 TB)。64 位 CPU 只用了低 48 位:

  • 用户空间:0x0000000000000000 ~ 0x00007FFFFFFFFFFF(128 TB)
  • 内核空间:0xFFFF800000000000 ~ 0xFFFFFFFFFFFFFFFF(128 TB)
  • 中间空洞:未使用

高 16 位必须与第 47 位一致,否则硬件报错,这叫规范地址


虚拟内存涉及的数据结构
#

进程级

  • mm_struct:进程地址空间总入口,含 VMA 链表、页目录指针(pgd
  • vm_area_struct:每个 VMA 记录起止地址、权限、映射类型
  • pgd_t / pmd_t / pud_t / pte_t:四级页表项,pgd 即 CR3 指向的页全局目录

全局级

  • 页表:VPN → PFN 的映射(多级树或反转表)
  • 页框数据库(PFN Database):追踪每个物理页框状态(Active/Standby/Modified 等)
  • 交换区映射(swap cache):记录被换出页面在磁盘的位置
  • TLB:VPN → PFN 硬件高速缓存,OS 在上下文切换时管理刷新

地址转换的作用
#

VA → PA 的翻译。没有它,地址空间隔离、按需调页、写时复制全部无法实现——每个进程都能直接看到物理地址,互相踩踏不可避免。地址转换是实现内存抽象和保护的基础硬件机制。


存储体系与虚拟内存空间的关系
#

虚拟内存空间横跨整个存储体系:

寄存器 → Cache → DRAM(物理页框) → 磁盘(交换区/文件)

  • DRAM 是虚拟内存的"高速层”——热数据驻留
  • 磁盘是虚拟内存的"容量层"——冷数据暂存
  • 进程看到 256TB 地址空间,实际物理 DRAM 只有几 GB,其余靠磁盘的页换入换出来支撑
  • 虚拟内存是连接硬件加速和软件管理的纽带

PTE 中引发 Page Fault 的控制位
#

  • P = 0:页面不在内存 → 缺页异常
  • R/W 违例:写只读页(COW 场景)→ PF
  • U/S 违例:用户态访问内核页 → PF
  • XD/NX:执行不可执行页 → PF

由 MMU 硬件检测,是硬件发出的,CPU 和 MMU 协作完成检测和处理:MMU 在地址翻译过程中发现 PTE 异常 → 硬件产生中断 0xe → CPU 陷入内核 → OS 缺页处理程序接管。


COW 为什么必须结合缺页异常
#

fork 后父子共享物理页,全部标记只读。进程写时 MMU 报 PF,OS 才分配新页、拷贝、更新 PTE。没有缺页异常,fork 只能把全部物理页立即复制,失去快速返回的能力。COW 借助 PF 把拷贝推迟到真正需要时(惰性),且 exec 直接覆盖后连拷贝都省了。


fork() 流程
#

  1. 分配新 PID + task_struct
  2. 复制 mm_struct(VMA 新建副本,不复制物理页)
  3. 两进程所有用户页标记只读,每个 VMA 标记私有 COW
  4. 创建子进程页表,PTE 指向与父进程相同的物理页
  5. 继承父进程文件描述符表、工作目录、信号处理等
  6. 子进程状态设为就绪,插入调度队列
  7. 父进程返回子进程 PID,子进程返回 0
  8. 后续任何一方的写入通过 COW 缺页触发真正的页复制

mmap 与 COW 的关系
#

MAP_PRIVATE 基于 COW 实现:映射后页面只读共享 → 进程写入时 MMU 报 PF → 内核分配新页、拷贝、更新 PTE。其他进程不受影响。MAP_SHARED 不触发 COW,写了直接改共享页,所有映射者可见。fork 的 COW 和 mmap 的 COW 底层走同一个 do_wp_page()


放置、置换、清除策略
#

放置策略:页放哪。分页系统任何空闲页框等效,段式需为变长段找合适位置。

置换策略:页框用完时赶谁。局部(只在缺页进程驻留集选) vs 全局(所有未锁定页框)。算法:OPT → FIFO → Second Chance → Clock → NRU → LRU → NFU/Aging → 工作集。

清除策略:脏页何时写回。分页守护进程批量写回脏页保证有大量干净空闲页框。双指针时钟:前指针写回脏页,后指针用于置换。


文件系统是对磁盘的抽象
#

磁盘只是一堆线性 LBA。文件系统在其上建了三层抽象:

  • 按名存取:给 LBA 起名字,查目录 → inode → 数据块 LBA
  • 结构化:目录树分门别类
  • 隐藏细节:碎片分配、空闲管理、读写调度由 FS 内部处理,用户只看到整齐的文件

类比:虚拟地址空间 = 对物理内存的抽象;文件系统 = 对磁盘地址的抽象。


万物皆是文件
#

不是所有东西都是文件,而是 OS 把所有东西当成文件操作。键盘、磁盘、socket、管道、/proc——不管底层是什么,都用 open/read/write/close 同一套接口。

OS 给每种资源分配 inode + VFS 接口 + file_operations。同一个 read 根据 inode 类型自动路由:普通文件 → ext4 驱动,socket → TCP/IP 协议栈,管道 → pipe 驱动。用户态完全不需要知道底层差异。


目录管理
#

目的:按名存取——文件名 → FCB/inode 的映射。

结构 特点 局限
单级 所有文件一个目录,简单 重名冲突,查找遍历全部
两级 每个用户一个目录 不能共享
树形 多级嵌套,绝对/相对路径 需处理路径解析
无环图 树 + 硬链接/软链接 删除需引用计数防悬空指针

文件打开流程 open()
#

  1. 路径解析:逐级查目录 /usrastmbox,拿到 inode 号
  2. 查系统打开文件表:inode 已打开 → 引用计数+1;否则填入空表项,计数=1
  3. 权限检查:对比打开方式与用户权限
  4. 填用户打开文件表:进程 PCB 中取空表项,填打开方式、读写指针(初值0),指向系统打开文件表
  5. 返回 fd:非负整数,后续 read/write 靠它索引

同步问题解法
#

软件解法

方案 思路 问题
方案1 while(free); free=true; — free=false lock() 不原子
方案2 while(not turn); — turn=other 强制轮流,无空不让进
方案3 pturn=true; while(qturn); — pturn=false After you 死锁
Dekker(1965) pturn/qturn + turn 裁决 首个正确解决
Peterson(1981) interested[2] + turn 更简洁,替代 Dekker

硬件解法——关中断:关中断 → 临界区 → 开中断。符合"有空让进"和"无空等待",但不符合"有限等待"和"让权等待":中断关闭期间调度器无法抢占,其他进程连 CPU 都得不到。仅限内核使用(保证临界区极短),不能暴露给用户进程。

硬件解法——TSL 指令:原子"读-改-写"。对多处理器有效——TSL 靠总线锁保证原子性,执行时锁住内存总线,所有 CPU 都无法同时访问同一地址。关中断只锁本 CPU 所以多核失效,TSL 用总线锁替代,对所有 CPU 一视同仁。但忙等待形成自旋锁,且存在优先级反转问题。


如何感知设备状态并管理设备
#

1. 感知设备状态
#

三种方式从主动到被动:

方式 原理 CPU开销 适用
轮询 CPU 循环读状态寄存器,检测 ready/busy 位 高(空转) 简单低速设备
中断 设备完成操作后发中断信号,CPU 响应后读状态 低(只响应时参与) 大多数设备
DMA 完成中断 DMA 控制器传输完一块数据后发中断 最低 磁盘、网卡

设备状态信息存储在设备控制器的寄存器中:控制寄存器(CPU 写命令)、状态寄存器(CPU 读 busy/ready/error)、数据寄存器(读写数据)。

2. 管理设备的数据结构
#

  • 设备控制块(DCB / UCB):每设备一个,记录设备类型、标识符、状态(忙/闲)、等待队列指针
  • 控制器控制块:记录控制器状态、连接设备
  • 设备队列:将同类设备链成队列,方便分配时遍历
  • I/O 请求包(IORB / IRP):每个 I/O 请求的动态结构,含操作类型、LBA、缓冲区地址、优先级等

3. 设备分配策略
#

策略 说明 适用设备
静态分配 进程创建时分配,结束时回收 独占设备
动态分配 请求时分配,用完立即回收 独占设备
共享/分时 请求排队,分时轮流服务 磁盘等共享设备
虚拟分配(SPOOLing) 用磁盘模拟独占设备,请求排队 打印机

4. 设备驱动与中断处理协同
#

应用程序: read() → VFS → 文件系统 → 块层
设备驱动程序: 填命令 → 写控制寄存器 → 启动 I/O → 阻塞自己
设备控制器: 独立完成操作
中断处理程序: 读状态寄存器 → 检查错误 → 唤醒驱动程序
驱动程序: 从数据寄存器读结果 → 拷到用户缓冲区 → 返回

5. 设备无关性
#

OS 通过分层屏蔽硬件差异——用户使用逻辑设备名,系统维护逻辑设备→物理设备的映射表。设备作为特殊文件统一用 open/read/write/ioctl 访问,不同设备对应不同 file_operations 实现。


如何解决设备差异性大的问题
#

核心思路:分层 + 抽象 + 多态
#

不同设备接口、命令集、速度、数据格式完全不同。OS 的对策是把差异封装在驱动层,向上提供统一接口

1. 设备分类抽象
#

将设备归为三大类,每类一个统一接口:

类型 统一接口 底层差异在哪
字符设备 read/write/ioctl(字节流) 键盘/鼠标/串口各不同,驱动实现各自的 read
块设备 read/write(块为单位)+ 文件系统 HDD/SSD/NVMe 寻址方式不同,驱动翻译 LBA
网络设备 send/recv(报文) 以太网/WiFi/蓝牙协议栈不同

2. 设备驱动的多态实现
#

Unix/Linux 使用 file_operations 函数指针表实现多态:

struct file_operations {
    int (*open)(struct inode *, struct file *);
    ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
    int (*ioctl)(struct inode *, struct file *, unsigned int, unsigned long);
    // ...
};

用户调用 read(fd) → VFS 查 fd 对应的 filefile->f_op->read() → 路由到具体设备驱动。切换设备只需换函数指针,上层代码一行不改。

3. 分层递进
#

用户程序             "打开摄像头" / "播放音频" / "读文件"
    ↓                    ↑ 统一接口,与设备无关
VFS / 设备无关层      设备命名、权限检查、缓冲区管理
    ↓                    ↑ 统一块大小,缓冲策略
设备驱动程序          键盘驱动 / 磁盘驱动 / 网卡驱动 (设备特异部分)
    ↓                    ↑ 读写寄存器、处理中断
设备控制器            硬件接口

4. 设备独立性的关键机制
#

  • 逻辑设备名 → 物理设备映射:用户用 /dev/tty,OS 查表转为具体设备号
  • inode 中的设备号major 号定驱动,minor 号定具体设备实例
  • I/O 重定向:标准输入可来自键盘、管道、文件,进程不用改代码
  • 缓冲区统一管理:不同块设备共用同一缓冲池,屏蔽扇区大小差异

以打印机为例说明 I/O 软件的分层思想
#

场景:用户执行 lpr mydoc.txt
#

用户进程:    lpr mydoc.txt
              ↓  系统调用 write()
──────────────────────────────────────────────
用户级 I/O:  格式化输出 → 放入 spool 目录  "/var/spool/printer/mydoc"
              ↓  发消息给打印 daemon
──────────────────────────────────────────────
设备无关层:  命名解析(/dev/lp0 → major,minor)
             权限检查(用户有写权限吗?)
             分配设备(打印机空闲吗? 否则排队)
             SPOOLing: 用磁盘文件模拟独占
              ↓  I/O 请求包(写操作 + 数据缓冲区)
──────────────────────────────────────────────
设备驱动程序: 接收抽象请求
             翻译: 通用格式 → 打印机控制命令(ESC/P)
             写控制寄存器: 设置页长、份数
             写数据寄存器: 逐个字节送打印缓冲区
             启动 I/O → 阻塞等待完成
──────────────────────────────────────────────
中断处理程序: 打印机缺纸/完成一页时产生中断
             读状态寄存器 → 缺纸? 完成? 错误?
             正常完成 → 唤醒驱动程序
             缺纸 → 发信号通知用户
──────────────────────────────────────────────
硬件:         打印机控制器接收命令 → 物理打印

分层的价值
#

  • 用户:只需要 lpr,不知道打印机型号
  • 设备无关层:只管排队、权限、SPOOLing,不关心 ESC/P 还是 PostScript
  • 驱动程序:只管翻译命令和操作寄存器,换打印机只换这一层
  • 中断处理:只管状态响应,不关心业务逻辑

每层职责单一,上层不依赖下层细节,新设备只需写一个新的驱动程序即可接入整个 I/O 栈。


为什么引入缓冲技术
#

缓冲是操作系统中最早引入的技术之一。核心动机:

1. 解决 CPU 与 I/O 速度不匹配
#

CPU 处理数据是纳秒级,磁盘读写是毫秒级,差 6 个数量级。没有缓冲,每一次 read 都要等磁盘寻道+旋转,CPU 几乎全部时间在空转。

无缓冲: CPU → 等磁盘 → 拿到 1 字节 → CPU → 等磁盘 → 拿到 1 字节 → ...
有缓冲: CPU → 等磁盘 → 一次读 4KB 进缓冲区 → CPU 从缓冲区逐字节取 → 全部命中

一次磁盘 I/O 搬一卡车(4KB),后续 CPU 从缓冲区零取——99% 的"读"直接走内存,不需要磁盘。

2. 减少中断次数
#

没有缓冲时每处理一个字符都要发一次中断(键盘每敲一个键就是一个中断)。有了缓冲,中断处理只把数据丢进缓冲区,等缓冲区积攒一批后再一次性通知上层处理——中断频率从"每字节一次"降到"每缓冲区一次"。

3. 提高 CPU 与设备的并行性
#

双缓冲的精髓在于流水线:

缓冲区 1 被 CPU 处理时       ← 同时 →      设备向缓冲区 2 填入新数据
缓冲区 2 被 CPU 处理时       ← 同时 →      设备向缓冲区 1 填入新数据

CPU 和设备永不互相等待,吞吐量接近两者的最大速率。

4. 缓冲区类型对比
#

方案 原理 并行度 开销
单缓冲 1 个缓冲区,CPU 和设备串行 最小
双缓冲 2 个缓冲区交替使用
缓冲池 N 个缓冲区,生产者-消费者模型 管理复杂

5. 实际案例:UNIX 缓冲池
#

约 200 个缓冲区(各 512/1024B),每个挂在 av 链(空闲)和 b 链(设备散列队列)上。读到缓冲后一直保留在 b 链中——同一盘块被再次访问时直接命中,零磁盘 I/O。这就是 page cache 的前身。


如何提升 CPU 与设备的访问性能
#

1. 减少 CPU 等待 I/O
#

技术 原理 效果
中断驱动 I/O 发起 I/O 后 CPU 切去干别的事,设备完成时中断通知 CPU 无需轮询等待
DMA 设备控制器直接访问总线,数据在内存和外设间搬运,CPU 只参与传输开始和结束 CPU 彻底摆脱数据搬运
异步 I/O 发起 I/O 后立即返回,CPU 继续执行,I/O 完成后回调通知 I/O 和计算完全重叠
I/O 通道 专用的 I/O 协处理器,有自己的指令集和局部内存 CPU 只需发高级指令

2. 减少 I/O 频率
#

技术 原理
缓冲/缓存 内存中保留热数据,读命中直接返回 page cache、buffer pool
预读 空间局部性——顺序读时提前把后续块调入 一次读 4KB → 预取后 64KB
批处理 多个小 I/O 合并为一个大 I/O elevator 算法合并相邻请求
延迟写 多次写合并到缓冲区,到期一次写回 写磁盘攒够一批再刷

3. 提高设备端效率
#

技术 原理
磁盘调度算法 SSTF → SCAN → C-SCAN,减少寻道时间
电梯算法 磁头来回扫,沿途处理所有请求,减少平均寻道
I/O 请求重排 块层将请求按 LBA 排序后下发,减少机械运动
多队列 NVMe 多队列并行处理,每个 CPU 核一个提交队列

4. 演进路径
#

CPU 直接控制 → 控制器 + 可编程 I/O → 中断驱动 → DMA → I/O 处理器

每一步都在让 CPU 离设备更远、做更少的事。


Windows PFN 数据库的设计思想
#

核心思想:状态机统一管理所有物理页框
#

PFN 数据库以一维数组(下标 = 页框号)追踪系统中每一个物理页框的状态,每个页框在以下状态间流转:

Active → Standby/Modified → Free → Zeroed → Active

体现的设计思想
#

1. 缓存思维的延续——软缺页

进程访问了一个不在工作集中但仍在 Standby 链表中的页,不需要任何磁盘 I/O,直接取回即用。物理内存本身被当作磁盘的缓存——被移出的页面不立即丢弃,而是保留内容挂在 Standby 链表,作为"可能还会用的缓存"。这和 CPU 的 L1/L2/L3 缓存层级在本质上是一个思想:用空间换时间,用上层缓存避免下层慢速访问。

2. 生产者-消费者分工

系统线程 角色 负责的状态转换
工作集管理器 决策者 Active → Standby/Modified(修剪)
Modified Page Writer 写回者 Modified → Standby(写回脏页)
零页线程 准备者 Free → Zeroed(清零备用)
平衡集管理器 调节者 全局并发度控制

每个线程干一件专一的事,通过状态机的约束自然协同——不需要复杂的锁协议,各自改各自负责的链表。

3. 懒惰策略(Lazy)

  • 脏页不立即写回,等攒一批由 Modified Page Writer 批量写(减少磁盘寻道)
  • 空闲页不立即清零,由零页线程后台异步做(分配时直接从 Zeroed 取)
  • 页面被移出工作集后不立即释放,先放 Standby 待复用(软缺页)

三个"不立即"反映同一个原则:推迟开销到真正需要的时候,用后台线程在 CPU 空闲时做脏活。

4. 物理内存的反向索引

传统页表是 VA → PA,PFN 数据库是 PA → 状态。它回答了"这个物理页框现在在干什么、被谁用着、能回收吗"——这是页面置换决策、工作集修剪、内存压力处理的基础。

5. 集中式管理 vs 分散式管理

Linux 用 buddy system + slab 管理物理内存,信息分散在各分配器数据结构中。Windows 选择集中式 PFN 数据库——查任何一个物理页框的状态只需一次数组索引 PFN[frame_num]。集中式的好处是 O(1) 查询,缺点是需要维护全局一致的状态转换规则。


I/O 管理的主要任务
#

  1. 设备分配与回收:响应进程请求,根据设备状态分配/回收独占或共享设备
  2. 提供统一接口:用户用逻辑设备名,系统完成逻辑→物理映射,屏蔽硬件差异
  3. 驱动 I/O 执行:翻译抽象请求为设备命令,写控制寄存器,启动 I/O 操作
  4. 中断处理:响应设备中断信号,读状态寄存器,唤醒阻塞进程
  5. 缓冲区管理:管理 I/O 缓冲池,减少 CPU 与设备速度差距
  6. 性能优化:利用中断、DMA、异步 I/O 提高 CPU 与设备的并行度

如何提升文件系统性能
#

1. 缓存——减少磁盘 I/O 的根本手段
#

缓存类型 缓存什么 命中效果
page cache 文件数据页 读写在内存完成,零磁盘 I/O
dentry cache 目录项(文件名→inode) 路径解析不走磁盘
inode cache 文件的 inode 信息 省去读 inode 区的磁盘访问
buffer cache 磁盘块原始数据 老 UNIX 的块级缓存

2. 预读与延迟写——减少 I/O 次数
#

技术 原理 效果
预读 顺序读时提前把后续块读入 page cache 读文件时大部分命中缓存
延迟写 修改先写缓存,后台定期批量写回 多次写合并为一次磁盘写
预分配 创建文件时一次分配多个连续块 减少后续扩展时的分配开销

3. 磁盘布局优化
#

技术 原理 代表
块组 数据块和 inode 放在同一块组,减少寻道 ext2/ext4
extent(区段) 用"起始块+连续长度"代替逐个块号,减少元数据 ext4、NTFS
延迟分配 写入缓存时不立即分配磁盘块,写回时一次分配连续块 ext4、XFS
碎片整理 后台重组文件数据为连续块 Windows 碎片整理

4. 目录查找加速
#

技术 原理
目录项索引 B 树代替线性表,大目录查找 O(log N)
散列 dentry 文件名 hash 直接定位,O(1)
目录项缓存 常用目录项在内存中,不走磁盘

5. 磁盘调度
#

算法 原理
SSTF 选寻道距离最近的请求先服务
SCAN/电梯 磁头来回扫,沿途处理所有请求
C-SCAN 单向扫描到底,减少最远端的等待方差
Deadline SCAN + 超时强制处理,防饥饿

6. 日志/写时复制——原子性换性能
#

技术 原理
日志(journal) 元数据修改先记日志,crash 后 replay,避免 fsck 慢扫全盘
写时复制(COW) 修改时写新位置,不原地覆盖,写入集中为连续写
Operating Systems::Notes - This article is part of a series.
Part 11: This Article