Linux修炼:Ext系列文件系统
Hello大家好!很高兴我们又见面啦!给生活添点passion,开始今天的编程之路!
我的博客:<但凡.
我的专栏:《编程之路》、《数据结构与算法之美》、《C++修炼之路》、《Linux修炼:终端之内 洞悉真理》、《Git 完全手册:从入门到团队协作实战》
感谢您打开这篇博客!希望这篇博客能为您带来帮助,也欢迎一起交流探讨,共同成长。
通过之前的学习,我们知道了被操作系统打开的文件是存储在哪里的,以及操作系统是如何管理他们的,那么没被操作系统打开的文件是存储在哪里的,以什么方式存储起来的呢?
目录
1、前置知识
1.1、磁盘存储数据的基本原理
1.2、磁盘的物理结构
1.3、数据写入过程
1.4、数据读取过程
1.5、数据组织方式
1.6、磁盘性能优化技术
1.7、磁盘扇区定位原理
1.8、CHS寻址模式
1.9、LBA寻址模式
1.10、技术实现细节
2、引入文件系统
2.1、块
2.2、分区
2.3、寄存器
3、Ext2文件系统
3.1、分组
3.2、inode
3.3、GDT
3.4、超级块
3.4、路径解析
3.5、路径缓存
3.6、inode和datablock的映射
3.7、软硬链接
3.7.1、软链接(符号链接)
3.7.2、硬链接
3.7.3、主要区别
1、前置知识
1.1、磁盘存储数据的基本原理
磁盘存储数据依赖于磁性材料的物理特性。硬盘驱动器(HDD)由多个涂有磁性材料的盘片组成,每个盘片被划分为多个同心圆的磁道,磁道又被划分为扇区。数据通过磁头的电磁感应原理写入或读取,磁头改变磁性材料的极性以表示二进制数据(0和1)。
1.2、磁盘的物理结构
盘片是磁盘的核心组件,通常由铝合金或玻璃制成,表面覆盖磁性材料。磁头悬浮在盘片上方几纳米处,通过电流改变磁性材料的极性。每个盘片有两面,每面都有一个磁头。主轴电机带动盘片高速旋转,通常转速为5400 RPM、7200 RPM或更高。
1.3、数据写入过程
写入数据时,控制器将电信号转换为磁头线圈的电流。电流方向决定磁场方向,从而改变盘片上磁性材料的极性。每个微小的磁性区域称为一个磁畴,其极性代表二进制位(例如南极表示0,北极表示1)。写入过程是非破坏性的,新数据会覆盖旧数据。
1.4、数据读取过程
读取数据时,磁头检测磁性材料的极性变化。极性变化会在磁头线圈中感应出微小电流,电流方向反映存储的二进制值。信号被放大并转换为数字信号,供计算机处理。读取过程是非接触式的,不会改变盘片上的数据。
1.5、数据组织方式
磁盘空间被逻辑划分为扇区(通常每扇区512字节或4KB)。多个扇区组成簇(或块),文件系统以簇为单位管理数据。文件分配表(FAT)或索引节点(inode)等结构记录文件占用的簇位置。磁盘控制器负责物理地址与逻辑地址的转换。
1.6、磁盘性能优化技术
缓存技术利用RAM暂存频繁访问的数据,减少磁头移动。预读取技术预测后续可能访问的数据并提前加载。磁盘碎片整理重组分散的文件片段,提高连续访问速度。现代磁盘采用区域位记录(ZBR),外圈磁道存储更多数据以提升容量。
1.7、磁盘扇区定位原理
磁盘通过物理地址和逻辑地址结合的方式定位扇区,主要依赖柱面号(Cylinder)、磁头号(Head)和扇区号(Sector)组成的CHS寻址模式,或现代系统中更常见的LBA(逻辑块地址)转换。
1.8、CHS寻址模式
- 柱面号(Cylinder):确定磁头在盘片上的径向位置。柱面由所有盘面上相同半径的磁道组成,通过步进电机控制磁头移动到对应半径。
- 磁头号(Head):选择具体的盘面。每个盘面有独立的磁头,编号从0开始。例如双面盘片的磁头号为0或1。
- 扇区号(Sector):指定目标磁道上的具体扇区。扇区是磁盘最小的物理存储单元,通常大小为512字节或4KB。
早期的磁盘使用CHS寻址,但这样寻址的磁盘容量有限。随着时代的发展,现在的磁盘大多以LBA方式寻址,这种寻址方式最终也会被转换成CHS地址。
公式示例(CHS到LBA转换):
对于总扇区数计算:
其中 () 和 (
) 分别为最大磁头数和每磁道扇区数。
1.9、LBA寻址模式
- 逻辑块地址(LBA):现代操作系统直接使用连续编号的LBA,由磁盘控制器转换为物理CHS地址。LBA从0开始线性递增,简化了寻址过程。
- 转换过程:磁盘固件或控制器根据几何参数(如每磁道扇区数)将LBA映射到实际物理位置。
1.10、技术实现细节
- 伺服信息:磁盘表面嵌入伺服标记,帮助磁头精确校准位置。
- ZBR(区位记录):外圈磁道扇区数多于内圈,通过自适应转速(CAV或ZCLV)优化存储密度。
- 坏扇区管理:控制器通过重映射将逻辑扇区替换到保留的备用区域。
LBA与CHS之间的转换:
CHS转LBA:
磁头数*每磁道扇区数=单个柱⾯的扇区总数
LBA=柱面号C*单个柱面的扇区总数+磁头号H*每磁道扇区数+扇区号S-1
即:LBA=柱面号C*(磁头数*每磁道扇区数)+磁头号H*每磁道扇区数+扇区号S-1
扇区号通常是从1开始的,而在LBA中,地址是从0开始的
柱面和磁道都是从0开始编号的
总柱面,磁道个数,扇区总数等信息,在磁盘内部会自动维护,上层开机的时候,会获取到 这些参数。
LBA转CHS:
柱面号C=LBA//(磁头数*每磁道扇区数)【就是单个柱⾯的扇区总数】
磁头号H=(LBA%(磁头数*每磁道扇区数))//每磁道扇区数
扇区号S=(LBA%每磁道扇区数)+1
"//":表示除取整
我们可以把CHS寻址方式看作一个三维数组,第一维是柱面(Cylinder),第二维是磁头(Head),第三维是扇区(Sector)。而我们可以把这个三维数组通过数学计算的方式转换为一维数组,而这个一维数组的下标,就是LBA寻址方式。操作系统在访问磁盘中文件时只需要LBA即可,LBA再由磁盘自己转换成CHS地址。
2、引入文件系统
2.1、块
其实对于系统来说,再读取硬盘数据时,并不是一个扇区一个扇区的读,而是连续读取多个扇区,即一次性读取一个块。这样做的目的一是降低和硬件的耦合度,二是提高效率。需要注意的是,块这个概念是系统划分的,而不是磁盘划分的。
一个块的大小是由格式化的时候确定的,并且不可以更改。块的大小通常都是4KB,八个扇区组成一个块。块是文件读取的最小单位。
因为LBA地址可以看作是一维数组的下标,那么我们可以通过数学计算的方式找到块号:
块号=LBA/8,LAB=块号*8+n(n是块内第几个扇区)。
所以,在文件系统角度,磁盘就是一个块设备。

