Ext系列文件系统
Etx系列文件系统
在上一节基础IO中介绍了操作系统对被打开文件的管理,但是对于一台计算机来说,磁盘上大部分的文件是未被打开的,而这些文件也需要被静态管理起来,方便用户随时找到并打开。
而操作系统对未打开文件的管理,称为文件系统。文件系统的基本诉求是支持文件的定位和访问。
1. 理解硬件磁盘结构
要理解操作系统如何对磁盘上的未打开文件进行管理,首先我们需要对磁盘这个设备的物理结构、存储结构与逻辑结构进行理解,然后再在此基础上理解操作系统对磁盘的管理方法。
1.1 认识磁盘、服务器、机柜和机房
首先,需要明确本文讨论的“磁盘”特指传统的机械硬盘(HDD),而非当前在个人笔记本电脑中普遍使用的固态硬盘(SSD)。尽管SSD在读写速度上优势明显,但在企业级后端服务器,尤其是在需要存储海量数据的场景下,机械硬盘因其巨大的容量和相对低廉的成本,至今仍是主流的存储设备。
磁盘根据其用途和性能,可主要分为两类:
- 企业级磁盘:为服务器和数据中心设计,具有大容量、高效率、高可靠性和更长的使用寿命,当然价格也更昂贵。
- 桌面级磁盘:用于个人电脑,性能和可靠性标准相对较低,成本也更亲民。
本次讨论的范畴将聚焦于企业级应用中的机械硬盘。
在企业环境中,这些磁盘通常被安装在服务器中。服务器是一种高性能计算机,通常没有配备显示器、键盘或鼠标,而是通过网络进行管理和访问。一台服务器可以容纳多块可插拔的磁盘,例如在一个4x6的磁盘矩阵中安装24块硬盘装24块硬盘。


多台服务器则会被集中放置在机柜(Rack)中,而大量的机柜则共同构成了一个机房(Data Center)。机房是公司的核心数据资产所在地,对环境要求极为严格,尤其是防火。因为机械硬盘通过磁性介质存储数据,高温会导致盘片上的磁性颗粒消磁,从而造成永久性的、不可恢复的数据丢失。

1.2 磁盘物理结构
机械硬盘是现代计算机体系中为数不多的精密机械设备。相比于CPU、内存等电子元件,其机械运动的本质决定了它的访问速度较慢,但也带来了容量大、成本低的优点。
一块机械硬盘主要由以下几个核心物理部件构成:
- 盘片(Platters):通常由金属或玻璃制成,表面覆盖有磁性涂层,是存储数据的载体。一块硬盘内可以有多张盘片,堆叠在一起。
- 主达(Spindle Motor):带动所有盘片围绕中心轴高速旋转,常见的转速有7200 RPM(转/分钟)、15000 RPM等。转速越高,读写数据的速度通常越快。
- **磁头(Read/Write Heads):**每个盘片的表面(正面和反面)都对应一个磁头。磁头负责在盘片旋转时,读取或改变其表面的磁性颗粒的南北极(N/S)状态,以此来表示二进制的0和1。
- 传动臂(Acttor Arm):所有磁头被固定在一个传动臂组件上,该组件控制磁头在盘片的半径方向上协同移动,以便定位到不同的数据轨道。
- 密封外壳:为了防止灰尘等微小颗粒干扰磁头和盘片之间的精密工作(其间隙极小),整个盘体被密封在几乎真空的环境中。私自拆开外壳通常会导致硬盘报废。

磁盘读写数据的过程,本质上是一个电磁转换过程。写入数据时,电流通过磁头产生磁场,改变盘片上特定位置的磁性颗粒的极性;读取数据时,磁头感应盘片上磁性颗粒的磁场,将其转换为电信号。
注意:磁盘是计算机中唯一一个纯机械结构的设备,同时磁盘还是外设,所以磁盘进行数据读写的速度很慢。
1.3 磁盘存储结构
虽然盘片表面看起来光滑如镜,但在微观层面,它被组织成了非常精密的结构,以便于数据的定位和存取。

磁盘存储结构如下:
- 磁道(Track):磁盘的每一个盘面都被划分成一系列的同心圆,这些同心圆就是磁道。数据就记录在这些磁道上。
- 扇区(Sector):每个磁道又被进一步分割成若干个弧形的段,每一段被称为一个扇区。扇区是磁盘进行物理读写的最小。传统上,一个扇区的大小是 512字节。这意味着,即使操作系统只想修改1个字节的数据,也必须将整个512字节的扇区读入内存,完成修改后,再将整个扇区写回磁盘。这种以块为单位的I/O方式,远比按字节读写要高效。
- 柱面(Cylinder):这是一个至关重要的逻辑概念。由于所有磁头都固定在同一个传动臂上,它们在任何时候都位于所有盘面上半径相同的磁道上。所有盘面上相同半径的磁同构成了一个虚拟的、立体的圆柱,这个结构就被称为柱面。

磁盘寻址的过程:
磁盘在进行 IO 时,首先需要进行寻址,而寻址的过程如下:首先定位磁道,即在哪一个柱面 (cylinder);然后再定位盘面,即定位到寻找哪一个盘面的磁头 (head),最后再定位在哪一个扇区 (sector)。
上述过程在物理上的表现方式如下:启动主轴后,所有的盘片以同样的方式进行高速旋转,同时所有的磁头也共同从圆心到半径左右摆动,当定位到指定柱面后,磁头停止摆动,盘片继续旋转,当盘片对应扇区旋转到磁头下方后,对应盘面的磁头向扇区中写入/读取数据。
所以,在磁盘中定位任意一个/多个扇区,采用的基本硬件定位方式是 柱面、磁头、扇区定位,即 CHS 定位法。
拓展知识:
磁盘容量 = 磁头数 × 磁道(柱面)数 × 每道扇区数 × 每扇区字节数
细节:传动臂上的磁头是共进退的。
引入柱面的概念意义重大。当需要写入大量连续数据时,磁盘会倾向于在同一个柱面内,从上到下依次使用各个磁头进行写入。这样做的好处是,传动臂无需移动,只需等待盘片旋转即可连续写入多个磁道的数据,极大地减少了被称为“寻道时间”的机械延迟,从而提高了读写效率。
📌 CHS 寻址
对早期的磁盘非常有效,知道用哪个磁头、读取哪个柱面上的第几扇区就可以读到数据了。
但是 CHS 模式支持的硬盘容量有限,因为系统用 8bit 来存储磁头地址,用 10bit 来存储柱面地址,用 6bit 来存储扇区地址,而一个扇区共有 512 Byte,这样使用 CHS 寻址一块硬盘最大容量为:
256 × 1024 × 63 × 512B = 8064MB即:1MB = 1048576B,若按 1MB = 1000000B 来算就是 8.4GB。
1.4 磁盘逻辑结构
1.4.1 理解过程
这里以磁带的结构来引出磁盘的逻辑结构,如图,磁带盒里面一共有两个齿轮,其中一个齿轮上面缠绕着一圈圈的磁带,把磁带盒插入磁带录音机后,磁带里面的音频数据就会读取然后通过录音机播放出来;把磁带盒拆开后,可以发现,磁带扯出来后其结构是线性的,也就是说,磁带里面的数据是按线性方式来读取的。

