6. Ext系列⽂件系统(上)
本节重点:
- 理解磁盘物理结构
- 掌握CHS和LBA地址
- 掌握Ext系列⽂件系统原理
- 理解分区,格式化,路径解析,挂载等过程和操作
- 理解软硬连接使⽤和⽤途
1.理解硬件
1-1 磁盘、服务器、机柜、机房
- 机械磁盘是计算机中唯⼀的⼀个机械设备
- 磁盘—外设
- 慢
- 容量⼤,价格便宜
题外话:
- 关于机房
- 关于磁盘—磁铁
1-2 磁盘物理结构
1-3 磁盘的存储结构
扇区:是磁盘存储数据的基本单位,512字节,块设备
如何定位⼀个扇区呢?
- 可以先定位磁头(header)
- 确定磁头要访问哪⼀个柱⾯(磁道)(cylinder)
- 定位⼀个扇区(sector)
- CHS地址定位
⽂件=内容+属性都是数据,⽆⾮就是占据那⼏个扇区的问题!能定位⼀个扇区了,能不能定位多个扇 区呢?
- 扇区是从磁盘读出和写⼊信息的最⼩单位,通常⼤⼩为 512 字节。
- 磁头(head)数:每个盘⽚⼀般有上下两⾯,分别对应1个磁头,共2个磁头
- 磁道(track)数:磁道是从盘⽚外圈往内圈编号0磁道,1磁道…,靠近主轴的同⼼圆⽤于停靠磁 头,不存储数据
- 柱⾯(cylinder)数:磁道构成柱⾯,数量上等同于磁道个数
- 扇区(sector)数:每个磁道都被切分成很多扇形区域,每道的扇区数量相同
- 圆盘(platter)数:就是盘⽚的数量
- 磁盘容量=磁头数× 磁道(柱⾯)数× 每道扇区数× 每扇区字节数
- 细节:传动臂上的磁头是共进退的(这点⽐较重要,后⾯会说明)
柱⾯(cylinder),磁头(head),扇区(sector),显然可以定位数据了,这就是数据定位(寻址)⽅ 式之⼀,CHS寻址⽅式。
1-3-1 CHS寻址
CHS寻址:对早期的磁盘⾮常有效,知道⽤哪个磁头,读取哪个柱⾯上的第⼏扇区就可以读到数据了。 但是CHS模式⽀持的硬盘容量有限,因为系统⽤8bit来存储磁头地址,⽤10bit来存储柱⾯地 址,⽤6bit来存储扇区地址,⽽⼀个扇区共有512Byte,这样使⽤CHS寻址⼀块硬盘最⼤容量为256102463*512B=8064MB(1MB=1048576B)(若按1MB=1000000B来算就是 8.4GB)
1-4 磁盘的逻辑结构
1-4-1 理解过程
磁带上⾯可以存储数据,我们可以把磁带“拉直”,形成线性结构
那么磁盘本质上虽然是硬质的,但是逻辑上我们可以把磁盘想象成为卷在⼀起的磁带,那么磁盘的逻 辑存储结构我们也可以类似于:
这样每⼀个扇区,就有了⼀个线性地址(其实就是数组下标),这种地址叫做LBA
1-4-2 真实过程
⼀个细节:传动臂上的磁头是共进退的
柱⾯是⼀个逻辑上的概念,其实就是每⼀⾯上,相同半径的磁道逻辑上构成柱⾯。
所以,磁盘物理上分了很多⾯,但是在我们看来,逻辑上,磁盘整体是由“柱⾯”卷起来的
所以,磁盘的真实情况是:
磁道:
某⼀盘⾯的某⼀个磁道展开:
即:⼀维数组
柱⾯:
整个磁盘所有盘⾯的同⼀个磁道,即柱⾯展开:
- 柱⾯上的每个磁道,扇区个数是⼀样的
- 这不就是⼆维数组吗
整盘:
整个磁盘不就是多张⼆维的扇区数组表(三维数组?)
所以,寻址⼀个扇区:先找到哪⼀个柱⾯(Cylinder),在确定柱⾯内哪⼀个磁道(其实就是磁头位置, Head),在确定扇区(Sector),所以就有了CHS。
我们之前学过C/C++的数组,在我们看来,其实全部都是⼀维数组:
所以,每⼀个扇区都有⼀个下标,我们叫做LBA(Logical Block Address)地址,其实就是线性 地址。所以怎么计算得到这个LBA地址呢?
LBA,CHS必须要! LBA地址转成CHS地址,CHS如何转换成为LBA地址。
OS只需要使⽤LBA就可以了!! LBA地址转成CHS地址,CHS如何转换成为LBA地址。谁做啊??磁盘 ⾃⼰来做!固件(硬件电路,伺候系统)
1-5 CHS&&LBA地址
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地址,⽽是直接使⽤LBA地址,磁盘内部⾃⼰ 转换。
因此:
从现在开始,磁盘就是⼀个元素为扇区的⼀维数组,数组的下标就是每⼀个扇区的LBA地址。OS使⽤ 磁盘,就可以⽤⼀个数字访问磁盘扇区了。
2. 引⼊⽂件系统
2-1 引⼊"块"概念
其实硬盘是典型的“块”设备,操作系统读取硬盘数据的时候,其实是不会⼀个个扇区地读取,这样 效率太低,⽽是⼀次性连续读取多个扇区,即⼀次性读取⼀个”块”(block)。
硬盘的每个分区是被划分为⼀个个的”块”。⼀个”块”的⼤⼩是由格式化的时候确定的,并且不可 以更改,最常⻅的是4KB,即连续⼋个扇区组成⼀个”块”。”块”是⽂件存取的最⼩单位。
注意:
- 磁盘就是⼀个三维数组,我们把它看待成为⼀个"⼀维数组",数组下标就是LBA,每个元素都是扇 区
- 每个扇区都有LBA,那么8个扇区⼀个块,每⼀个块的地址我们也能算出来。
- 知道LBA:块号=LBA/8
- 知道块号:LAB=块号*8+n.(n是块内第⼏个扇区)
2-2 引⼊"分区"概念
其实磁盘是可以被分成多个分区(partition)的,以Windows观点来看,你可能会有⼀块磁盘并且将 它分区成C,D,E盘。那个C,D,E就是分区。分区从实质上说就是对硬盘的⼀种格式化。但是Linux的设备 都是以⽂件形式存在,那是怎么分区的呢?
柱⾯是分区的最⼩单位,我们可以利⽤参考柱⾯号码的⽅式来进⾏分区,其本质就是设置每个区的起 始柱⾯和结束柱⾯号码。此时我们可以将硬盘上的柱⾯(分区)进⾏平铺,将其想象成⼀个⼤的平 ⾯,如下图所⽰:
注意:
柱⾯⼤⼩⼀致,扇区个位⼀致,那么其实只要知道每个分区的起始和结束柱⾯号,知道每 ⼀个柱⾯多少个扇区,那么该分区多⼤,其实和解释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.c每⾏包含7列:
- 模式
- 硬链接数
- ⽂件所有者
- 组
- ⼤⼩
- 最后修改时间
- ⽂件名
ls-l读取存储在磁盘上的⽂件信息,然后显⽰出来
其实这个信息除了通过这种⽅式来读取,还有⼀个stat命令能够看到更多信息
[root@localhost linux]# stat test.c File: "test.c" Size: 654 Blocks: 8 Device: 802h/2050d Inode: 263715 Access: (0644/-rw-r--r--) Uid: ( IO Block: 4096 普通⽂件 Links: 1 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到这我们要思考⼀个问题,⽂件数据都储存在”块”中,那么很显然,我们还必须找到⼀个地⽅储存 ⽂件的元信息(属性信息),⽐如⽂件的创建者、⽂件的创建⽇期、⽂件的⼤⼩等等。这种储存⽂件 元信息的区域就叫做inode,中⽂译名为”索引节点”。
每⼀个⽂件都有对应的inode,⾥⾯包含了与该⽂件有关的⼀些信息。为了能解释清楚inode,我们需 要是深⼊了解⼀下⽂件系统。
注意:
- Linux下⽂件的存储是属性和内容分离存储的
- Linux下,保存⽂件属性的集合叫做inode,⼀个⽂件,⼀个inode,inode内有⼀个唯⼀ 的标识符,叫做inode号
所以⼀个⽂件的属性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数据结构内部
- inode的⼤⼩⼀般是128字节或者256,我们后⾯统⼀128字节
- 任何⽂件的内容⼤⼩可以不同,但是属性⼤⼩⼀定是相同的
- 文件名不在 inode 中,存在目录的映射表里
- inode 管文件本身,目录管文件名
到⽬前为⽌,相信⼤家还有两个问题:
我们已经知道硬盘是典型的“块”设备,操作系统读取硬盘数据的时候,读取的基本单位 是”块”。“块”⼜是硬盘的每个分区下的结构,难道“块”是随意的在分区上排布的吗?那要怎 么找到“块”呢?
还有就是上⾯提到的存储⽂件属性的inode,⼜是如何放置的呢?
而文件系统这个大boss我们还没理清楚是什么,但我先提前告诉你,⽂件系统的存在就是为了组织管理这些的!
3. ext2 ⽂件系统
3-1 宏观认识
所有的准备⼯作都已经做完,是时候认识下⽂件系统了。我们想要在硬盘上储⽂件,必须先把硬盘格 式化为某种格式的⽂件系统,才能存储⽂件。⽂件系统的⽬的就是组织和管理硬盘中的⽂件。在 Linux 系统中,最常⻅的是ext2系列的⽂件系统。其早期版本为ext2,后来⼜发展出ext3和ext4。 ext3 和ext4虽然对ext2进⾏了增强,但是其核⼼设计并没有发⽣变化,我们仍是以较⽼的ext2作为 演⽰对象。
上图中启动块(BootBlock/Sector)的⼤⼩是确定的,为1KB,由PC标准规定,⽤来存储磁盘分区信 息和启动信息,任何⽂件系统都不能修改启动块。启动块之后才是ext2⽂件系统的开始。
3-2 Block Group
ext2⽂件系统会根据分区的⼤⼩划分为数个BlockGroup。⽽每个BlockGroup都有着相同的结构组 成。类似于政府管理各区
3-3 块组内部构成
3-3-1 超级块(SuperBlock)
存放⽂件系统本⾝的结构信息,描述整个分区的⽂件系统信息。记录的信息主要有:bolck和inode的 总量,未使⽤的block和inode的数量,⼀个block和inode的⼤⼩,最近⼀次挂载的时间,最近⼀次写 ⼊数据的时间,最近⼀次检验磁盘的时间等其他⽂件系统的相关信息。SuperBlock的信息被破坏,可 以说整个⽂件系统结构就被破坏了
超级块在每个块组的开头都有⼀份拷⻉(第⼀个块组必须有,后⾯的块组可以没有)。为了保证⽂ 件系统在磁盘部分扇区出现物理问题的情况下还能正常⼯作,就必须保证⽂件系统的superblock信 息在这种情况下也能正常访问。所以⼀个⽂件系统的superblock会在多个blockgroup中进⾏备份, 这些superblock区域的数据保持⼀致。
ext2_super_block结构体代码:
3-3-2 GDT(GroupDescriptorTable)
块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描 述符存储⼀个块组的描述信息,如在这个块组中从哪⾥开始是inodeTable,从哪⾥开始是Data Blocks,空闲的inode和数据块还有多少个等等。块组描述符在每个块组的开头都有⼀份拷⻉。
3-3-3 块位图(BlockBitmap)
- BlockBitmap中记录着DataBlock中哪个数据块已经被占⽤,哪个数据块没有被占⽤
3-3-4 inode位图(InodeBitmap)
- 每个bit表⽰⼀个inode是否空闲可⽤。
3-3-5 i节点表(InodeTable)
- 存放⽂件属性如⽂件⼤⼩,所有者,最近修改时间等
- 当前分组所有Inode属性的集合
- inode编号以分区为单位,整体划分,不可跨分区
3-3-6 Data Block
数据区:存放⽂件内容,也就是⼀个⼀个的Block。根据不同的⽂件类型有以下⼏种情况:
- 对于普通⽂件,⽂件的数据存储在数据块中。
- 对于⽬录,该⽬录下的所有⽂件名和⽬录名存储在所在⽬录的数据块中,除了⽂件名外,ls-l命令 看到的其它信息保存在该⽂件的inode中。
- Block 号按照分区划分,不可跨分区
3-4 inode和datablock映射(弱化)
inode内部存在
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */ , EXT2_N_BLOCKS=15,就是⽤来进⾏inode和block映射的这样⽂件=内容+属性,就都能找到了。
// ext2磁盘inode结构体注释 /* * Structure of an inode on the disk * 磁盘上inode的存储结构 */ struct ext2_inode { __le16 i_mode; /* 文件模式:存储文件类型(普通/目录/软链接)、读写执行权限位 */ __le16 i_uid; /* 文件属主UID低16位,标识文件所有者用户ID */ __le32 i_size; /* 文件字节大小,普通文件存文件长度,目录存目录数据块总字节 */ __le32 i_atime; /* 最后访问时间:文件最近一次被读取的时间戳(秒级Unix时间) */ __le32 i_ctime; /* 创建/属性修改时间:文件创建、权限/属主变更时更新 */ __le32 i_mtime; /* 文件内容修改时间:文件数据被写入覆盖时更新 */ __le32 i_dtime; /* 文件删除时间:unlink后记录删除的时间,恢复文件用 */ __le16 i_gid; /* 文件属组GID低16位,标识文件所属用户组ID */ __le16 i_links_count; /* 硬链接计数:记录指向此inode的目录条目数量 */ __le32 i_blocks; /* 文件占用磁盘块总数(512字节为单位,预分配块也计入) */ __le32 i_flags; /* 文件特殊标志:如只读、同步写入、不可删除等属性标记 */ // 多操作系统兼容联合体,不同系统占用同一块内存 union { struct { __le32 l_i_reserved1; // Linux系统保留字段1 } linux1; struct { __le32 h_i_translator; // Hurd系统专用翻译器字段 } hurd1; struct { __le32 m_i_reserved1; // Masix系统保留字段1 } masix1; } osd1; /* 操作系统相关预留区域1 */ // 磁盘块指针数组,EXT2核心,存放直接/一级/二级/三级间接块号 __le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks 数据块指针数组 */ __le32 i_generation; /* 文件版本号,NFS网络文件系统用来判断文件是否过期 */ __le32 i_file_acl; /* 文件访问控制列表(ACL)块号,精细化权限扩展 */ __le32 i_dir_acl; /* 目录访问控制列表(ACL)块号,仅目录文件生效 */ __le32 i_faddr; /* 碎片地址,EXT2早期碎片机制,现代系统基本废弃 */ // 第二块OS兼容联合体,存放碎片、高16位UID/GID等扩展信息 union { struct { __u8 l_i_frag; /* Linux:碎片编号,旧碎片机制使用 */ __u8 l_i_fsize; /* Linux:碎片尺寸 */ __u16 i_pad1; /* 内存对齐填充位,补齐字节对齐 */ __le16 l_i_uid_high; /* 属主UID高16位,支持超过65535的大UID */ __le16 l_i_gid_high; /* 属组GID高16位,支持超过65535的大GID */ __u32 l_i_reserved2; /* Linux系统第二块保留字段 */ } linux2; struct { __u8 h_i_frag; /* Hurd:碎片编号 */ __u8 h_i_fsize; /* Hurd:碎片尺寸 */ __le16 h_i_mode_high; /* Hurd:文件模式高位扩展 */ __le16 h_i_uid_high; /* Hurd:UID高16位 */ __le16 h_i_gid_high; /* Hurd:GID高16位 */ __le32 h_i_author; /* Hurd:文件创建者ID */ } hurd2; struct { __u8 m_i_frag; /* Masix:碎片编号 */ __u8 m_i_fsize; /* Masix:碎片尺寸 */ __u16 m_pad1; /* Masix对齐填充 */ __u32 m_i_reserved2[2];/* Masix第二块保留字段数组 */ } masix2; } osd2; /* 操作系统相关预留区域2 */ }; // ext2_inode 结构体结束 // 直接块数量:i_block数组前12个元素是直接数据块指针 #define EXT2_NDIR_BLOCKS 12 // 一级间接块下标:数组第12号元素,指向一级间接块 #define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS // 二级间接块下标:数组第13号元素,指向二级间接块 #define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1) // 三级间接块下标:数组第14号元素,指向三级间接块 #define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1) // i_block数组总长度:12直接 + 1一级 +1二级 +1三级 = 15个元素 #define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1) // inode 的大小通常是 128 字节 或 256 字节 // 128字节是EXT2默认inode尺寸,256是EXT3/4扩展inode,存放更多扩展属性3-4-1问题解答:已知 inode 号,在指定分区执行文件增、删、查、改分别做什么
前置基础逻辑: EXT2 文件系统分区格式化后会划分块组,每个块组包含:超级块 SB、块组描述符 GDT、块位图、inode 位图、inode 表、数据块。 给定分区 + inode 号,可快速定位:
- 计算 inode 归属哪个块组
- 在该块组的 inode 表中取出完整
ext2_inode结构体 - inode 里存全部文件属性 +
i_block[]数据块指针(文件真实内容)
1. 增(新建文件,如 touch、mkdir)
定位文件所在目录对应的 inode,读取目录数据块;
扫描目标分区对应块组的inode 位图,标记第一个空闲 inode 号;
在该 inode 号所属块组的 inode 表中,初始化
ext2_inode结构体:
填充 i_mode(文件类型 / 权限)、i_uid/i_gid、时间戳、i_links_count=1;
–_block 数组全部置 0(空文件无数据块),-i_size=0;
修改块组 inode 位图,将此 inode 标记为已占用;
在父目录的数据块中新增目录项:文件名 + 刚分配的 inode 号;
更新父目录 inode 的 i_mtime/i_ctime、i_size、i_blocks;
同步写入磁盘 inode 表、目录数据块、inode 位图。
2. 删(删除文件,rm/unlink)
- 根据 inode 号取出目标 inode,读取 i_links_count 硬链接计数;
- 若链接数 > 1:仅将父目录中对应目录项删除,inode 链接数
i_links_count -=1,不释放数据块与 inode; - 若链接数 == 1:
- 将 inode 的 i_dtime 写入当前删除时间戳;
- 遍历 inode 中
i_block[]全部直接 / 间接块,逐个在块位图标记为空闲; - 在 inode 位图中将该 inode 标记为空闲;
- 删除父目录内该文件对应的目录条目;
- 更新父目录 inode 的 i_mtime/i_ctime,落盘所有位图、inode、目录块。
3. 查(读取文件内容 / 查看属性,ls、cat、stat)
① 查看文件属性(stat/ls -i)
- 用 inode 号定位到块组、取出磁盘上的
ext2_inode完整结构体; - 解析结构体字段:权限 (i_mode)、属主 (i_uid/i_gid)、大小 (i_size)、时间、硬链接数、占用块数等,输出展示。
② 读取文件内容(cat/less)
- 通过 inode 号拿到 inode,读取文件长度 i_size;
- 根据要读取的文件偏移,计算对应数据落在
i_block[]哪一块(直接块 / 一级 / 二级 / 三级间接块); - 逐级解析间接块指针,找到对应磁盘数据块号;
- 读取磁盘数据块内字节,拼接得到文件内容返回;全程不修改磁盘任何位图、inode、数据块。
4. 改(修改文件,分两类:改属性 / 改文件内容)
(1)修改文件元数据(chmod/chown/chgrp/touch 更新时间)
- inode 号定位并加载磁盘 inode;
- 修改对应结构体字段:
- chmod:修改 i_mode 权限位;
- chown/chgrp:修改 i_uid/i_gid;
- touch:更新 i_atime/i_mtime;
- 强制更新 inode 的 i_ctime(属性修改时间);
- 将修改后的 inode 写回磁盘 inode 表,无数据块操作。
(2)修改文件内容(echo 覆盖、vim 编辑)
- 通过 inode 号加载 inode,根据读写偏移找到对应数据块;
- 场景 1:覆盖原有内容长度不变 → 直接修改对应数据块,更新 inode 的 i_mtime;
- 场景 2:写入后文件变大(追加 / 新增内容):
- 若当前块剩余空间不足,扫描块位图分配新数据块;
- 更新 inode 的
i_block[]指针数组(填充直接 / 间接块号); - 修改块位图标记新块为占用;
- 更新 inode 的 i_size、i_blocks、i_mtime;
- 场景 3:截断文件(truncate):释放超出新长度的数据块,块位图标记空闲,修改 i_size;
- 将变更的数据块、inode、块位图同步写入磁盘。
3-4-2 结论:
• 分区之后的格式化操作,就是对分区进⾏分组,在每个分组中写⼊SB、GDT、Block Bitmap、InodeBitmap等管理信息,这些管理信息统称:⽂件系统
• 只要知道⽂件的inode号,就能在指定分区中确定是哪⼀个分组,进⽽在哪⼀个分组确定 是哪⼀个inode
• 拿到inode⽂件属性和内容就全部都有了
下⾯,通过touch⼀个新⽂件来看看如何⼯作。
[root@localhost linux]# touch abc [root@localhost linux]# ls -i abc 263466 abc为了说明问题,我们将上图简化:
创建⼀个新⽂件主要有以下4个操作:
- 存储属性 内核先找到⼀个空闲的i节点(这⾥是263466)。内核把⽂件信息记录到其中。
- 存储数据 该⽂件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第⼀块 数据复制到300,下⼀块复制到500,以此类推。
- 记录分配情况 ⽂件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
- 添加⽂件名到⽬录 新的⽂件名abc。linux如何在当前的⽬录中记录这个⽂件?内核将⼊⼝(263466,abc)添加到 ⽬录⽂件。⽂件名和inode之间的对应关系将⽂件名和⽂件的内容及属性连接起来。
3-5 目录与文件名
问题:
• 我们访问⽂件,都是⽤的⽂件名,没⽤过inode号啊?
• ⽬录是⽂件吗?如何理解?
答案:
• ⽬录也是⽂件,但是磁盘上没有⽬录的概念,只有⽂件属性+⽂件内容的概念。
• ⽬录的属性不⽤多说,内容保存的是:⽂件名和Inode号的映射关系
验证说明代码:
// readdir.c #include <stdio.h> // 标准输入输出库,用于printf、fprintf #include <string.h> // 字符串处理库,strcmp字符串比较函数 #include <stdlib.h> // 标准工具库,提供exit退出函数 #include <dirent.h> // 目录操作头文件,DIR结构体、opendir/readdir/closedir、struct dirent #include <sys/types.h> // 系统基础类型定义,ino_t等类型依赖 #include <unistd.h> // POSIX标准头文件,部分系统调用依赖 // main函数:程序入口,argc参数个数,argv参数数组 int main(int argc, char *argv[]) { // 判断命令行参数数量,程序执行需要传入1个目录路径 if (argc != 2) { // 错误信息输出到标准错误流,打印使用方法 fprintf(stderr, "Usage: %s <directory>\n", argv[0]); // 异常退出,返回失败状态 exit(EXIT_FAILURE); } // 打开指定目录,返回目录流指针DIR* DIR *dir = opendir(argv[1]); // 系统调用,打开目录 // 判断目录是否打开失败(返回NULL) if (!dir) { // 打印系统层面的错误原因 perror("opendir"); exit(EXIT_FAILURE); } // 定义目录项结构体指针,存放readdir读取到的目录条目 struct dirent *entry; // 循环读取目录内每一个条目,readdir读到末尾返回NULL while ((entry = readdir(dir)) != NULL) { // 系统调用,读取目录项 // 跳过 "."(当前目录)和 ".."(上级目录)这两个特殊目录项 if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; // 跳过本次循环,不打印该条目 } // 打印文件名以及对应的inode编号 printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned long)entry->d_ino); } // 关闭打开的目录流,释放资源 closedir(dir); // 程序正常结束,返回0 return 0; }- 所以,访问⽂件,必须打开当前⽬录,根据⽂件名,获得对应的inode号,然后进⾏⽂件访问
- 所以,访问⽂件必须要知道当前⼯作⽬录, 本质是必须能打开当前⼯作⽬录⽂件,查看⽬录⽂件的内容!
3-6 路径解析
问题:打开当前⼯作⽬录⽂件,查看当前⼯作⽬录⽂件的内容?当前⼯作⽬录不也是⽂件吗?我们访问 当前⼯作⽬录不也是只知道当前⼯作⽬录的⽂件名吗?要访问它,不也得知道当前⼯作⽬录的inode 吗?
- 答案1:所以也要打开:当前⼯作⽬录的上级⽬录,额…,上级⽬录不也是⽬录吗??不还是上⾯的问 题吗?
- 答案2:所以类似"递归",需要把路径中所有的⽬录全部解析,出⼝是"/"根⽬录。
- 最终答案3:⽽实际上,任何⽂件,都有路径,访问⽬标⽂件,⽐如:
/home/whb/code/test/test/test.c都要从根⽬录开始,依次打开每⼀个⽬录,根据⽬录名,依次访问每个⽬录下指定的⽬录,直到访问 到test.c。这个过程叫做Linux路径解析。
注意:
- 所以,我们知道了:访问⽂件必须要有⽬录+⽂件名=路径的原因
- 根⽬录固定⽂件名,inode号,⽆需查找,系统开机之后就必须知道
可是路径谁提供?
- 你访问⽂件,都是指令/⼯具访问,本质是进程访问,进程有CWD!进程提供路径。
- 你open⽂件,提供了路径
既然如此,最开始的路径从哪⾥来?
- 所以Linux为什么要有根⽬录,根⽬录下为什么要有那么多缺省⽬录?
- 你为什么要有家⽬录,你⾃⼰可以新建⽬录?
- 上⾯所有⾏为:本质就是在磁盘⽂件系统中,新建⽬录⽂件。⽽你新建的任何⽂件,都在你或者系 统指定的⽬录下新建,这不就是天然就有路径了嘛!
- 系统+⽤⼾共同构建Linux路径结构.
3-7路径缓存
问题1:Linux磁盘中,存在真正的⽬录吗?
答案:不存在,只有⽂件。只保存⽂件属性+⽂件内容
问题2:访问任何⽂件,都要从/⽬录开始进⾏路径解析?
答案:原则上是,但是这样太慢,所以Linux会缓存历史路径结构
问题3:Linux⽬录的概念,怎么产⽣的?
答案:打开的⽂件是⽬录的话,由OS⾃⼰在内存中进⾏路径维护
Linux中,在内核中维护树状路径结构的内核结构体叫做:struct dentry
struct dentry { atomic_t d_count; // 引用计数:多少地方正在使用这个书签。计数为0才可以回收销毁 unsigned int d_flags; // 状态标记:比如是否是负目录项、是否缓存有效等 spinlock_t d_lock; // 自旋锁,保护当前这张书签并发访问安全 struct inode *d_inode; // 【最重要】书签指向真正的文件本体inode // 如果是NULL → 负dentry:代表这个名字不存在,缓存“文件找不到”结果 // 下面三个字段放在一起,为了CPU缓存优化,查找文件名时高频访问 struct hlist_node d_hash;// 挂入哈希链表,内核查找文件名时靠哈希快速找到dentry struct dentry *d_parent; // 父书签:这个文件所在目录的dentry(上级目录) struct qstr d_name; // 保存当前文件名(字符串) struct list_head d_lru; // LRU链表。长时间不用的书签,内核会回收释放内存 union { struct list_head d_child; // 挂载到父目录的子文件链表 struct rcu_head d_rcu; // RCU机制,安全并发删除dentry } d_u; struct list_head d_subdirs; // 如果当前是目录,保存它下面所有子文件/子目录书签 struct list_head d_alias; // 别名链表:所有指向同一个inode的dentry(硬链接集合) unsigned long d_time; // 时效时间,用来判断缓存是否过期(网络文件系统常用) struct dentry_operations *d_op; // dentry专属操作函数集,不同文件系统自定义行为 struct super_block *d_sb; // 指向所属文件系统超级块:知道这个文件属于哪个磁盘分区 void *d_fsdata; // 各个文件系统自己私有的扩展数据 #ifdef CONFIG_PROFILING struct dcookie_struct *d_cookie; // 性能调试用cookie,普通内核不用管 #endif int d_mounted; // 标记:这个目录是否是挂载点(mount挂载的入口) unsigned char d_iname[DNAME_INLINE_LEN_MIN]; // 短小文件名直接存在结构体内部,不用额外分配内存,提升速度 };通俗讲解:Linuxstruct dentry(目录项结构体)
先一句话总结:dentry = 文件 / 目录的「名字路由卡片」,内核用来把【文件名】快速找到对应的 inode(文件本体)。
1. 先理清 Linux 文件系统两个核心概念
- inode:文件真正本体,保存文件大小、权限、磁盘块位置、修改时间,但是 inode 没有文件名!
- dentry(目录项):就是一张「名字标签」,记录:
文件名 → 指向哪个inode。
举个生活化例子: inode = 一本书(真正内容) dentry = 图书馆的书签,书签上写书名,书签指向这本书。 同一个书 (inode) 可以有多张书签 (dentry),也就是硬链接。
2.整体工作流程举例
执行命令cat /home/test.txt
- 内核拆分路径
/→home→test.txt - 依次在内存里查找对应的dentry(书签)
- 通过 dentry 里的
d_inode找到真正文件 inode - 通过 inode 读取磁盘上文件内容
3. 几个关键特性通俗说
- 负 dentry
d_inode = NULL,代表曾经查过这个名字,确认不存在。内核缓存下来,下次不用再去磁盘查询,直接返回 “文件不存在”,提升性能。 - 硬链接原理多个不同 dentry(不同文件名),指向同一个 inode;这些 dentry 通过
d_alias链表串在一起。 - dentry 缓存(dcache) 内核把大量用过的 dentry 缓存在内存,就是目录项缓存。不用每次查文件名都访问慢速磁盘。
d_lru就是用来清理长期闲置的书签,防止内存占满。 d_mounted比如/mnt/cdrom如果挂载光盘,这个 dentry 的 d_mounted 标记置 1,内核识别这是挂载点。
4. 总结
struct dentry就是 Linux 内核文件名缓存载体。 负责维护「文件名、父目录、对应 inode」的映射,构建内存中的目录树,让 open/read 等系统调用查找文件极快;
件真正本体,保存文件大小、权限、磁盘块位置、修改时间,但是 inode 没有文件名!
- dentry(目录项):就是一张「名字标签」,记录:
文件名 → 指向哪个inode。
举个生活化例子: inode = 一本书(真正内容) dentry = 图书馆的书签,书签上写书名,书签指向这本书。 同一个书 (inode) 可以有多张书签 (dentry),也就是硬链接。