概述

本章讲的是操作系统如何与各种输入/输出(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)
流程:

  1. OS 向设备发出请求
  2. OS 把当前进程睡眠,切换去跑其他进程
  3. 设备完成后,发出硬件中断信号
  4. CPU 跳转到**中断服务例程(ISR)**处理结果
  5. 唤醒等待 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 从搬运工作中解放出来。
工作流程:

  1. OS 告诉 DMA 引擎:数据在哪里、搬多少、搬到哪个设备
  2. CPU 去做别的事
  3. DMA 自己完成数据搬运
  4. 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 解决方案:抽象层次

POSIX API: open/read/write/close

通用块接口:
block read/write

协议专用接口

协议专用接口

协议专用接口

应用程序

文件系统

通用块层

SCSI 驱动

ATA 驱动

USB 驱动

SCSI 硬盘

IDE 硬盘

USB 闪存

  • 设备驱动程序(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 演示结束 ===
===== 演示结束 =====

九、完整知识结构图

I/O 设备

系统架构

设备结构

通信协议

效率优化

驱动程序

内存总线
最快最短

通用IO总线
如PCI

外围总线
如SATA/USB

接口层
寄存器: 状态/命令/数据

内部实现
微控制器/内存/专用芯片

轮询 Polling
CPU一直问

中断 Interrupt
设备主动通知

DMA
专用搬运硬件

中断合并
多次合一次

专用IO指令
in/out

内存映射IO
load/store

设备驱动抽象层
统一接口

十、核心概念总结


概念 一句话解释 解决的问题
轮询 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+TIOTcompute0(当 TIOTcompute
使用中断后的理想 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 n1
  • 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 720015000 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 49 ms
  • 最坏情况(从最内到最外)约为平均值的 2~3 倍
    平均寻道距离推导:
    设磁盘共有 N N N 条磁道(编号 0 0 0 N N N),任意两条磁道 x x x y y y 之间的寻道距离为 ∣ x − y ∣ |x - y| xy
    所有可能的寻道距离之和:
    ∑ 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=0Ny=0Nxy0N0Nxydydx(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(xy)dy+y=xN(yx)dy(37.6)
    化简得 x 2 − N x + 1 2 N 2 x^2 - Nx + \frac{1}{2}N^2 x2Nx+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(x2Nx+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,Ttransfer0.03 ms
T I / O ≈ 6  ms T_{I/O} \approx 6\text{ ms} TI/O6 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 KB0.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×2600004.2 ms,Ttransfer0.04 ms
T I / O ≈ 13.2  ms T_{I/O} \approx 13.2\text{ ms} TI/O13.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 KB0.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,Cheetah4+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,Cheetah125 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 选择=argrrequestsmin(Tseek(r)+Trotation(r))
    SPTF 通常在磁盘控制器内部实现,因为只有控制器知道磁头的精确位置。

6.4 三种算法对比

磁盘调度算法

SSTF
最短寻道优先

SCAN/电梯
来回扫描

SPTF
最短定位时间

优点:减少寻道时间

缺点:可能饥饿

优点:无饥饿
公平性好

缺点:忽略旋转延迟

优点:最接近SJF
性能最优

缺点:需磁盘内部实现
OS难以做到

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

八、知识全景图

硬盘驱动器 HDD

物理结构

I/O 时间模型

物理细节

磁盘调度

盘片 Platter
磁性涂层存数据

磁道 Track
同心圆形排列

磁头 Head
读写磁信号

磁臂 Arm
移动磁头

寻道时间 T_seek
磁臂移动到目标磁道

旋转延迟 T_rotation
等扇区转到磁头下

传输时间 T_transfer
实际读写数据

T_IO = T_seek + T_rotation + T_transfer

磁道偏斜
跨道连续读不丢扇区

多区域磁盘
外轨更多扇区

磁盘缓存
写回 vs 写穿

FIFO
按到达顺序

SSTF
最近磁道优先
可能饥饿

SCAN/电梯
来回扫描
无饥饿

SPTF
同时考虑寻道和旋转
性能最优

九、核心公式汇总


公式 含义
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 SR
    计算 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 MB47.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 KB0.981 MB/s
    因此 S / R ≈ 50 S/R \approx 50 S/R50,顺序访问比随机访问快约 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 NB(最优,无浪费)
可靠性 0(任何一块盘坏,所有数据丢失)
顺序读/写吞吐 N ⋅ S N \cdot S NS
随机读/写吞吐 N ⋅ R N \cdot R NR
单请求延迟(读/写) 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 (NB)/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 NR 可以同时用所有磁盘读
随机写吞吐 ( 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=C0C1C2C3
    示例(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=C0C1C3P
块级别的 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=C0C1C2newC3
需要读取的块数随磁盘数增加而增加,规模不好。
方法二:减法奇偶(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=(ColdCnew)Pold(38.1)
过程(3步):

  1. 读旧数据 C o l d C_{old} Cold 和旧校验 P o l d P_{old} Pold(2次读)
  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=(ColdCnew)Pold
  3. 写新数据 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 N1<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 (N1)B
可靠性 容忍 1 块磁盘失效
顺序读吞吐 ( N − 1 ) ⋅ S (N-1) \cdot S (N1)S
顺序写吞吐 ( N − 1 ) ⋅ S (N-1) \cdot S (N1)S(全条带写,并行)
随机读吞吐 ( N − 1 ) ⋅ R (N-1) \cdot R (N1)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=4NR
    分母 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 (N1)B ( N − 1 ) B (N-1)B (N1)B
可靠性 1 块 1 块
顺序读 ( N − 1 ) S (N-1)S (N1)S ( N − 1 ) S (N-1)S (N1)S
顺序写 ( N − 1 ) S (N-1)S (N1)S ( N − 1 ) S (N-1)S (N1)S
随机读 ( N − 1 ) R (N-1)R (N1)R N ⋅ R N \cdot R NR
随机写 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 NB ( N / 2 ) ⋅ B (N/2) \cdot B (N/2)B ( N − 1 ) ⋅ B (N-1) \cdot B (N1)B ( N − 1 ) ⋅ B (N-1) \cdot B (N1)B
可靠性 0(无) 1(确定),最多 N / 2 N/2 N/2 1 1
顺序读 N ⋅ S N \cdot S NS ( N / 2 ) ⋅ S (N/2) \cdot S (N/2)S ( N − 1 ) ⋅ S (N-1) \cdot S (N1)S ( N − 1 ) ⋅ S (N-1) \cdot S (N1)S
顺序写 N ⋅ S N \cdot S NS ( N / 2 ) ⋅ S (N/2) \cdot S (N/2)S ( N − 1 ) ⋅ S (N-1) \cdot S (N1)S ( N − 1 ) ⋅ S (N-1) \cdot S (N1)S
随机读 N ⋅ R N \cdot R NR N ⋅ R N \cdot R NR ( N − 1 ) ⋅ R (N-1) \cdot R (N1)R N ⋅ R N \cdot R NR
随机写 N ⋅ R N \cdot R NR ( 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?

纯性能
不在乎可靠性

随机 I/O 性能
且需要可靠性

容量利用率
且需要可靠性

顺序 I/O 为主

随机小写很多

选择 RAID 级别

最看重什么?

RAID-0
条带化

RAID-1
镜像

主要是顺序还是随机?

RAID-5
旋转奇偶

RAID-5
注意小写代价

优: 速度最快
容量最大
缺: 无保护

优: 随机读最好
可靠
缺: 容量损失一半

优: 容量好
顺序性能好
缺: 随机写有代价

七、完整可运行的 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

目标

评估维度

RAID 级别

关键技术

更快 - 并行I/O

更大 - 容量叠加

更可靠 - 冗余存储

透明 - 对上层透明

容量

可靠性

性能
顺序/随机
读/写

RAID-0
条带化
纯性能

RAID-1
镜像
可靠+随机好

RAID-4
专用校验盘
小写有瓶颈

RAID-5
旋转校验
综合最优

XOR 奇偶校验
故障恢复

条带化
并行读写

减法奇偶更新
高效写

一致性更新
写前日志

九、核心公式汇总

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=C0C1C2CN2
减法奇偶更新:
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=(ColdCnew)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:NBRAID-1:2NBRAID-4/5:(N1)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:NRRAID-1:2NRRAID-4:2RRAID-5:4NR

十、一句话记忆法


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 表示:

/(根目录)

foo/

bar/

bar.txt

bar/

foo/

bar.txt

绝对路径:从根目录 / 开始,用 / 分隔每一层。

二、文件描述符:打开文件的"通行证"

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

存储路径: file

原来指向

现在是悬空引用

file2(软链接)

file(已删除)

inode #1000(已释放)

???

七、权限位与访问控制

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

十、关键概念关系图

文件系统接口

文件操作

目录操作

链接机制

元数据与权限

挂载

open() 打开/创建
返回文件描述符

read() / write()
读写数据

lseek() 定位
改变当前偏移

fsync() 强制落盘
保证持久性

rename() 原子重命名
安全更新

mkdir() 创建目录

opendir/readdir
遍历目录内容

rmdir() 删空目录

link() 硬链接
同一 inode 多个名字

symlink() 软链接
存储路径的文件

unlink() 删链接
引用计数为0才真正删除

stat() 获取元数据
大小/inode/时间/权限

chmod() 修改权限
rwx × 3组

mkfs 格式化设备

mount 接入目录树

十一、文件描述符与打开文件表关系图

进程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 权限运行的服务数量
  • 使用事务性文件系统(目前部署较少)
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