1.4.2 真实过程
对比到磁盘,因为传动臂上的磁头是共进退的,所以每一个盘面上相同半径的磁道在逻辑上就构成了柱面。所以在宏观的角度看待磁盘,虽然在物理层面上其分了很多盘面,但是在逻辑上磁盘整体式由“柱面”卷起来的。


所以结合上面磁带的数据读取的理解方式:
磁道:
首先视角放到一个盘面的某一个磁道上,按线性逻辑展开磁道:

即:一维数组
柱面:
再将视角转到由每一层盘面的相同半径的磁道构成的柱面上,再展开柱面:


即:二维数组
整个磁盘:
然后再聚焦视角到整个磁盘,整个磁盘实际就是由多个不同的柱面卷起来而形成的,现在将每一个柱面都展开,然后排列即可得到整个磁盘的逻辑结构:

即:整个磁盘就是多张二维扇区数组表,也就是三维数组。
1.4.3 CHS 与 LAB 地址
如上可以知道寻址一个扇区:先找到哪一个柱面(Cylinder),在确定柱面内哪个磁道(其实就是磁头位置,Head),再确定扇区(Sector),所以就有了 CHS。
然后结合了录像带的线性结构的思路,即可与磁盘结合可以发现,一块磁盘可以理解为一个三维数组,再结合 C/C++ 中的知识,可以将其转化为一维数组:

这样理解每一个扇区都用数组中的一个下标所指代,也就叫做 LAB(Logical Block Address)地址。
而针对于硬件层之上的操作系统层只需要LBA即可,将LBA地址转化成CHS地址是磁盘自己来做。
LBA 地址转 CHS 定位例子:
假设一个磁盘有两个盘片,每个盘片有两个盘面,每个盘面有10个磁道,每个磁道有100个扇区。现在,某个扇区的LBA地址为1234,求该扇区在磁盘上的具体位置:
**操作系统为什么要对 CHS 进行逻辑抽象呢?**有如下两个原因:
- 数组更便于管理。
- 不让操作系统代码与底层硬件强耦合,即使磁盘的存储结构改变,操作系统仍然可以使用 LBA 地址进行定位寻找,只需要改变 LBA 与磁盘扇区的映射关系即可。
2. 引入文件系统
2.1 引入“块”概念
2.1.1 概念介绍
其实硬盘是典型的“块”设备,操作系统读取硬盘数据的时候,其实是不会一个个扇区(512 byte)地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个**“块”(block)**。
硬盘的每个分区是被划分为一个个的“块”。一个“块”的大小是由格式化的时候确定,如 1KB/2KB/4KB,最常见的是 4KB,即连续八个扇区组成一个“块”。“块”是文件存取的最小单位。

引入“块”的概念后,文件系统看待磁盘的视角也随之改变。它不再将磁盘视为一个由海量512字节扇区构成的精细阵列,而是将其视为一个由容量更大的4KB块组成的逻辑阵列。
2.1.1 “块”与LAB
上面在介绍磁盘逻辑结构的时候已经说过,**磁盘就是一个三维数组。但是可以把它看待成一个“一维数组”,数组下标就是 LBA,每个元素都是扇区。**并且文件系统以“块”为单位进行管理,而底层磁盘硬件仍然以LBA地址标识的扇区为单位工作,那么二者中间就一定要有一套清晰地转换机制:
- 知道 LBA:块号 = LBA / 8
- 知道块号:LBA = 块号 * 8 + n (n 是块内第几个扇区)

通过上面简单的数学换算,文件系统可以轻松地将对某个逻辑块的读写请求,转化为对一组连续扇区的硬件操作指令。
2.2 引入“分区”概念
至此,磁盘在文件系统眼中已成为一个由4KB块组成的庞大线性数组。同时已经拥有了读写这些基本数据单元的能力。然而,面对一个容量巨大(例如800GB)的单一存储空间如何对其进行高效、有序的管理?就需要引入分区这个概念。
2.2.1 介绍“分区”
宏观管理:分区(Partitioning)
直接管理整个庞大的空间,其复杂度和成本都很高。这可以类比于一个国家对领土的管理:一个国家不会由中央机构直接管理每一寸土地,而是会将其划分为省、市、县等行政单位,逐级进行管理,以适应不同区域的情况并降低管理难度。
磁盘管理借鉴了类似的思想。与其将整个800GB空间视为一个整体,不如将其分割成数个更小、更易于管理的逻辑区域。这个过程就称为分区(Partitioning)。
对于大多数用户而言,最直观的例子就是Windows操作系统中的C盘、D盘、E盘。它们通常位于同一块物理硬盘上,但被划分为了不同的分区。每个分区都可以被视为一个独立的逻辑磁盘,可以被独立地格式化和使用。

微观管理:块组(Block Groups)
然而,即便是一个分区(例如300GB),其空间仍然相当可观。为了实现更精细化的管理,文件系统(特别是ext系列文件系统)会在一个分区内部进行第二次划分,将其分割成连续的、大小相等的块组(Block Groups)。
如果说“分区”是对磁盘的第一次宏观分割,那么“分组”就是对分区的第二次微观分割。通过这种方式,管理一个300GB分区的问题,又被进一步简化为管理一个更小的单元(例如30GB的块组)的问题。能管理好这样一个小单元就可以将管理方法复制到其他小单元,这样就实现了整体的管理。
核心思想:分而治之
从“磁盘”到“分区”,再从“分区”到“块组”,这种层层分解、化整为零的管理策略,是计算机科学中一种经典的思想——分而治之(Divide and Conquer)。
其逻辑层级如下:物理磁盘 -> 分区1, 分区2, … -> (在每个分区内) -> 块组0, 块组1, …
通过这种方式,管理整个磁盘的复杂任务,最终被简化为如何有效管理一个标准大小的“块组”。只要我们理解了一个块组的内部结构和运作机制,就能举一反三,掌握整个文件系统的管理模式。
2.2.2 如何“分区”
柱面是分区的最小单位
可以利用参考柱面号码的方式来进行分区,其本质就是设置每个区的起始柱面和结束柱面号码。此时可以将硬盘上的柱面(分区)进行平铺,将其想象成一个大的平面,如下图所示:

