前言:上一篇留下了一个问题,文件名没有保存在 inode 中,那我们是怎么通过文件名找到文件的?这篇接着认识文件系统的组织方式。
1.文件系统怎样组织文件
磁盘分区提供了一片存储空间,但要在这片空间里保存文件,还需要知道哪些位置已经用了、哪些位置空着,以及每个文件的属性和内容分别放在哪里。
这些事情由文件系统来管理。这里以 Ext2 文件系统为例,把它的布局拆开来看。
1.1块组和各部分的分工
一个分区的空间可能很大,如果把所有管理信息都堆在一起,查找和管理也不方便。Ext2 会进一步把空间划分成多个块组,每组管理其中的一部分。

从上往下看,就是:磁盘划分出分区,分区中建立文件系统,文件系统再划分出多个块组。
块组中有保存文件内容的区域,也有专门用于管理的区域。先认识下面这几部分:
| 名称 | 主要作用 |
|---|---|
| Super Block,超级块 | 保存整个文件系统的基本信息,比如块的大小、块和 inode 的总数、空闲数量等 |
| GDT,块组描述符表 | 保存各块组的管理信息,指出该组位图、inode 表等放在什么位置 |
| Block Bitmap,块位图 | 记录这一组中哪些块已经占用、哪些块还空着 |
| inode Bitmap,inode 位图 | 记录这一组中哪些 inode 已经使用、哪些 inode 可以分配 |
| inode Table,inode 表 | 集中保存一个个 inode,也就是文件的属性及查找内容所需的信息 |
| Data Blocks,数据块区域 | 用来保存文件内容,也包括目录中保存的记录 |
位图可以理解成用一个个二进制位来做标记。 一个位对应一个块或者一个 inode,0 表示空闲,1 表示已占用。需要分配空间时,就可以通过位图寻找空闲位置。这也是为什么我们删除文件的时候可以这么快的原因,因为只需要把对应的比特位进行修改就可以了,反之写入就很慢(尤其是我这种农村网。。。)
管理信息本身也要占用磁盘空间,所以块位图中标记为已占用的块,既可能保存文件内容,也可能用于保存 inode 表等管理信息。
超级块保存的是整个文件系统的信息,块组描述符则帮助我们找到各组的具体管理区域。超级块等重要信息还会留有备份,所以超级块并不是每一个组都有,之所以有多个主要是为了做备份免得一个蓝屏给数据全干没了。
我们平时说的格式化,其中一项重要工作就是建立这些管理结构。
1.2通过 inode 找到文件内容
每个文件都有自己的属性,比如类型、权限、所属用户和大小等。这些信息集中保存在 inode 中,inode 还记录着寻找文件内容的方法。
inode 号相当于文件在这个文件系统中的编号。 在同一个文件系统内,可以用它找到对应的 inode;换到另一个文件系统,相同的编号就可能代表另一个文件。
inode 表中的记录按固定大小排列。比如按一个 inode 占 128 字节、一个块占 4KB 来算,一个块就能放下 32 个 inode。
有了 inode 号,系统就能根据每组的 inode 数量,算出它属于哪个块组、在组内排在什么位置,再通过块组描述符找到 inode 表。
这和通过数组下标找到元素有些相似,不需要把所有文件的属性挨个翻一遍。
找到 inode 后,又怎么找到内容呢?可以看传统 Ext2 中的这张索引图:

前面的 12 个是直接指针,可以理解为直接记录数据块的编号。知道块号,就能找到对应的数据块。
文件比较大,这些位置不够用了,就会用到后面的间接索引:先找到一个专门保存块号的索引块,再通过其中的块号寻找数据块。一级间接索引不够时,还可以继续使用二级、三级间接索引。
这样,inode 本身不需要跟着文件内容一起变大。文件内容增加时,可以分配更多数据块,再通过索引把它们找出来。
inode 中还记录了文件的几个时间,查看文件信息时经常会碰到:
| 时间 | 含义 |
|---|---|
| Access,atime | 文件最后访问的时间 |
| Modify,mtime | 文件内容最后修改的时间 |
| Change,ctime | 文件状态最后改变的时间,例如权限、所有者或内容发生变化 |
2.从文件名找到文件
2.1目录中保存了什么
前面已经有了"inode → 数据块"这条关系,但平时访问文件时,我们写的是文件名,怎么得到 inode 号呢?
答案就在文件所在的目录中。
目录也是文件,也有自己的 inode 和内容。普通文本文件的内容可以是文字,而目录中保存的是一个个目录项,其中包含文件名与 inode 号的对应关系。

图中,目录记录了文件名 abc 对应的 inode 号是 263466。找到这个 inode 后,又可以找到保存内容的 300、500、800 号数据块。
因此,访问文件可以先按这条顺序理解:
在目录中查找文件名 → 得到 inode 号 → 找到 inode → 找到文件内容。
反过来看,创建并写入一个需要三个数据块的文件,也就涉及几件事情:
- 从 inode 位图中找到空闲 inode,保存新文件的属性。
- 从块位图中找到空闲块,用来保存写入的内容。
- 记录文件使用了哪些块,并更新相应的占用标记。
- 在所在目录中增加"文件名 → inode 号"的记录。
当然刚创建出来的空文件还没有正文,所以通常不需要立即分配保存正文的数据块。
我们也可以通过目录接口,直接读出目录项中的名字和 inode 号:
c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <directory>\n", argv[0]);
exit(EXIT_FAILURE);
}
DIR *dir = opendir(argv[1]);
if (!dir) {
perror("opendir");
exit(EXIT_FAILURE);
}
struct dirent *entry;
while ((entry = readdir(dir)) != NULL){
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned long)entry->d_ino);
}
closedir(dir);
return 0;
}
结果:

