进阶 #os#file-system#directory

目录结构与文件分配

目录(Directory)是文件系统中组织文件的"树形索引"——它把文件名映射到 inode,而文件分配策略决定了磁盘块如何分配给文件

目录是什么?

目录(Directory) 本质上是一个特殊的文件——它的内容是<文件名, inode 号>的映射表。

目录" /home/user "的内容(简化):
┌──────────────┬───────┬──────────────┐
│ 文件名        │ inode │ 类型          │
├──────────────┼───────┼──────────────┤
│ .             │ 123   │ 目录(自身)   │
│ ..            │ 100   │ 目录(父目录)  │
│ hello.txt    │ 456   │ 普通文件      │
│ photos/      │ 789   │ 目录          │
│ main.c       │ 321   │ 普通文件      │
└──────────────┴───────┴──────────────┘

🏫 类比:图书馆的索引卡片 图书馆的目录柜(目录文件)里有一张张卡片(目录项),每张卡片写着一本书的名字(文件名)和它的书架位置(inode 号)。你查卡片找到位置,然后去书架上拿书。

目录的实现方式

简单方式:线性列表

目录文件的内容就是一系列固定长度的条目:

| inode | 文件名(16字节) | 文件类型 | ... |
|-------|-----------------|---------|-----|
| 456   | hello.txt\0...  | file    | ... |
| 789   | photos\0......  | dir     | ... |

缺点:查找需要线性扫描——目录文件越大越慢。

现代方式:哈希表 / B 树

Linux ext4 用 哈希树(HTree) 加速大目录查找:

目录项按文件名哈希值组织成 B 树结构
查找 "hello.txt" → 计算哈希值 → B 树搜索 → O(log n)

💡 这就是为什么一个目录下有几十万个文件也不影响查找速度——小目录直接线性扫描,大目录自动启用 HTree 索引。

目录结构:从根到叶子

/                    ← 根目录(root)
├── bin/             ← 系统命令(ls, cp, mv)
│   ├── ls
│   ├── cp
│   └── ...
├── etc/             ← 配置文件
│   ├── passwd
│   ├── fstab
│   └── ...
├── home/            ← 用户目录
│   └── user/
│       ├── docs/
│       ├── photos/
│       └── main.c
├── dev/             ← 设备文件(一切皆文件)
│   ├── sda
│   ├── tty
│   └── ...
└── proc/            ← 进程信息虚拟文件系统
    ├── 1234/
    └── ...

绝对路径 vs 相对路径

# 绝对路径——从根 / 开始
cd /home/user/docs

# 相对路径——从当前工作目录开始
cd docs

每个进程有一个当前工作目录(CWD)——pwd 查看,cd 修改。

路径解析

输入路径:/home/user/main.c

1. 从根目录 / 的 inode 开始
2. 在 / 的目录中查找 "home" → inode 100
3. 访问 inode 100 → 读取目录数据 → 查找 "user" → inode 200
4. 访问 inode 200 → 读取目录数据 → 查找 "main.c" → inode 300
5. 访问 inode 300 → 文件内容!

文件分配策略

磁盘空间怎么分配给文件?有三种基本策略:

1. 连续分配

每个文件占用磁盘上一段连续的空间。

文件 A(占块 2-5):
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ 0 │ 1 │ A0│ A1│ A2│ A3│   │   │ ...
└───┴───┴───┴───┴───┴───┴───┴───┘

优点:顺序读性能极好(磁盘不需要寻道)
缺点:外部碎片——删除文件后留下空洞

2. 链式分配

每个数据块中包含指向下一个块的指针。

文件 A:
块 7 → 块 3 → 块 12 → 块 5 → end
  ↓      ↓      ↓      ↓
数据    数据    数据    数据
+指针   +指针   +指针   +指针

优点:无外部碎片
缺点:随机访问慢(要顺着链找),指针占空间

3. 索引分配(ext4 等主流文件系统)

每个文件有一个索引块(inode),包含指向所有数据块的指针。

inode 的块指针数组:
[0]  → 块 7
[1]  → 块 3
[2]  → 块 12
[3]  → 块 5
...

优点:随机访问快(直接索引),无碎片
缺点:小指针数组对大文件不够用 → 多级索引

💡 ext4 使用**区段(Extent)**代替块指针——一个 extent 可以表示一段连续的块范围(起始块 + 长度),减少了指针数量。对于大文件,extent 比指针数组高效得多。

空闲空间管理

文件系统需要跟踪哪些块是空闲的:

位图法(主流方式)

用一位(bit)表示一个块是否空闲:

位图(每 1 位 = 1 个块):
块 0  1  2  3  4  5  6  7  8  9 ...
1  1  0  1  0  0  1  1  0  0 ...

1 = 已用,0 = 空闲

分配:在位图中找到第一个 0,设为 1,返回块号。 释放:在位图中设对应位为 0。

空闲链表

把所有空闲块用链表串起来——Linux 早期的 ext2 用过,现在已淘汰。

目录操作的系统调用

#include <stdio.h>
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>

int main() {
    // 创建目录
    mkdir("mydir", 0755);
    
    // 遍历目录
    DIR* dir = opendir(".");
    struct dirent* entry;
    while ((entry = readdir(dir)) != NULL) {
        printf("  %s (inode: %lu)\n", entry->d_name, entry->d_ino);
    }
    closedir(dir);
    
    // 删除文件
    unlink("oldfile.txt");
    
    // 删除目录(必须为空)
    rmdir("emptydir");
    
    return 0;
}

输出示例:

  . (inode: 12345)
  .. (inode: 10000)
  hello.txt (inode: 45678)
  mydir (inode: 78901)

小结

概念要点
目录文件名→inode 的映射表
路径解析按 / 分割,逐级查找目录
连续分配性能好,有碎片
索引分配ext4 等主流文件系统使用
空闲空间位图管理,O(1)分配释放
硬链接多个目录项指向同一 inode

为什么先学这个? 理解了目录和分配策略,你就知道文件在磁盘上是怎么”摆放”的了。下一节看看磁盘调度——操作系统如何优化对机械硬盘的访问。