柱面大小一致,扇区个数一致,那么其实只要知道每个分区的起始和结束柱面号,知道每一个柱面多少个扇区,那么该分区多大,其实和解释 LBA 是多少也就清楚了。
2.3 引入“inode”概念
之前我们说过 文件 = 数据 + 属性,我们使用 ls -l 的时候看到的除了看到文件名,还能看到文件元数据(属性)。
[root@localhost linux]# ls -l
总用量 12
-rwxr-xr-x. 1 root root 7438 "9月 13 14:56" a.out
-rw-r--r--. 1 root root 654 "9月 13 14:56" test.
每行包含 7 列:
- 模式
- 硬链接数
- 文件所有者
- 组
- 大小
- 最后修改时间
- 文件名
ls -l 读取存储在磁盘上的文件信息,然后显示出来

到这要思考一个问题,文件数据都储存在 “块” 中,那么很显然,系统也必须找到一个地方储存文件的属性信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做 inode,中文译名为 “索引节点”。
每一个文件都有对应的 inode ,里面包含了该文件有关的属性信息,但是其具体内容和如何存储需要结合下面文件系统的知识统一叙述。
**注意:**下面有几个必须知道的知识点
Linux 下文件的存储是属性和内容分离存储的。
Linux 下,保存文件属性的集合叫做
inode,一个文件一个inode,inode中有一个唯一的标识符,叫做indoe号。使用
ls -li可以进行查询:
文件名并没有纳入
indoe结构中。
indoe因为本质是一个结构体,结构体就一定是一个固定大小,所以一般为128字节或者256字节,本文统一默认为128字节。并且因为所有文件都需要要有inode所以任何文件的内容大小可以不同,但是属性大小一定相同。
3. ext2 文件系统
经过对磁盘物理结构、逻辑抽象、分区与分组等概念的铺垫,所有的准备工作已经完成。现在,是时候正式认识文件系统了。我们想要在硬盘上存储文件,必须先将磁盘分区格式化为某种特定的文件系统,才能对其进行有效的组织和管理。而文件系统的目的就是组织和管理硬盘中的文件。在Linux系统中,最常见、也最具代表性的就是ext系列文件系统。
本系列文章将以其经典版本ext2为主要剖析对象。尽管后续的ext3和ext4通过引入日志等技术对ext2进行了功能增强,但其核心设计思想和存储结构一脉相承。掌握了ext2,就掌握了理解现代Linux文件系统的钥匙。
3.1 宏观认识:从分区到块组
ext2文件系统的核心管理思想是分而治之。它将一个独立的磁盘分区(Partition)看作一个需要管理的总“领地”,然后将这个“领地”划分为若干个大小相等的块组(Block Group)。

这种结构的好处在于,管理整个分区的复杂问题被简化为了管理单个块组的问题。由于每个块组的内部结构都是相同的,因此只要我设计出一套能够有效管理一个块组的机制,就可以将这套机制复制到所有块组,从而实现对整个分区的管理。
在探讨文件系统结构之前,需要了解一个特殊区域:
- 启动块(Boot Sector):位于分区的最开始,其大小通常是固定的1KB。这个区域由PC标准规定,用于存储引导加载程序(Bootloader)和分区表信息,它不属于ext2文件系统的一部分,文件系统无权也无法使用这块空间。真正的ext2文件系统从启动块之后才开始。也就是上图的EXT2 File System。
3.2 块组(Block Group)的统一结构
紧随启动块之后,就是上图的EXT2 File System。ext2文件系统将其切分为连续的块组。每个块组都像一个功能完备的“微型文件系统”(如Block Group 0、Block Group 1 等等),拥有着完全相同的内部结构。这种设计不仅简化了管理,也优化了数据的存放,减少磁头寻道时间,提高读写性能。

