# 第05讲：磁盘空间管理

> 🎯 **本节目标**：理解磁盘空间的外存组织方式，掌握FAT/NTFS/Ext2文件系统的结构，理解空闲空间管理和磁盘容错机制

## 📋 前置知识
- [[04_文件IO编程]] — 文件读写的基本概念
- [[01_系统运行机制]] — 存储层次结构

---

## 🤔 为什么需要这个？

当你保存一个4GB的电影文件时，操作系统需要决定：把文件存在磁盘的哪些位置？如何记录哪些空间已用、哪些空闲？如果某个磁盘块坏了怎么办？不同的组织方式会直接影响文件的读写速度和磁盘空间利用率。

**生活比喻**：
- **连续分配**就像电影院的连续座位：一整排坐在一起，找人很快，但如果有零散空位就浪费了
- **链接分配**就像寻宝游戏：每个地点告诉你下一个地点在哪，灵活但不能直接跳到第N个
- **索引分配**就像书的目录：通过目录直接找到对应页码，高效且灵活

---

## 📖 核心概念

### 1. 外存组织方式

文件在磁盘上的存放方式有三种基本策略：

```mermaid
graph TD
    A[外存组织方式] --> B[连续组织
顺序文件]
    A --> C[链接组织
隐式链接/FAT]
    A --> D[索引组织
单级/多级/混合]

    B --> B1["优点：顺序和随机访问都快"]
    B --> B2["缺点：外部碎片，不能动态增长"]
    C --> C1["优点：消除了外部碎片"]
    C --> C2["缺点：不能高效随机访问"]
    D --> D1["优点：支持随机访问，无外部碎片"]
    D --> D2["缺点：索引块有开销"]

    style A fill:#e1f5fe
```

#### 连续组织（顺序文件）

```mermaid
graph LR
    A[文件目录] -->|"起始块号=2, 长度=3"| B[磁盘块2]
    B --> C[磁盘块3]
    C --> D[磁盘块4]

    style B fill:#e8f5e9
    style C fill:#e8f5e9
    style D fill:#e8f5e9
```

- 文件占用一组**连续的磁盘块**
- 目录项只需记录：起始块号 + 长度
- 支持顺序访问和随机访问（直接计算偏移）
- 问题：外部碎片严重，文件不能动态增长

#### 链接组织

**隐式链接**：每个磁盘块末尾存储指向下一个块的指针。

```mermaid
graph LR
    A[目录] -->|"起始块号=2"| B[块2] -->|"指针→5"| C[块5] -->|"指针→8"| D[块8] -->|"指针→EOF"| E[结束]

    style B fill:#e1f5fe
    style C fill:#e1f5fe
    style D fill:#e1f5fe
```

- 优点：消除了外部碎片，文件可以动态增长
- 缺点：只能顺序访问，指针占用存储空间，可靠性差（一个指针损坏后续全部丢失）

**显式链接（FAT）**：将所有块的链接指针集中存放在一张**文件分配表（FAT）**中。

```mermaid
graph TD
    subgraph 文件分配表FAT
        F0["0: —"]
        F1["1: —"]
        F2["2: 5"]
        F3["3: —"]
        F4["4: —"]
        F5["5: 8"]
        F6["6: —"]
        F7["7: —"]
        F8["8: EOF"]
    end

    subgraph 磁盘块
        D2[块2]
        D5[块5]
        D8[块8]
    end

    D2 -.->|"FAT[2]=5"| D5
    D5 -.->|"FAT[5]=8"| D8
    D8 -.->|"FAT[8]=EOF"| STOP[结束]

    style F2 fill:#ffcdd2
    style F5 fill:#ffcdd2
    style F8 fill:#ffcdd2
```

- FAT常驻内存，随机访问时只需查表，不需要读磁盘
- 比隐式链接更可靠，但也更占内存

#### 索引组织

为每个文件建立一个**索引块**，集中存储所有数据块的块号：

```mermaid
graph TD
    A[文件目录] -->|"索引块号=10"| B[索引块]
    B -->|"指针0"| C[块2]
    B -->|"指针1"| D[块5]
    B -->|"指针2"| E[块8]
    B -->|"指针3"| F[块12]

    style B fill:#fff3e0
    style C fill:#e1f5fe
    style D fill:#e1f5fe
    style E fill:#e1f5fe
    style F fill:#e1f5fe
```