2.2、分区
用过电脑的都知道,在windows系统中,硬盘是可以分区的,分为C盘,D盘,E盘...我们可能只由一块硬盘,但是这个硬盘可以被分为很多盘。这个分盘,或者严格说是分区,本质上就是对硬盘的一种格式化。
那么怎么理解分区呢?其实我们只需要知道一个区域的开始块号和结束块号,就可以划分出区域。每个分区可以独立管理文件系统、操作系统或数据存储。
2.3、寄存器
寄存器是中央处理器(CPU)内部的一种高速存储单元,用于暂存指令、数据和地址。其读写速度远高于内存,是CPU直接操作的核心组件之一。
寄存器的功能特点
- 高速访问:由触发器或锁存器构成,物理上紧邻运算单元,延迟极低。
- 临时存储:存放当前执行的指令、运算中间结果或内存地址。
- 容量有限:通常为4-64位宽度,数量从几十到几百个不等。
寄存器与内存的差异
| 特性 | 寄存器 | 内存 |
|---|---|---|
| 速度 | 纳秒级 | 微秒级 |
| 容量 | 数十字节 | GB-TB级 |
| 寻址方式 | 直接命名访问 | 地址总线访问 |
在磁盘那内部也有寄存器。磁盘内部的寄存器是硬盘控制器(HDD Controller)中的关键组件,用于临时存储数据、控制信号和状态信息。寄存器又分为数据寄存器,状态寄存器,命令寄存器,错误寄存器等。
当操作系统想往磁盘中写入数据时,要先把方向(读或者写),LBA地址,写入的内容,通过IO总线写入到磁盘内部的寄存器中,接着磁盘内部的控制器解析命令,移动磁头到目标磁道,等待盘片旋转至目标扇区,写入数据到数据寄存器。
3、Ext2文件系统
3.1、分组
在分区之后,我们还要分组,对每个组进行管理。

