Operating Systems: Three Easy Pieces(OSTEP)学习:第36章:I/O 设备详解
本章讲的是操作系统如何与各种输入/输出(I/O)设备打交道。想象一下:没有输入的程序每次运行结果都一样,没有输出的程序运行了也没意义。所以 I/O 对计算机系统来说至关重要。计算机里的设备不是随便接的,而是按照速度和重要性分层连接:为什么要这样分层?存储接口的演进历史:ATA → SATA → eSATA,每一代性能都更强。一个设备在逻辑上分为两部分:接口:操作系统通过读写寄存器来控制设备,就像操
概述
本章讲的是操作系统如何与各种输入/输出(I/O)设备打交道。想象一下:没有输入的程序每次运行结果都一样,没有输出的程序运行了也没意义。所以 I/O 对计算机系统来说至关重要。
一、系统架构:设备是怎么连到电脑上的?
1.1 经典层次结构
计算机里的设备不是随便接的,而是按照速度和重要性分层连接:
+------------------+
| CPU |
+------------------+
|
内存总线(最快)
|
+------------------+
| 内存 (RAM) |
+------------------+
|
通用 I/O 总线(如 PCI)
|
+------------------+
| 高性能设备 | ← 显卡等
+------------------+
|
外围 I/O 总线(如 SCSI, SATA, USB)
|
+------------------+
| 慢速设备 | ← 磁盘、鼠标、键盘
+------------------+
为什么要这样分层?
- 物理限制:总线越快,物理长度就必须越短,能插的设备就越少
- 成本:高性能总线造价昂贵
- 实用性:慢速设备多,放在外围总线上可以插很多个
1.2 现代系统架构(Intel Z270 芯片组为例)
CPU ←→ 内存(最近,最快)
|
└→ 显卡(PCIe,高性能图形)
|
└→ I/O 芯片(通过 DMI 连接)
|
├→ 硬盘(eSATA)
├→ 键盘/鼠标(USB)
├→ 网卡(PCIe)
└→ 高性能存储(NVMe,通过 PCIe)
存储接口的演进历史:ATA → SATA → eSATA,每一代性能都更强。
二、标准设备的结构
一个设备在逻辑上分为两部分:
+---------------------------+
| 设备 |
| +---------------------+ |
| | 接口(对外) | | ← 操作系统看到的部分
| | 状态寄存器 | |
| | 命令寄存器 | |
| | 数据寄存器 | |
| +---------------------+ |
| +---------------------+ |
| | 内部实现(对内) | | ← 设备自己的逻辑
| | 微控制器(CPU) | |
| | 内存(DRAM/SRAM) | |
| | 专用芯片 | |
| +---------------------+ |
+---------------------------+
- 接口:操作系统通过读写寄存器来控制设备,就像操控一台机器的按钮和旋钮
- 内部实现:设备内部怎么运作,OS 不关心,就像你开车不需要懂发动机原理
三、标准协议:OS 怎么和设备通信?
最基本的通信流程(伪代码):
// 第一步:等设备空闲
while (STATUS == BUSY)
; // 一直等,啥都不做(轮询)
// 第二步:写入数据
write data to DATA register;
// 第三步:写入命令(告诉设备"开始干活")
write command to COMMAND register;
// 第四步:等设备完成
while (STATUS == BUSY)
; // 再次等待
这种方式叫做轮询(Polling):操作系统不断地问设备"你好了没?你好了没?"
问题:CPU 在等待期间什么有用的事都没做,白白浪费了算力。
四、中断:让 CPU 不再傻等
4.1 中断的思路
与其让 CPU 一直问设备好没好,不如让设备干完了主动通知 CPU。这就是中断(Interrupt)。
流程:
- OS 向设备发出请求
- OS 把当前进程睡眠,切换去跑其他进程
- 设备完成后,发出硬件中断信号
- CPU 跳转到**中断服务例程(ISR)**处理结果
- 唤醒等待 I/O 的进程,继续执行
4.2 轮询 vs 中断的时间线对比
轮询方式(低效):
时间线 →
CPU: [进程1][进程1][进程1][等][等][等][等][等][进程1][进程1]
磁盘: [磁盘工作中............][完成]
CPU 在磁盘工作期间完全空转,浪费严重。
中断方式(高效):
时间线 →
CPU: [进程1][进程1][进程1][进程2][进程2][进程2][进程1][进程1]
磁盘: [磁盘工作中............][完成→中断]
CPU 在等待磁盘时去跑进程2,磁盘完成后中断通知,再回来继续进程1。
4.3 中断并非万能
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 设备很慢(如机械硬盘) | 中断 | 等待时间长,值得切换进程 |
| 设备很快(如高速SSD) | 轮询 | 切换成本反而大于等待成本 |
| 速度不确定 | 混合(先轮询,超时再中断) | 两者折中 |
| 网络大量数据包涌入 | 轮询 | 防止"活锁"(livelock) |
活锁(Livelock):网络包不断到来,每个包都触发中断,OS 一直在处理中断,用户进程永远得不到运行机会,系统表面上很忙但实际上没干有用的事。
4.4 中断合并(Interrupt Coalescing)
设备不立刻发中断,而是等一小会儿,把多个完成的操作合并成一个中断,减少中断处理开销。
权衡:等太久会增加单次请求的延迟。
五、DMA:解放 CPU 的数据搬运工
5.1 问题:PIO 的低效
程序化 I/O(PIO, Programmed I/O):CPU 亲自把数据一个字一个字地从内存搬到设备(或反过来)。
时间线(PIO 方式):
CPU: [进程1][进程1][搬][搬][搬][进程2][进程2][进程1]
磁盘: [工作中.........][完成]
其中 [搬] 表示 CPU 在做数据复制,这段时间 CPU 既没在跑用户程序,也没在做有意义的计算。
5.2 解决方案:DMA(直接内存访问)
DMA 引擎是一个专门负责搬运数据的硬件,让 CPU 从搬运工作中解放出来。
工作流程:
- OS 告诉 DMA 引擎:数据在哪里、搬多少、搬到哪个设备
- CPU 去做别的事
- DMA 自己完成数据搬运
- DMA 完成后,发中断通知 CPU
时间线(DMA 方式):
CPU: [进程1][进程1][进程2][进程2][进程2][进程1][进程1]
DMA: [搬数据........][完成→中断]
磁盘: [工作中.....][完成]
CPU 和 DMA 并行工作,效率大幅提升。
六、设备通信的两种方式
方式一:专用 I/O 指令
用专门的汇编指令(如 x86 的 in/out)直接读写设备寄存器。
// x86 汇编概念示意(非真实C代码)
// 向端口 port 写入一个字节
outb(port, value);
// 从端口 port 读取一个字节
value = inb(port);
这类指令是特权指令,只有操作系统才能执行,普通用户程序不能直接访问设备。
方式二:内存映射 I/O(Memory-Mapped I/O)
把设备寄存器映射到内存地址空间,OS 用普通的内存读写指令(load/store)来操控设备。硬件负责把这些访问路由到设备而不是真实内存。
内存地址空间:
0x0000 ~ 0x7FFF → 真实内存
0x8000 ~ 0x8003 → 设备A的寄存器(映射)
0x8004 ~ 0x8007 → 设备B的寄存器(映射)
两种方式各有优缺点,现代系统两者都在用。
七、设备驱动程序:统一接口的秘密
7.1 问题
文件系统想读写数据,但底层可能是 SCSI 硬盘、IDE 硬盘、USB 闪存……每种设备接口都不同,文件系统怎么统一处理?
7.2 解决方案:抽象层次
- 设备驱动程序(Device Driver):一段知道如何与特定设备通信的内核代码
- 文件系统只需要调用通用接口,具体细节由驱动处理
- 上层完全不知道底层是什么设备
7.3 驱动程序的现实
- Linux 内核中超过 70% 的代码是设备驱动
- 驱动通常由硬件厂商的"业余"开发者编写(相对于核心内核开发者)
- 驱动的 bug 数量约是核心内核代码的 7倍,是内核崩溃的主要来源
八、实战:IDE 磁盘驱动代码详解
8.1 IDE 设备寄存器
| 地址 | 寄存器 | 作用 |
|---|---|---|
| 0x1F0 | 数据端口 | 读写数据 |
| 0x1F1 | 错误寄存器 | 发生错误时查看原因 |
| 0x1F2 | 扇区数 | 要读写几个扇区 |
| 0x1F3~0x1F5 | LBA 低/中/高字节 | 逻辑块地址(要读写哪个位置) |
| 0x1F6 | 设备选择 | 主盘/从盘 |
| 0x1F7 | 命令/状态 | 发命令或读状态 |
| 0x3F6 | 控制寄存器 | 复位、中断使能 |
状态寄存器各位含义(0x1F7):
状态寄存器 = B U S Y ⏟ b 7 R E A D Y ⏟ b 6 F A U L T ⏟ b 5 S E E K ⏟ b 4 D R Q ⏟ b 3 C O R R ⏟ b 2 I N D E X ⏟ b 1 E R R O R ⏟ b 0 \text{状态寄存器} = \underbrace{BUSY}_{b7} \underbrace{READY}_{b6} \underbrace{FAULT}_{b5} \underbrace{SEEK}_{b4} \underbrace{DRQ}_{b3} \underbrace{CORR}_{b2} \underbrace{INDEX}_{b1} \underbrace{ERROR}_{b0} 状态寄存器=b7
BUSYb6
READYb5
FAULTb4
SEEKb3
DRQb2
CORRb1
INDEXb0
ERROR
- B U S Y = 1 BUSY = 1 BUSY=1:设备正忙,不能接受命令
- R E A D Y = 1 READY = 1 READY=1:设备准备好了
- D R Q = 1 DRQ = 1 DRQ=1:设备请求数据传输
- E R R O R = 1 ERROR = 1 ERROR=1:发生错误,查看 0x1F1
8.2 完整可运行的 C 语言模拟代码
以下代码在 Linux 用户态模拟 IDE 驱动的逻辑(实际驱动需要在内核态运行,使用 inb/outb 指令)。
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
/* =====================================================
* IDE 寄存器地址定义
* ===================================================== */
#define IDE_DATA_PORT 0x1F0 /* 数据端口 */
#define IDE_ERROR_PORT 0x1F1 /* 错误寄存器 */
#define IDE_SECTOR_COUNT 0x1F2 /* 扇区数量 */
#define IDE_LBA_LOW 0x1F3 /* LBA 低8位 */
#define IDE_LBA_MID 0x1F4 /* LBA 中8位 */
#define IDE_LBA_HIGH 0x1F5 /* LBA 高8位 */
#define IDE_DRIVE_HEAD 0x1F6 /* 设备/磁头选择 */
#define IDE_CMD_STATUS 0x1F7 /* 命令/状态寄存器 */
#define IDE_CONTROL 0x3F6 /* 控制寄存器 */
/* 状态寄存器各位的掩码 */
#define IDE_BSY 0x80 /* Busy: 设备忙,bit7 */
#define IDE_DRDY 0x40 /* Drive Ready: 设备就绪,bit6 */
#define IDE_DF 0x20 /* Drive Fault: 设备故障,bit5 */
#define IDE_DRQ 0x08 /* Data Request: 数据请求,bit3 */
#define IDE_ERR 0x01 /* Error: 错误标志,bit0 */
/* IDE 命令 */
#define IDE_CMD_READ 0x20 /* 读扇区命令 */
#define IDE_CMD_WRITE 0x30 /* 写扇区命令 */
/* 扇区大小:512 字节 */
#define SECTOR_SIZE 512
/* 模拟磁盘的总扇区数 */
#define DISK_SECTORS 1024
/* =====================================================
* 模拟磁盘存储空间(真实驱动中这是实际硬件)
* ===================================================== */
static uint8_t fake_disk[DISK_SECTORS][SECTOR_SIZE];
/* 模拟寄存器状态 */
static uint8_t reg_status = IDE_DRDY; /* 初始状态:设备就绪 */
static uint8_t reg_error = 0;
static uint8_t reg_sectors = 0;
static uint32_t reg_lba = 0;
static uint8_t reg_command = 0;
/* =====================================================
* 模拟 outb:向端口写一个字节
* 真实内核中是汇编指令 outb %al, %dx
* ===================================================== */
static void outb(uint16_t port, uint8_t value) {
/* 这里只更新我们的模拟寄存器 */
if (port == IDE_SECTOR_COUNT) reg_sectors = value;
else if (port == IDE_LBA_LOW) reg_lba = (reg_lba & ~0xFF) | value;
else if (port == IDE_LBA_MID) reg_lba = (reg_lba & ~0xFF00) | ((uint32_t)value << 8);
else if (port == IDE_LBA_HIGH) reg_lba = (reg_lba & ~0xFF0000) | ((uint32_t)value << 16);
else if (port == IDE_CMD_STATUS) reg_command = value;
/* 其他寄存器忽略 */
}
/* =====================================================
* 模拟 inb:从端口读一个字节
* 真实内核中是汇编指令 inb %dx, %al
* ===================================================== */
static uint8_t inb(uint16_t port) {
if (port == IDE_CMD_STATUS) return reg_status;
if (port == IDE_ERROR_PORT) return reg_error;
return 0;
}
/* =====================================================
* 第一步:等待设备就绪
* 不断读状态寄存器,直到 BUSY=0 且 READY=1
* 这就是"轮询(Polling)"
* ===================================================== */
static int ide_wait_ready(void) {
uint8_t r;
int timeout = 10000;
/* 轮询:反复读状态寄存器 */
while (timeout-- > 0) {
r = inb(IDE_CMD_STATUS);
/* 设备不忙 且 设备就绪 → 可以操作了 */
if (!(r & IDE_BSY) && (r & IDE_DRDY)) {
return 0; /* 成功 */
}
}
/* 超时:设备没有响应 */
fprintf(stderr, "错误:IDE 设备超时,未就绪\n");
return -1;
}
/* =====================================================
* 第二步:向寄存器写入参数,发出命令
* lba = 逻辑块地址(要读写第几个扇区)
* is_write = 1 表示写操作,0 表示读操作
* buf = 数据缓冲区
* ===================================================== */
static int ide_start_request(uint32_t lba, int is_write, uint8_t *buf) {
/* 等待设备就绪 */
if (ide_wait_ready() < 0) return -1;
/* 写控制寄存器:启用中断(真实驱动中需要) */
outb(IDE_CONTROL, 0);
/* 写扇区数量:一次操作1个扇区 */
outb(IDE_SECTOR_COUNT, 1);
/* 写 LBA 地址(28位模式)
* LBA(Logical Block Address)= 逻辑块地址
* 把28位地址分3个字节写入
*/
outb(IDE_LBA_LOW, (uint8_t)(lba & 0xFF)); /* 低8位 */
outb(IDE_LBA_MID, (uint8_t)((lba >> 8) & 0xFF)); /* 中8位 */
outb(IDE_LBA_HIGH, (uint8_t)((lba >> 16) & 0xFF)); /* 高8位 */
/* 写设备选择:LBA模式,主盘,LBA最高4位 */
/* 0xE0 = 1110 0000,bit6=LBA模式,bit5=1,bit7=1 */
outb(IDE_DRIVE_HEAD, 0xE0 | ((lba >> 24) & 0x0F));
if (is_write) {
/* 发出写命令 */
outb(IDE_CMD_STATUS, IDE_CMD_WRITE);
/* 模拟数据写入磁盘 */
if (lba >= DISK_SECTORS) {
fprintf(stderr, "错误:LBA %u 超出磁盘范围\n", lba);
return -1;
}
memcpy(fake_disk[lba], buf, SECTOR_SIZE);
printf(" [模拟] 数据已写入扇区 %u\n", lba);
} else {
/* 发出读命令 */
outb(IDE_CMD_STATUS, IDE_CMD_READ);
/* 等待数据就绪(DRQ=1) */
if (ide_wait_ready() < 0) return -1;
/* 模拟从磁盘读数据 */
if (lba >= DISK_SECTORS) {
fprintf(stderr, "错误:LBA %u 超出磁盘范围\n", lba);
return -1;
}
memcpy(buf, fake_disk[lba], SECTOR_SIZE);
printf(" [模拟] 数据已从扇区 %u 读出\n", lba);
}
return 0;
}
/* =====================================================
* 对外接口:读一个扇区
* lba = 扇区号,buf = 存放数据的缓冲区(至少512字节)
* ===================================================== */
int ide_read_sector(uint32_t lba, uint8_t *buf) {
printf("→ IDE 读操作:扇区 %u\n", lba);
if (ide_start_request(lba, 0, buf) < 0) return -1;
printf(" 读操作完成\n");
return 0;
}
/* =====================================================
* 对外接口:写一个扇区
* lba = 扇区号,buf = 要写入的数据(512字节)
* ===================================================== */
int ide_write_sector(uint32_t lba, const uint8_t *buf) {
printf("→ IDE 写操作:扇区 %u\n", lba);
/* 去掉 const 以复用函数,实际不会修改 */
if (ide_start_request(lba, 1, (uint8_t *)buf) < 0) return -1;
printf(" 写操作完成\n");
return 0;
}
/* =====================================================
* 演示中断处理逻辑(简化版)
* 真实中断处理程序(ISR)会在设备完成后被硬件触发
* ===================================================== */
void ide_interrupt_handler(void) {
printf(" [中断] 设备操作完成,唤醒等待进程\n");
/* 真实驱动中还会:
* 1. 读取状态确认完成
* 2. 如果是读操作,从数据端口取出数据
* 3. 唤醒等待该 I/O 的进程
* 4. 如果队列中还有请求,发起下一个
*/
}
/* =====================================================
* 演示 DMA 工作原理(概念演示)
* ===================================================== */
void demo_dma_concept(void) {
printf("\n=== DMA 工作流程演示 ===\n");
printf("1. OS 告诉 DMA:数据在内存地址 X,大小 512B,发给磁盘\n");
printf("2. CPU 去做别的事(运行其他进程)\n");
printf("3. DMA 引擎自己把数据从内存搬到设备...\n");
printf("4. [中断] DMA 完成,通知 CPU\n");
printf(" CPU 知道传输完成,继续原来的任务\n");
printf("=== DMA 演示结束 ===\n\n");
}
/* =====================================================
* 主函数:演示完整的读写流程
* ===================================================== */
int main(void) {
uint8_t write_buf[SECTOR_SIZE];
uint8_t read_buf[SECTOR_SIZE];
const char *msg = "Hello, IDE Driver! 这是写入磁盘的数据。";
printf("===== IDE 磁盘驱动模拟程序 =====\n\n");
/* 准备写入数据 */
memset(write_buf, 0, SECTOR_SIZE);
strncpy((char *)write_buf, msg, SECTOR_SIZE - 1);
/* 演示写操作 */
printf("-- 写操作 --\n");
if (ide_write_sector(0, write_buf) != 0) {
fprintf(stderr, "写入失败!\n");
return 1;
}
/* 模拟中断通知 */
ide_interrupt_handler();
printf("\n-- 读操作 --\n");
/* 演示读操作 */
memset(read_buf, 0, SECTOR_SIZE);
if (ide_read_sector(0, read_buf) != 0) {
fprintf(stderr, "读取失败!\n");
return 1;
}
ide_interrupt_handler();
/* 验证数据一致性 */
printf("\n-- 验证 --\n");
if (memcmp(write_buf, read_buf, SECTOR_SIZE) == 0) {
printf("验证通过:读写数据完全一致\n");
printf("读出的内容:\"%s\"\n", (char *)read_buf);
} else {
printf("验证失败:数据不一致!\n");
}
/* DMA 概念演示 */
demo_dma_concept();
printf("===== 演示结束 =====\n");
return 0;
}
https://godbolt.org/z/jGxPn5qTr
8.3 代码运行示例输出
===== IDE 磁盘驱动模拟程序 =====
-- 写操作 --
→ IDE 写操作:扇区 0
[模拟] 数据已写入扇区 0
写操作完成
[中断] 设备操作完成,唤醒等待进程
-- 读操作 --
→ IDE 读操作:扇区 0
[模拟] 数据已从扇区 0 读出
读操作完成
[中断] 设备操作完成,唤醒等待进程
-- 验证 --
验证通过:读写数据完全一致
读出的内容:"Hello, IDE Driver! 这是写入磁盘的数据。"
=== DMA 工作流程演示 ===
1. OS 告诉 DMA:数据在内存地址 X,大小 512B,发给磁盘
2. CPU 去做别的事(运行其他进程)
3. DMA 引擎自己把数据从内存搬到设备...
4. [中断] DMA 完成,通知 CPU
CPU 知道传输完成,继续原来的任务
=== DMA 演示结束 ===
===== 演示结束 =====
九、完整知识结构图
十、核心概念总结
| 概念 | 一句话解释 | 解决的问题 |
|---|---|---|
| 轮询 | CPU 不断询问设备状态 | 简单,但浪费 CPU |
| 中断 | 设备完成后主动通知 CPU | CPU 可以去做别的事 |
| DMA | 专用硬件负责搬数据 | CPU 从数据搬运中解放 |
| 中断合并 | 多个完成事件合并一次通知 | 减少中断次数 |
| 设备驱动 | 封装设备细节的内核代码 | 上层无需关心设备差异 |
| 内存映射 I/O | 设备寄存器当内存用 | 无需专用指令 |
| 专用 I/O 指令 | in/out 指令直接访问设备 | 明确区分设备访问 |
十一、关键公式与量化关系
轮询的 CPU 利用率(最坏情况,设备很慢时):
C P U u t i l = T c o m p u t e T c o m p u t e + T I O ≈ 0 (当 T I O ≫ T c o m p u t e ) CPU_{util} = \frac{T_{compute}}{T_{compute} + T_{IO}} \approx 0 \quad \text{(当 } T_{IO} \gg T_{compute}\text{)} CPUutil=Tcompute+TIOTcompute≈0(当 TIO≫Tcompute)
使用中断后的理想 CPU 利用率:
C P U u t i l = T c o m p u t e + T I O T c o m p u t e + T I O = 1 (完全重叠) CPU_{util} = \frac{T_{compute} + T_{IO}}{T_{compute} + T_{IO}} = 1 \quad \text{(完全重叠)} CPUutil=Tcompute+TIOTcompute+TIO=1(完全重叠)
实际中断开销分析——当设备很快时,中断反而慢于轮询:
T i n t e r r u p t = T c o n t e x t _ s w i t c h + T I S R + T c o n t e x t _ s w i t c h _ b a c k T_{interrupt} = T_{context\_switch} + T_{ISR} + T_{context\_switch\_back} Tinterrupt=Tcontext_switch+TISR+Tcontext_switch_back
若 T I O < T i n t e r r u p t T_{IO} < T_{interrupt} TIO<Tinterrupt,则轮询更优。
第37章:硬盘驱动器(Hard Disk Drives)详解
概述
硬盘是计算机中最重要的持久化存储设备,几十年来一直是文件系统的基石。理解硬盘的工作原理,是构建高效文件系统的前提。本章从物理结构出发,逐步建立硬盘的性能模型,并介绍磁盘调度算法。
一、硬盘的接口:OS 眼中的磁盘长什么样?
从操作系统的角度看,硬盘非常简单:
- 磁盘 = 一个巨大的扇区数组
- 每个扇区 = 512 字节,编号从 0 0 0 到 n − 1 n-1 n−1
- OS 通过扇区号(地址)读写数据
磁盘地址空间:
[ 0 ][ 1 ][ 2 ][ 3 ][ 4 ] ... [ n-1 ]
512B 512B 512B ...
重要约定(“不成文契约”):
- 地址相邻的两个扇区,访问速度比地址相距很远的快
- 顺序访问(连续读写)是最快的访问模式
- 跳跃式随机访问会大幅降低性能
原子性保证:
单个 512 字节的写操作是原子的——要么全部写完,要么完全没写。更大的写操作则不保证原子性,断电可能导致"撕裂写(torn write)",即只写了一部分。
二、硬盘的物理结构
2.1 主要部件
主轴(Spindle)
|
+---------+---------+
| 盘片(Platter) |
| +-------------+ |
| | 磁性涂层 | |
| +-------------+ |
+---------+---------+
|
电机驱动旋转
| 部件 | 说明 |
|---|---|
| 盘片(Platter) | 圆形硬质铝片,涂有磁性材料,数据存在上面 |
| 盘面(Surface) | 盘片的每一面,一个盘片有上下两个盘面 |
| 主轴(Spindle) | 连接所有盘片的轴,由电机驱动旋转 |
| 磁道(Track) | 盘面上的同心圆,数据按磁道存储 |
| 磁头(Disk Head) | 负责读写磁性信号,每个盘面一个 |
| 磁臂(Disk Arm) | 承载磁头,带动磁头在盘面上移动 |
2.2 磁道与扇区的关系
盘面俯视图(单轨道示例,12个扇区):
8
7 9
6 10
5 ◎ 11 ◎ = 主轴
4 0
3 1
2
磁头当前在扇区6上方,盘片逆时针旋转
- 一个盘面有成千上万条磁道,紧密排列
- 人的一根头发丝宽度里可以容纳数百条磁道
- 每条磁道由若干扇区组成
2.3 转速(RPM)
典型转速: 7200 ∼ 15000 7200 \sim 15000 7200∼15000 RPM(每分钟转数)
单次旋转时间的计算(量纲分析法):
T r o t a t i o n = 1 min 10000 rot × 60 s 1 min × 1000 ms 1 s = 60000 ms 10000 rot = 6 ms rot T_{rotation} = \frac{1\text{ min}}{10000\text{ rot}} \times \frac{60\text{ s}}{1\text{ min}} \times \frac{1000\text{ ms}}{1\text{ s}} = \frac{60000\text{ ms}}{10000\text{ rot}} = 6\frac{\text{ms}}{\text{rot}} Trotation=10000 rot1 min×1 min60 s×1 s1000 ms=10000 rot60000 ms=6rotms
即 10000 RPM 的磁盘,每转一圈需要 6 ms。
三、访问一个扇区需要经历什么?
完整的 I/O 时间由三部分组成:
T I / O = T s e e k + T r o t a t i o n + T t r a n s f e r (37.1) T_{I/O} = T_{seek} + T_{rotation} + T_{transfer} \tag{37.1} TI/O=Tseek+Trotation+Ttransfer(37.1)
3.1 寻道时间(Seek Time) T s e e k T_{seek} Tseek
磁臂移动到目标磁道所需的时间,包含四个阶段:
磁臂运动过程:
速度
| /------\
| / \
| / \------
|-----/
+-----|-----|-----|------> 时间
加速 匀速 减速 定位
定位(Settling)阶段最关键:0.5 ~ 2 ms
必须精确停在正确磁道上
- 典型平均寻道时间: 4 ∼ 9 4 \sim 9 4∼9 ms
- 最坏情况(从最内到最外)约为平均值的 2~3 倍
平均寻道距离推导:
设磁盘共有 N N N 条磁道(编号 0 0 0 到 N N N),任意两条磁道 x x x 和 y y y 之间的寻道距离为 ∣ x − y ∣ |x - y| ∣x−y∣。
所有可能的寻道距离之和:
∑ x = 0 N ∑ y = 0 N ∣ x − y ∣ ≈ ∫ 0 N ∫ 0 N ∣ x − y ∣ d y d x (37.4, 37.5) \sum_{x=0}^{N}\sum_{y=0}^{N}|x-y| \approx \int_{0}^{N}\int_{0}^{N}|x-y|\,dy\,dx \tag{37.4, 37.5} x=0∑Ny=0∑N∣x−y∣≈∫0N∫0N∣x−y∣dydx(37.4, 37.5)
拆开绝对值计算内层积分:
∫ y = 0 x ( x − y ) d y + ∫ y = x N ( y − x ) d y (37.6) \int_{y=0}^{x}(x-y)\,dy + \int_{y=x}^{N}(y-x)\,dy \tag{37.6} ∫y=0x(x−y)dy+∫y=xN(y−x)dy(37.6)
化简得 x 2 − N x + 1 2 N 2 x^2 - Nx + \frac{1}{2}N^2 x2−Nx+21N2,再计算外层积分:
∫ 0 N ( x 2 − N x + 1 2 N 2 ) d x = N 3 3 (37.7, 37.8) \int_{0}^{N}\left(x^2 - Nx + \frac{1}{2}N^2\right)dx = \frac{N^3}{3} \tag{37.7, 37.8} ∫0N(x2−Nx+21N2)dx=3N3(37.7, 37.8)
除以总可能次数 N 2 N^2 N2,得平均寻道距离:
d ˉ s e e k = N 3 / 3 N 2 = N 3 \bar{d}_{seek} = \frac{N^3/3}{N^2} = \frac{N}{3} dˉseek=N2N3/3=3N
结论:平均寻道距离 = 全程距离的 1 3 \frac{1}{3} 31,即平均寻道时间约为最大寻道时间的三分之一。
3.2 旋转延迟(Rotational Delay) T r o t a t i o n T_{rotation} Trotation
磁头到达目标磁道后,还需要等待目标扇区转到磁头下方。
- 最坏情况:等待几乎一整圈 ≈ T f u l l _ r o t a t i o n \approx T_{full\_rotation} ≈Tfull_rotation
- 平均情况:等待半圈
T r o t a t i o n , a v g = 1 2 × T f u l l _ r o t a t i o n T_{rotation,avg} = \frac{1}{2} \times T_{full\_rotation} Trotation,avg=21×Tfull_rotation
对于 15000 RPM 的磁盘:
T f u l l _ r o t a t i o n = 60000 ms 15000 = 4 ms T_{full\_rotation} = \frac{60000\text{ ms}}{15000} = 4\text{ ms} Tfull_rotation=1500060000 ms=4 ms
T r o t a t i o n , a v g = 2 ms T_{rotation,avg} = 2\text{ ms} Trotation,avg=2 ms
3.3 传输时间(Transfer Time) T t r a n s f e r T_{transfer} Ttransfer
数据实际从磁盘读出(或写入)的时间,取决于传输大小和峰值传输速率:
T t r a n s f e r = S i z e t r a n s f e r R a t e p e a k T_{transfer} = \frac{Size_{transfer}}{Rate_{peak}} Ttransfer=RatepeakSizetransfer
例如,以 125 MB/s 速率传输 512 KB:
T t r a n s f e r = 512 KB 125 MB/s = 0.5 MB 125 MB/s = 4 ms T_{transfer} = \frac{512\text{ KB}}{125\text{ MB/s}} = \frac{0.5\text{ MB}}{125\text{ MB/s}} = 4\text{ ms} Ttransfer=125 MB/s512 KB=125 MB/s0.5 MB=4 ms
对于 4 KB 小读请求,传输时间极短,仅约 30 30 30 微秒,几乎可忽略。
3.4 I/O 速率
R I / O = S i z e t r a n s f e r T I / O (37.2) R_{I/O} = \frac{Size_{transfer}}{T_{I/O}} \tag{37.2} RI/O=TI/OSizetransfer(37.2)
四、性能计算实例
4.1 两款磁盘参数对比
| 参数 | Cheetah 15K.5(高性能) | Barracuda(大容量) |
|---|---|---|
| 容量 | 300 GB | 1 TB |
| 转速 | 15,000 RPM | 7,200 RPM |
| 平均寻道 | 4 ms | 9 ms |
| 峰值传输 | 125 MB/s | 105 MB/s |
| 接口 | SCSI | SATA |
4.2 随机读(4 KB)性能计算
Cheetah:
T s e e k = 4 ms , T r o t a t i o n = 2 ms , T t r a n s f e r ≈ 0.03 ms T_{seek} = 4\text{ ms},\quad T_{rotation} = 2\text{ ms},\quad T_{transfer} \approx 0.03\text{ ms} Tseek=4 ms,Trotation=2 ms,Ttransfer≈0.03 ms
T I / O ≈ 6 ms T_{I/O} \approx 6\text{ ms} TI/O≈6 ms
R I / O = 4 KB 6 ms ≈ 0.66 MB/s R_{I/O} = \frac{4\text{ KB}}{6\text{ ms}} \approx 0.66\text{ MB/s} RI/O=6 ms4 KB≈0.66 MB/s
Barracuda:
T s e e k = 9 ms , T r o t a t i o n = 60000 7200 × 2 ≈ 4.2 ms , T t r a n s f e r ≈ 0.04 ms T_{seek} = 9\text{ ms},\quad T_{rotation} = \frac{60000}{7200 \times 2}\approx 4.2\text{ ms},\quad T_{transfer} \approx 0.04\text{ ms} Tseek=9 ms,Trotation=7200×260000≈4.2 ms,Ttransfer≈0.04 ms
T I / O ≈ 13.2 ms T_{I/O} \approx 13.2\text{ ms} TI/O≈13.2 ms
R I / O = 4 KB 13.2 ms ≈ 0.31 MB/s R_{I/O} = \frac{4\text{ KB}}{13.2\text{ ms}} \approx 0.31\text{ MB/s} RI/O=13.2 ms4 KB≈0.31 MB/s
4.3 顺序读(100 MB)性能计算
顺序读只需一次寻道和一次旋转延迟,之后持续传输:
T I / O , C h e e t a h ≈ 4 + 2 + 100 MB 125 MB/s × 1000 = 806 ms T_{I/O,Cheetah} \approx 4 + 2 + \frac{100\text{ MB}}{125\text{ MB/s}} \times 1000 = 806\text{ ms} TI/O,Cheetah≈4+2+125 MB/s100 MB×1000=806 ms
R I / O , C h e e t a h ≈ 125 MB/s R_{I/O,Cheetah} \approx 125\text{ MB/s} RI/O,Cheetah≈125 MB/s
4.4 随机 vs 顺序性能差距
| 工作负载 | Cheetah | Barracuda |
|---|---|---|
| 随机 4KB | 0.66 MB/s | 0.31 MB/s |
| 顺序 100MB | ~125 MB/s | ~105 MB/s |
| 差距倍数 | ~190 倍 | ~340 倍 |
顺序访问比随机访问快约 200~340 倍! 这是磁盘设计中最重要的性能规律。
五、几个重要的物理细节
5.1 磁道偏斜(Track Skew)
问题:连续读取跨越磁道边界时,磁头需要时间移动到下一条磁道,此时盘片还在转,目标扇区可能已经转过去了。
解决:相邻磁道的起始扇区错开若干个扇区,这叫磁道偏斜。
无偏斜(问题): 有偏斜(正确):
外轨: 0 1 2 3 4 外轨: 10 11 0 1 2
内轨: 12 13 14 15 16 内轨: 12 13 14 15 16
读完内轨16,移到外轨, 读完内轨16,移到外轨,
磁头到达时0已转走了! 磁头到达时0恰好到位!
5.2 多区域磁盘(Multi-Zone)
外圈磁道比内圈物理周长更长,可以容纳更多扇区。磁盘将盘面划分为多个区域(Zone),外区每磁道扇区数多于内区,从而充分利用存储空间。
外区(Zone 0):每磁道 30 个扇区
中区(Zone 1):每磁道 20 个扇区
内区(Zone 2):每磁道 10 个扇区
5.3 磁盘缓存(Track Buffer)
磁盘内部有少量内存(通常 8 MB 或 16 MB),用于缓存数据:
- 读缓存:读某个扇区时,顺带把整条磁道都缓存起来,下次访问同轨数据极快
- 写缓存(两种策略):
| 策略 | 说明 | 优点 | 风险 |
|---|---|---|---|
| 写回(Write Back) | 数据进缓存即报告完成 | 速度快 | 断电丢数据 |
| 写穿(Write Through) | 数据真正写到盘才报告 | 安全 | 速度慢 |
六、磁盘调度算法
由于 I/O 代价高昂,OS 不会按请求到达顺序服务磁盘,而是重新排序,尽量减少寻道和旋转等待。这就是磁盘调度(Disk Scheduling)。
磁盘调度的目标:尽量接近 SJF(最短作业优先),即优先服务耗时最短的请求。
6.1 SSTF:最短寻道时间优先
Shortest Seek Time First:每次选择距离当前磁头最近的磁道上的请求。
示例:磁头在内轨,待处理请求为扇区 21(中轨)和扇区 2(外轨):
ASCII 示意(三轨磁盘,磁头在内轨):
外轨(0~11): [ 2 ]
中轨(12~23): [21 ] ← 先服务(距离近)
内轨(24~35): [磁头] ← 当前位置
缺点:饥饿(Starvation)
如果内轨请求源源不断,外轨的请求永远得不到服务。
6.2 SCAN / 电梯算法
思路:磁头从一端扫到另一端,依次服务沿途请求,到头后反向。就像电梯:不管谁按,按顺序上下,不会跳楼层。
磁头运动轨迹(SCAN):
磁道编号: 0 -----> 内轨 -----> 最内 -----> 外轨 -----> 0
方向: →→→→→→→→→→→→→→→→→→ 到头反向 ←←←←←←←←←←
服务顺序: 沿途遇到的请求都服务,不会跳过,不会饥饿
变种:
- F-SCAN:扫描时冻结请求队列,扫描期间新来的请求等下一轮,防止对近处请求的偏袒
- C-SCAN(循环扫描):只从外到内单向扫,到内轨后直接跳回外轨重新开始,对各位置更公平
SCAN 与电梯的比喻:
SSTF 就像电梯在3层,有人要去4层也有人要去1层,电梯因为4层更近就去4层——这会让要去1层的人很愤怒。SCAN 像正常电梯,按方向依次停靠。
6.3 SPTF:最短定位时间优先
Shortest Positioning Time First(也叫 SATF):同时考虑寻道时间和旋转延迟,选择总定位时间( T s e e k + T r o t a t i o n T_{seek} + T_{rotation} Tseek+Trotation)最短的请求。
为什么 SSTF 有时不够好?
示意图:磁头在内轨扇区30上方
请求A:中轨扇区16
请求B:外轨扇区8
问题:选A还是B?
答案:看旋转速度!
- 若寻道慢、旋转快:选近磁道的A(16),因为旋转等不了太久
- 若寻道快、旋转慢:选远磁道的B(8),因为A虽然近,但16需要几乎转满一圈才能到磁头下方
SPTF 选择 = arg min r ∈ r e q u e s t s ( T s e e k ( r ) + T r o t a t i o n ( r ) ) \text{SPTF 选择} = \arg\min_{r \in requests}(T_{seek}(r) + T_{rotation}(r)) SPTF 选择=argr∈requestsmin(Tseek(r)+Trotation(r))
SPTF 通常在磁盘控制器内部实现,因为只有控制器知道磁头的精确位置。
6.4 三种算法对比
6.5 其他调度优化
I/O 合并(I/O Merging):
请求扇区 33、8、34 时,调度器把 33 和 34 合并为一次两扇区读取,减少磁盘操作次数。
合并前: 请求33 请求8 请求34
合并后: 请求[33,34] 请求8
非保守调度(Anticipatory Scheduling):
有时候等一小会儿,可能会来一个更好的请求(比如和当前位置相邻的扇区),整体效率反而更高。这叫预期调度,牺牲一点响应时间换取整体吞吐量。
七、完整可运行的 C 语言模拟代码
以下代码模拟硬盘的 I/O 时间计算和三种调度算法(FIFO、SSTF、SCAN),完整可运行。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h> /* 需要链接 -lm */
/* =====================================================
* 磁盘参数定义(模拟 Cheetah 15K.5)
* ===================================================== */
#define RPM 15000 /* 转速:每分钟转数 */
#define AVG_SEEK_MS 4.0 /* 平均寻道时间(毫秒) */
#define PEAK_RATE_MBS 125.0 /* 峰值传输速率(MB/s) */
#define SECTOR_SIZE_KB 0.5 /* 扇区大小(KB = 0.5 KB = 512 B) */
#define TRACKS 3 /* 模拟磁盘轨道数 */
#define SECTORS_PER_TRACK 12 /* 每轨扇区数 */
#define MAX_REQUESTS 20 /* 最大请求数 */
/* =====================================================
* 计算单次旋转时间(毫秒)
* 公式:T = 60000 ms / RPM
* ===================================================== */
double rotation_period_ms(void) {
return 60000.0 / RPM;
}
/* =====================================================
* 计算平均旋转延迟(毫秒)= 半圈时间
* ===================================================== */
double avg_rotation_delay_ms(void) {
return rotation_period_ms() / 2.0;
}
/* =====================================================
* 计算传输时间(毫秒)
* size_kb:传输大小(KB)
* ===================================================== */
double transfer_time_ms(double size_kb) {
/* T = size / rate,注意单位换算:KB / (MB/s) = ms */
return (size_kb / 1024.0) / PEAK_RATE_MBS * 1000.0;
}
/* =====================================================
* 计算单次 I/O 总时间(毫秒)
* 公式:T_IO = T_seek + T_rotation + T_transfer
* ===================================================== */
double io_time_ms(double seek_ms, double rotation_ms, double size_kb) {
return seek_ms + rotation_ms + transfer_time_ms(size_kb);
}
/* =====================================================
* 计算 I/O 速率(MB/s)
* ===================================================== */
double io_rate_mbs(double size_kb, double time_ms) {
if (time_ms <= 0) return 0.0;
/* rate = size / time,注意单位换算 */
return (size_kb / 1024.0) / (time_ms / 1000.0);
}
/* =====================================================
* 打印磁盘性能分析报告
* ===================================================== */
void print_performance_report(void) {
printf("========================================\n");
printf(" 磁盘性能分析(Cheetah 15K.5)\n");
printf("========================================\n\n");
double rot_period = rotation_period_ms();
double avg_rot = avg_rotation_delay_ms();
printf("基本参数:\n");
printf(" 转速:%d RPM\n", RPM);
printf(" 单次旋转时间:%.2f ms\n", rot_period);
printf(" 平均旋转延迟:%.2f ms\n", avg_rot);
printf(" 平均寻道时间:%.2f ms\n\n", AVG_SEEK_MS);
/* ---------- 随机 4KB 读 ---------- */
double seek_r = AVG_SEEK_MS;
double rot_r = avg_rot;
double size_r = 4.0; /* 4 KB */
double tio_r = io_time_ms(seek_r, rot_r, size_r);
double rio_r = io_rate_mbs(size_r, tio_r);
printf("随机 4KB 读:\n");
printf(" T_seek = %.2f ms\n", seek_r);
printf(" T_rotation = %.2f ms\n", rot_r);
printf(" T_transfer = %.4f ms\n", transfer_time_ms(size_r));
printf(" T_IO = %.2f ms\n", tio_r);
printf(" R_IO = %.4f MB/s\n\n", rio_r);
/* ---------- 顺序 100MB 读 ---------- */
/* 顺序读:一次寻道 + 一次旋转 + 长时间传输 */
double size_s = 100.0 * 1024.0; /* 100 MB = 102400 KB */
double tio_s = io_time_ms(seek_r, rot_r, size_s);
double rio_s = io_rate_mbs(size_s, tio_s);
printf("顺序 100MB 读:\n");
printf(" T_seek = %.2f ms\n", seek_r);
printf(" T_rotation = %.2f ms\n", rot_r);
printf(" T_transfer = %.2f ms\n", transfer_time_ms(size_s));
printf(" T_IO = %.2f ms\n", tio_s);
printf(" R_IO = %.4f MB/s\n\n", rio_s);
printf("性能差距(顺序 / 随机)= %.1f 倍\n\n", rio_s / rio_r);
}
/* =====================================================
* 磁盘调度模拟
* 以下用"扇区号"作为磁盘位置的简化模型
* 全盘共 TRACKS * SECTORS_PER_TRACK = 36 个扇区
* 磁道号 = 扇区号 / SECTORS_PER_TRACK
* ===================================================== */
/* 根据扇区号计算所在磁道 */
int sector_to_track(int sector) {
return sector / SECTORS_PER_TRACK;
}
/* 计算两磁道间的寻道时间(简化模型:每跨一条磁道 1ms) */
double seek_time_between(int track1, int track2) {
int diff = abs(track1 - track2);
if (diff == 0) return 0.0;
/* 简化:每条磁道 1ms,加上基础定位时间 0.5ms */
return 0.5 + diff * 1.0;
}
/* =====================================================
* FIFO 调度:按请求到达顺序服务
* requests[]:请求的扇区号数组
* n:请求数量
* start_track:磁头初始磁道
* ===================================================== */
void schedule_fifo(int *requests, int n, int start_track) {
printf("--- FIFO 调度 ---\n");
printf("服务顺序:");
int current_track = start_track;
double total_seek = 0.0;
for (int i = 0; i < n; i++) {
int target_track = sector_to_track(requests[i]);
double seek = seek_time_between(current_track, target_track);
total_seek += seek;
printf("扇区%d(磁道%d)", requests[i], target_track);
if (i < n - 1) printf(" -> ");
current_track = target_track;
}
printf("\n总寻道时间:%.1f ms\n\n", total_seek);
}
/* =====================================================
* SSTF 调度:每次选距当前磁头最近的磁道上的请求
* ===================================================== */
void schedule_sstf(int *requests, int n, int start_track) {
printf("--- SSTF 调度(最短寻道时间优先)---\n");
printf("服务顺序:");
/* 复制请求数组,用 -1 标记已服务的请求 */
int reqs[MAX_REQUESTS];
int visited[MAX_REQUESTS];
memcpy(reqs, requests, n * sizeof(int));
memset(visited, 0, sizeof(visited));
int current_track = start_track;
double total_seek = 0.0;
int served = 0;
while (served < n) {
/* 找距当前磁道最近的未服务请求 */
int best_idx = -1;
int best_dist = 9999;
for (int i = 0; i < n; i++) {
if (visited[i]) continue;
int dist = abs(sector_to_track(reqs[i]) - current_track);
if (dist < best_dist) {
best_dist = dist;
best_idx = i;
}
}
if (best_idx == -1) break;
int target_track = sector_to_track(reqs[best_idx]);
double seek = seek_time_between(current_track, target_track);
total_seek += seek;
printf("扇区%d(磁道%d)", reqs[best_idx], target_track);
if (served < n - 1) printf(" -> ");
visited[best_idx] = 1;
current_track = target_track;
served++;
}
printf("\n总寻道时间:%.1f ms\n\n", total_seek);
}
/* =====================================================
* SCAN 调度(电梯算法):磁头来回扫,沿途服务请求
* direction:初始方向,1=向外(磁道号增大),-1=向内
* ===================================================== */
void schedule_scan(int *requests, int n, int start_track, int direction) {
printf("--- SCAN 调度(电梯算法)---\n");
printf("初始方向:%s\n", direction > 0 ? "向外(磁道号增大)" : "向内(磁道号减小)");
printf("服务顺序:");
/* 把请求按磁道号排序 */
int tracks[MAX_REQUESTS];
for (int i = 0; i < n; i++) {
tracks[i] = sector_to_track(requests[i]);
}
/* 简单冒泡排序 */
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (tracks[j] > tracks[j + 1]) {
int tmp = tracks[j];
tracks[j] = tracks[j + 1];
tracks[j + 1] = tmp;
}
}
}
double total_seek = 0.0;
int current_track = start_track;
int served = 0;
int first = 1; /* 用于格式化输出 */
/* 先按初始方向扫描 */
if (direction > 0) {
/* 向外:从 start_track 向高磁道号方向 */
for (int i = 0; i < n; i++) {
if (tracks[i] >= current_track) {
double seek = seek_time_between(current_track, tracks[i]);
total_seek += seek;
if (!first) printf(" -> ");
printf("磁道%d", tracks[i]);
current_track = tracks[i];
first = 0;
served++;
}
}
/* 反向:向内扫描剩余请求 */
for (int i = n - 1; i >= 0; i--) {
if (tracks[i] < start_track) {
double seek = seek_time_between(current_track, tracks[i]);
total_seek += seek;
if (!first) printf(" -> ");
printf("磁道%d", tracks[i]);
current_track = tracks[i];
first = 0;
served++;
}
}
} else {
/* 向内:从 start_track 向低磁道号方向 */
for (int i = n - 1; i >= 0; i--) {
if (tracks[i] <= current_track) {
double seek = seek_time_between(current_track, tracks[i]);
total_seek += seek;
if (!first) printf(" -> ");
printf("磁道%d", tracks[i]);
current_track = tracks[i];
first = 0;
served++;
}
}
/* 反向:向外扫描剩余请求 */
for (int i = 0; i < n; i++) {
if (tracks[i] > start_track) {
double seek = seek_time_between(current_track, tracks[i]);
total_seek += seek;
if (!first) printf(" -> ");
printf("磁道%d", tracks[i]);
current_track = tracks[i];
first = 0;
served++;
}
}
}
printf("\n总寻道时间:%.1f ms\n\n", total_seek);
}
/* =====================================================
* 主函数
* ===================================================== */
int main(void) {
/* 打印性能分析报告 */
print_performance_report();
/* ---- 调度算法演示 ---- */
printf("========================================\n");
printf(" 磁盘调度算法演示\n");
printf(" 磁盘:%d 磁道,每轨 %d 扇区\n",
TRACKS, SECTORS_PER_TRACK);
printf(" 扇区总数:%d\n",
TRACKS * SECTORS_PER_TRACK);
printf("========================================\n\n");
/*
* 请求序列:扇区 7, 30, 8
* 对应磁道:
* 扇区 7 → 磁道 0(外轨,扇区 0~11)
* 扇区 30 → 磁道 2(内轨,扇区 24~35)
* 扇区 8 → 磁道 0(外轨,扇区 0~11)
*
* 磁头初始位置:磁道 1(中轨)
*/
int requests[] = {7, 30, 8};
int n = 3;
int start_track = 1; /* 磁头初始在中轨 */
printf("请求扇区:");
for (int i = 0; i < n; i++) {
printf("%d(磁道%d)", requests[i], sector_to_track(requests[i]));
if (i < n - 1) printf(", ");
}
printf("\n磁头初始位置:磁道 %d\n\n", start_track);
/* 三种调度算法对比 */
schedule_fifo(requests, n, start_track);
schedule_sstf(requests, n, start_track);
schedule_scan(requests, n, start_track, 1); /* 初始方向向外 */
printf("========================================\n");
printf("关键结论:\n");
printf(" 1. 顺序访问比随机访问快约 200-340 倍\n");
printf(" 2. SSTF 减少寻道但可能导致饥饿\n");
printf(" 3. SCAN 保证公平,避免饥饿\n");
printf(" 4. SPTF 同时考虑寻道和旋转,性能最优\n");
printf("========================================\n");
return 0;
}
代码运行示例输出
========================================
磁盘性能分析(Cheetah 15K.5)
========================================
基本参数:
转速:15000 RPM
单次旋转时间:4.00 ms
平均旋转延迟:2.00 ms
平均寻道时间:4.00 ms
随机 4KB 读:
T_seek = 4.00 ms
T_rotation = 2.00 ms
T_transfer = 0.0381 ms
T_IO = 6.04 ms
R_IO = 0.6470 MB/s
顺序 100MB 读:
T_seek = 4.00 ms
T_rotation = 2.00 ms
T_transfer = 800.00 ms
T_IO = 806.00 ms
R_IO = 126.6501 MB/s
性能差距(顺序 / 随机)= 195.8 倍
========================================
磁盘调度算法演示
========================================
请求扇区:7(磁道0), 30(磁道2), 8(磁道0)
磁头初始位置:磁道 1
--- FIFO 调度 ---
服务顺序:扇区7(磁道0) -> 扇区30(磁道2) -> 扇区8(磁道0)
总寻道时间:5.5 ms
--- SSTF 调度(最短寻道时间优先)---
服务顺序:扇区7(磁道0) -> 扇区8(磁道0) -> 扇区30(磁道2)
总寻道时间:3.0 ms
--- SCAN 调度(电梯算法)---
初始方向:向外(磁道号增大)
服务顺序:磁道2 -> 磁道0
总寻道时间:3.5 ms
八、知识全景图
九、核心公式汇总
| 公式 | 含义 |
|---|---|
| T I / O = T s e e k + T r o t a t i o n + T t r a n s f e r T_{I/O} = T_{seek} + T_{rotation} + T_{transfer} TI/O=Tseek+Trotation+Ttransfer | 单次 I/O 总时间 |
| R I / O = S i z e t r a n s f e r T I / O R_{I/O} = \dfrac{Size_{transfer}}{T_{I/O}} RI/O=TI/OSizetransfer | I/O 速率 |
| T r o t a t i o n , f u l l = 60000 ms R P M T_{rotation,full} = \dfrac{60000\text{ ms}}{RPM} Trotation,full=RPM60000 ms | 单圈旋转时间 |
| T r o t a t i o n , a v g = T r o t a t i o n , f u l l 2 T_{rotation,avg} = \dfrac{T_{rotation,full}}{2} Trotation,avg=2Trotation,full | 平均旋转延迟 |
| T t r a n s f e r = S i z e K B / 1024 R a t e M B / s × 1000 T_{transfer} = \dfrac{Size_{KB}/1024}{Rate_{MB/s}} \times 1000 Ttransfer=RateMB/sSizeKB/1024×1000 | 传输时间 |
| d ˉ s e e k = N 3 \bar{d}_{seek} = \dfrac{N}{3} dˉseek=3N | 平均寻道距离( N N N 为磁道总数) |
十、一句话总结各调度算法
| 算法 | 策略 | 优点 | 缺点 |
|---|---|---|---|
| FIFO | 来了就服务 | 公平,无饥饿 | 效率最低 |
| SSTF | 选最近磁道 | 减少寻道 | 可能饥饿 |
| SCAN | 来回扫全盘 | 无饥饿,公平 | 忽略旋转延迟 |
| C-SCAN | 单向扫后跳回 | 对各位置更公平 | 同上 |
| SPTF | 最短总定位时间 | 最接近 SJF,性能最优 | 需在磁盘内部实现 |
本文档对应教材《操作系统:三个简单部件》第 37 章,版本 1.10。
第38章:RAID(廉价磁盘冗余阵列)详解
概述
单块磁盘有三个痛点:慢、小、不可靠。RAID(Redundant Array of Inexpensive Disks)把多块普通磁盘组合成一个整体,同时解决这三个问题。从外部看,RAID 就像一块大磁盘,上层的操作系统和文件系统完全不需要改动——这种透明性是 RAID 成功的关键。
一、RAID 的基本概念
1.1 RAID 解决什么问题?
| 痛点 | RAID 的解法 |
|---|---|
| 太慢 | 多块磁盘并行工作,提升吞吐量 |
| 太小 | 多块磁盘的容量叠加 |
| 不可靠 | 冗余存储,一块盘坏了数据不丢 |
1.2 RAID 的内部结构
RAID 本质上是一台专用计算机:
+----------------------------------------------+
| RAID 控制器 |
| +----------+ +--------+ +--------------+ |
| | 微控制器 | | DRAM | | 非易失性内存 | |
| | (CPU) | | 读写缓冲| | (写日志用) | |
| +----------+ +--------+ +--------------+ |
| +--+ +--+ +--+ +--+ |
| |磁| |磁| |磁| |磁| ← 多块物理磁盘 |
| |盘| |盘| |盘| |盘| |
| | 0| | 1| | 2| | 3| |
| +--+ +--+ +--+ +--+ |
+----------------------------------------------+
对外表现:一块大磁盘
1.3 故障模型:停止-失效模型
RAID 设计基于一个简化假设——**停止-失效(Fail-Stop)**模型:
- 磁盘只有两种状态:正常工作 或 完全失效
- 失效是可以被立即检测到的
- 不考虑"悄悄出错"(数据静默损坏)等复杂情况
1.4 评估 RAID 的三个维度
RAID 评估 = { 容量(Capacity) 可靠性(Reliability) 性能(Performance) \text{RAID 评估} = \begin{cases} \text{容量(Capacity)} \\ \text{可靠性(Reliability)} \\ \text{性能(Performance)} \end{cases} RAID 评估=⎩
⎨
⎧容量(Capacity)可靠性(Reliability)性能(Performance)
性能分析的两种工作负载:
- 顺序工作负载:连续读写大块数据,磁盘效率最高,速率记为 S S S MB/s
- 随机工作负载:小块随机读写,寻道旋转开销大,速率记为 R R R MB/s,且 S ≫ R S \gg R S≫R
计算 S S S 和 R R R 的示例(磁盘参数:寻道 7ms,旋转 3ms,传输率 50MB/s):
顺序传输(10MB):
S = 10 MB 7 ms + 3 ms + 10 MB 50 MB/s = 10 MB 210 ms ≈ 47.62 MB/s S = \frac{10\text{ MB}}{7\text{ ms} + 3\text{ ms} + \frac{10\text{ MB}}{50\text{ MB/s}}} = \frac{10\text{ MB}}{210\text{ ms}} \approx 47.62\text{ MB/s} S=7 ms+3 ms+50 MB/s10 MB10 MB=210 ms10 MB≈47.62 MB/s
随机传输(10KB):
R = 10 KB 7 ms + 3 ms + 10 KB 50 MB/s = 10 KB 10.195 ms ≈ 0.981 MB/s R = \frac{10\text{ KB}}{7\text{ ms} + 3\text{ ms} + \frac{10\text{ KB}}{50\text{ MB/s}}} = \frac{10\text{ KB}}{10.195\text{ ms}} \approx 0.981\text{ MB/s} R=7 ms+3 ms+50 MB/s10 KB10 KB=10.195 ms10 KB≈0.981 MB/s
因此 S / R ≈ 50 S/R \approx 50 S/R≈50,顺序访问比随机访问快约 50 倍。
二、RAID-0:条带化(Striping)
2.1 基本思路
没有冗余,纯粹追求性能和容量。 把数据块轮流分布到各个磁盘上:
4 块磁盘,块大小 = 4KB:
Disk 0 Disk 1 Disk 2 Disk 3
+----+ +----+ +----+ +----+
| 0 | | 1 | | 2 | | 3 | ← 第0条带
+----+ +----+ +----+ +----+
| 4 | | 5 | | 6 | | 7 | ← 第1条带
+----+ +----+ +----+ +----+
| 8 | | 9 | | 10 | | 11 |
+----+ +----+ +----+ +----+
| 12 | | 13 | | 14 | | 15 |
+----+ +----+ +----+ +----+
同一行(如 0,1,2,3)称为一个"条带(stripe)"
2.2 地址映射公式
已知逻辑块地址 A A A,磁盘数为 N N N:
Disk = A m o d N \text{Disk} = A \bmod N Disk=AmodN
Offset = ⌊ A / N ⌋ \text{Offset} = \lfloor A / N \rfloor Offset=⌊A/N⌋
示例:块 14,4 块磁盘:
Disk = 14 m o d 4 = 2 (第3块磁盘,从0计) \text{Disk} = 14 \bmod 4 = 2 \quad \text{(第3块磁盘,从0计)} Disk=14mod4=2(第3块磁盘,从0计)
Offset = ⌊ 14 / 4 ⌋ = 3 (该磁盘上的第4个块) \text{Offset} = \lfloor 14 / 4 \rfloor = 3 \quad \text{(该磁盘上的第4个块)} Offset=⌊14/4⌋=3(该磁盘上的第4个块)
2.3 块大小(Chunk Size)的选择
| 块大小小 | 块大小大 |
|---|---|
| 单文件分散到多磁盘,并行度高 | 单文件集中在少数磁盘,并行度低 |
| 跨磁盘的定位时间成本更高 | 定位时间集中在少数磁盘,较低 |
| 适合大量并发随机请求 | 适合少量顺序大文件 |
2.4 RAID-0 性能分析
设共 N N N 块磁盘,单盘顺序速率 S S S,随机速率 R R R:
| 指标 | 值 |
|---|---|
| 容量 | N ⋅ B N \cdot B N⋅B(最优,无浪费) |
| 可靠性 | 0(任何一块盘坏,所有数据丢失) |
| 顺序读/写吞吐 | N ⋅ S N \cdot S N⋅S |
| 随机读/写吞吐 | N ⋅ R N \cdot R N⋅R |
| 单请求延迟(读/写) | T T T(与单盘相同) |
三、RAID-1:镜像(Mirroring)
3.1 基本思路
每个数据块保存两份,存在不同磁盘上。 一块坏了,另一块还有数据。
4 块磁盘,两两镜像:
Disk 0 Disk 1 Disk 2 Disk 3
+----+ +----+ +----+ +----+
| 0 | | 0 | | 1 | | 1 | ← 块0和1各有两份
+----+ +----+ +----+ +----+
| 2 | | 2 | | 3 | | 3 |
+----+ +----+ +----+ +----+
| 4 | | 4 | | 5 | | 5 |
+----+ +----+ +----+ +----+
| 6 | | 6 | | 7 | | 7 |
+----+ +----+ +----+ +----+
Disk 0 和 Disk 1 内容完全相同(镜像对)
Disk 2 和 Disk 3 内容完全相同(镜像对)
- 读:可以读任意一个副本(灵活调度)
- 写:必须同时写两个副本(保证一致性)
3.2 一致性更新问题
写操作需要更新两块磁盘,若中途断电,会导致两份副本不一致:
写入流程(存在风险):
RAID → 写磁盘0 → [断电!] → 磁盘1 未写
结果:磁盘0=新数据,磁盘1=旧数据,不一致!
解决方案:写前日志(Write-Ahead Log)
RAID 控制器在**非易失性内存(电池供电的RAM)**中记录"即将做什么",崩溃后可以重放日志恢复一致性。
3.3 RAID-1 性能分析
设共 N N N 块磁盘(两两镜像,即 N / 2 N/2 N/2 对),单盘延迟为 T T T:
| 指标 | 值 | 说明 |
|---|---|---|
| 容量 | ( N ⋅ B ) / 2 (N \cdot B)/2 (N⋅B)/2 | 一半浪费在冗余上 |
| 可靠性 | 至少容忍 1 块坏,最多 N / 2 N/2 N/2 块(运气好) | |
| 顺序读吞吐 | ( N / 2 ) ⋅ S (N/2) \cdot S (N/2)⋅S | 每对只能用一块顺序读 |
| 顺序写吞吐 | ( N / 2 ) ⋅ S (N/2) \cdot S (N/2)⋅S | 每次逻辑写 = 两次物理写 |
| 随机读吞吐 | N ⋅ R N \cdot R N⋅R | 可以同时用所有磁盘读 |
| 随机写吞吐 | ( N / 2 ) ⋅ R (N/2) \cdot R (N/2)⋅R | 每次逻辑写 = 两次物理写 |
| 读延迟 | T T T | 等同单盘 |
| 写延迟 | ≈ T \approx T ≈T(稍高) | 两次并行写,取最坏值 |
四、RAID-4:奇偶校验(Parity)
4.1 基本思路
用 1 块专用磁盘存奇偶校验信息,其余磁盘存数据。 比镜像节省空间,但奇偶盘是瓶颈。
5 块磁盘(4数据 + 1校验):
Disk 0 Disk 1 Disk 2 Disk 3 Disk 4(校验)
+----+ +----+ +----+ +----+ +----+
| 0 | | 1 | | 2 | | 3 | | P0 | ← P0 = 0⊕1⊕2⊕3
+----+ +----+ +----+ +----+ +----+
| 4 | | 5 | | 6 | | 7 | | P1 |
+----+ +----+ +----+ +----+ +----+
| 8 | | 9 | | 10 | | 11 | | P2 |
+----+ +----+ +----+ +----+ +----+
| 12 | | 13 | | 14 | | 15 | | P3 |
+----+ +----+ +----+ +----+ +----+
4.2 XOR 奇偶校验原理
XOR(异或)规则:
- 同一行所有位(包括校验位)中,1 的个数必须是偶数
- P = C 0 ⊕ C 1 ⊕ C 2 ⊕ C 3 P = C_0 \oplus C_1 \oplus C_2 \oplus C_3 P=C0⊕C1⊕C2⊕C3
示例(4位数据 + 1位校验):
C0 C1 C2 C3 P(校验)
0 0 1 1 0 ← 有两个1,P=0(偶数)
0 1 0 0 1 ← 有一个1,P=1(补成偶数)
故障恢复:若 C 2 C_2 C2 丢失,从其他列重建:
C 2 = C 0 ⊕ C 1 ⊕ C 3 ⊕ P C_2 = C_0 \oplus C_1 \oplus C_3 \oplus P C2=C0⊕C1⊕C3⊕P
块级别的 XOR(4KB 块,逐位进行异或):
Block0: 0011
Block1: 1001
Block2: 1010
Block3: 0110
Parity: 0110 ← 每一列分别 XOR
||||
||||__ 位3: 1⊕1⊕0⊕0 = 0
|||___ 位2: 1⊕0⊕1⊕1 = 1
||____ 位1: 0⊕0⊕0⊕1 = 1 (前三个块的位1是0,0,1,异或得1)
|_____ 位0: 0⊕1⊕1⊕0 = 0
4.3 写操作的两种方式
方法一:加法奇偶(Additive Parity)
重新读取条带中所有其他数据块,重新计算 P:
P n e w = C 0 ⊕ C 1 ⊕ C 2 n e w ⊕ C 3 P_{new} = C_0 \oplus C_1 \oplus C_2^{new} \oplus C_3 Pnew=C0⊕C1⊕C2new⊕C3
需要读取的块数随磁盘数增加而增加,规模不好。
方法二:减法奇偶(Subtractive Parity)
只读旧数据块和旧校验块,用差量更新:
P n e w = ( C o l d ⊕ C n e w ) ⊕ P o l d (38.1) P_{new} = (C_{old} \oplus C_{new}) \oplus P_{old} \tag{38.1} Pnew=(Cold⊕Cnew)⊕Pold(38.1)
过程(3步):
- 读旧数据 C o l d C_{old} Cold 和旧校验 P o l d P_{old} Pold(2次读)
- 计算新校验: P n e w = ( C o l d ⊕ C n e w ) ⊕ P o l d P_{new} = (C_{old} \oplus C_{new}) \oplus P_{old} Pnew=(Cold⊕Cnew)⊕Pold
- 写新数据 C n e w C_{new} Cnew 和新校验 P n e w P_{new} Pnew(2次写)
共 4 次 I/O(2读 + 2写),无论磁盘数量多少都是固定开销。
两种方法的选择:当 N − 1 < 2 N-1 < 2 N−1<2(即磁盘数小于3)时加法更优,否则减法更优(交叉点在磁盘数 = 3 时)。
4.4 RAID-4 的性能瓶颈:小写问题
两个并发的小写请求(写块4和块13):
Disk 0 Disk 1 Disk 2 Disk 3 Disk 4(校验)
+----+ +----+ +----+ +----+ +----+
| 0 | | 1 | | 2 | | 3 | | P0 |
+----+ +----+ +----+ +----+ +----+
| *4 | | 5 | | 6 | | 7 | |+P1 | ← 写4,需读写P1
+----+ +----+ +----+ +----+ +----+
| 8 | | 9 | | 10 | | 11 | | P2 |
+----+ +----+ +----+ +----+ +----+
| 12 | |*13 | | 14 | | 15 | |+P3 | ← 写13,需读写P3
+----+ +----+ +----+ +----+ +----+
* = 要写的数据块
+ = 相关校验块(都在 Disk 4 上!)
数据写可以并行(Disk 0 和 Disk 1 同时写),
但校验块都在 Disk 4,必须串行!
校验盘是瓶颈:每次小写都要访问校验盘 2 次(1读+1写),所有写请求在校验盘上串行化。
R r a n d o m _ w r i t e = R 2 MB/s(仅由校验盘决定) R_{random\_write} = \frac{R}{2} \text{ MB/s(仅由校验盘决定)} Rrandom_write=2R MB/s(仅由校验盘决定)
4.5 RAID-4 性能分析
设 N N N 块磁盘(含1块校验盘),每盘 B B B 块,单盘延迟 T T T:
| 指标 | 值 |
|---|---|
| 容量 | ( N − 1 ) ⋅ B (N-1) \cdot B (N−1)⋅B |
| 可靠性 | 容忍 1 块磁盘失效 |
| 顺序读吞吐 | ( N − 1 ) ⋅ S (N-1) \cdot S (N−1)⋅S |
| 顺序写吞吐 | ( N − 1 ) ⋅ S (N-1) \cdot S (N−1)⋅S(全条带写,并行) |
| 随机读吞吐 | ( N − 1 ) ⋅ R (N-1) \cdot R (N−1)⋅R |
| 随机写吞吐 | R / 2 R/2 R/2(校验盘瓶颈) |
| 读延迟 | T T T |
| 写延迟 | ≈ 2 T \approx 2T ≈2T(2读+2写,读并行,写并行) |
五、RAID-5:旋转奇偶校验(Rotating Parity)
5.1 基本思路
把校验块分散到所有磁盘上,消除 RAID-4 的校验盘瓶颈。
5 块磁盘,校验块轮流存放:
Disk 0 Disk 1 Disk 2 Disk 3 Disk 4
+----+ +----+ +----+ +----+ +----+
| 0 | | 1 | | 2 | | 3 | | P0 | ← P0 在 Disk 4
+----+ +----+ +----+ +----+ +----+
| 5 | | 6 | | 7 | | P1 | | 4 | ← P1 在 Disk 3
+----+ +----+ +----+ +----+ +----+
| 10 | | 11 | | P2 | | 8 | | 9 | ← P2 在 Disk 2
+----+ +----+ +----+ +----+ +----+
| 15 | | P3 | | 12 | | 13 | | 14 | ← P3 在 Disk 1
+----+ +----+ +----+ +----+ +----+
| P4 | | 16 | | 17 | | 18 | | 19 | ← P4 在 Disk 0
+----+ +----+ +----+ +----+ +----+
校验块在每条带轮流分布,没有固定的"校验盘"
5.2 RAID-5 性能分析
RAID-5 大多数指标与 RAID-4 相同,区别在于随机写:
- RAID-4 随机写:所有写都竞争同一块校验盘,串行化,吞吐 = R / 2 = R/2 =R/2
- RAID-5 随机写:校验块分散,不同条带的写请求可以使用不同磁盘并行
R R A I D 5 , r a n d o m _ w r i t e = N 4 ⋅ R R_{RAID5,random\_write} = \frac{N}{4} \cdot R RRAID5,random_write=4N⋅R
分母 4 的来源:每次逻辑写仍然产生 4 次物理 I/O(2读+2写),但 N N N 块磁盘都能参与,整体并行度 = N / 4 = N/4 =N/4。
| 指标 | RAID-4 | RAID-5 |
|---|---|---|
| 容量 | ( N − 1 ) B (N-1)B (N−1)B | ( N − 1 ) B (N-1)B (N−1)B |
| 可靠性 | 1 块 | 1 块 |
| 顺序读 | ( N − 1 ) S (N-1)S (N−1)S | ( N − 1 ) S (N-1)S (N−1)S |
| 顺序写 | ( N − 1 ) S (N-1)S (N−1)S | ( N − 1 ) S (N-1)S (N−1)S |
| 随机读 | ( N − 1 ) R (N-1)R (N−1)R | N ⋅ R N \cdot R N⋅R |
| 随机写 | R / 2 R/2 R/2 | ( N / 4 ) R (N/4)R (N/4)R |
| 读延迟 | T T T | T T T |
| 写延迟 | 2 T 2T 2T | 2 T 2T 2T |
六、四种 RAID 全面对比
6.1 完整对比表
设 N N N 块磁盘,每盘 B B B 块,单盘顺序速率 S S S,随机速率 R R R,单盘延迟 T T T:
| 指标 | RAID-0 | RAID-1 | RAID-4 | RAID-5 |
|---|---|---|---|---|
| 容量 | N ⋅ B N \cdot B N⋅B | ( N / 2 ) ⋅ B (N/2) \cdot B (N/2)⋅B | ( N − 1 ) ⋅ B (N-1) \cdot B (N−1)⋅B | ( N − 1 ) ⋅ B (N-1) \cdot B (N−1)⋅B |
| 可靠性 | 0(无) | 1(确定),最多 N / 2 N/2 N/2 | 1 | 1 |
| 顺序读 | N ⋅ S N \cdot S N⋅S | ( N / 2 ) ⋅ S (N/2) \cdot S (N/2)⋅S | ( N − 1 ) ⋅ S (N-1) \cdot S (N−1)⋅S | ( N − 1 ) ⋅ S (N-1) \cdot S (N−1)⋅S |
| 顺序写 | N ⋅ S N \cdot S N⋅S | ( N / 2 ) ⋅ S (N/2) \cdot S (N/2)⋅S | ( N − 1 ) ⋅ S (N-1) \cdot S (N−1)⋅S | ( N − 1 ) ⋅ S (N-1) \cdot S (N−1)⋅S |
| 随机读 | N ⋅ R N \cdot R N⋅R | N ⋅ R N \cdot R N⋅R | ( N − 1 ) ⋅ R (N-1) \cdot R (N−1)⋅R | N ⋅ R N \cdot R N⋅R |
| 随机写 | N ⋅ R N \cdot R N⋅R | ( N / 2 ) ⋅ R (N/2) \cdot R (N/2)⋅R | R / 2 R/2 R/2 | ( N / 4 ) ⋅ R (N/4) \cdot R (N/4)⋅R |
| 读延迟 | T T T | T T T | T T T | T T T |
| 写延迟 | T T T | ≈ T \approx T ≈T | 2 T 2T 2T | 2 T 2T 2T |
6.2 如何选择 RAID?
七、完整可运行的 C 语言模拟代码
以下代码模拟四种 RAID 的:块地址映射、容量计算、性能估算,以及奇偶校验的 XOR 计算和故障恢复。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* =====================================================
* 常量定义
* ===================================================== */
#define MAX_DISKS 8 /* 最多支持 8 块磁盘 */
#define BLOCKS_PER_DISK 16 /* 每块磁盘 16 个块(模拟用) */
#define BLOCK_SIZE 4 /* 每块 4 字节(模拟用,实际是 4KB) */
/* RAID 级别枚举 */
typedef enum {
RAID_0 = 0,
RAID_1 = 1,
RAID_4 = 4,
RAID_5 = 5
} RAID_Level;
/* RAID 配置结构 */
typedef struct {
RAID_Level level; /* RAID 级别 */
int num_disks; /* 磁盘数量 */
int chunk_size; /* 块大小(块数,模拟中 = 1) */
/* 模拟磁盘存储(二维数组:disks[磁盘号][块号]) */
unsigned char disks[MAX_DISKS][BLOCKS_PER_DISK * BLOCK_SIZE];
} RAID;
/* =====================================================
* 工具函数:XOR 两个数据块
* dst = dst XOR src,逐字节进行
* ===================================================== */
void xor_block(unsigned char *dst,
const unsigned char *src,
int size) {
for (int i = 0; i < size; i++) {
dst[i] ^= src[i]; /* 逐字节异或 */
}
}
/* =====================================================
* 打印块内容(十六进制)
* ===================================================== */
void print_block(const char *label,
const unsigned char *data,
int size) {
printf("%s: [", label);
for (int i = 0; i < size; i++) {
printf("%02X", data[i]);
if (i < size - 1) printf(" ");
}
printf("]\n");
}
/* =====================================================
* RAID-0 地址映射
* 逻辑块 A → (磁盘号, 磁盘内偏移)
*
* 公式:
* disk = A % num_disks
* offset = A / num_disks
* ===================================================== */
void raid0_map(int logical_block, int num_disks,
int *disk, int *offset) {
*disk = logical_block % num_disks;
*offset = logical_block / num_disks;
}
/* =====================================================
* RAID-1 地址映射
* 每对磁盘(0,1)、(2,3)互为镜像
* 逻辑块 A:
* 主盘 = (A % (num_disks/2)) * 2
* 镜像盘 = 主盘 + 1
* 偏移 = A / (num_disks/2)
* ===================================================== */
void raid1_map(int logical_block, int num_disks,
int *primary_disk, int *mirror_disk,
int *offset) {
int half = num_disks / 2;
*primary_disk = (logical_block % half) * 2;
*mirror_disk = *primary_disk + 1;
*offset = logical_block / half;
}
/* =====================================================
* RAID-4 地址映射
* 最后一块磁盘专用于奇偶校验
* 数据磁盘数 = num_disks - 1
* disk = A % (num_disks - 1)
* offset = A / (num_disks - 1)
* parity_disk = num_disks - 1
* parity_offset = offset(同一条带行号)
* ===================================================== */
void raid4_map(int logical_block, int num_disks,
int *disk, int *offset,
int *parity_disk, int *parity_offset) {
int data_disks = num_disks - 1;
*disk = logical_block % data_disks;
*offset = logical_block / data_disks;
*parity_disk = num_disks - 1; /* 最后一块是校验盘 */
*parity_offset = *offset;
}
/* =====================================================
* RAID-5 地址映射(左对称布局)
* 校验块在每条带轮转:条带 i 的校验块在磁盘 (num_disks - 1 - i % num_disks)
*
* 对于逻辑块 A:
* stripe = A / (num_disks - 1) ← 第几条带
* pos = A % (num_disks - 1) ← 在条带内的位置(跳过校验盘)
* parity_disk = (num_disks - 1 - stripe % num_disks + num_disks) % num_disks
* 实际磁盘 = pos < parity_disk ? pos : pos + 1
* ===================================================== */
void raid5_map(int logical_block, int num_disks,
int *disk, int *offset, int *parity_disk) {
int data_disks = num_disks - 1;
int stripe = logical_block / data_disks; /* 条带号 */
int pos = logical_block % data_disks; /* 条带内位置 */
/* 校验盘在每个条带轮转(简单递减轮转) */
*parity_disk = (num_disks - 1 - stripe % num_disks + num_disks) % num_disks;
*offset = stripe;
/* 实际磁盘号:跳过校验盘 */
if (pos < *parity_disk) {
*disk = pos;
} else {
*disk = pos + 1;
}
}
/* =====================================================
* 计算并打印 RAID 容量
* ===================================================== */
void print_capacity(RAID_Level level, int num_disks,
int blocks_per_disk) {
int capacity = 0;
switch (level) {
case RAID_0:
capacity = num_disks * blocks_per_disk;
break;
case RAID_1:
capacity = (num_disks / 2) * blocks_per_disk;
break;
case RAID_4:
case RAID_5:
capacity = (num_disks - 1) * blocks_per_disk;
break;
}
printf(" 有效容量 = %d 块(共 %d 块,利用率 %.1f%%)\n",
capacity,
num_disks * blocks_per_disk,
100.0 * capacity / (num_disks * blocks_per_disk));
}
/* =====================================================
* 打印 RAID 性能分析
* S = 单盘顺序速率(MB/s)
* R = 单盘随机速率(MB/s)
* T = 单盘延迟(ms)
* ===================================================== */
void print_performance(RAID_Level level, int num_disks,
double S, double R, double T) {
printf(" 性能分析(S=%.0f MB/s, R=%.2f MB/s, T=%.1f ms):\n",
S, R, T);
switch (level) {
case RAID_0:
printf(" 顺序读/写吞吐 = %.0f / %.0f MB/s\n",
num_disks * S, num_disks * S);
printf(" 随机读/写吞吐 = %.2f / %.2f MB/s\n",
num_disks * R, num_disks * R);
printf(" 读/写延迟 = %.1f / %.1f ms\n", T, T);
break;
case RAID_1:
printf(" 顺序读/写吞吐 = %.0f / %.0f MB/s\n",
(num_disks / 2.0) * S, (num_disks / 2.0) * S);
printf(" 随机读/写吞吐 = %.2f / %.2f MB/s\n",
num_disks * R, (num_disks / 2.0) * R);
printf(" 读/写延迟 = %.1f / ~%.1f ms\n", T, T);
break;
case RAID_4:
printf(" 顺序读/写吞吐 = %.0f / %.0f MB/s\n",
(num_disks - 1.0) * S, (num_disks - 1.0) * S);
printf(" 随机读/写吞吐 = %.2f / %.2f MB/s\n",
(num_disks - 1.0) * R, R / 2.0);
printf(" 读/写延迟 = %.1f / %.1f ms(小写代价高)\n",
T, 2 * T);
break;
case RAID_5:
printf(" 顺序读/写吞吐 = %.0f / %.0f MB/s\n",
(num_disks - 1.0) * S, (num_disks - 1.0) * S);
printf(" 随机读/写吞吐 = %.2f / %.2f MB/s\n",
num_disks * R, (num_disks / 4.0) * R);
printf(" 读/写延迟 = %.1f / %.1f ms\n", T, 2 * T);
break;
}
}
/* =====================================================
* 演示 XOR 奇偶校验的计算和恢复
* ===================================================== */
void demo_parity(void) {
printf("\n=== XOR 奇偶校验演示 ===\n\n");
/* 模拟 4 个数据块,每块 4 字节 */
unsigned char block0[BLOCK_SIZE] = {0x0A, 0x1B, 0x2C, 0x3D};
unsigned char block1[BLOCK_SIZE] = {0xFF, 0x00, 0xAB, 0xCD};
unsigned char block2[BLOCK_SIZE] = {0x12, 0x34, 0x56, 0x78};
unsigned char block3[BLOCK_SIZE] = {0x9E, 0xF0, 0x11, 0x22};
unsigned char parity[BLOCK_SIZE];
unsigned char recovered[BLOCK_SIZE];
/* 初始化校验块为全0 */
memset(parity, 0, BLOCK_SIZE);
/* 计算奇偶校验:P = B0 XOR B1 XOR B2 XOR B3 */
memcpy(parity, block0, BLOCK_SIZE);
xor_block(parity, block1, BLOCK_SIZE); /* P = B0 XOR B1 */
xor_block(parity, block2, BLOCK_SIZE); /* P = ... XOR B2 */
xor_block(parity, block3, BLOCK_SIZE); /* P = ... XOR B3 */
printf("原始数据块:\n");
print_block(" Block0", block0, BLOCK_SIZE);
print_block(" Block1", block1, BLOCK_SIZE);
print_block(" Block2", block2, BLOCK_SIZE);
print_block(" Block3", block3, BLOCK_SIZE);
print_block(" Parity", parity, BLOCK_SIZE);
/* 模拟 Block2 所在磁盘故障,尝试恢复 */
printf("\n--- 模拟 Block2 所在磁盘故障 ---\n");
printf("Block2 的数据已丢失,从其余块恢复...\n\n");
/* 恢复:recovered = B0 XOR B1 XOR B3 XOR Parity */
memcpy(recovered, block0, BLOCK_SIZE);
xor_block(recovered, block1, BLOCK_SIZE);
xor_block(recovered, block3, BLOCK_SIZE);
xor_block(recovered, parity, BLOCK_SIZE);
print_block(" 恢复的Block2", recovered, BLOCK_SIZE);
/* 验证恢复是否正确 */
if (memcmp(recovered, block2, BLOCK_SIZE) == 0) {
printf(" 验证通过:恢复的数据与原始 Block2 完全一致\n");
} else {
printf(" 验证失败!\n");
}
}
/* =====================================================
* 演示减法奇偶更新(Subtractive Parity)
* 公式:P_new = (C_old XOR C_new) XOR P_old
* ===================================================== */
void demo_subtractive_parity(void) {
printf("\n=== 减法奇偶更新演示 ===\n\n");
unsigned char c_old[BLOCK_SIZE] = {0x12, 0x34, 0x56, 0x78}; /* 旧数据 */
unsigned char c_new[BLOCK_SIZE] = {0xAB, 0xCD, 0xEF, 0x01}; /* 新数据 */
unsigned char p_old[BLOCK_SIZE] = {0xFF, 0x10, 0x20, 0x30}; /* 旧校验 */
unsigned char p_new[BLOCK_SIZE];
printf("旧数据 C_old: ");
print_block("", c_old, BLOCK_SIZE);
printf("新数据 C_new: ");
print_block("", c_new, BLOCK_SIZE);
printf("旧校验 P_old: ");
print_block("", p_old, BLOCK_SIZE);
/* 步骤1:计算差量:delta = C_old XOR C_new */
memcpy(p_new, c_old, BLOCK_SIZE);
xor_block(p_new, c_new, BLOCK_SIZE); /* p_new = C_old XOR C_new */
/* 步骤2:新校验 = 差量 XOR 旧校验 */
xor_block(p_new, p_old, BLOCK_SIZE); /* P_new = delta XOR P_old */
printf("新校验 P_new = (C_old XOR C_new) XOR P_old:\n");
print_block(" ", p_new, BLOCK_SIZE);
printf("\n(共需 2次读 + 2次写 = 4次 I/O)\n");
}
/* =====================================================
* 演示 RAID-0 地址映射
* ===================================================== */
void demo_raid0_mapping(int num_disks) {
printf("\n=== RAID-0 地址映射(%d 块磁盘)===\n", num_disks);
printf("逻辑块 → (磁盘号, 磁盘内偏移)\n\n");
/* 打印布局表头 */
for (int d = 0; d < num_disks; d++) {
printf("Disk%-4d", d);
}
printf("\n");
/* 打印前 16 个逻辑块的映射 */
for (int row = 0; row < 4; row++) {
for (int d = 0; d < num_disks; d++) {
int logical = row * num_disks + d;
printf("%-8d", logical);
}
printf("\n");
}
printf("\n具体映射示例:\n");
int test_blocks[] = {0, 5, 14, 7};
for (int i = 0; i < 4; i++) {
int lb = test_blocks[i];
int disk, offset;
raid0_map(lb, num_disks, &disk, &offset);
printf(" 逻辑块 %2d → Disk %d, Offset %d\n",
lb, disk, offset);
}
}
/* =====================================================
* 演示 RAID-5 奇偶轮转布局
* ===================================================== */
void demo_raid5_layout(int num_disks) {
printf("\n=== RAID-5 布局(%d 块磁盘)===\n", num_disks);
printf("P = 奇偶块\n\n");
int data_disks = num_disks - 1;
int total_logical = data_disks * 4; /* 打印前4条带 */
/* 先建立布局矩阵 */
/* layout[stripe][disk] = 逻辑块号,-1表示校验块 */
int layout[4][MAX_DISKS];
memset(layout, -1, sizeof(layout));
for (int lb = 0; lb < total_logical; lb++) {
int disk, offset, parity_disk;
raid5_map(lb, num_disks, &disk, &offset, &parity_disk);
if (offset < 4) {
layout[offset][disk] = lb;
}
}
/* 打印表头 */
for (int d = 0; d < num_disks; d++) {
printf("Disk%-4d", d);
}
printf("\n");
/* 打印每条带 */
for (int stripe = 0; stripe < 4; stripe++) {
int parity_disk = (num_disks - 1 - stripe % num_disks + num_disks) % num_disks;
for (int d = 0; d < num_disks; d++) {
if (d == parity_disk) {
printf("P%-7d", stripe);
} else if (layout[stripe][d] >= 0) {
printf("%-8d", layout[stripe][d]);
} else {
printf("? ");
}
}
printf("\n");
}
}
/* =====================================================
* 主函数
* ===================================================== */
int main(void) {
/* 磁盘参数 */
double S = 47.62; /* 顺序速率 MB/s */
double R = 0.981; /* 随机速率 MB/s */
double T = 10.0; /* 单盘延迟 ms */
int num_disks = 4; /* 磁盘数(RAID-4/5 用 5 块) */
printf("========================================\n");
printf(" RAID 综合演示程序\n");
printf("========================================\n\n");
/* ---- RAID-0 ---- */
printf("[RAID-0:条带化]\n");
print_capacity(RAID_0, num_disks, BLOCKS_PER_DISK);
printf(" 可靠性:无(任何一块磁盘失效即丢失全部数据)\n");
print_performance(RAID_0, num_disks, S, R, T);
demo_raid0_mapping(num_disks);
printf("\n");
/* ---- RAID-1 ---- */
printf("[RAID-1:镜像]\n");
print_capacity(RAID_1, num_disks, BLOCKS_PER_DISK);
printf(" 可靠性:可容忍每对中1块磁盘失效\n");
print_performance(RAID_1, num_disks, S, R, T);
printf("\n");
/* ---- RAID-4 ---- */
printf("[RAID-4:专用校验盘]\n");
print_capacity(RAID_4, num_disks + 1, BLOCKS_PER_DISK);
printf(" 可靠性:可容忍1块磁盘失效\n");
print_performance(RAID_4, num_disks + 1, S, R, T);
printf("\n");
/* ---- RAID-5 ---- */
printf("[RAID-5:旋转奇偶校验]\n");
print_capacity(RAID_5, num_disks + 1, BLOCKS_PER_DISK);
printf(" 可靠性:可容忍1块磁盘失效\n");
print_performance(RAID_5, num_disks + 1, S, R, T);
demo_raid5_layout(num_disks + 1);
/* ---- XOR 奇偶演示 ---- */
demo_parity();
/* ---- 减法奇偶更新演示 ---- */
demo_subtractive_parity();
printf("\n========================================\n");
printf(" 选择建议\n");
printf("========================================\n");
printf("纯性能优先 → RAID-0\n");
printf("随机I/O + 可靠性 → RAID-1\n");
printf("容量效率 + 顺序I/O → RAID-5\n");
printf("容量效率 + 只有大写 → RAID-4\n");
printf("========================================\n");
return 0;
}
代码编译和运行
gcc -o raid raid.c
./raid
示例输出片段
[RAID-0:条带化]
有效容量 = 64 块(共 64 块,利用率 100.0%)
可靠性:无(任何一块磁盘失效即丢失全部数据)
性能分析(S=48 MB/s, R=0.98 MB/s, T=10.0 ms):
顺序读/写吞吐 = 190 / 190 MB/s
随机读/写吞吐 = 3.92 / 3.92 MB/s
读/写延迟 = 10.0 / 10.0 ms
=== XOR 奇偶校验演示 ===
原始数据块:
Block0: [0A 1B 2C 3D]
Block1: [FF 00 AB CD]
Block2: [12 34 56 78]
Block3: [9E F0 11 22]
Parity: [60 CF 88 B0]
--- 模拟 Block2 所在磁盘故障 ---
恢复的Block2: [12 34 56 78]
验证通过:恢复的数据与原始 Block2 完全一致
八、知识全景图
九、核心公式汇总
RAID-0 地址映射:
Disk = A m o d N , Offset = ⌊ A / N ⌋ \text{Disk} = A \bmod N, \quad \text{Offset} = \lfloor A / N \rfloor Disk=AmodN,Offset=⌊A/N⌋
奇偶校验计算:
P = C 0 ⊕ C 1 ⊕ C 2 ⊕ ⋯ ⊕ C N − 2 P = C_0 \oplus C_1 \oplus C_2 \oplus \cdots \oplus C_{N-2} P=C0⊕C1⊕C2⊕⋯⊕CN−2
减法奇偶更新:
P n e w = ( C o l d ⊕ C n e w ) ⊕ P o l d P_{new} = (C_{old} \oplus C_{new}) \oplus P_{old} Pnew=(Cold⊕Cnew)⊕Pold
各 RAID 容量:
RAID-0 : N ⋅ B RAID-1 : N 2 ⋅ B RAID-4/5 : ( N − 1 ) ⋅ B \text{RAID-0}: N \cdot B \quad \text{RAID-1}: \frac{N}{2} \cdot B \quad \text{RAID-4/5}: (N-1) \cdot B RAID-0:N⋅BRAID-1:2N⋅BRAID-4/5:(N−1)⋅B
各 RAID 随机写吞吐:
RAID-0 : N ⋅ R RAID-1 : N 2 ⋅ R RAID-4 : R 2 RAID-5 : N 4 ⋅ R \text{RAID-0}: N \cdot R \quad \text{RAID-1}: \frac{N}{2} \cdot R \quad \text{RAID-4}: \frac{R}{2} \quad \text{RAID-5}: \frac{N}{4} \cdot R RAID-0:N⋅RRAID-1:2N⋅RRAID-4:2RRAID-5:4N⋅R
十、一句话记忆法
| RAID | 记忆 |
|---|---|
| RAID-0 | “并驾齐驱”,快但危险,一块坏全完 |
| RAID-1 | “影子备份”,每块都有副本,贵但安全 |
| RAID-4 | “一人管账”,校验盘是瓶颈,小写慢 |
| RAID-5 | “轮流管账”,校验轮转,小写好于RAID-4 |
本文档对应教材《操作系统:三个简单部件》第 38 章,版本 1.10。
第39章:文件与目录详解
概述
操作系统有三大核心虚拟化:CPU 虚拟化(进程)、内存虚拟化(地址空间)、存储虚拟化(文件系统)。前两个让程序感觉自己独占 CPU 和内存,文件系统则让程序感觉自己面对的是一个整洁有序的文件世界——而不是一堆原始磁盘扇区。本章介绍文件系统的用户接口(API)。
一、两个核心抽象:文件与目录
1.1 文件(File)
文件 = 字节的线性数组,可以读,可以写。
每个文件有两个名字:
| 名字类型 | 示例 | 说明 |
|---|---|---|
| 低级名(inode号) | 67158084 | 文件系统内部使用的数字编号 |
| 用户可读名 | foo.txt | 人类看到的文件名 |
文件系统不关心文件内容是什么(图片、代码、音乐……),只负责存储和取回这些字节。文件名后缀(.c、.jpg)只是约定,没有强制效果。
1.2 目录(Directory)
目录 = 一张映射表,把人类可读的名字映射到低级名(inode号)。
目录 /foo 的内容(概念上):
+----------+----------+
| 用户名 | inode号 |
+----------+----------+
| bar.txt | 10 |
| baz | 25 |
| . | 7 | ← 指向自身
| .. | 1 | ← 指向父目录
+----------+----------+
目录可以嵌套,形成目录树(Directory Tree)。
1.3 目录树结构
/ ← 根目录
├── foo/
│ └── bar.txt → 路径:/foo/bar.txt
└── bar/
├── bar/
│ └── (空)
└── foo/
└── bar.txt → 路径:/bar/foo/bar.txt
用 mermaid 表示:
绝对路径:从根目录 / 开始,用 / 分隔每一层。
二、文件描述符:打开文件的"通行证"
2.1 什么是文件描述符?
调用 open() 成功后,OS 返回一个文件描述符(File Descriptor,fd)——一个非负整数,是进程访问文件的"钥匙"。
进程的文件描述符表(每个进程私有):
fd → 含义
0 标准输入 (stdin) ← 键盘
1 标准输出 (stdout) ← 屏幕
2 标准错误 (stderr) ← 屏幕(错误信息)
3 第一个自己打开的文件(open()返回3)
4 第二个自己打开的文件
...
每次 open() 都会分配一个新的描述符,从 3 开始递增(0、1、2 已被占用)。
2.2 打开文件表(Open File Table)
系统维护一个全局的打开文件表,每个条目记录:
struct file {
int ref; /* 引用计数:有多少fd指向这个条目 */
char readable; /* 是否可读 */
char writable; /* 是否可写 */
struct inode *ip; /* 指向底层inode(文件元数据) */
uint off; /* 当前偏移量:下次读写从哪里开始 */
};
关系示意图:
进程A的fd表 全局打开文件表 磁盘上的inode
+---------+ +------------+
| fd=3 | ------> | off=0 | ------> inode #1000
| fd=4 | -. | readable=1 | (foo.txt 的元数据)
+---------+ | +------------+
| +------------+
`----> | off=100 | ------> inode #1000
| readable=1 | (同一个文件!)
+------------+
三、文件操作系统调用详解
3.1 创建文件:open()
int fd = open("foo", O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR);
| 标志 | 含义 |
|---|---|
O_CREAT |
如果文件不存在则创建 |
O_WRONLY |
只写模式打开 |
O_TRUNC |
若文件已存在,清空其内容(截断为0字节) |
O_RDONLY |
只读模式打开 |
S_IRUSR |
所有者可读(权限位) |
S_IWUSR |
所有者可写(权限位) |
3.2 读写文件:read() / write()
读文件(以 cat foo 为例,通过 strace 追踪):
系统调用追踪:
open("foo", O_RDONLY) = 3 ← 打开文件,得到fd=3
read(3, "hello\n", 4096) = 6 ← 读最多4096字节,实际读到6字节
write(1, "hello\n", 6) = 6 ← 写到标准输出(屏幕)
read(3, "", 4096) = 0 ← 返回0表示文件读完了
close(3) = 0 ← 关闭文件
read() 和 write() 的签名:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
3.3 随机访问:lseek()
默认情况下,读写是顺序的。lseek() 可以跳到文件任意位置:
off_t lseek(int fd, off_t offset, int whence);
whence 参数说明:
| whence 值 | 含义 |
|---|---|
SEEK_SET |
偏移 = offset(从文件开头) |
SEEK_CUR |
偏移 = 当前位置 + offset |
SEEK_END |
偏移 = 文件末尾 + offset |
注意:lseek() 只修改内存中的偏移量变量,不会引发任何磁盘 I/O!只有随后的 read()/write() 才会真正访问磁盘。
偏移量变化追踪示例(文件大小 300 字节):
系统调用 返回值 当前偏移
fd = open("file", O_RDONLY) 3 0
read(fd, buf, 100) 100 100
read(fd, buf, 100) 100 200
read(fd, buf, 100) 100 300
read(fd, buf, 100) 0 300 ← 返回0,已到文件末尾
close(fd) 0
3.4 强制写盘:fsync()
write() 只是把数据写到内存缓冲区,OS 会在某个时刻再刷到磁盘(可能延迟 5~30 秒)。
若要确保数据立即持久化(如数据库崩溃恢复场景):
int fd = open("foo", O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR);
write(fd, buffer, size); /* 写到缓冲区 */
fsync(fd); /* 强制刷到磁盘,返回后数据一定已持久化 */
close(fd);
注意:有时还需要对包含该文件的目录也调用 fsync(),确保目录条目也落盘。
3.5 原子重命名:rename()
文件重命名使用 rename(old, new),这个操作是原子的:
- 系统崩溃时,文件要么叫旧名,要么叫新名,不会出现中间状态
文件编辑器的安全写入模式(先写临时文件,再原子替换):
/* 1. 写到临时文件 */
int fd = open("foo.txt.tmp", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
write(fd, new_content, size);
/* 2. 强制落盘 */
fsync(fd);
close(fd);
/* 3. 原子替换:tmp → 正式文件名 */
rename("foo.txt.tmp", "foo.txt");
/* 此后 foo.txt 要么是旧版本,要么是新版本,不会出现损坏的中间态 */
3.6 获取文件信息:stat()
struct stat sb;
stat("foo.txt", &sb);
stat 结构包含的信息:
| 字段 | 含义 |
|---|---|
st_ino |
inode 号(低级名) |
st_size |
文件大小(字节) |
st_nlink |
硬链接计数 |
st_uid/st_gid |
所有者/组 ID |
st_mode |
权限位 |
st_atime/st_mtime/st_ctime |
访问/修改/状态变更时间 |
命令行查看:stat file 输出示例:
File: 'file'
Size: 6 Blocks: 8 IO Block: 4096 regular file
Device: 811h Inode: 67158084 Links: 1
Access: (0640/-rw-r-----) Uid: (30686/remzi)
Modify: 2011-05-03 15:50:20
四、fork() 与 dup() 共享文件表项
4.1 fork() 后父子共享同一文件表项
int fd = open("file.txt", O_RDONLY);
int rc = fork();
if (rc == 0) {
/* 子进程:把偏移移到10 */
lseek(fd, 10, SEEK_SET);
} else {
wait(NULL);
/* 父进程:查看当前偏移 */
printf("parent offset = %d\n", (int)lseek(fd, 0, SEEK_CUR));
/* 输出:parent offset = 10 ← 父子共享同一条打开文件表项! */
}
fork() 后的关系:
父进程 fd表 打开文件表 inode
fd=3 --------> [ off=10, ref=2 ] --> #1000 (file.txt)
^
子进程 fd表 |
fd=3 ----------------+
ref=2 表示有两个 fd 引用同一条表项,只有两个都关闭后,表项才会释放。
4.2 dup():创建指向同一文件表项的新描述符
int fd = open("README", O_RDONLY);
int fd2 = dup(fd); /* fd 和 fd2 现在指向同一个打开文件表项 */
/* 两个 fd 可以互换使用,共享同一个偏移量 */
dup2(old_fd, new_fd) 常用于 shell 中的输出重定向:把 stdout(fd=1)重定向到某个文件的 fd。
五、目录操作
5.1 创建目录:mkdir()
mkdir("foo", 0777); /* 创建目录,权限777 */
新建的"空"目录其实有两个默认条目:
新建目录 foo/ 的内容:
. → 指向自身(foo 本身的 inode)
.. → 指向父目录的 inode
5.2 读取目录内容
DIR *dp = opendir(".");
struct dirent *d;
while ((d = readdir(dp)) != NULL) {
printf("%lu %s\n", (unsigned long)d->d_ino, d->d_name);
}
closedir(dp);
struct dirent 的关键字段:
struct dirent {
ino_t d_ino; /* inode 号 */
char d_name[256]; /* 文件名 */
off_t d_off; /* 下一个条目的偏移 */
unsigned short d_reclen; /* 本条目长度 */
unsigned char d_type; /* 文件类型 */
};
5.3 删除目录:rmdir()
rmdir("foo"); /* 只能删除空目录(只含 . 和 .. 的目录) */
非空目录调用 rmdir() 会失败,必须先清空内容。
六、硬链接与软链接
6.1 硬链接(Hard Link)
硬链接 = 给同一个 inode 再起一个名字。
ln file file2 # 创建硬链接
目录条目:
file → inode #67158084
file2 → inode #67158084 ← 同一个 inode!
删除与引用计数:
echo hello > file → inode #67158084, link count = 1
ln file file2 → inode #67158084, link count = 2
ln file2 file3 → inode #67158084, link count = 3
rm file → inode #67158084, link count = 2(inode 未删除)
rm file2 → inode #67158084, link count = 1
rm file3 → inode #67158084, link count = 0 → 真正删除文件!
为什么删文件叫 unlink() 而不是 delete()?
因为"删除"其实是"断开"某个名字与 inode 之间的链接(link),将引用计数减 1。只有引用计数归零,inode 和数据块才会被真正释放。
硬链接的限制:
- 不能链接目录(防止目录树出现环)
- 不能跨文件系统(inode 号只在同一文件系统内唯一)
6.2 软链接(Symbolic Link / Soft Link)
软链接 = 一个独立的文件,内容是目标文件的路径字符串。
ln -s file file2 # 创建软链接
软链接文件 file2 的本质:
+---------------------+
| 数据:"file" | ← 存储的是目标路径字符串
| 类型:符号链接 |
| 大小:4字节 | ← 路径字符串"file"的长度
+---------------------+
软链接 vs 硬链接对比:
| 比较项 | 硬链接 | 软链接 |
|---|---|---|
| 本质 | 同一 inode 的另一个名字 | 存储路径的独立文件 |
| 能否链接目录 | 不能 | 能 |
| 能否跨文件系统 | 不能 | 能 |
| 原文件删除后 | 仍可访问 | 悬空引用(dangling reference) |
| inode | 与原文件相同 | 拥有自己的 inode |
| 文件大小 | 与原文件相同 | 路径字符串的字节数 |
悬空引用示例:
echo hello > file
ln -s file file2 # 创建软链接
rm file # 删除原文件
cat file2 # 错误:file2 指向的路径不存在了!
# 输出:cat: file2: No such file or directory
七、权限位与访问控制
7.1 权限位结构
ls -l foo.txt 输出:
-rw-r--r-- 1 remzi wheel 0 Aug 24 foo.txt
|||||||||
|||||||||__ 其他人:r--(只读)
||||||_____ 所属组:r--(只读)
|||________ 所有者:rw-(读写)
||_________ 文件类型:-(普通文件),d(目录),l(软链接)
每组三位:r(读=4)、w(写=2)、x(执行=1)
权限数字计算:chmod 640 foo.txt
640 8 = 6 ⏟ 所有者 : r w − 4 ⏟ 组 : r − − 0 ⏟ 其他 : − − − 640_{8} = \underbrace{6}_{所有者:rw-} \underbrace{4}_{组:r--} \underbrace{0}_{其他:-{}-{}-} 6408=所有者:rw−
6组:r−−
4其他:−−−
0
6 = 4 + 2 = r + w , 4 = r , 0 = 无权限 6 = 4+2 = r+w, \quad 4 = r, \quad 0 = 无权限 6=4+2=r+w,4=r,0=无权限
| 数字 | 二进制 | 权限 |
|---|---|---|
| 7 | 111 | rwx |
| 6 | 110 | rw- |
| 5 | 101 | r-x |
| 4 | 100 | r– |
| 0 | 000 | — |
7.2 目录的执行位
对目录来说,执行位(x)意味着可以进入(cd)该目录,而不是"可以执行"。
7.3 访问控制列表(ACL)
比权限位更精细,可以指定具体某个用户或用户组的权限:
fs listacl private # 查看目录的ACL
Access list for private is
system:administrators rlidwka
remzi rlidwka
fs setacl private/ andrea rl # 给 andrea 只读权限
八、挂载文件系统
8.1 创建文件系统:mkfs
mkfs -t ext3 /dev/sda1 # 在磁盘分区上创建 ext3 文件系统
8.2 挂载:mount
mount 把一个文件系统"粘"到目录树的某个位置:
mount -t ext3 /dev/sda1 /home/users
挂载前后的目录树:
挂载前: 挂载后:
/ /
├── home/ ├── home/
│ └── users/ │ └── users/ ← /dev/sda1 接入这里
└── ... │ ├── a/
│ │ └── foo
│ └── b/
│ └── foo
└── ...
挂载使所有文件系统共享一棵统一的目录树,路径命名一致。
九、完整可运行 C 语言代码
以下代码演示本章所有重要系统调用:open、read、write、lseek、stat、rename、mkdir、opendir、readdir、link、unlink,以及硬链接引用计数追踪。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h> /* open(), O_RDONLY 等标志 */
#include <unistd.h> /* read(), write(), close(), lseek(), unlink() */
#include <sys/stat.h> /* stat(), mkdir() */
#include <sys/types.h> /* 各种类型定义 */
#include <dirent.h> /* opendir(), readdir(), closedir() */
#include <assert.h> /* assert() */
#include <errno.h> /* errno */
/* =====================================================
* 演示1:创建、写入、读取文件
* 对应系统调用:open, write, read, close
* ===================================================== */
void demo_basic_file_ops(void) {
printf("\n=== 演示1:基本文件读写 ===\n");
const char *filename = "/tmp/demo_ostep.txt";
const char *content = "Hello, File System!\n你好,文件系统!\n";
/* --- 创建并写入文件 --- */
/* O_CREAT: 不存在则创建
* O_WRONLY: 只写
* O_TRUNC: 若已存在则清空
* 0644: 所有者读写,其他人只读 */
int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd < 0) {
perror("open(写)失败");
return;
}
printf("创建文件 fd=%d(0=stdin,1=stdout,2=stderr,所以从3开始)\n", fd);
ssize_t written = write(fd, content, strlen(content));
printf("写入 %zd 字节\n", written);
/* fsync 确保数据落盘(模拟数据库等对持久性要求高的场景) */
if (fsync(fd) == 0) {
printf("fsync 成功:数据已强制写到磁盘\n");
}
close(fd);
/* --- 打开并读取文件 --- */
fd = open(filename, O_RDONLY);
if (fd < 0) {
perror("open(读)失败");
return;
}
char buf[256];
memset(buf, 0, sizeof(buf));
/* 第一次读:读前半部分 */
ssize_t n = read(fd, buf, 20);
printf("第一次 read:读了 %zd 字节,内容:\"%.*s\"\n",
n, (int)n, buf);
/* 查看当前偏移(用 SEEK_CUR 偏移0来查询) */
off_t cur = lseek(fd, 0, SEEK_CUR);
printf("当前偏移量:%lld\n", (long long)cur);
/* 用 lseek 跳回文件开头 */
lseek(fd, 0, SEEK_SET);
printf("lseek 跳回开头,新偏移:0\n");
/* 再次读取 */
memset(buf, 0, sizeof(buf));
n = read(fd, buf, sizeof(buf) - 1);
printf("重新读取全部内容:\n%s", buf);
/* 读到文件末尾,返回0 */
n = read(fd, buf, sizeof(buf));
printf("继续读取,返回值=%zd(0 表示已到文件末尾)\n", n);
close(fd);
}
/* =====================================================
* 演示2:stat() 获取文件元数据
* ===================================================== */
void demo_stat(const char *path) {
printf("\n=== 演示2:stat() 文件元数据 ===\n");
struct stat sb;
if (stat(path, &sb) < 0) {
perror("stat 失败");
return;
}
printf("文件路径:%s\n", path);
printf(" inode 号(低级名):%lu\n", (unsigned long)sb.st_ino);
printf(" 文件大小:%lld 字节\n", (long long)sb.st_size);
printf(" 硬链接计数:%lu\n", (unsigned long)sb.st_nlink);
printf(" 分配块数:%llu\n", (unsigned long long)sb.st_blocks);
/* 解析文件类型 */
const char *type = "未知";
if (S_ISREG(sb.st_mode)) type = "普通文件";
else if (S_ISDIR(sb.st_mode)) type = "目录";
else if (S_ISLNK(sb.st_mode)) type = "符号链接";
printf(" 文件类型:%s\n", type);
/* 解析权限位 */
char perm[10];
perm[0] = (sb.st_mode & S_IRUSR) ? 'r' : '-'; /* 所有者读 */
perm[1] = (sb.st_mode & S_IWUSR) ? 'w' : '-'; /* 所有者写 */
perm[2] = (sb.st_mode & S_IXUSR) ? 'x' : '-'; /* 所有者执行 */
perm[3] = (sb.st_mode & S_IRGRP) ? 'r' : '-'; /* 组读 */
perm[4] = (sb.st_mode & S_IWGRP) ? 'w' : '-'; /* 组写 */
perm[5] = (sb.st_mode & S_IXGRP) ? 'x' : '-'; /* 组执行 */
perm[6] = (sb.st_mode & S_IROTH) ? 'r' : '-'; /* 其他读 */
perm[7] = (sb.st_mode & S_IWOTH) ? 'w' : '-'; /* 其他写 */
perm[8] = (sb.st_mode & S_IXOTH) ? 'x' : '-'; /* 其他执行 */
perm[9] = '\0';
printf(" 权限位:%s\n", perm);
}
/* =====================================================
* 演示3:硬链接与 unlink()
* 观察 link count 的变化
* ===================================================== */
void demo_hard_link(void) {
printf("\n=== 演示3:硬链接与 unlink ===\n");
const char *file1 = "/tmp/demo_link1.txt";
const char *file2 = "/tmp/demo_link2.txt";
const char *file3 = "/tmp/demo_link3.txt";
struct stat sb;
/* 清理旧文件 */
unlink(file1);
unlink(file2);
unlink(file3);
/* 创建原文件 */
int fd = open(file1, O_CREAT | O_WRONLY | O_TRUNC, 0644);
write(fd, "hello\n", 6);
close(fd);
stat(file1, &sb);
printf("创建 file1,inode=%lu,link count=%lu\n",
(unsigned long)sb.st_ino, (unsigned long)sb.st_nlink);
/* 创建第一个硬链接 */
link(file1, file2);
stat(file1, &sb);
printf("ln file1 file2 后,link count=%lu(同一 inode:%lu)\n",
(unsigned long)sb.st_nlink, (unsigned long)sb.st_ino);
/* 创建第二个硬链接 */
link(file1, file3);
stat(file1, &sb);
printf("ln file1 file3 后,link count=%lu\n",
(unsigned long)sb.st_nlink);
/* 删除原文件(unlink = 断开一个名字,引用计数-1) */
unlink(file1);
stat(file2, &sb);
printf("rm file1 后,file2 的 link count=%lu(文件未真正删除)\n",
(unsigned long)sb.st_nlink);
/* 验证 file2 和 file3 仍然可读 */
fd = open(file2, O_RDONLY);
char buf[20];
memset(buf, 0, sizeof(buf));
read(fd, buf, sizeof(buf) - 1);
close(fd);
printf("通过 file2 仍可读取内容:\"%s\"", buf);
/* 删除剩余两个链接 */
unlink(file2);
unlink(file3);
/* 此时 link count = 0,inode 和数据块真正被释放 */
printf("删除 file2 和 file3 后,inode 和数据块被真正释放\n");
}
/* =====================================================
* 演示4:软链接(符号链接)
* ===================================================== */
void demo_symlink(void) {
printf("\n=== 演示4:软链接 ===\n");
const char *target = "/tmp/demo_target.txt";
const char *link = "/tmp/demo_symlink.txt";
/* 清理 */
unlink(target);
unlink(link);
/* 创建目标文件 */
int fd = open(target, O_CREAT | O_WRONLY | O_TRUNC, 0644);
write(fd, "target content\n", 15);
close(fd);
/* 创建软链接 */
symlink(target, link);
printf("创建软链接:%s -> %s\n", link, target);
/* 通过软链接读取 */
fd = open(link, O_RDONLY);
char buf[32];
memset(buf, 0, sizeof(buf));
ssize_t n = read(fd, buf, sizeof(buf) - 1);
close(fd);
printf("通过软链接读到 %zd 字节:\"%s\"", n, buf);
/* 查看软链接本身的大小 */
struct stat sb;
lstat(link, &sb); /* lstat 获取链接本身的信息,不跟踪链接 */
printf("软链接文件大小:%lld 字节(= 目标路径字符串的长度)\n",
(long long)sb.st_size);
/* 演示悬空引用 */
unlink(target); /* 删除目标文件 */
fd = open(link, O_RDONLY);
if (fd < 0) {
printf("删除目标后,软链接变成悬空引用:%s\n", strerror(errno));
}
unlink(link);
}
/* =====================================================
* 演示5:目录操作与内容读取
* ===================================================== */
void demo_directory(void) {
printf("\n=== 演示5:目录操作 ===\n");
const char *dir = "/tmp/demo_dir_ostep";
/* 创建目录(如果已存在先删除) */
rmdir(dir);
if (mkdir(dir, 0755) == 0) {
printf("创建目录:%s\n", dir);
}
/* 在目录中创建几个文件 */
char path[256];
const char *names[] = {"alpha.txt", "beta.txt", "gamma.txt"};
for (int i = 0; i < 3; i++) {
snprintf(path, sizeof(path), "%s/%s", dir, names[i]);
int fd = open(path, O_CREAT | O_WRONLY, 0644);
write(fd, "data\n", 5);
close(fd);
}
/* 读取目录内容 */
printf("目录 %s 的内容:\n", dir);
DIR *dp = opendir(dir);
if (dp == NULL) {
perror("opendir 失败");
return;
}
struct dirent *d;
while ((d = readdir(dp)) != NULL) {
/* 跳过 . 和 .. */
if (strcmp(d->d_name, ".") == 0 || strcmp(d->d_name, "..") == 0)
continue;
/* 用 stat 获取更多信息 */
snprintf(path, sizeof(path), "%s/%s", dir, d->d_name);
struct stat sb;
stat(path, &sb);
printf(" inode=%-8lu 大小=%-6lld 名字=%s\n",
(unsigned long)d->d_ino,
(long long)sb.st_size,
d->d_name);
}
closedir(dp);
/* 清理:先删文件再删目录 */
for (int i = 0; i < 3; i++) {
snprintf(path, sizeof(path), "%s/%s", dir, names[i]);
unlink(path);
}
rmdir(dir);
printf("清理完成\n");
}
/* =====================================================
* 演示6:原子文件更新(先写临时文件,再 rename)
* ===================================================== */
void demo_atomic_rename(void) {
printf("\n=== 演示6:原子文件更新(rename) ===\n");
const char *final_file = "/tmp/demo_final.txt";
const char *tmp_file = "/tmp/demo_final.txt.tmp";
/* 第一步:写内容到临时文件 */
int fd = open(tmp_file, O_CREAT | O_WRONLY | O_TRUNC, 0644);
const char *new_content = "新版本的文件内容\n更新成功!\n";
write(fd, new_content, strlen(new_content));
/* 第二步:强制落盘 */
fsync(fd);
close(fd);
/* 第三步:原子替换
* rename 是原子的:系统崩溃时,要么旧文件还在,要么新文件已替换
* 不会出现"半新不旧"的损坏状态 */
if (rename(tmp_file, final_file) == 0) {
printf("原子重命名成功:tmp -> final\n");
}
/* 验证 */
fd = open(final_file, O_RDONLY);
char buf[128];
memset(buf, 0, sizeof(buf));
read(fd, buf, sizeof(buf) - 1);
close(fd);
printf("最终文件内容:\n%s", buf);
unlink(final_file);
}
/* =====================================================
* 主函数
* ===================================================== */
int main(void) {
printf("=============================================\n");
printf(" 第39章:文件与目录 API 演示程序\n");
printf("=============================================\n");
demo_basic_file_ops();
demo_stat("/tmp/demo_ostep.txt");
/* 清理 demo1 的文件 */
unlink("/tmp/demo_ostep.txt");
demo_hard_link();
demo_symlink();
demo_directory();
demo_atomic_rename();
printf("\n=============================================\n");
printf(" 演示完成\n");
printf("=============================================\n");
return 0;
}
https://godbolt.org/z/PGn9vxn9o
编译与运行
gcc -o files_demo files_demo.c
./files_demo
十、关键概念关系图
十一、文件描述符与打开文件表关系图
进程A 全局打开文件表(OFT) 磁盘 inode
+-------+ +-------------------+
| fd=0 |---------> [stdin 条目 ] --> 键盘
| fd=1 |---------> [stdout 条目 ] --> 屏幕
| fd=2 |---------> [stderr 条目 ] --> 屏幕
| fd=3 |---------> [off=100, ref=1 ] --> inode #1000 (foo.txt)
+-------+ +-------------------+
|
进程B(fork后) +-------------------+ |
+-------+ | | |
| fd=3 |----------> [off=100, ref=2 ] ------+
+-------+ |(ref=2: 父子共享)|
+-------------------+
十二、核心系统调用速查表
| 系统调用 | 功能 | 关键说明 |
|---|---|---|
open(path, flags, mode) |
打开/创建文件 | 返回文件描述符 |
read(fd, buf, n) |
读最多 n 字节 | 返回实际读取字节数,0=EOF |
write(fd, buf, n) |
写 n 字节 | 返回实际写入字节数 |
lseek(fd, offset, whence) |
移动当前偏移 | 不产生磁盘 I/O |
close(fd) |
关闭文件 | 减少引用计数 |
fsync(fd) |
强制落盘 | 确保数据持久化 |
stat(path, &sb) |
获取文件元数据 | 跟踪软链接 |
lstat(path, &sb) |
获取链接本身的元数据 | 不跟踪软链接 |
rename(old, new) |
原子重命名 | 崩溃安全 |
link(old, new) |
创建硬链接 | 引用计数+1 |
unlink(path) |
删除文件名 | 引用计数-1,归零才真正删除 |
symlink(target, link) |
创建软链接 | target 可不存在 |
mkdir(path, mode) |
创建目录 | 默认含 . 和 … |
rmdir(path) |
删除空目录 | 非空则失败 |
opendir/readdir/closedir |
遍历目录 | 返回 dirent 结构 |
十三、TOCTTOU 安全问题简述
检查-使用时间竞态(Time Of Check To Time Of Use):
时间线:
1. 服务以 root 身份 lstat("inbox") → 检查:是普通文件,安全
2. [攻击者此刻把 inbox rename 到 /etc/passwd !]
3. 服务写入数据 → 实际写的是 /etc/passwd!
防御措施:
- 打开文件时使用
O_NOFOLLOW标志,防止跟踪软链接 - 减少需要 root 权限运行的服务数量
- 使用事务性文件系统(目前部署较少)
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)