一、预备知识引入

1、文件系统基础定义

文件系统是操作系统的核心机制,主要用于抽象、管理磁盘、固态硬盘等存储设备上的数据。而Ext文件系统是Linux系统中生态覆盖最广、功能支持最完善的文件系统家族,也是Linux系统的主流文件系统。

2、磁盘结构与寻址方式

大众认知中的磁盘单圈存储结构并不完整,完整的物理磁盘是由多片盘面叠加组成的立体存储结构。我们可以通过拆分理解磁盘核心结构:将单个盘面的环形存储轨道定义为磁道;所有盘面同一位置的磁道组合形成柱面;单条磁道会被均匀划分为多个扇形存储单元,每个单元即为扇区,扇区是磁盘最小的物理存储单元。

为了方便理解磁盘寻址逻辑,我们可以用数组类比:将单个盘面的所有磁道展开,可看作一维数组;多个盘面叠加形成的柱面,可看作二维数组;海量柱面组合成的完整磁盘,就形成了三维数组。

磁盘传统寻址方式为CHS寻址,寻址顺序为:先定位目标数据所在的柱面,再确定柱面内对应的磁道(对应磁头位置),最后精准定位到对应扇区。但计算机底层不会使用复杂的多维数组寻址,而是将磁盘所有扇区统一线性编号,形成LBA逻辑块地址,也就是一维线性下标,以此简化数据寻址流程,提升读写效率。

3、“块”核心概念

硬盘属于典型的块设备,操作系统不会以单个扇区为单位读写数据,而是采用批量读写的方式,一次性读取连续多个扇区的数据,这一组连续扇区就构成了一个数据块(Block)

数据块的大小在磁盘格式化时确定,一经设定无法修改,业界最常用的块大小为4KB。在文件系统中,块是文件数据存取的最小逻辑单位,所有文件的内容数据都以块为单位存储。

4、“分区”核心概念

物理磁盘可以划分为多个独立分区,Windows系统中常见的C盘、D盘、E盘就是磁盘分区的可视化体现。分区本质是对物理磁盘的逻辑格式化与空间划分,目的是实现数据分类存储、系统隔离,提升磁盘数据管理的安全性与规范性。

Linux系统的分区规则有明确限制,柱面是Linux分区的最小单位。Linux分区的核心逻辑是通过设定分区的起始柱面编号和结束柱面编号,划定专属的磁盘存储空间,完成分区创建。

5、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.c

上述输出内容共7列,分别对应:文件模式权限、硬链接数、文件所有者、所属用户组、文件大小、最后修改时间、文件名。这些信息只是文件属性的一部分,使用stat命令可以查看文件完整属性信息。

[root@localhost linux]# stat test.c 
File: "test.c" 
Size: 654 Blocks: 8 IO Block: 4096 普通⽂件 
Device: 802h/2050d Inode: 263715 Links: 1 
Access: (0644/-rw-r--r--) Uid: ( 0/ root) Gid: ( 0/ root) 
Access: 2017-09-13 14:56:57.059012947 +0800 
Modify: 2017-09-13 14:56:40.067012944 +0800 
Change: 2017-09-13 14:56:40.069012948 +0800

stat命令输出的信息中,新增了各类时间戳、inode编号、数据块占用数等详细参数。其中inode(索引节点)是存储文件所有属性信息的核心载体,前文提到的文件内容存储在磁盘数据块中,而文件的权限、所有者、大小、时间戳等所有属性,全部存储在inode中。

每个Linux文件都拥有唯一对应的inode,ext2文件系统的inode结构体源码如下,完整定义了文件的所有属性字段:

/* 
* Structure of an inode on the disk 
*/ 
struct ext2_inode { 
__le16 i_mode; /* File mode */ 
__le16 i_uid; /* Low 16 bits of Owner Uid */ 
__le32 i_size; /* Size in bytes */ 
__le32 i_atime; /* Access time */ 
__le32 i_ctime; /* Creation time */ 
__le32 i_mtime; /* Modification time */ 
__le32 i_dtime; /* Deletion Time */ 
__le16 i_gid; /* Low 16 bits of Group Id */ 
__le16 i_links_count; /* Links count */ 
__le32 i_blocks; /* Blocks count */ 
__le32 i_flags; /* File flags */ 
union { 
struct { 
__le32 l_i_reserved1; 
} linux1; 
struct { 
__le32 h_i_translator; 
} hurd1; 
struct { 
__le32 m_i_reserved1; 
} masix1; 
} osd1; /* OS dependent 1 */ 
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */ 
__le32 i_generation; /* File version (for NFS) */ 
__le32 i_file_acl; /* File ACL */ 
__le32 i_dir_acl; /* Directory ACL */ 
__le32 i_faddr; /* Fragment address */ 
union { 
struct { 
__u8 l_i_frag; /* Fragment number */ 
__u8 l_i_fsize; /* Fragment size */ 
__u16 i_pad1; 
__le16 l_i_uid_high; /* these 2 fields */ 
__le16 l_i_gid_high; /* were reserved2[0] */ 
__u32 l_i_reserved2; 
} linux2; 
struct { 
__u8 h_i_frag; /* Fragment number */
__u8 h_i_fsize; /* Fragment size */ 
__le16 h_i_mode_high; 
__le16 h_i_uid_high; 
__le16 h_i_gid_high; 
__le32 h_i_author; 
} hurd2; 
struct { 
__u8 m_i_frag; /* Fragment number */ 
__u8 m_i_fsize; /* Fragment size */ 
__u16 m_pad1; 
__u32 m_i_reserved2[2]; 
} masix2; 
} osd2; /* OS dependent 2 */ 
}; 
/* 
* Constants relative to the data blocks 
*/ 
#define EXT2_NDIR_BLOCKS 12 
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS 
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1) 
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1) 
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)

备注:代码中定义的 EXT2_N_BLOCKS 数值为15。结合inode结构体,我们可以明确三个核心知识点:

1. 文件名不属于inode的存储范畴,inode只存储文件属性,不记录文件名;

2. inode的大小固定,主流规格为128字节或256字节;

3. 不同文件的内容大小、占用空间可以各不相同,但所有文件的inode属性数据大小完全一致。

二、ext2文件系统

1、宏观认知

磁盘无法直接存储文件,必须先格式化为指定类型的文件系统,才能实现文件的存储、组织与管理,这也是文件系统的核心作用。ext2作为经典的Linux文件系统,采用模块化管理思想,会将整个磁盘分区均匀划分为多个大小一致的块组。所有块组的结构、管理规则完全相同,只要掌握单个块组的管理逻辑,就能理解整个分区乃至整块磁盘的文件管理机制。

2、块组(Block Group)核心结构

每个块组由六大核心模块组成,各司其职、相互配合,完成文件数据与属性的存储管理,具体结构如下:

2-1、超级块(Super Block)

超级块是文件系统的核心“总台账”,专门存储文件系统的全局结构信息,完整描述整个分区的文件系统状态。其主要记录内容包括:分区内block总数量、inode总数量、空闲未使用的block与inode数量、单个block和inode的大小、文件系统最近挂载时间、最近数据写入时间、磁盘校验时间等关键全局参数。

超级块直接决定文件系统的完整性,一旦超级块数据损坏,整个分区的文件系统结构会彻底失效,无法正常读写数据。为了规避该风险,每个块组的开头都会备份一份超级块数据,所有备份的超级块信息完全一致,当某一块扇区损坏时,系统可通过其他块组的备份数据恢复文件系统,保障稳定性。

2-2、GDT块组描述符表(Group Descriptor Table)