3.3 块组内部构成详解
一个标准的ext2块组由以下几个核心部分组成,它们协同工作,共同管理着组内的数据和元数据。
3.3.1 超级块(Super Block)
3.3.1.1 基本介绍
超级块用于存放文件系统本身的结构信息,用于描述整个分区的文件系统信息,它是一个数据结构。这些信息对于文件系统的正常挂载和运行至关重要。其记录的主要内容包括:
- 资源统计:整个分区的数据块(Block)和索引节点(inode)的总量。
- 状态信息:当前未使用的空闲Block和Inode的数量。
- 配置参数:一个Block和Inode的标准大小(例如,Block为4KB,Inode为128字节)。
- 分组信息:每个块组包含多少个Block和Inode。
- 时间戳:最近一次挂载(Mount)的时间、最近一次写入数据的时间、最近一次检验磁盘(fsck)的时间等。
- 文件系统标识:一个“魔法数”(Magic Number),用于校验该分区是否为ext2文件系统。
3.3.1.2 备份机制
鉴于超级块的极端重要性,一旦其信息被破坏,整个文件系统结构将可能无法识别,导致数据无法访问。因此,ext2采取了冗余备份机制。主超级块存放在第一个块组(Group 0)的开头,同时,其完整的拷贝会备份到多个其他块组中。当主超级块损坏时,系统可以从这些备份中恢复文件系统的元数据。
而这也是平时我们打开电脑时可能会出现“文件损坏,系统正在修复”,然后过两分钟自动重启,电脑恢复正常的原因。
3.3.1.3 源码剖析
在Linux内核源码中,超级块由 ext2_super_block 结构体定义,其关键字段映射了上述信息:
/*
* Structure of the super block
*/
struct ext2_super_block {
__le32 s_inodes_count; /* Inodes count */
__le32 s_blocks_count; /* Blocks count */
__le32 s_r_blocks_count; /* Reserved blocks count */
__le32 s_free_blocks_count; /* Free blocks count */
__le32 s_free_inodes_count; /* Free inodes count */
__le32 s_first_data_block; /* First Data Block */
__le32 s_log_block_size; /* Block size */
__le32 s_log_frag_size; /* Fragment size */
__le32 s_blocks_per_group; /* # Blocks per group */
__le32 s_frags_per_group; /* # Fragments per group */
__le32 s_inodes_per_group; /* # Inodes per group */
__le32 s_mtime; /* Mount time */
__le32 s_wtime; /* Write time */
__le16 s_mnt_count; /* Mount count */
__le16 s_max_mnt_count; /* Maximal mount count */
__le16 s_magic; /* Magic signature */
__le16 s_state; /* File system state */
__le16 s_errors; /* Behaviour when detecting errors */
__le16 s_minor_rev_level; /* minor revision level */
__le32 s_lastcheck; /* time of last check */
__le32 s_checkinterval; /* max. time between checks */
__le32 s_creator_os; /* OS */
__le32 s_rev_level; /* Revision level */
__le16 s_def_resuid; /* Default uid for reserved blocks */
__le16 s_def_resgid; /* Default gid for reserved blocks */
/*
* These fields are for EXT2_DYNAMIC_REV superblocks only.
*
* Note: the difference between the compatible feature set and
* the incompatible feature set is that if there is a bit set
* in the incompatible feature set that the kernel doesn't
* know about, it should refuse to mount the filesystem.
*
* e2fsck's requirements are more strict; if it doesn't know
* about a feature in either the compatible or incompatible
* feature set, it must abort and not try to meddle with
* things it doesn't understand...
*/
__le32 s_first_ino; /* First non-reserved inode */
__le16 s_inode_size; /* size of inode structure */
__le16 s_block_group_nr; /* block group # of this superblock */
__le32 s_feature_compat; /* compatible feature set */
__le32 s_feature_incompat; /* incompatible feature set */
__le32 s_feature_ro_compat; /* readonly-compatible feature set */
__u8 s_uuid[16]; /* 128-bit uuid for volume */
char s_volume_name[16]; /* volume name */
char s_last_mounted[64]; /* directory where last mounted */
__le32 s_algorithm_usage_bitmap; /* For compression */
/*
* Performance hints. Directory preallocation should only
* happen if the EXT2_COMPAT_PREALLOC flag is on.
*/
__u8 s_prealloc_blocks; /* Nr of blocks to try to preallocate*/
__u8 s_prealloc_dir_blocks; /* Nr to preallocate for dirs */
__u16 s_padding1;
/*
* Journaling support valid if EXT3_FEATURE_COMPAT_HAS_JOURNAL set.
*/
__u8 s_journal_uuid[16]; /* uuid of journal superblock */
__u32 s_journal_inum; /* inode number of journal file */
__u32 s_journal_dev; /* device number of journal file */
__u32 s_last_orphan; /* start of list of inodes to delete */
__u32 s_hash_seed[4]; /* HTREE hash seed */
__u8 s_def_hash_version; /* Default hash version to use */
__u8 s_reserved_char_pad;
__u16 s_reserved_word_pad;
__le32 s_default_mount_opts;
__le32 s_first_meta_bg; /* First metablock block group */
__u32 s_reserved[190]; /* Padding to the end of the block */
};
3.3.2 GDT(Group Descriptor Table)
3.3.2.1 基础介绍
GDT(块述符表)用于描述块组属性信息。它是一个由 ext2_group_desc 结构体组成的数组,分区中有多少个块组就对应有多少个块组描述符。每一个元素(块组描述符)都详细描述了对应块组的属性信息。
具体来说,每个块组描述符记录了:
- 该块组的**块位图(Block Bitmap)**位于哪个块。
- 该块组的**Inode位图(Inode Bitmap)**位于哪个块。
- 该块组的**Inode表(Inode Table)**从哪个块开始。
- 该块组中空闲的Block和Inode的数量。
3.3.2.2 备份机制
与超级块一样,GDT对于文件系统的结构完整性也至关重要,因此它也在每个块组中都存有一份完整的拷贝,以实现冗余备份。
3.3.2.3 源码剖析
其在内核中的定义如下:
// 磁盘级blockgroup的数据结构
/*
* Structure of a blocks group descriptor
*/
struct ext2_group_desc
{
__le32 bg_block_bitmap; /* Blocks bitmap block */
__le32 bg_inode_bitmap; /* Inodes bitmap */
__le32 bg_inode_table; /* Inodes table block*/
__le16 bg_free_blocks_count; /* Free blocks count */
__le16 bg_free_inodes_count; /* Free inodes count */
__le16 bg_used_dirs_count; /* Directories count */
__le16 bg_pad;
__le32 bg_reserved[3];
};
3.3.3 块位图(Block Bitmap)
这是一个专门用来追踪**数据块(Data Blocks)**使用状态的数据结构,它本身占用一个块。位图中的每一个比特位(bit)与该块组中的一个数据块一一对应。如果比特位为 1,表示其对应的数据块已被占用;如果比特位为 0,则表示该数据块空闲可用。
2.3.4 inode位图(Inode Bitmap)
与块位图原理完全相同,Inode位图用于追踪**Inode表(Inode Table)**中Inode的使用状态。每一个bit表示一个Inode是否已被分配给某个文件。1代表已占用,0代表空闲。
3.3.5 i节点表(Inode Table)
这是块组内所有**Inode(索引节点)**的集合,由连续的多个块组成。Inode是Linux文件系统的核心,存放了除文件名之外的所有文件属性(元数据),如:
- 文件类型(普通文件、目录、链接等)
- 文件权限(读、写、执行)
- 文件所有者(UID)和所属组(GID)
- 文件大小
- 时间戳(创建时间、最近修改时间、最近访问时间)
- 指向存储文件内容的数据块的指针
需要强调的是,Inode编号是以分区为单位进行统一编址的,不可跨分区。这意味着在一个分区内,每个文件的Inode号都是独一无二的。
3.3.6 数据块(Data Blocks)
这是块组中占据空间最大的部分,也是真正存放文件内容的地方。根据文件类型的不同,数据块的用途也有所区别:
- 对于普通文件:数据块中存储的就是文件的二进制内容。
- 对于目录:目录本身也是一种特殊的文件。它的数据块中存储的不是用户数据,而是一张映射表。这张表由一系列条目组成,每个条目包含了该目录下的一个文件名(或子目录名)以及其对应的Inode编号。当我们通过路径访问文件时,系统正是通过逐级查找目录的数据块,将文件名解析为Inode编号,最终才找到文件的元数据和内容。
3.3.7 有关问题的解释
3.3.7.1 Inode与数据块的精确定位
一个分区内的Inode和数据块都拥有全局唯一的编号。这意味着,即使分区被划分为数百个块组,Inode号为 1001 的节点也只有一个。那么,系统是如何通过这个编号找到它的呢?
答案在于固定的块组大小。在文件系统格式化时,超级块(Super Block)中已经明确记录了每个块组包含的Inode数量(s_inodes_per_group)和数据块数量(s_blocks_per_group)。这两个值是恒定的。因此,通过简单的数学运算,就可以完成定位:
- 确定目标块组:
目标块组索引 = ⌊ Inode号 − 1 每组Inode数 ⌋ \text{目标块组索引} = \left\lfloor \frac{\text{Inode号} - 1}{\text{每组Inode数}} \right\rfloor 目标块组索引=⌊每组Inode数Inode号−1⌋ - 确定在组内的索引:
组内Inode索引 = ( Inode号 − 1 ) % 每组Inode数 \text{组内Inode索引} = (\text{Inode号} - 1) \% \text{每组Inode数} 组内Inode索引=(Inode号−1)%每组Inode数
举例说明:假设每个块组有8192个Inode,我们要寻找Inode号为 8193 的文件。
- 计算目标块组:
(8193 - 1) / 8192 = 1。这意味着目标Inode位于索引为1的块组中(即第二个块组,Group 1)。 - 计算组内索引:
(8193 - 1) % 8192 = 0。这意味着它是Group 1中的第0个(也就是第一个)Inode。
通过这两步计算,文件系统就能准确地知道应该去哪个块组的Inode位图和Inode表中进行操作。同样的除法与取模运算逻辑,也完全适用于通过数据块号定位其所在的块组和具体位置。这种高效的计算寻址方式,是文件系统能够快速访问任意资源的基础。
3.3.7.2 操作系统对 etx 文件系统的管理
一个常见的误解是,所有文件系统的管理操作(如修改位图、更新GDT)都是直接在磁盘上实时进行的。事实并非如此。考虑到磁盘I/O的巨大延迟,这种方式的效率将是灾难性的。
真正的管理者是操作系统内核。当一个文件系统被挂载时,操作系统会执行一个关键步骤:将文件系统的核心管理数据从磁盘加载到内存中。
- 加载元数据:操作系统会读取分区的超级块(Super Block)、组描述符表(GDT)、以及所有块组的块位图(Block Bitmap)**和**Inode位图(Inode Bitmap),并在内存中为它们创建相应的数据结构(如C语言结构体、链表等)。
- 内存中操作:之后,所有对文件系统的操作,如创建文件、删除文件、分配数据块等,都首先在这些内存中的数据结构上完成。例如,删除一个文件时,内核会直接修改内存中对应的Inode位图和块位图,将相应的比特位置0。
- 延迟写回(Dirty Sync):这些在内存中的修改并不会立即写回磁盘。内核会将这些被修改过的元数据标记为“脏”(Dirty),并在稍后的某个适当时机(如系统空闲、定时任务、或调用
sync命令时),才将这些变更批量地、高效地“刷”回到磁盘的相应位置。
这种机制的本质是将内存作为磁盘元数据的高速缓存。它极大地减少了对慢速磁盘的访问次数,将大量零碎的写操作合并为少数几次集中的写操作,从而显著提升了文件系统的整体性能和响应速度。其核心思想与前文介绍的“用户缓冲区”或“页缓存”异曲同工,都是“先在内存中描述和组织,再统一与硬件交互”。
3.4 inode 和 datablock 映射
3.4.1 问题引入
现在知道了如何找到一个文件的Inode,也知道了Inode中存储着文件的所有属性。那么,最后一个,也是最关键的问题是:系统如何通过Inode找到存储文件内容的那些数据块(Data Blocks)呢?
这个关联的桥梁,就建立在 ext2_inode 结构体内部。该结构体中包含一个名为 i_block 的数组:
struct ext2_inode {
// ... 其他属性字段 (mode, uid, size, etc.)
__le32 i_block[EXT2_N_BLOCKS]; /* Pointers to blocks */
};
这个 i_block 数组就是解开谜题的钥匙。它是一个指针数组,但里面存储的不是内存地址,而是数据块的块号。

