我打开了一个目录,里面全是乱码------目录到底是个什么东西?
先说结论。目录也是文件。目录的内容,存的是"文件名→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,系统做的事情是:
- 把
dir这个目录打开(opendir) - 在
dir的内容里,找到文件名hello.txt对应的 inode 编号 - 拿这个 inode 编号,在文件系统里定位到
hello.txt的 inode - 从 inode 里拿到属性、数据块编号
- 把内容读出来
整个过程中,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 这一长串?
不是一步到位。是从左向右,一个目录一个目录地打开。
- 打开根目录
/→ 拿到根目录的内容(根目录也是一个目录文件) - 在根目录内容里找
home→ 拿到 home 的 inode - 打开
/home→ 拿到 home 的内容 - 在 home 内容里找
whb→ 拿到 whb 的 inode - 打开
/home/whb→ 拿到 whb 的内容 - 在 whb 内容里找
code→ 拿到 code 的 inode - ......一直这样递推下去
- 最后打开
/home/whb/code/dir,在 dir 内容里找到hello.txt的 inode - 拿到 inode,查属性、读内容
这个过程叫路径解析。
路径就像一张地图。每一级目录名是地图上的一个检查点。系统拿着你的路径字符串,从根开始,逐个检查点查过去。每过一级,就把当前目录的内容加载进内存,在里面找下一级名字对应的 inode。找不到就报错。
有一个有意思的细节:根目录的 inode 编号在系统开机时就确定了。 系统挂载根文件系统的时候,根目录的 inode 是已知的。如果你的文件系统坏了,找不到根目录 inode,系统启不来------会报 mount root error 之类的。
路径缓存:为什么要搞一棵多叉树
你想想,每次读 dir/hello.txt 都要走一遍完整的路径解析。读完 hello.txt,你再读 dir/test.txt,又要从根开始再走一遍。
这不是疯了?每次读同一个目录下的不同文件,都要重复打开 dir 这层目录。
所以内核里搞了一个东西:dcache(dentry cache,目录项缓存)。
走过的路径,解析过的目录,内容直接缓存在内存里。下次走同样的路径,内存里直接查,不用再碰磁盘。
第一次在 /home/whb/code 下 find 某个文件,慢得要死------它要把整个目录树递归展开,大量路径解析,大量磁盘 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 file→dentry→ 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 编号怎么找到文件?
在一个组里:
- 拿着 inode 编号 → 查 inode bitmap 的对应比特位,确认是 1(合法)
- 读 inode table 中对应位置那行,拿到 inode 的属性
- 属性里有数据块编号 → 去 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 是怎么来的?
newdir这个名字在上级目录里有一条映射 →newdir → inode 70995newdir目录内部有一个.文件,.这个文件名也映射到 inode 70995
. 不是什么特殊符号,它是一个文件名,映射到当前目录自己。
同理,你在 newdir 里再建一个子目录 a,newdir 的引用计数就变成 3。因为 a 里面有一个 ..,.. 映射到上级目录 newdir。
根目录的引用计数是 18------你知道根目录下面有多少个子目录吗?18 - 2 = 16 个。自己去数一下。
所以文件系统到底是什么
不要以为文件系统只是磁盘上的 superblock、GDT、bitmap、inode table。那是磁盘上的存储结构。
真正的文件系统,是磁盘上的管理信息 + 内存里的管理结构,两者合在一起,才构成一套完整的数据管理方案。
内存里的那部分------struct file、struct dentry、struct inode(内核内虚化过的)、dcache 树------没有它们,进程根本没法访问磁盘上的文件。
而用户和文件系统之间的唯一入口,是文件描述符。文件描述符 → struct file → dentry → inode → 数据块。这条链路走通了,属性和内容就都拿到了。
就这么回事。
所以到底发生了什么?就三件事。
- 目录的内容是文件名到 inode 的映射表。 你每次用文件名访问文件,系统先打开目录读这张表,拿到 inode 编号,再用 inode 去找属性和内容。
- 路径解析从根目录开始,用 dcache 多叉树缓存。 每个被访问过的目录和文件在内存里都有一个 dentry 节点,形成一个树。所谓的"Linux 是树状结构",指的是这棵内存里的树,不是磁盘上的物理结构。
- 挂载点把分区和目录关联起来。 文件通过路径解析确定自己属于哪个分区。软链接是快捷方式(独立文件存路径),硬链接是别名(新文件名指向同一个 inode,引用计数管生死)。