文件等于内容加属性,同时,内容和属性都是数据,都需要存储在磁盘中。我们对每个分区继续划分成许多块组,对于每个组中,又包含了许多内容,首先是data blocks数据块区域,数据块区域中只存储数据。data blocks中又有很多的数据块,假设说一个文件的内容大小为16kb,那么他就占据4个数据块。
为了标记哪些数据块占用了哪些没占用,块组中另一区域block bitmap(块位图)就起到了标记的作用。如果某个数据块被占用了,就把他对应的比特位标记为1,否则为0。
3.2、inode
那么文件属性是怎么存储的呢?在Linux中,用结构体inode表示文件属性。当创建一个文件时,首先在内存中先创建struct inode,然后填好信息,把这个struct inode的信息以二进制的形式存储到inode table中。
(struct inode简化版)
struct inode {umode_t i_mode; // 文件类型和权限(如S_IFREG、S_IFDIR)uid_t i_uid; // 所有者的用户IDgid_t i_gid; // 所有者的组IDloff_t i_size; // 文件大小(字节)struct timespec i_atime; // 最后一次访问时间struct timespec i_mtime; // 最后一次修改时间struct timespec i_ctime; // 最后一次状态变更时间unsigned long i_ino; // inode编号dev_t i_rdev; // 设备号(如果是设备文件)struct super_block *i_sb; // 指向超级块的指针struct address_space *i_mapping; // 文件数据页缓存struct file_operations *i_fop; // 文件操作函数表// 其他成员(如链接计数、扩展属性等)
};
任何一个文件的属性个数是一样的,也就是说,任何文件的inode结构体大小是固定的128字节(一般情况下)。操作系统每次读取inode,同样是每次读取4KB。所以说,操作系统读取struct inode时,一次可以读取32个文件的inode。
一个文件只有一个inode,所以为了表示文件的唯一性,inode中存储着唯一的inode number。在inodetable中,存储着很多个inode number,每个文件的属性根据这个inode number来把该文件的属性罗列出来。
接着我们介绍inode map。inode map也是一个位图,比特位的位置(从右到左)表示第几个inode。比特位的内容表示对应的inode是否被占用。
将来我们更改文件内容时,首先拿到inode号,然后根据inode到数据块的映射关系表拿到文件块块号,这个映射关系表存储在struct inode内部。也就是说,只要通过inode块号,就能找到文件的inode属性,进一步找到文件的内容。
3.3、GDT
接下来我们介绍一下GDT(Group Descriptor Table),GDT即块组描述符表,描述块组属性信息,整个分区有多少块组就有多少块组描述符。每个描述符存储着这一块组的描述信息,比如从哪里开始是inode table,从哪里开始是data blocks...
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.4、超级块
最后我们来介绍超级块(super block)。超级块描述的是整个分区的信息,比如block和inode的总量,未使用的block和inode的总量,最近一次写入数据的时间等等。他和GDT不同的是,GDB描述的是这一个块组的信息,而超级块描述的是整个分区的信息。
如果超级块信息损毁了,整个分区就无法使用了。所以说,超级块在这个分区的每个块组中都存在,防止超级块信息损坏。
我们要想使用一块硬盘,首先要对他进行分区,然后再进行格式化。硬盘格式化的过程就是给当前的分区写入文件系统和分区分组相关的管理数据,文件数据可以暂时不要。
当我们新建一个文件时,要先指定的块组中查找inode bitmap,从右向左找到一个为0的比特位,把该比特位更改为1,然后就得到了inode号,我们根据这个比特位在bitmap中的偏移量,在inode table中申请空间,接着,就可以写入文件属性。
当删除文件时,首先根据inode号,去inode bitmap中确认对应的比特位是不是1,如果是1,把他改为0。没错,不用清空数据块。正因此,删除和下载一个同样大小的文件,删除文件要快得多。计算机删除数据,只要把数据设置无效就可以了。所以,如果我们能得到被删掉的文件的inode号,就可以恢复这个文件。
inode编号和datablock编号是全分区统一分配的,不是只在分组内有效。inode编号和datablock编号不能跨区域,一个分区,一个文件系统,相互独立。也就是说,在两个不同的分区中,inode编号和datablock编号可能重复。即inode编号和datablock编号是以分区为单位的来分配的。
不同分区中的inode可能会重复,那我们怎么确定我们要访问的这个inode对应的文件时哪个分区的呢?其实这是根据文件路径来区分的。
那么文件名存储在哪里呢?
首先,在Linux系统中目录也是文件。从文件系统的角度,存储目录和存储普通文件没有任何区别。在目录中同样也存在着datablock。而文件名和到inode的映射关系就存在目录的daatablock中。
这也就解释了,为什么在目录下新建文件需要对目录有w权限,而读取一个文件属性需要对目录具有r权限。这两种操作本质上都是通过文件名,建立或者访问文件名和inode的映射关系,那既然要涉及到写入或访问目录的datablock,就必须具有对应的权限。对目录设置rw权限的本质也是为了约束用户访问datablock。
如果我们想打开一个特定路径下的文件,必须对这个文件所在的路径进行解析!正因此,在Linux下访问文件,必须知道这个文件的路径。当我们尝试对一个文件系统中的属性进行增删查改的时候,都必须要先把对应的文件系统信息,加载到内存中,在内存中修改,写回对应的磁盘位置。
在Linux系统中,当用户访问指定路径下的文件,包括路上目录,最终的目标文件在内,Linux会在路径解析的过程中,在内核中形成目录树和路径缓存。也就是说目录结构是内存级的。
3.4、路径解析
说了半天,到底什么是路径解析呢?
路径解析是指将相对路径或符号链接转换为绝对路径的过程,确保程序或系统能够准确识别和访问文件或目录的位置。其核心是处理路径中的特殊符号(如.、..、~)或环境变量,最终生成规范化的完整路径。
路径解析的常见场景:
- 相对路径转绝对路径
例如,将./docs/file.txt解析为/home/user/docs/file.txt。 - 符号链接展开
若路径中包含软链接(如/usr/bin/python可能指向/usr/bin/python3.11),需解析实际路径。 - 环境变量替换
如$HOME/docs会被替换为/home/user/docs(假设HOME变量值为/home/user)。
3.5、路径缓存
在内核中,打开任何一个目录或普通文件都会在内核中都会有对应的struct dentry结构。这个结构就表明一个叶子结点,当访问指定路径时,在内核中会把这个路径下的目录用一个树形结构连接起来。所以在进行路径解析时,只有第一次是慢的,以后的每一次路径解析都会优先从dentry树结构中进行解析。
Dentry树主要存储在内存中的dentry_cache缓存池中,属于内核动态分配的内存区域。每个dentry对象通过struct dentry结构体表示,包含文件名、父目录指针、inode指针等信息。内核使用哈希表和LRU(最近最少使用)算法管理这些对象。
Dentry树是虚拟文件系统(VFS)层的一部分,作为文件路径与inode之间的桥梁。当用户访问文件路径时,内核会先在dentry缓存中查找,未命中时才会访问磁盘文件系统。缓存的dentry对象会保持到内存压力触发回收或主动释放。
当某些文件长时间不访问,内核线程kswapd定期清理这些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_PROFILINGstruct dcookie_struct *d_cookie; /* cookie, if any */
#endifint d_mounted;unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */
};
3.6、inode和datablock的映射
在inode内部,还存储着一个i_block,其中记录着inode对应的datablock数据块的块号。
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)
其中,前12个位置是直接映射到数据块,而第十三个,十四个,十五个分别是一级,二级,三级间接块索引表指针,直接块指针直接指向存储文件数据的数据块,而间接块指针指向一个索引块,该索引块内存储了多个数据块的地址。间接块指针通过一个中间索引块间接寻址数据块。
系统通过一级间接块指针找到一个索引块,索引块中存储了多个数据块地址。假设一个块大小为4KB,每个地址占用4字节,则一个索引块可以存储1024个数据块地址。
当文件的直接块指针不足以存储所有数据块地址时,系统会使用间接块指针来管理更多数据块。

