Linux文件系统深度解析与软硬链接

我打开了一个目录,里面全是乱码------目录到底是个什么东西?

先说结论。目录也是文件。目录的内容,存的是"文件名→inode 编号"的映射表。

你要是不信,我跑一段代码给你看。

先搞一段代码:把目录当文件读出来

我们用 vim 打开一个普通文件没问题,打开目录就是乱码。不是目录打不开------是 vim 不知道怎么解释目录里的内容。vim 是给文本文件准备的。目录里的东西根本不是文本。

所以得自己写程序读。系统调用给了我们一套工具:

c 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>

int main(int argc, char *argv[]) {
    DIR *dir = opendir(argv[1]);
    struct dirent *entry;
    while ((entry = readdir(dir)) != NULL) {
        if (entry->d_name[0] == '.') continue;  // 跳过 . 和 ..
        printf("%s -> inode: %lu\n", entry->d_name, entry->d_ino);
    }
    closedir(dir);
    return 0;
}

opendir 打开目录,返回一个 DIR* 对象------你就把它当成一个目录的句柄。readdir 一次读一条目录项,返回 struct dirent。这个结构体里面,两个字段我们最关心:d_name(文件名,最多 256 字节)和 d_ino(inode 编号)。还有一个 d_type 字段存文件类型,但这里我们用不上。

编译,跑起来:

bash 复制代码
gcc -o readdir readdir.c
./readdir .

输出大概是:

复制代码
readdir.c -> inode: 4876
dir -> inode: 4878
readdir -> inode: 48786

当前目录下有三个文件,每个文件名后面跟着一个数字------就是 inode 编号。这里有一条容易漏掉的:readdir 自己也在里面,因为编译出来的可执行文件也在当前目录。

再读根目录:

bash 复制代码
./readdir /

输出:

复制代码
mnt -> inode: 917506
root -> inode: 9175050
...

换个目录玩一下:

bash 复制代码
mkdir dir
cd dir
touch test.txt
cd ..
./readdir ./dir

输出:

复制代码
test.txt -> inode: 1967085

dir 里再建几个文件:

bash 复制代码
cd dir
touch hello.txt word.txt
cd ..
./readdir ./dir

输出:

复制代码
hello.txt -> inode: 8805089
test.txt -> inode: 1967085
word.txt -> inode: 8889

结论一句话:你在目录里新建文件时,系统把「文件名 + inode 编号」这一条记录,写进了目录文件的内容里。

就这么回事。

那目录到底是文件吗?

废话。当然是。

目录有 inode------你 ls -li 看一下,每个目录都有一个 inode 编号。目录有权限、有所属组、有大小、有时间戳,和普通文件一模一样。就是文件类型那一栏写的是 d 而不是 -

在磁盘上,目录和普通文件的存储方式没有任何区别。都是 inode 存属性,data block 存内容。普通文件的内容是你写的文本或二进制数据;目录的内容,是文件名到 inode 编号的映射表。

换个问法:磁盘上有没有"目录"这个东西?没有。磁盘上只有 inode + 字节字符串 。有的 inode 类型是 d,有的 inode 类型是 -,仅此而已。

那我平时用文件名找文件,怎么回事?

你从来没用过 inode 编号找文件吧?你用的都是文件名。但文件名不在 inode 的属性里------inode 里没有"文件名"这个字段。文件名存在目录的内容里。

所以访问一个文件的流程是这样的:

先去目录内容里查文件名 → 拿到 inode 编号 → 拿 inode 编号去文件系统里找这个文件的属性和内容。