3.4.2 获取一次完整的文件信息步骤
一个的内容与属性的完整查找路径如下:
- 从一个Inode号开始。
- 通过除法和取模运算,定位到该Inode所在的块组和组内索引。
- 从该块组的Inode表中,读取出对应的
ext2_inode结构体。至此,获得了文件的所有属性。 - 解析该
ext2_inode结构体中的i_block数组。 i_block数组中直接记录了存储该文件内容的一个或多个数据块的块号。- 根据这些块号,文件系统就能从相应块组的Data locks区域读取到文件的全部内容。
通过i_block这个核心纽带,文件系统成功地将分离存储的“属性”和“内容”紧密地联系在了一起,有了属性+内容就掌握了这个文件的所有信息。所以只要掌握了一个文件的Inode号,就等于掌握了访问该文件所有信息的“钥匙”。
3.4.3 文件内容可以跨块组存储
一个文件在创建时,其inode会被分配到某个特定的块组中。为了优化性能和数据局部性,文件系统会优先从该文件inode所在的同一个块组内,为其分配存储内容所需的Data Block。
这就引出了一个实际问题:如果一个文件持续增大,其大小超过了它所在块组内所有剩余可用Data Block的总容量,文件系统将如何存储这个超大文件?文件是会写入失败,还是有其他的解决机制?
解答:文件内容可以跨块组存储
这个问题的核心答案是:文件的元数据(inode)和其内容数据(Data Block)在存储策略上是解耦的。inode的位置是固定的,但其指向的Data Block可以来自文件系统内的任何一个块组。这个绝对块号(Absolute Block Number)在当前文件系统(分区)的范围内是全局唯一的。所以尽管可能 Indoe 和 Datd Block 不在同一个块组内,但二者仍可以互相找到
3.4.4 创建一个新文件的步骤
[root@localhost linux]# touch abc
[root@localhost linux]# ls -i abc
263466 abc
示例如下图:

创建一个新文件主要有以下 4 个操作:
-
存储属性 :
内核先找到一个空闲的节点(这里是 263466)。内核把文件信息记录到其中。
-
存储数据 :
该文件需要存储在三个磁盘块,内核找到了三个空闲块:300、500、800。 将内核缓冲区的第一块数据复制到 300,下一块复制到 500,以此类推。
-
记录分配情况 :
文件内容按顺序 300、500、800 存放。 内核在 inode 上的磁盘分布区记录了上述块列表。
-
添加文件名到目录 :
新的文件名 abc。Linux 如何在当前的目录中记录这个文件? 内核将入口(263466,abc)添加到目录文件。 文件名和 inode 之间的对应关系将文件名和文件的内容及属性连接起来。
3.5 目录与文件名
3.5.1 基础介绍
上文已经介绍清楚,文件系统通过Inode来管理文件的属性和定位其数据块。然而,在际操作中,用户几乎从不直接与Inode编号打交道,而是使用的是直观的文件名和目录路径。这就引出了两个核心问题:
- 用户访问文件,用的都是文件名,这与底层的Inode号是如何关联的?
- 目录(Directory)到底是什么?它是一种特殊的存在还是也是文件?
答案是:目录本身也是一种文件。
从磁盘存储的根本层面看,并不存在“目录”这种特殊的物理结构。磁盘上只存在两种基本概念:文件属性(由Inode存储)和文件内容(由数据块存储)。
目录的特殊性不在于其存储方式,而在于其数据块内容的特殊含义。
- 目录的Inode:与普通文件一样,目录也有自己的Inode,用于存储其元数据,如权限、所有者、时间戳等。其
i_mode字段会明确标识这是一个目录文件(例如,ls -l输出权限位的第一个字符为d)。 - 目录的数据块(内容):这部分是理解目录工作机制的关键。目录的数据块中存储的不是用户可见的普通数据,而是一张映射表。这张表记录了该目录下所有子文件和子目录的文件名与其对应的Inode编号之间的关系。
3.5.2 实验验证
验证:读取目的内容
// readdir.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)
{
printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned long)entry->d_ino);
}
closedir(dir);
return 0;
}