假设块不够了,操作系统就会去其他组中“抢”块,毕竟数据块号是以分区为单位划分的,不同组之间也不会重复。但需要注意的是,无论是块还是inode,早晚都有用完的时候,所以就算磁盘仍有空间,也有可能出现文件无法创建的情况。
一个磁盘,必须分区格式化,才能具有使用的前提,一个分区,要真正能被使用,必须挂载到指定的目录才可以。
分区写入文件系统,无法直接使用,需要和指定的目录关联,进行挂载才能使用。 所以,可以根据访问目标文件的"路径前缀"准确判断我在哪一个分区。
挂载(Mount)是计算机操作系统中将存储设备或文件系统连接到目录树的过程,使设备中的内容可通过指定目录访问。在Linux中,挂载是文件系统管理的基础操作,Windows系统中类似概念称为“映射网络驱动器”或“加载卷”。
挂载的作用就是将硬盘,U盘等物理设备或虚拟文件系统关联到目录,用户可以用过目录读写设备数据。
我们可以执行以下命令来手动挂载设备:
通过mount命令手动挂载设备,例如将U盘挂载到/mnt/usb目录:
sudo mount /dev/sdb1 /mnt/usb
卸载使用umount命令:
sudo umount /mnt/usb
3.7、软硬链接
3.7.1、软链接(符号链接)
软链接(Symbolic Link)是一种特殊的文件类型,它指向另一个文件或目录的路径。类似于Windows中的快捷方式。软链接是一个独立的文件,存储的是目标文件的路径信息。
- 特点:软链接可以跨文件系统创建,可以指向目录或文件。删除软链接不会影响目标文件,但如果目标文件被删除或移动,软链接会失效(称为“悬挂链接”)。
- 创建命令:使用
ln -s命令创建软链接。ln -s 目标文件路径 软连接路径 - 示例:
ln -s /home/user/file.txt /home/user/link_to_file
3.7.2、硬链接
硬链接(Hard Link)是文件系统中指向同一inode的多个文件名。硬连接与原始文件共享相同的磁盘数据块,本质上它们是同一个文件的多个名称。也就是说,硬链接的过程就是新建文件名和目标inode的映射关系,并没有创建新的文件。
通过硬链接,可以对文件进行备份。我们查看文件属性:

其中,文件权限后面的数字1代表的是硬链接数,也就是之前我们说的引用计数。如果我们对这个文件进行硬链接,这个数字就会+1。只要这个数字不为0,文件的内容就不会被删除,所以再硬链接之后的文件,哪怕我们执行命令把原文件删除了,这个内容也不会消失,因为我们已经通过硬链接对其完成了备份。
文件的默认硬链接数是1,空目录默认硬链接数是2,因为除了自身以外,还有一个 . 对这个目录进行了硬链接。如果我们在空目录中新建一个目录,他的默认硬链接数就成了3,因为除了自身和.以外,他还有一个..进行了硬链接。
我们不能对目录设置硬链接,因为会产生路径环路问题。当然,.和..除外,因为这俩被系统进行特殊处理了。
- 特点:硬链接不能跨文件系统创建,也不能指向目录。删除硬链接或原始文件不会影响其他硬链接,只有当所有硬链接都被删除时,文件数据才会被释放。
- 创建命令:使用
ln命令(不加-s选项)创建硬连接。ln 目标文件路径 硬连接路径 - 示例:
ln /home/user/file.txt /home/user/hard_link_to_file
3.7.3、主要区别
-
存储方式:
- 软链接是一个独立的文件,有独立的inode,存储目标路径。软链接的内容中保存的是指向目标文件的路径字符串。
- 硬链接是同一inode的多个名称。
-
跨文件系统:
- 软链接可以跨文件系统。
- 硬链接不能跨文件系统。
-
指向目录:
- 软链接可以指向目录。
- 硬链接不能指向目录(某些系统可能支持,但通常不推荐)。
-
删除影响:
- 删除软链接不影响目标文件。
- 删除硬链接或原始文件不会立即释放数据,直到所有硬链接被删除。
-
大小:
- 软链接占用独立的磁盘空间(存储路径信息)。
- 硬链接不额外占用磁盘空间(共享inode和数据块)。
好了,今天的内容就分享到这,我们下期再见!