- 支持随机访问：访问第N个数据块，直接查索引块的第N个指针
- 无外部碎片
- 缺点：索引块本身占用空间，大文件需要多级索引

**三种方式对比**：

| 特性 | 连续组织 | 链接组织 | 索引组织 |
|------|----------|----------|----------|
| 顺序访问 | 快 | 快 | 快 |
| 随机访问 | 快（直接计算） | 慢（需遍历链） | 快（查索引表） |
| 外部碎片 | 严重 | 无 | 无 |
| 文件增长 | 困难 | 容易 | 容易 |
| 可靠性 | 高 | 低（指针损坏） | 高 |

### 2. FAT文件系统

FAT（File Allocation Table）是Windows早期广泛使用的文件系统。

```mermaid
graph TD
    subgraph FAT磁盘布局
        A[引导扇区] --> B[FAT1]
        B --> C[FAT2 备份]
        C --> D[根目录区]
        D --> E[数据区]
    end

    style A fill:#ffcdd2
    style B fill:#fff3e0
    style C fill:#fff3e0
    style D fill:#e1f5fe
    style E fill:#e8f5e9
```

**簇（Cluster）**：FAT文件系统的最小分配单位，由若干连续扇区组成。

| FAT版本 | 最大分区 | 簇大小 | FAT表项位数 |
|---------|---------|--------|-------------|
| FAT12 | 2MB | 512B~4KB | 12位 |
| FAT16 | 2GB | 2KB~32KB | 16位 |
| FAT32 | 2TB | 4KB~32KB | 32位（实际28位） |

**FAT表结构**：每个簇在FAT表中占一个表项，表项内容的含义：

| 表项值 | 含义 |
|--------|------|
| 0 | 空闲簇 |
| 2~N | 下一个簇的簇号 |
| FF8H~FFFH (FAT12) | 文件结束标记 |
| FFF8H~FFFFH (FAT16) | 文件结束标记 |
| 0FFFFFF8H~0FFFFFFFH (FAT32) | 文件结束标记 |
| 其他特殊值 | 坏簇标记 |

**FAT16的问题**：每个分区最多65536个簇，簇最小2KB时最大分区为2GB。小文件也会浪费一个簇的空间（簇内碎片）。

### 3. NTFS文件系统

NTFS（New Technology File System）是Windows NT系列的现代文件系统。

```mermaid
graph TD
    subgraph NTFS磁盘布局
        A[引导扇区
MBR/分区表] --> B[主控文件表MFT]
        B --> C[MFT副本]
        C --> D[系统文件区]
        D --> E[用户数据区]
    end

    style A fill:#ffcdd2
    style B fill:#fff3e0
    style C fill:#fff3e0
    style D fill:#e1f5fe
    style E fill:#e8f5e9
```

**关键特性**：

| 特性 | 说明 |
|------|------|
| 64位磁盘地址 | 支持超大分区（理论2^64字节） |
| MFT主控文件表 | 核心数据结构，每个文件/目录对应一条MFT记录 |
| LCN逻辑簇号 | 从分区开头算起的绝对簇号 |
| VCN虚拟簇号 | 文件内部的相对簇号（从0开始） |
| 日志文件 | 记录文件操作日志，崩溃后可恢复一致性 |
| 文件加密 | 支持EFS加密文件系统 |
| 文件压缩 | 支持透明压缩 |
| 硬链接 | 多个文件名指向同一个MFT记录 |

**MFT结构**：每条MFT记录通常1KB，包含多个属性（文件名、时间戳、数据内容等）。小文件的数据直接存储在MFT记录中（驻留属性），无需额外数据块。

### 4. Ext2文件系统

Ext2是Linux的经典文件系统，采用**块组**组织磁盘空间。

#### 磁盘总体布局

```mermaid
graph LR
    subgraph "Ext2磁盘布局"
        A["引导块
Block 0"] --> B["块组0"]
        B --> C["块组1"]
        C --> D["块组2"]
        D --> E["..."]
    end

    style A fill:#ffcdd2
```

#### 块组内部结构