现在,使用ls -li /命令来对比查看,可以发现文件名和Inode号完全吻合:

实验结果清晰地证明了:目录的内容就是一张包含“文件名 -> Inode号”映射关系的列表。
3.5.3 访问文件的基本流程
因此,访问文件的基本流程是:
- 首先必须打开文件所在的当前目录(它本身也是一个文件)。
- 读取该目录文件的内容(即映射表)。
- 根据用户提供的文件名,在映射表中查找到其对应的Inode号。
- 利用这个Inode号,文件系统才能进一步访问该文件的属性和数据。
3.6 路径解析
3.6.1 基本介绍
上述结论引出了一个连锁反应:要访问文件 test.c,必须先打开其父目录 test/ 以获取test.c 的Inode。但 test/ 本身也是一个文件,要访问它,也必须先获取它的Inode。这似乎陷入了一个无限递归的困境。
这个“递归”的出口在于文件系统的根目录 (/)。
在Linux系统中,任何文件的访问都离不开其路径。一个典型的路径如 /home/tcq/code/test/test.c,其访问过程被称为路径解析,具体步骤如下:
- 从根目录开始:根目录
/的Inode号是文件系统创建时就固定的(通常是2号),操作系统在挂载文件系统时会将其加载,作为所有解析的起点。 - 解析第一级:通过根目录的Inode,读取其数据块,在映射表中查找名为
home的条目,获得home目录的Inode号。 - 逐级向下:通过
home的Inode,读取其内容,找到tcq的Inode号;再通过whb的Inode,找到code的Inode号… 这个过程不断重复,直到路径的倒数第二部分。 - 找到目标:通过
test目录的Inode,读取其内容,最终在映射表中找到test.c对应的Inode号。 - 完成解析:至此,系统成功获得了目标文件的Inode,路径解析完成。
3.6.2 补充问答
3.6.2.1 路径由谁提供
用户在命令行中执行 ls test.c 时,只提供了文件名。完整的路径是由进程提供的。每个进程都维护着一个当前工作目录 (Current Working Directory, CWD)。操作系统会将CWD与用户提供的相对路径拼接,形成一个用于解析的完整路径。
3.6.2.2 最初的路径从何而来
Linux的目录结构是由系统和用户共同构建的。系统安装时会创建 /bin, /etc, /home 等基础目录。用户登录后,会处于自己的家目录中,并可以在其中创建新的子目录。每一次创建文件或目录的行为,都必须在某个已存在的目录下进行,这就天然地为新文件赋予了路径。
3.7 路径缓存
3.7.1 基础介绍
理论上,每次访问文件都需要从根目录开始进行一次完整的路径解析。对于深层路径,这意味着大量的、重复的磁盘I/O操作,效率极低。
为了解决这个性能瓶颈,操作系统引入了一套核心的优化机制——路径缓存(Path Cache),在Linux内核中其实现被称为 dcache (directory cache)。
目录树的真相
dcache的存在揭示了一个深刻的事实:通常所见的、层次分明的“目录树”结构,在很大程度上是操作系统为了提升性能,在内存中动态构建和维护的一个数据结构。
- 磁盘上:存储是相对“扁平”的,由独立的Inode和数据块组成,它们之间通过目录文件内容中的映射关系相互链接。
- 内存中:操作系统通过dcache,将这些链接关系显式地组织成一棵高效、易于遍历的树状结构。
3.7.2 内核中的dentry
dentry:内存中的路径节点
在内核中,路径的每一个组成部分(如 home, whb, test.c)都被抽象为一个名为dentry(directory entry)的内核结构体。
struct dentry {
atomic_t d_count;
unsigned int d_flags; /* protected by d_lock */
spinlock_t d_lock; /* per dentry lock */
struct inode *d_inode; /* Where the name belongs to - NULL is
* negative */
/*
* The next three fields are touched by __d_lookup. Place them here
* so they all fit in a cache line.
*/
struct hlist_node d_hash; /* lookup hash list */
struct dentry *d_parent; /* parent directory */
struct qstr d_name;
struct list_head d_lru; /* LRU list */
/*
* d_child and d_rcu can share memory
*/
union {
struct list_head d_child; /* child of parent list */
struct rcu_head d_rcu;
} d_u;
struct list_head d_subdirs; /* our children */
struct list_head d_alias; /* inode alias list */
unsigned long d_time; /* used by d_revalidate */
struct dentry_operations *d_op;
struct super_block *d_sb; /* The root of the dentry tree */
void *d_fsdata; /* fs-specific data */
#ifdef CONFIG_PROFILING
struct dcookie_struct *d_cookie; /* cookie, if any */
#endif
int d_mounted;
unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */
};
dentry结构的设计非常精巧,它同时参与构成三个关键数据结构:
- 树状结构:通过
d_parent和d_child/d_subdirs指针,所有被缓存的dentry在内存中形成一棵目录树,完美映射了路径的层级关系。 - 哈希表:为了能根据“父目录Inode + 文件名”快速查找到一个
dentry,所有dentry也会被放入一个全局的哈希表中,将查找时间复杂度降至O(1)。 - LRU链表 (Least Recently Used):内存有限,dcache不能无限增长。所有
dentry节点同时被链接在一个LRU链表中。当内存紧张时,操作系统会沿着LRU链表,淘汰那些“最近最少使用”的dentry,释放内存空间。
3.7.3 缓存工作流程
有了dcache,文件访问的流程变为:
- 系统接收到路径,首先在dcache哈希表中快速查找该路径。
- 缓存命中 (Cache Hit):如果路径的
dentry存在于缓存中,系统直接从内存中的dentry获取Inode指针。整个过程无需访问磁盘,速度极快。 - 缓存未命中 (Cache Miss):如果路径的某个部分不存在于缓存中,系统则会回到磁盘,执行传统的I/O密集型路径解析。
- 在解析过程中,系统会为新发现的路径节点创建新的
dentry对象,并将其同时加入到内存的树、哈希表和LRU链表中。 - 解析完成后,返回结果。未来的访问就可以从步骤2的缓存命中中受益。
通过这套精密的“空间换时间”策略,Linux内核将一个缓慢的、基于磁盘的查找过程,转变成了一个高效的、主要在内存中完成的操作,极大地提升了文件系统的整体性能。