看图右侧,程序已经把一个个 Filename 和 Inode 打印出来了。
2.2路径怎样一步步找到文件
一个文件名要结合所在目录才有意义。不同目录里可以存在同名文件,所以访问文件时,还需要知道它位于哪个目录。
比如路径 /home/hjy/testdir/MyShell,查找过程就可以顺着路径逐级往下走:

先从根目录 / 中找到 home,再到 home 中找到 hjy,然后找到 testdir,最后在其中找到 MyShell。
每往下走一级,都在利用当前目录中保存的"名字 → inode 号"的关系。这个查找过程由系统完成,我们把路径交给文件接口即可。
绝对路径从根目录开始,相对路径从进程的当前工作目录开始。 比如已经位于 /home/hjy/testdir 时,使用相对路径 MyShell,就可以从当前目录查找这个名字。
如果每次访问都重新从磁盘读取这些目录信息,重复的工作就太多了。因此,系统会把查找中用到的一部分信息缓存在内存里。

这里的 dentry 可以先理解为内存中保存的一份目录项信息。它记录名字、父目录等关系,对于已经找到的文件,还会关联到对应的 inode。
再次查找某段路径时,如果需要的信息已经在缓存中,就可以直接利用,减少重复的磁盘访问。
那相对路径中的"当前目录",又是从哪里知道的呢?可以把它和前面的进程、文件描述符联系起来:

上半部分表示进程会记录自己的根目录和当前工作目录,因此系统知道路径查找应该从哪里开始。
下半部分则是已经打开的文件:进程通过文件描述符找到打开文件的相关信息,再联系到目录项、inode 等对象。
所以,路径主要帮助我们找到并打开文件;文件打开之后,后续读写就可以使用文件描述符,不需要每次再传入完整路径。
2.3把文件系统挂到目录树中
一台机器上可以有多个分区,也可以连接其他磁盘,而 Linux 中看到的目录却从 / 一直往下展开。这些不同位置的文件系统,是通过挂载接到目录树中的。
比如把一个文件系统挂载到 /mnt/mydisk,这个目录就是它的挂载点 。之后访问 /mnt/mydisk/a.txt,就会进入挂载在这里的文件系统,查找其中的 a.txt。
这样,我们仍然通过路径访问文件,不需要在文件名中额外写出磁盘或分区编号。根文件系统对应目录树的起点 /,其他文件系统可以继续接到下面的目录中,我的这个云服务器只有一个盘。
mount 用于挂载,umount 用于卸载;df -h 可以查看已挂载文件系统的空间使用情况。
3.软链接与硬链接
3.1硬链接
前面说目录保存着"文件名 → inode 号"的对应关系。如果再增加一个名字,让它指向同一个 inode,会发生什么?
这就是硬链接。

我们先创建 ab,再使用 ln ab cd 建立硬链接。随后查看,可以发现 ab 和 cd 的 inode 号都是 921458,链接数也都变成了 2。
这表示现在有两个名字指向同一个文件。通过 ab 修改内容,再通过 cd 读取,也会看到修改后的内容,因为它们找到的是同一个 inode 和同一份数据。
两个名字也可以位于不同目录,只要仍在同一个文件系统中。
此时删除 ab,相当于去掉一个名字与 inode 的对应关系,链接数从 2 变成 1。cd 仍然能找到这个文件,文件内容也还在。
等最后一个硬链接被删除,并且没有进程继续打开这个文件时,文件占用的空间才会被回收,所以这个东西可以启动备份的作用。
3.2软链接
软链接也叫符号链接,可以把它理解为指向目标文件的快捷方式。
下面是另一个独立的例子,这次使用 ln -s ab cd:

图中,ab 的 inode 号是 921458,而 cd 的 inode 号是 921777,两者不同。cd 的类型显示为 l,后面还有 cd -> ab,表示它指向路径 ab。
软链接有自己的 inode,保存的是目标路径。 访问 cd 时,系统会根据其中保存的路径继续寻找 ab。
图中 cd 的大小是 2,就是因为它保存的目标路径 ab 占两个字节,并不是把 ab 的内容复制了一份。
如果把目标 ab 删除,cd 这个软链接本身还可以存在,但原来的路径已经找不到目标,这时就成了失效的链接。反过来,单独删除软链接 cd,不会删除目标 ab。初中第一次接触电脑时就以为删除快捷就是删除了这个应用程序哈哈
软链接通过路径找目标,因此可以指向目录,也可以跨越文件系统。目标位置很深、路径很长时,就可以在方便的位置放一个软链接,作为访问入口。
把两种链接放在一起看:
| 对比项 | 硬链接 | 软链接 |
|---|---|---|
| 找到目标的方式 | 名字直接对应同一个 inode | 保存目标路径,再根据路径查找 |
| 是否有新的 inode | 与原文件共用 inode | 有自己的 inode |
| 对原文件硬链接数的影响 | 增加硬链接数 | 不增加原文件的硬链接数 |
| 能否跨文件系统 | 不能 | 可以 |
| 能否指向目录 | 一般不允许为目录建立硬链接 | 可以 |
| 删除原来的名字后 | 其他硬链接仍能访问文件 | 原目标路径失效时,软链接也会失效 |
需要让同一份文件拥有多个名字时,可以用硬链接;需要一个指向某个路径的访问入口,尤其是目录或其他文件系统中的目标时,可以用软链接。
完