具体讲,你要读 dir/hello.txt,系统做的事情是:

  1. dir 这个目录打开(opendir
  2. dir 的内容里,找到文件名 hello.txt 对应的 inode 编号
  3. 拿这个 inode 编号,在文件系统里定位到 hello.txt 的 inode
  4. 从 inode 里拿到属性、数据块编号
  5. 把内容读出来

整个过程中,dir 目录的内容就像一本电话簿。你告诉系统一个名字,系统帮你查出号码,再拨号过去。

这也就解释了为什么同一个目录下不能有重名文件------不是系统去查了每个文件的属性发现冲突了,而是你在新建文件时,系统去检查了当前目录内容里已有的所有文件名。发现重名,直接终止创建。

权限实验:如果模型是错的,这个实验就解释不了

光说不够。两个实验,自己动手试。

实验一:去掉目录的写权限。

bash 复制代码
chmod u-w dir
cd dir
touch hello.txt    # 报错:Permission denied

hello.txt 还没建呢,凭什么不让我建?因为建文件的过程,最后一步是要把 hello.txt → inode_xxx 这条记录写进 dir 目录的内容里。去掉 w 权限,就没有权利修改目录内容------往里面追加新的文件名映射。所以创建失败。

实验二:去掉目录的读权限。

bash 复制代码
chmod u-r dir
ls -l dir/         # 报错:Permission denied

ls -l 是查看目录下所有文件的属性。它不是直接去读每个文件的 inode------它是先打开 dir 目录,把目录内容里所有的(文件名 → inode)映射调出来,再逐个拿 inode 去查属性。去掉 r 权限,连目录内容都读不了,映射表拿不到,inode 编号全不知道,自然啥也查不出来。

两个实验指向同一个结论:目录的内容就是文件名到 inode 的映射表。 写权限控制的是"能不能修改这张表",读权限控制的是"能不能读这张表"。

换一个模型试试------如果文件名存在 inode 里,或者存在什么别的地方------这两个实验就解释不通了。为什么 chmod u-w dir 会影响在 dir 下建新文件?新文件又不是 dir,写新文件跟 dir 的写权限有什么关系?解释不了,是因为你的模型是错的。

路径解析:每次都是从根目录开始的

你说"读 hello.txt",系统以为你写的是 /home/whb/code/dir/hello.txt

系统只认绝对路径。相对路径是 shell 或者进程帮你拼上去的------shell 一直在后台记录当前的 pwd,你的进程启动时从父进程(shell)那里继承这个路径。你每次访问文件只写文件名,系统自动在前面拼上完整的路径前缀。你 cd 换目录,本质就是改了进程当前记录的那个路径字符串。

那系统怎么处理 /home/whb/code/dir/hello.txt 这一长串?

不是一步到位。是从左向右,一个目录一个目录地打开。

  1. 打开根目录 / → 拿到根目录的内容(根目录也是一个目录文件)
  2. 在根目录内容里找 home → 拿到 home 的 inode
  3. 打开 /home → 拿到 home 的内容
  4. 在 home 内容里找 whb → 拿到 whb 的 inode
  5. 打开 /home/whb → 拿到 whb 的内容
  6. 在 whb 内容里找 code → 拿到 code 的 inode
  7. ......一直这样递推下去
  8. 最后打开 /home/whb/code/dir,在 dir 内容里找到 hello.txt 的 inode
  9. 拿到 inode,查属性、读内容

这个过程叫路径解析

路径就像一张地图。每一级目录名是地图上的一个检查点。系统拿着你的路径字符串,从根开始,逐个检查点查过去。每过一级,就把当前目录的内容加载进内存,在里面找下一级名字对应的 inode。找不到就报错。

有一个有意思的细节:根目录的 inode 编号在系统开机时就确定了。 系统挂载根文件系统的时候,根目录的 inode 是已知的。如果你的文件系统坏了,找不到根目录 inode,系统启不来------会报 mount root error 之类的。

路径缓存:为什么要搞一棵多叉树

你想想,每次读 dir/hello.txt 都要走一遍完整的路径解析。读完 hello.txt,你再读 dir/test.txt,又要从根开始再走一遍。

这不是疯了?每次读同一个目录下的不同文件,都要重复打开 dir 这层目录。

所以内核里搞了一个东西:dcache(dentry cache,目录项缓存)

走过的路径,解析过的目录,内容直接缓存在内存里。下次走同样的路径,内存里直接查,不用再碰磁盘。

第一次在 /home/whb/codefind 某个文件,慢得要死------它要把整个目录树递归展开,大量路径解析,大量磁盘 IO。第二次跑同样的 find 命令,飞快------因为路径都缓存好了。

这就是为什么你 tree / 的时候特别慢。它本质上是把根目录下的所有路径都解析一遍,该缓存缓存,该淘汰淘汰。

缓存长什么样?一棵多叉树。

内核里有一个结构体叫 struct dentry。每个被打开的目录或文件,内核里都有一个 dentry 对象。

打开 /home/whb/code/dir/hello.txt 之后,内核里的 dentry 树大概长这样:

复制代码
/ 
├── home
│   └── whb
│       └── code
│           └── dir
│               └── hello.txt  ← 叶子节点

每个 dentry 里面存着:

  • 保护标志位
  • inode 指针(指向这个目录项对应的 inode)
  • 目录名(d_name
  • 指向父节点的指针(d_parent
  • 指向子节点链表的指针(d_subdirs,多叉树)
  • 自己的子节点链表节点(d_child,把自己挂到父节点的子链表上)
  • 一个哈希表节点(串到全局 dentry 哈希表里,快速查找)
  • LRU 链表节点(d_lru,用于淘汰最近最少使用的 dentry)
  • 指向所属文件系统的指针
  • 操作函数表(不同文件系统可以实现不同的 dentry 操作)
  • 是否被挂载的标志

你平时在命令行里用 pwd 显示当前路径,内核做的事情就是从你当前进程指向的那个 dentry 节点,一路沿着 d_parent 向上追溯到根,把沿路的目录名拼起来。

磁盘上没有"树"。 在磁盘上,普通文件和目录的存储方式完全一样------都是 inode 存属性,data block 存内容。所谓的"Linux 是树状结构",指的是内核内存里的这棵 dentry 多叉树。

换个问法:如果把 dentry 树从内存里抹掉,磁盘上的数据还在吗?在。文件系统的数据完整无缺。但你失去了快速路径查找的能力------每次访问文件都得从头解析。

进程和文件的关系

每个进程有一个 files_struct,里面存着打开的文件描述符表。文件描述符指向 struct file 对象。struct file 对象里有个指针,指向这个文件在 dentry 树中的节点。

所以从进程出发:

  • 打开的文件 → struct filedentry → inode(属性)
  • inode 里有数据块编号 → 拿到数据块 → 拿到内容

每个进程还记录了自己的"根目录 dentry"和"当前目录 dentry"。pwd 就是从当前 dentry 溯源到根拼出来的。你 cd 换目录,本质上就是把你进程的"当前 dentry"指针换了一个节点。

等等------磁盘上这些 inode、数据块、bitmap 到底是怎么来的?

前面我们一直在内存里转悠。dentry 树、路径解析、文件名→inode 映射------全在内核内存里。但磁盘上到底什么样?没有磁盘上的存储结构,内存里的东西全是空中楼阁。

所以退一步,从硬件开始。

磁盘是什么?一个三维数组。

别把磁盘想象成一片一片的。那是一种错觉。

磁盘真正的物理结构:多个盘面叠在一起,相同半径的磁道聚合成柱面(cylinder) 。你把一个磁道展开,得到一个一维数组(扇区排成一条线)。把一个柱面展开,得到多组磁道构成的二维数组。把所有柱面叠起来------磁盘是一个三维数组

访问任意一个扇区,需要三个下标:C (哪个柱面)、H (哪个磁头/盘面)、S(磁道里的哪个扇区)。这就是 CHS 寻址。

但三维数组用起来太麻烦。在逻辑上,三维数组可以拍扁成一维数组------这件事在学 C++ 的时候就搞清楚了,无非是 a[i][j][k] 偏移到 a[i * cols * depth + j * depth + k]。所以磁盘可以看作一个从头到尾排列扇区的一维数组。扇区的下标,从 0 开始,叫做 LBA(逻辑块地址)。

CHS 和 LBA 之间的转换,就是除法和取模。磁盘总容量 ÷ 512 字节,就是 LBA 的地址空间大小。

块:八个扇区绑一起

操作系统觉得每次访问一个扇区(512 字节)太琐碎,而且和硬件耦合太紧。所以把连续 8 个扇区绑成一个块(block),作为 IO 的基本单元。块大小 = 8 × 512B = 4KB。

磁盘从此变成了块设备。块 0、块 1、块 2......块和 LBA 之间也是除法和取模的关系。

分区:整盘太大,切成几块用

一个磁盘可能太大。把它分成几个区域,每个区域叫一个分区。分区表(存在 MBR 里)记录每个分区的起始 LBA 和结束 LBA。一个分区 = 8 字节(起止 LBA 各 4 字节),四个分区 = 24 字节。

系统开机时,BIOS 加载 MBR,操作系统就能读出分区信息。

一个分区内部:文件系统的骨架

一个分区还是一大块空间。文件系统把它进一步分成若干个块组(block group)。每个块组大小相同,结构相同。搞懂一个块组怎么管,整个分区就搞懂了。

每个分区都有一个 superblock,存的是整个分区的全局信息:

  • 分区里一共有多少个 inode
  • 一共有多少个数据块
  • 空闲了多少 inode、多少数据块
  • 第一个数据块的编号是多少
  • 每个组里有多少个块、多少个 inode

每个块组有一个 GDT(组描述符),描述这个组内部的布局:

  • block bitmap 的起始块号
  • inode bitmap 的起始块号
  • inode table 的起始块号
  • 组内空闲 block 数、空闲 inode 数

这些数据------superblock、GDT------在内核里都是结构体,格式化的时候把结构体的二进制数据直接写到磁盘上。

一个块组内部:四块区域

每个块组里有四样东西:

1. block bitmap(数据块位图)

一个比特位对应一个数据块。比特位的位置 表示第几个数据块,比特位的表示:0 = 空闲,1 = 已占用。

2. inode bitmap(inode 位图)

同理。一个比特位对应一个 inode。位置 = 第几个 inode,值 = 0/1 = 空闲/占用。

3. inode table(inode 表)

每一行是一个 inode,128 字节一个标准大小。存的是文件属性------类型、权限、大小、时间戳、数据块指针数组......唯一不存的是文件名。

4. data blocks(数据块)

存文件的实际内容。每个块 4KB。

拿着 inode 编号怎么找到文件?

在一个组里:

  1. 拿着 inode 编号 → 查 inode bitmap 的对应比特位,确认是 1(合法)
  2. 读 inode table 中对应位置那行,拿到 inode 的属性
  3. 属性里有数据块编号 → 去 data blocks 区域读对应块 → 拿到内容

删文件到底删了什么?

什么都不清。 只做一件事:把该文件占据的 inode bitmap 比特位从 1 改成 0,把对应的 block bitmap 比特位从 1 改成 0。属性和内容原封不动留在磁盘上。

能删就能恢复。恢复就是把那两个 bitmap 位从 0 改回 1。只要原来那块空间没被新文件覆盖,数据就还在。

跨组查找

inode 编号是分区内唯一的。块号也是分区内唯一。怎么在分区内定位到一个 inode 在第几个组的第几个位置?

复制代码
组号 = inode编号 / 每组的inode数     (整除)
组内偏移 = inode编号 % 每组的inode数  (取模)

举个具体数字。假设每个组有 10000 个 inode,你的 inode 编号是 11010:

  • 11010 / 10000 = 1 → 在组 1 里面
  • 11010 % 10000 = 10 → 组 1 里面的第 10 个 inode

块号同理。拿着块号 ÷ 每组的块数,就知道在第几组;取模就知道在组内的第几个块。

顺便说一句:mkfs 的输出里,每个组 2040 个 inode、8192 个块,比例大概是 1:4------一个 inode 对应四个数据块的空间配额。

块号同理。拿着块号 ÷ 每组的块数,就知道在第几组;取模就知道在组内的第几个块。

文件的数据块可以跨组------组一放不下就在组二申请块,改组二的 bitmap,把块号写到 inode 的指针数组里。

现在回到内存层的讨论。

inode 到数据块的映射:15 个指针怎么存下几个 G 的文件

inode 里面有一个数组,15 个元素。在 ext 系列文件系统里,每个元素存的是数据块编号。块大小是 4KB。

15 × 4KB = 60KB。那大文件怎么存?

不是 15 个全存数据。

  • 前 12 个(0~11):直接指针。每个指向一个 4KB 数据块,直接存文件内容。12 × 4KB = 48KB。小文件(≤48KB)只用到这 12 个。
  • 第 13 个(12 号下标):一级间接指针。它指向的 4KB 数据块不存文件内容,存的是其他数据块的编号。假设一个块号用 4 字节表示,4KB ÷ 4B = 1024 个块号。每个块号指向一个 4KB 数据块。所以这一层能表示 1024 × 4KB ≈ 4MB 的文件。
  • 第 14 个(13 号下标):二级间接指针。它指向的数据块里存 1024 个指针,每个指针指向的数据块里又存 1024 个指针,每个最终指向 4KB 数据。1024 × 1024 × 4KB ≈ 4GB。
  • 第 15 个(14 号下标):三级间接指针。1024 × 1024 × 1024 × 4KB ≈ 4TB。

文件可以跨组。 以前我们讲过一个文件的所有数据块不一定在同一个块组里。块号是分区内唯一的,组一放不下就把数据块放在组二、组三,只是修改对应组里的 bitmap 即可。inode 编号也是分区内唯一的。所以拿着 inode 编号,在整个分区里可以唯一定位到一个 inode;拿着块号,在整个分区里可以唯一定位到一个数据块。

这里还有一个容易忽略的细节:VFS(虚拟文件系统)层 。Linux 支持 ext2、ext3、ext4、xfs 等多种文件系统,每个文件系统在磁盘上的 inode 结构都不一样。内核不可能为每种文件系统写一套文件操作代码。所以内核抽象出了一层 VFS------struct inode 是 VFS 级的 inode,它内部有一个 private 指针,指向具体文件系统的真实 inode(比如 ext4 的 inode)。这跟 struct file 里套操作函数表的思路一样------面向对象里"接口 + 实现"那一套,在内核里用 C 语言的结构体指针就玩出来了。

现在回到内存层的讨论。

分区、格式化、挂载------亲手做一个

前面说的全是在一个分区里的事。但一台机器可能有好几个分区。你怎么确定你要找的文件在哪个分区?

先搞明白:一个分区要能被使用,得走三步。

第一步:分区

fdisk 之类的工具把磁盘切成几个区域。分区表(MBR 里)存的是每个分区的起始 LBA 和结束 LBA。

第二步:格式化(写入文件系统)

格式化的本质,是向分区里写入管理信息。把 superblock、GDT、bitmap、inode table 这些数据结构从内核写到磁盘上。写完以后,磁盘上才有文件系统的骨架------分组怎么划、每个组有多少 inode、多少数据块、哪些空闲------全写进去了。

第三步:挂载

一个格式化好的分区,在 Linux 里没办法直接用。你得把它挂载到一个目录上。

什么叫挂载?就是把分区和某个目录关联起来。你 cd 进那个目录,就等于进了那个分区。在那个目录里的所有增删查改,都作用在那个分区上。

原理上,内核里两个数据结构关联起来:分区在内核里有自己的结构体,目录在 dentry 树里有自己的节点。挂载就是把这两个结构体指到一起。

亲手实验

我们只有一块盘(vda1),一个分区挂载在根目录。那怎么做分区实验?用 dd 制造一个大文件,把它当成一个分区。

bash 复制代码
# 从 /dev/zero 读 50 兆零数据,写成一个大文件
dd if=/dev/zero of=disk.img bs=1M count=50

/dev/zero 是一个特殊文件(字符设备类型),你一直读它就一直给你返回零。还有一个兄弟叫 /dev/null------你往里面写什么都会被丢弃,像一个数据黑洞。这两个文件是系统提供的测试工具,本质上是在内存里解释成某种动作:读 /dev/zero 立马返回零,写 /dev/null 直接丢弃。

dd 绕开文件系统,直接以扇区/块级别拷数据。bs=1M 表示每次读写 1 兆,count=50 表示读写 50 次。结果:disk.img 占据了 50MB 的磁盘空间------相当于我们拿到了一个 50MB 的"分区"。

格式化它:

bash 复制代码
mkfs.ext4 disk.img

mkfs.ext4 会先弹一个警告:"不是块设备,确定要继续?"------因为 disk.img 是一个普通文件,不是 /dev/vda1 这种块设备。敲 yes 继续。

输出大概是:

复制代码
块大小=1024
inode 数量=12240
数据块数量=48960
预留块比例=5%
共分 6 个块组
每个块组 8192 个块
每个块组 2040 个 inode
superblock 备份在块 8193、24577......

顺便说一句,格式化完成后分区里会自动出现一个 lost+found 目录------这是 ext 系列文件系统的特性,用来存放 fsck 修复文件系统时找回的孤儿文件碎片。

你可以用 od 把这个文件以十六进制/八进制形式打开,看到里面二进制的文件系统数据------superblock 的魔数、bitmap 的位、inode table 的字节。这是体力活,知道能做就行。

挂载它:

bash 复制代码
sudo mount -t ext4 disk.img ./dir

df -h 会多出一行:

复制代码
/dev/loop0   43M  1.1M  39M   3% /path/to/dir

43M 而不是 50M,是因为文件系统预留了一部分空间给超级用户和元数据。磁盘厂商说 4T 的盘实际上只有 3.几 T,也是同理------1024 进制和 1000 进制的区别。

cd dir 进去以后,原来 dir 目录里的文件看不到了,看到的是新分区里的内容。你在里面 touch 一个文件,是建在这个新分区里的。

卸载:

bash 复制代码
# 先退出 dir 目录,否则会报 "target is busy"
cd ..
sudo umount ./dir

为什么必须先 cd 出去?因为你人还在这个目录里面的时候,目录在内核里是"正在被使用"的状态------你掐着自己的脖子,不可能把自己拎起来。退出去之后,目录的引用释放了,才能卸载。

卸载之后,原来 dir 的内容又露出来了。

怎么确定文件在哪个分区?

路径解析的时候,系统一直记录着"当前走到的分区"。每遇到一个目录是挂载点,就更新"当前分区"这个记录。路径解析完,"当前分区"的值就是离目标文件最近的挂载点对应的分区。

文件必须有路径。 这个路径帮你做路径解析、帮你形成 dcache 树、帮你确定在哪个分区。路径是整个文件系统的"地图"------缺了它,你根本不知道要访问的 inode 在哪个分区里。

软链接和硬链接------文件系统的两个小把戏

先把核心结论扔出来:

软链接是一个独立的文件,内容存的是目标文件的路径。

硬链接不是新文件,只是一个新的文件名(指向同一个 inode 的映射关系)。

软链接

bash 复制代码
ln -s readdir.c link-soft
ls -li

输出:

复制代码
4876 -rw-r--r-- 1 ... readdir.c
4880 lrwxrwxrwx 1 ... link-soft -> readdir.c

软链接有自己独立的 inode (4880 ≠ 4876)。有独立 inode,就是独立文件。文件类型是 l(link)。既然是独立文件,就有属性和内容。属性好说------权限是 lrwxrwxrwx。内容存什么?存的是目标文件的路径字符串

所以软链接就是 Linux 下的快捷方式。Windows 桌面上那个小箭头------右键属性,里面有个"目标"字段指向真实的 exe。软链接就是一样的东西。

为什么要用软链接? 加速查找。你装了一个软件,可执行程序藏在 /opt/app/v2.3.1/bin/app 这么深的路径里。每次打这么长的路径很烦。在 /usr/local/bin 下建一个同名软链接,以后直接输命令就能用。

还有一个场景:库升级。系统里有一个 /usr/lib/libc.so 软链接指向 libc-2.17.so。升级到 libc-2.18.so 以后,只需要删掉旧软链接,建一个同名软链接指向新版库。所有依赖 libc.so 的程序不用重新编译。

删除软链接可以用 rm,也可以用 unlink 系统调用------效果一样。删软链接不会影响目标文件。

硬链接

bash 复制代码
ln readdir.c link-hard
ls -li

输出:

复制代码
4876 -rw-r--r-- 2 ... link-hard
4876 -rw-r--r-- 2 ... readdir.c

inode 编号一样。 4876。引用计数(ls -l 第二列的数字)从 1 变成了 2。

硬链接不是独立文件。它只是一个新的文件名,指向同一个 inode 。在你建硬链接的那个目录的内容里,新插进了一条记录:link-hard → inode 4876

inode 内部维护了一个引用计数器。 这个计数器表示"有几个文件名指向我"。默认创建文件时,计数是 1------只有一个文件名和这个 inode 做了映射。建一个硬链接,计数加 1。删原文件,计数减 1。计数到 0,文件才真正被删除------才去清 bitmap。

你试一下:

bash 复制代码
rm readdir.c
ls -li

link-hard 还在,引用计数变回 1。文件的内容没丢,因为还有一个文件名指向它。文件真正被删除的条件不是"原文件被 rm",而是"引用计数归零"。

这像什么?像 C++ 里的 shared_ptr。多个 shared_ptr 指向同一个对象,对象在最后一个 shared_ptr 释放时才销毁。

硬链接有什么用?

最大的用处:做备份不拷数据。

你有一个 10GB 的文件,想在另一个地方备份。不需要真拷 10GB。只需要在另一个目录里建一个硬链接。用户把原文件删了,你的硬链接还在------因为引用计数没到 0。你可以在用户看不到的隐藏目录里建硬链接,万一用户误删,直接恢复。

mv 命令的本质也是硬链接。mv 不是拷贝文件内容,而是把"文件名 → inode"这条映射从源目录剪切到目标目录的内容里。

目录的引用计数为什么默认是 2?

bash 复制代码
mkdir newdir
ls -lai
复制代码
70995 drwxr-xr-x 2 ... newdir

2 是怎么来的?

  1. newdir 这个名字在上级目录里有一条映射 → newdir → inode 70995
  2. newdir 目录内部有一个 . 文件,. 这个文件名也映射到 inode 70995

. 不是什么特殊符号,它是一个文件名,映射到当前目录自己。

同理,你在 newdir 里再建一个子目录 anewdir 的引用计数就变成 3。因为 a 里面有一个 .... 映射到上级目录 newdir

根目录的引用计数是 18------你知道根目录下面有多少个子目录吗?18 - 2 = 16 个。自己去数一下。

所以文件系统到底是什么

不要以为文件系统只是磁盘上的 superblock、GDT、bitmap、inode table。那是磁盘上的存储结构。

真正的文件系统,是磁盘上的管理信息 + 内存里的管理结构,两者合在一起,才构成一套完整的数据管理方案。

内存里的那部分------struct filestruct dentrystruct inode(内核内虚化过的)、dcache 树------没有它们,进程根本没法访问磁盘上的文件。

而用户和文件系统之间的唯一入口,是文件描述符。文件描述符 → struct filedentry → inode → 数据块。这条链路走通了,属性和内容就都拿到了。

就这么回事。


所以到底发生了什么?就三件事。

  1. 目录的内容是文件名到 inode 的映射表。 你每次用文件名访问文件,系统先打开目录读这张表,拿到 inode 编号,再用 inode 去找属性和内容。
  2. 路径解析从根目录开始,用 dcache 多叉树缓存。 每个被访问过的目录和文件在内存里都有一个 dentry 节点,形成一个树。所谓的"Linux 是树状结构",指的是这棵内存里的树,不是磁盘上的物理结构。
  3. 挂载点把分区和目录关联起来。 文件通过路径解析确定自己属于哪个分区。软链接是快捷方式(独立文件存路径),硬链接是别名(新文件名指向同一个 inode,引用计数管生死)。