3.8 挂载分区
3.8.1 问题引入
在已经了解了目录、路径解析以及dcache缓存机制,现在对文件访问的理解深入了很多。然而,一个关键问题仍然存在:inode编号是**分区本地(partition-local)**的。这意味着在不同的磁盘分区上,完全可能存在相同的inode号,它们指向截然不同的文件。
例如,分区 /dev/sda1 上的 inode 5 可能是一个文本文件,而分区 /dev/sda2 上的 inode 5 可能是一个目录。既然路径解析的最终结果是获得一个inode号,那么操作系统如何确切地知道应该去哪个分区里查找这个inode呢?
首先需要建立一个认识:单纯地对一块磁盘进行分区和格式化,并不能让操作系统直接使用它。例如,在Linux系统中,可能会看到 /dev/vda1、/dev/sdb2 这样的设备文件,它们代表了具体的物理分区。但是,你无法直接 cd 到 /dev/vda1 里面去创建文件,因为它是一个块设备文件,而不是一个目录。

这就引出了文件系统使用的核心概念之一:挂载(Mounting)
3.8.2 挂载:连接分区与目录树的桥梁
挂载是一个将一个独立的文件系统(通常存在于一个分区上)与一个已存在的目录进行关联的操作。这个被关联的目录被称为挂载点(Mount Point)。
其工作原理可以概括为:
- 准备一个“入口”:在当前已经被挂载的文件系统树中(例如根文件系统
/下),创建一个空目录作为挂载点。 - 执行挂载:使用
mount命令,告诉操作系统将指定的分区(如/dev/vda2)“附加”到这个挂载点目录(如/mnt/data)上。 - 访问分区:挂载完成后,这个挂载点目录就成为了访问新分区的唯一入口。当用户
cd /mnt/data时,他们实际上进入了/dev/sdb2这个分区的根目录。在此目录下创建、修改或删除文件,所有操作都将真实地发生在该分区上。
简而言之,Linux通过挂载机制,将多个独立的分区“拼接”成一个统一的、树状的虚拟文件系统(VFS, Virtual File System)。无论一个文件物理上存储在哪块硬盘的哪个分区,它在逻辑上都拥有一个从根目录 / 开始的唯一路径。
3.8.3 挂载操作的模拟实验
由于直接操作物理分区有风险,我们可以通过创建一个大文件来模拟一个分区,并对其进行格式化和挂载,以直观地理解这个过程。
-
创建虚拟磁盘文件:使用
dd命令创建一个指定大小、内容全为零的文件。这个文件将扮演一个“虚拟磁盘分区”的角色。# 从/dev/zero(一个提供无限零字节的设备)读取数据 # 输出到当前目录下的 mydisk.img 文件 # 每次读写1MB (bs=1M) # 共读写512次 (count=512),从而创建一个512MB的文件 dd if=/dev/zero of=mydisk.img bs=1M count=512
-
格式化虚拟磁盘:将这个文件格式化为一种文件系统格式,比如
ext4。mkfs.ext4 mydisk.img
-
创建挂载点并挂载:
# 创建一个名为 mymountpoint 的目录作为挂载点 mkdir mymountpoint # 将 mydisk.img 文件挂载到 mymountpoint 目录 # -o loop 选项告诉mount命令将一个普通文件当作块设备来处理 # 注意提权 sudo mount -o loop mydisk.img mymountpoint/
-
验证和使用:
-
使用
df -h命令可以看到,mydisk.img已经像一个真正的分区一样被挂载了。
可以看到,
disk.img文件通过一个名为/dev/loop0的设备被成功挂载到了/home/tcq/lesson17/mymountpoint目录。现在,进入/home/tcq/lesson17/mymountpoint目录就等同于进入了disk.img这个文件系统的根目录,可以在其中进行所有文件操作。关于
/dev/loop0(循环设备)
循环设备(loop device)是Linux中的一种伪设备,它允许将一个普通文件作为块设备来使用。mount命令通过循环设备,将disk.img这个文件“伪装”成一个真实的物理分区,从而能够将其挂载。 -
进入挂载点
cd mymountpoint,此时你就在mydisk.img这个文件系统的内部了。在其中创建文件touch hello.txt,这个hello.txt文件的数据实际上被写入了mydisk.img文件中。
-
-
卸载:使用完毕后,可以通过
umount命令卸载。(注意提权)
卸载后,
/home/tcq/lesson17/mymountpoint恢复为一个普通空目录,df -h的输出也会恢复到挂载前的状态。
这个实验清晰地展示了:一个独立的文件系统,必须通过挂载到某个目录上,才能被整合到主目录树中并被访问。
3.8.4 结论:路径前缀决定分区
这个实验清晰地证明了以下结论:
- 分区必须挂载才能使用:一个独立的分区(或含有文件系统的设备),即使已经格式化,也无法直接被访问。它必须被挂载到一个已存在的目录上,才能成为整个文件系统树的一部分。
- 路径前缀决定所在分区:Linux通过将不同分区挂载到不同目录的方式,巧妙地解决了跨分区寻址的问题。当访问一个文件时,系统会根据其路径的前缀来判断它属于哪个挂载点,从而确定它位于哪个物理分区。
例如:
- 访问
/home/user/file.txt时,系统看到路径前缀是/,它知道这属于根分区(如/dev/vda1)。 - 访问
/home/tcq/lesson17/mymountpoint/hello.txt时,系统在解析路径时发现/home/tcq/lesson17/mymountpoint是一个挂载点,它便知道路径的剩余部分hello.txt应该去/dev/loop0(即disk.img)所代表的文件系统中查找。
这个过程由**虚拟文件系统(VFS)**层自动处理,对用户和应用程序来说是完全透明的,最终呈现出一个统一、无缝的目录树结构
3.8.5 fopen 完整流程回顾
现在,可以描绘出调用 fopen 打开一个文件时,操作系统内核所做的完整工作:
- 接收路径:用户态的
fopen调用将文件路径(如/mnt/data/log.txt)传递给内核。 - 路径解析与分区定位:
- 内核从根目录
/开始,在dcache中逐级查找路径组件 (mnt,data,log.txt)。 - 当解析到
mnt目录下的data时,内核通过dentry的信息发现它是一个挂载点。 - 内核自动切换上下文,转到
data所挂载的分区的文件系统上,并从该分区的根目录继续解析路径的剩余部分log.txt。 - 在这个过程中,如果dcache未命中,则会从相应的磁盘分区进行I/O,读取目录内容,并创建新的
dentry填充到dcache中。
- 内核从根目录
- 获取Inode:路径解析最终在目标分区上找到了
log.txt的dentry,并从中获得了其对应的inode结构。 - 创建内核对象:
- 内核在内存中创建/获取代表此文件的
struct file对象(文件对象)和struct inode对象(VFS Inode)。 struct inode中填充从磁盘读取的文件属性(权限、大小、时间戳等)。struct file中则包含了文件的访问模式、当前读写位置(offset),以及一个指向dentry的指针,从而将打开的文件与文件系统中的路径牢固地关联起来。
- 内核在内存中创建/获取代表此文件的
- 分配文件描述符:内核在当前进程的文件描述符表中找到一个空闲位置,将该位置的指针指向刚刚创建的
struct file对象。这个位置的索引,就是返回给用户态的文件描述符(File Descriptor, fd)。 - 返回fd:内核将文件描述符返回给用户进程。从此,进程对该文件的所有后续操作(
read,write,close等)都将通过这个小小的整数fd来进行,内核通过它就能快速定位到内存中完整的struct file对象,进而操作文件。
4. 软硬链接
4.1 软链接
软链接,通常也被称为符号链接,是最直观也最常用的一种链接方式。
4.1.1 创建与特性
# 创建一个名为 code_soft 的软链接,指向 code.c
$ ln -s code.c code_soft