块组描述符表用于精准描述每个块组的专属属性信息。分区被划分为多少个块组,就对应存在多少个块组描述符。单个块组描述符会记录对应块组的核心参数:inode表的起始位置、数据块的起始位置、当前块组空闲inode数量、空闲数据块数量等。和超级块一致,每个块组开头均会备份一份GDT数据,防止数据丢失。

2-3、块位图(Block Bitmap)

块位图是数据块的状态记录表,以二进制bit位为最小单位。每一个bit位对应块组内的一个数据块,通过0、1两种状态标记数据块的使用情况:0代表对应数据块空闲可用,1代表对应数据块已被占用,系统可通过块位图快速检索空闲数据块。

2-4、inode位图(Inode Bitmap)

inode位图的逻辑与块位图完全一致,同样以bit位为单位。每一个bit位对应一个inode节点,用于标记inode的空闲状态,快速筛选出可用于存储新文件属性的空闲inode节点。

2-5、inode节点表(Inode Table)

inode节点表是当前块组内所有inode节点的集合,专门用于存放所有文件的属性信息,包括文件大小、所有者、权限、各类时间戳、数据块指针等。需要注意的是,inode编号以单个分区为单位统一编排,无法跨分区使用,不同分区的inode编号允许重复。

2-6、DataBlock数据块区

数据块区是文件内容的唯一存储区域,由大量统一规格的Block组成,不同类型的文件,数据块的存储内容有所区别:

1. 普通文件:文件的实际内容、文本数据、程序代码等全部存储在对应的数据块中;

2. 目录文件:目录本身也是特殊文件,其数据块中仅存储子文件名与对应inode号的映射关系。日常ls -l看到的目录权限、大小、所有者等属性,均存储在目录文件的inode中,而非数据块中。

同时,数据块的编号也以单个分区为单位划分,数据块无法跨分区调用和存储数据

3、Linux新建文件完整流程

在ext2文件系统中创建新文件,是内核依次完成属性存储、数据存储、状态记录、目录挂载的完整流程,具体步骤如下:

1. 分配inode、存储文件属性:内核通过inode位图检索空闲inode节点,分配一个空闲inode后,将文件的权限、所有者、大小、修改时间等所有属性信息写入该inode节点中;

2. 分配数据块、存储文件内容:根据文件大小确定需要的磁盘块数量,通过块位图检索对应数量的空闲数据块(如示例中的300、500、800号块),将内核缓冲区中的文件内容,分批复制到对应的数据块中;

3. 记录存储映射关系:内核在该文件的inode数据块指针区域,记录文件内容对应的所有数据块编号,建立“inode-数据块”的映射,确保后续可快速读取文件内容;

4. 挂载文件名到目录:将新文件的文件名与inode号的映射关系,写入当前所在目录的数据块中。至此,系统可通过目录匹配文件名,再通过文件名匹配inode,最终读取文件属性与内容,完成文件创建。

4、目录与文件名的本质

Linux磁盘中不存在独立的“目录结构”,磁盘底层仅有一种存储单元——文件,所有数据均以「属性+内容」的统一形式存储。目录只是系统定义的特殊文件,其核心作用是管理下属文件。

目录文件的内容十分单一,仅保存「子文件名-inode号」的映射表。用户访问任意文件时,必须先读取文件所在目录的内容,通过文件名匹配到对应的inode号,再通过inode读取文件属性和数据块内容,最终完成文件访问。

5、文件路径解析逻辑

当我们访问绝对路径/home/whb/code/test/test.c时,系统采用逐层递归解析的逻辑查找文件:从根目录出发,先找到home目录,再通过home目录匹配whb目录,依次递归匹配code、test目录,最后在test目录中通过文件名匹配test.c的inode号,读取文件数据。

为了保证Linux系统目录结构统一、功能规范,系统内核预设了固定的根目录及基础系统路径,各路径承担专属功能,是系统运行的基础:

/bin:存储基础系统命令,存放ls、cat等系统启动和日常运维必备工具;

/etc:存储系统与服务的配置文件,系统、软件的启动参数和配置均从该目录读取;