```mermaid
graph TD
    subgraph "单个块组的结构"
        A["超级块 Super Block
文件系统全局信息"] --> B["块组描述符表
所有块组的描述信息"]
        B --> C["数据块位图
记录哪些块已用"]
        C --> D["inode位图
记录哪些inode已用"]
        D --> E["inode表
所有inode的数组"]
        E --> F["数据块
存放文件内容"]
    end

    style A fill:#ffcdd2
    style B fill:#ffcdd2
    style C fill:#fff3e0
    style D fill:#fff3e0
    style E fill:#e1f5fe
    style F fill:#e8f5e9
```

**超级块**（Super Block）：存储整个文件系统的元数据
- 块大小、inode总数、块总数、空闲块数、空闲inode数
- 挂载计数、上次检查时间等

**块组描述符**（Group Descriptor）：描述每个块组的状态
- 本块组的块位图位置、inode位图位置、inode表位置、空闲块数等

**块位图**（Block Bitmap）：每个bit对应一个数据块，1=已用，0=空闲

**inode位图**（inode Bitmap）：每个bit对应一个inode，1=已用，0=空闲

#### inode结构（128字节）

```mermaid
graph TD
    subgraph "inode 128字节"
        A["模式/权限 2B"] --> B["所有者ID 2B"]
        B --> C["文件大小 4B"]
        C --> D["时间戳 12B
访问/修改/创建"]
        D --> E["链接计数 2B"]
        E --> F["数据块指针 60B"]
    end

    subgraph "15个地址项（60字节）"
        G["直接块指针 0~9
10个 × 4B"]
        H["一次间接指针
1个 × 4B"]
        I["二次间接指针
1个 × 4B"]
        J["三次间接指针
1个 × 4B"]
    end

    F --> G
    F --> H
    F --> I
    F --> J

    style A fill:#e1f5fe
    style G fill:#e8f5e9
    style H fill:#fff3e0
    style I fill:#ffcdd2
    style J fill:#fce4ec
```

#### 混合索引容量计算（重点）

设盘块大小为**4KB**，盘块号占**4字节**，inode有**13个地址项**（10直接 + 1一次间接 + 1二次间接 + 1三次间接）：

每个盘块可存放盘块号个数：
$$4096 \div 4 = 1024 \text{ 个指针}$$

| 索引级别 | 计算过程 | 容量 |
|----------|----------|------|
| 直接块（10个） | 10 x 4KB | **40KB** |
| 一次间接 | 1024 x 4KB | **4MB** |
| 二次间接 | 1024 x 1024 x 4KB | **4GB** |
| 三次间接 | 1024 x 1024 x 1024 x 4KB | **4TB** |
| **总计** | | **约4TB** |

```mermaid
graph TD
    subgraph "混合索引寻址过程"
        A["inode"] -->|"直接块指针0~9"| B["10个数据块
共40KB"]
        A -->|"一次间接指针"| C["索引块1
1024个指针"]
        C -->|"指针0~1023"| D["1024个数据块
共4MB"]
        A -->|"二次间接指针"| E["二级索引块
1024个指针"]
        E -->|"指针0~1023"| F["1024个一级索引块"]
        F -->|"每个指向1024个数据块"| G["1024x1024个数据块
共4GB"]
        A -->|"三次间接指针"| H["三级索引块"]
        H --> I["1024个二级索引块"]
        I --> J["1024x1024个一级索引块"]
        J --> K["1024^3个数据块
共4TB"]
    end

    style A fill:#ffcdd2
    style B fill:#e8f5e9
    style D fill:#e8f5e9
    style G fill:#e8f5e9
    style K fill:#e8f5e9
```

**寻址示例**：假设要读取文件的第15000个字节（盘块大小4KB）：
- 逻辑块号 = 15000 / 4096 = 3（第4个块，索引为3）
- 块内偏移 = 15000 % 4096 = 2660
- 因为索引3 < 10，所以使用直接块指针[3]找到数据块
- 在该数据块的偏移2660处读取数据

#### 目录项 ext2_dir_entry_2

目录在Ext2中也是文件，内容是一系列目录项：