通过 ls -li 命令,可以观察到软链接的核心特性。
观察输出,可以得到以下结论:
- 独立的Inode:软链接
code_soft拥有一个与原文件code.c不同的inode号(1055218vs1055234)。这证明软链接是一个独立存在的文件。 - 文件类型为’l’:其文件权限位的第一个字符是
l,明确标识这是一个链接(link)文件。 - 内容共享:通过软链接
code_soft访问或修改文件内容,其效果与直接操作原文件code.c完全相同。
4.1.2 软链接的本质与用途
既然软链接是一个独立的文件,那么它的文件内容存储的是什么呢?
软链接文件的内容,就是它所指向的目标文件的路径字符串。
在上面的例子中,code_soft 这个文件的实际内容就是字符串 “code.c”。当操作系统访问 code_soft 时,它会读取其内容,发现这是一个路径,然后根据这个路径去访问真正的目标文件 code.c 。
这使得软链接的行为非常类似于Windows操作系统中的快捷方式。
主要用途:
快捷访问。当一个程序或文件深藏在某个复杂的目录路径下时(例如 /opt/apps/version-2.1/bin/executable ),每次执行它都需要输入完整的路径,非常不便。用户可以在一个方便的位置(如用户的家目录或 /usr/local/bin )为它创建一个软链接。
# 为深层路径下的程序创建一个易于访问的软链接
$ ln -s ./apps/version-2.1/bin/executable my_app

之后,用户只需执行 ./my_app 即可启动该程序。由于软链接是一个独立文件,删除软链接(删除快捷方式)不会对原始文件产生任何影响。
4.2 硬链接
硬链接是一种更底层的链接方式,它与文件系统的inode机制紧密相关。
4.2.1 创建与特性
使用不带 -s 参数的 ln 命令可以创建硬链接。
# 为 code.c 创建一个名为 code_hard 的硬链接
$ ln code.c code_hard

同样,使用 ls -li 来观察其特性。
与软链接的对比非常鲜明:
- 相同的Inode:硬链接
code_hard与原文件code.c拥有完全相同的inode号(均为1055218)。这证明硬链接并不是一个独立的文件,它仅仅是目标文件的一个“别名”。 - 链接数增加:可以注意到,文件权限位后面的数字(硬链接数)从1变成了2。
4.2.2 硬链接的本质
硬链接的本质,是在当前目录的文件内容(文件名与inode的映射表)中,新增一个文件名条目,并让它指向已存在的目标文件的inode。
换句话说,创建硬链接code_hard,并没有创建新的inode,只是让code_hard这个名字和code.c这个名字共同指向了同一个inode(131742)。
而inode中有一个非常重要的属性,称为引用计数(i_link),也就是我们看到的那个数字。它的含义是:当前有多少个文件名正指向这个inode。
- 创建一个文件时,只有一个文件名指向其
inode,所以引用计数为1。 - 每为它创建一个硬链接,就增加了一个指向它的文件名,其
inode的引用计数就会加1。 - 每删除一个指向它的文件名(删除硬链接或原始文件名),引用计数就会减1。
4.2.3 硬链接的用途
硬链接最重要的一个用途是提供一种高效、可靠的文件备份机制。
当一个文件的inode引用计数大于1时,删除其中任何一个文件名(例如 rm code.c),并不会真正删除文件的数据。系统仅仅是移除了那个文件名与inode的映射关系,并将引用计数减1。只要引用计数还不为0,文件的inode和其对应的数据块(Data Blocks)就会被一直保留。只有当引用计数降至0时,文件系统才会认为这个文件不再被需要,从而真正回收其inode和所有数据块。
这提供了一种瞬间完成的备份方法:
# 为重要文件 important.dat 创建一个备份
$ ln important.dat important.dat.bak
# 即使不小心删除了原文件
$ rm important.dat
# 数据依然安全,可以通过备份文件名访问
$ cat important.dat.bak
4.3 深入理解链接数
链接数(引用计数)的行为解释了文件系统中一些有趣的现象。
为何普通文件的链接数默认为1?
当用户创建一个新的普通文件时,文件系统只在它的父目录中创建了一个“文件名 -> inode”的映射关系。因此,只有一个文件名指向这个新文件的inode,其链接数自然为1。

为何空目录的链接数默认为2?
当你创建一个新的空目录(例如 mkdir dir1)时,其链接数默认为2。这是因为:
- 父目录中的条目:在
dir1的父目录中,有一个文件名dir1指向了这个新目录的inode。这是第一个链接。 - 目录自身的
.条目:Linux系统会在任何新创建的目录内部,自动创建两个特殊的硬链接条目:.和..。其中,.(点)这个文件名,指向的是目录自身的inode。这是第二个链接。
因此,一个空目录的inode同时被其在父目录中的名字和它自己内部的 . 所指向,链接数即为2。

为何目录的链接数会增加?
当你在一个目录(如dir1)内部再创建一个子目录(如mkdir hello)时,你会发现父目录dir1的链接数从2变成了3。

原因在于子目录 hello 内部自动创建的 .. (点点)条目。这个..文件名,是一个指向其父目录(即dir1)inode的硬链接。
所以,此时dir1的inode被三个文件名指向:
- 其在父目录中的名字 (
.../dir1)。 - 其自身的
.条目 (dir1/.)。 - 其子目录
hello中的..条目 (dir1/hello/..)。

结论:一个目录的硬链接数 = 2 + 其包含的子目录数量。



889

被折叠的 条评论
为什么被折叠?