/home:用户家目录,为不同用户提供独立的存储空间,实现多用户隔离;

/var:存储系统可变数据,包括运行日志、程序缓存、动态生成的运行数据等;

/tmp:全局临时文件目录,所有进程、用户均可读写,用于存放临时数据,重启后数据清空;

/dev:设备文件目录,用于映射硬件设备,实现操作系统对硬件的访问控制;

/proc:虚拟文件系统目录,不占用磁盘空间,实时反映内核进程、系统硬件运行状态。

系统以预设基础路径为框架,用户和软件可在此基础上新建目录、文件,最终生长为完整的系统路径树。

6、路径缓存机制

针对文件路径解析,我们先解答三个核心问题,帮助理解缓存的意义:

问题1:Linux磁盘中存在真实的目录实体吗? 答案:不存在。磁盘底层只有文件,仅存储文件的属性和内容,目录是系统上层抽象出的概念。

问题2:每次访问文件都需要从根目录逐层解析路径吗? 答案:原则上初始访问需要逐层解析,但该方式效率极低,因此Linux内核引入了路径缓存机制,优化访问速度。

问题3:Linux的目录树形结构如何产生? 答案:系统将已打开的文件、目录信息,通过内核结构体在内存中动态维护,形成虚拟的树形目录结构。

Linux内核通过 dentry(目录项)结构体 维护内存中的路径树,也是路径缓存的核心载体,结构体源码如下:

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结构体的核心工作逻辑:

1. 系统中所有被打开的普通文件、目录文件,都会对应一个独立的dentry结构体,所有dentry在内存中拼接成完整的树形路径结构;

2. 内存空间有限,dentry不会永久缓存所有路径,系统通过LRU最近最少使用算法,自动淘汰长期未访问的路径节点,释放内存空间;

3. 所有dentry节点同时挂载在哈希表中,可通过哈希匹配快速检索路径,大幅提升文件查找效率;

4. 树形dentry集合构成Linux的路径缓存,访问文件时优先查询缓存:路径已缓存则直接读取inode与文件数据;未缓存则从磁盘逐层解析路径,生成新的dentry节点加入缓存。

三、软硬链接

1、硬链接

Linux系统支持多个不同文件名,绑定同一个inode节点,这种多文件名共享同一inode的关系,就是硬链接

[root@localhost linux]# touch abc 
[root@localhost linux]# ln abc def 
[root@localhost linux]# ls -li abc def 
263466 abc 
263466 def

上述示例中,abc和def两个文件名对应同一个inode(263466),互为硬链接。内核会为该inode维护一个硬链接计数,此时该inode的链接数为2。

硬链接的文件删除逻辑:删除硬链接文件时,系统会执行两个操作,一是删除目录中该文件名与inode的映射记录,二是将inode的硬链接计数减1。仅当链接计数减为0时,系统才会释放该inode对应的磁盘数据块,真正删除文件数据

2、软链接

软链接也叫符号链接,和硬链接的实现逻辑完全不同。硬链接是文件名与inode的映射关系,而软链接是一个独立的特殊文件,其数据块中不存储普通文件数据,仅存储目标文件的绝对/相对路径字符串,功能类似于Windows系统的快捷方式。

3、软硬链接核心对比

1. 软链接:属于独立文件,拥有自己专属的inode和数据块,依赖目标文件路径生效;

2. 硬链接:不属于独立文件,只是文件名与目标inode的映射关系,不生成新inode。

4、软硬链接核心用途

硬链接用途

1. 系统默认的`.`(当前目录)、`..`(上级目录)本质就是硬链接,用于支撑目录层级跳转;

2. 可实现简单的文件备份,多个硬链接可保障文件数据不被误删,任意一个链接文件可正常读取数据。

软链接用途

主要用于创建文件快捷方式,适配程序版本切换、路径迁移、目录便捷访问等场景,操作灵活、使用广泛。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