| 字段 | 大小 | 说明 |
|------|------|------|
| inode号 | 4字节 | 指向的inode编号 |
| rec_len | 2字节 | 本目录项总长度 |
| name_len | 1字节 | 文件名长度 |
| file_type | 1字节 | 文件类型（普通文件/目录/符号链接等） |
| name | 变长 | 文件名（不超过255字节） |

### 5. HDFS

HDFS（Hadoop Distributed File System）是大数据领域的分布式文件系统。

```mermaid
graph TD
    subgraph HDFS架构
        NN["名称节点 NameNode
元数据管理
文件→块映射
块→数据节点映射"]
        DN1["数据节点 DataNode1
块1, 块3"]
        DN2["数据节点 DataNode2
块2, 块4"]
        DN3["数据节点 DataNode3
块1副本, 块5"]
    end

    NN -->|"心跳/块报告"| DN1
    NN -->|"心跳/块报告"| DN2
    NN -->|"心跳/块报告"| DN3
    DN1 -->|"数据流"| CLIENT["客户端"]

    style NN fill:#ffcdd2
    style DN1 fill:#e1f5fe
    style DN2 fill:#e1f5fe
    style DN3 fill:#e1f5fe
```

**特点**：
- 文件被分割为固定大小的块（默认128MB），分布在多个数据节点上
- 每个块默认3副本，分布在不同机架
- 名称节点管理元数据（内存中），数据节点存储实际数据
- 适合大文件顺序读取，不适合小文件和随机写入

### 6. 空闲空间管理

#### 空闲表法

用一张表记录所有空闲区的起始块号和长度：

| 起始块号 | 空闲块数 |
|----------|----------|
| 2 | 3 |
| 10 | 5 |
| 20 | 2 |

适用于连续分配方式，适合少量空闲区的情况。

#### 空闲链表法

将所有空闲磁盘块用指针链接成一个链表。分配时从链头取，回收时插入链尾。缺点是指针占用空间，分配效率低。

#### 位示图法

用一个**位图（bitmap）**记录每个磁盘块的使用状态，1=已用，0=空闲（或反之）。

```
位示图示例（每行16位）：
第0行: 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0   → 块0~3已用
第1行: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0   → 块16~31空闲
第2行: 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0   → 块32~33已用
```

**地址转换公式**：

设每行有 **j** 位（通常为字长，如16位、32位），则第 **n** 个磁盘块对应位示图中：

$$\text{行号 } i = n \div j$$
$$\text{列号 } k = n \mod j$$

即：
$$n = j \times i + k$$

**经典公式**（每行16位时）：$n = 16 \times i + j$

**位示图法示例**：设磁盘共200个块，每行16位，位示图需要 200/16 = 13 行。

要找到第75个空闲块：$i = 75 \div 16 = 4$，$j = 75 \mod 16 = 11$，即第4行第11列。

#### 成组链接法（UNIX）

UNIX采用**成组链接法**管理空闲磁盘块，是空闲表法和空闲链表法的结合：

```mermaid
graph TD
    SB["超级块
栈：存放当前组的空闲块号
栈顶指针"] -->|"指向"| G1["空闲块组1
栈底块存储下一组的信息"]
    G1 -->|"下一组指针"| G2["空闲块组2"]
    G2 -->|"下一组指针"| G3["空闲块组3"]
    G3 -->|"下一组指针"| G4["更多组..."]

    style SB fill:#ffcdd2
    style G1 fill:#e8f5e9
    style G2 fill:#e8f5e9
    style G3 fill:#e8f5e9
```

**分配过程**：
1. 从超级块的栈中弹出一个空闲块号
2. 如果栈中只剩一个元素（它是下一组的指针），先将该组信息读入超级块，再弹出
3. 更新超级块

**回收过程**：
1. 将回收的块号压入超级块的栈中
2. 如果栈已满，将超级块中的栈信息写入回收块（成为新组），清空栈，将回收块号作为唯一元素

**优点**：分配和回收只需读写超级块（内存中），效率极高。

### 7. 磁盘IO优化

| 优化技术 | 原理 |
|----------|------|
| 磁盘高速缓存 | 在内存中开辟缓冲区缓存磁盘块，减少磁盘访问 |
| 提前读 | 顺序读取时，预先把后续块读入缓存 |
| 延迟写 | 先写入缓存，延迟到合适时机再写入磁盘 |
| 虚拟盘 | 用内存模拟磁盘（RAM Disk），速度极快但断电丢失 |

### 8. RAID

RAID（Redundant Array of Independent Disks）通过多块磁盘组合提高性能和可靠性。

```mermaid
graph TD
    subgraph RAID0 条带化
        A0["数据块1"] --> S0[磁盘0]
        A1["数据块2"] --> S1[磁盘1]
        A2["数据块3"] --> S0
        A3["数据块4"] --> S1
    end

    subgraph RAID1 镜像
        B0["数据"] --> M0[磁盘0]
        B0 --> M1[磁盘1 镜像]
    end

    subgraph RAID5 分布式校验
        C0["数据A"] --> R0[磁盘0]
        C1["数据B"] --> R1[磁盘1]
        C2["P校验"] --> R2[磁盘2]
        C3["数据C"] --> R3[磁盘3]
    end

    style S0 fill:#e1f5fe
    style S1 fill:#e1f5fe
    style M0 fill:#e8f5e9
    style M1 fill:#fff3e0
    style R0 fill:#e1f5fe
    style R1 fill:#e1f5fe
    style R2 fill:#ffcdd2
    style R3 fill:#e1f5fe
```

| RAID级别 | 原理 | 冗余 | 最少磁盘 | 利用率 | 特点 |
|----------|------|------|----------|--------|------|
| RAID0 | 数据条带化分布 | 无 | 2 | 100% | 高性能，无容错 |
| RAID1 | 数据完全镜像 | 100% | 2 | 50% | 高可靠，成本高 |
| RAID3 | 位交叉+专用校验盘 | 1块校验盘 | 3 | (N-1)/N | 校验盘成瓶颈 |
| RAID5 | 块交叉+分布式校验 | 分布式校验 | 3 | (N-1)/N | 性能与可靠性平衡 |

### 9. 磁盘容错

| 容错级别 | 技术 | 内容 |
|----------|------|------|
| SFT-I | 一级容错 | 双份目录和FAT表、热修复重定向（写入坏块时重定向到备用块） |
| SFT-II | 二级容错 | 磁盘镜像（同一控制器两个磁盘）、磁盘双工（不同控制器两个磁盘） |
| 集群容错 | 三级容错 | 多台服务器组成集群，一台故障其他接管 |

---

## 💻 动手实践

### 查看文件系统信息
```bash
# 查看磁盘使用情况
df -h

# 查看inode使用情况
df -i

# 查看文件系统类型和块大小
tune2fs -l /dev/sda1 | grep -E "Block size|Inode count"

# 查看文件的块分配
stat filename

# 查看文件的inode号
ls -i filename
```

---

## 🔗 知识关联
- 文件IO的read/write最终需要将数据写入 [[04_文件IO编程]] 中的物理磁盘
- 磁盘调度算法在 [[17_IO系统]] 中有详细讲解
- 文件系统是 [[13_存储管理基础]] 中存储管理的重要组成部分
- 分页存储管理的思想与Ext2的块分配有相似之处，见 [[14_分页存储管理]]

---

## 📝 思考题

1. **FAT表计算**：一个FAT16分区，每簇4KB，最多能管理多大的分区？为什么？
2. **Ext2容量计算**：如果盘块大小为1KB（而非4KB），盘块号占4字节，那么混合索引支持的最大文件是多少？（提示：每个间接块只能放256个指针）
3. **inode寻址**：给定盘块大小4KB，盘块号4字节，要读取文件偏移5GB处的数据，需要经过几级间接索引？
4. **成组链接法**：如果超级块栈最多容纳100个空闲块号，那么分配第101个空闲块时会发生什么？
5. **RAID选择**：一个视频监控系统需要大容量存储、持续写入、偶尔丢失可接受，应选择RAID几？为什么？

---

## 📚 扩展阅读
- 《计算机操作系统》（汤小丹）第5章：文件管理
- 《操作系统概念》第11-12章：文件系统接口与实现
- [ext2文件系统详解](https://www.nongnu.org/ext2-intro/)
- [RAID级别详解](https://www.techtarget.com/searchstorage/definition/RAID)
