6. Ext系列⽂件系统
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*512B8064MB(1MB1048576B)若按1MB1000000B来算就是 8.4GB1-4 磁盘的逻辑结构1-4-1 理解过程磁带上⾯可以存储数据我们可以把磁带“拉直”形成线性结构那么磁盘本质上虽然是硬质的但是逻辑上我们可以把磁盘想象成为卷在⼀起的磁带那么磁盘的逻 辑存储结构我们也可以类似于这样每⼀个扇区就有了⼀个线性地址(其实就是数组下标)这种地址叫做LBA1-4-2 真实过程⼀个细节传动臂上的磁头是共进退的柱⾯是⼀个逻辑上的概念其实就是每⼀⾯上相同半径的磁道逻辑上构成柱⾯。所以磁盘物理上分了很多⾯但是在我们看来逻辑上磁盘整体是由“柱⾯”卷起来的所以磁盘的真实情况是磁道某⼀盘⾯的某⼀个磁道展开即⼀维数组柱⾯整个磁盘所有盘⾯的同⼀个磁道即柱⾯展开柱⾯上的每个磁道扇区个数是⼀样的这不就是⼆维数组吗整盘整个磁盘不就是多张⼆维的扇区数组表(三维数组)所以寻址⼀个扇区先找到哪⼀个柱⾯(Cylinder),在确定柱⾯内哪⼀个磁道(其实就是磁头位置 Head)在确定扇区Sector所以就有了CHS。我们之前学过C/C的数组在我们看来其实全部都是⼀维数组所以每⼀个扇区都有⼀个下标我们叫做LBA(Logical Block Address)地址,其实就是线性 地址。所以怎么计算得到这个LBA地址呢LBACHS必须要 LBA地址转成CHS地址CHS如何转换成为LBA地址。OS只需要使⽤LBA就可以了 LBA地址转成CHS地址CHS如何转换成为LBA地址。谁做啊磁盘 ⾃⼰来做固件(硬件电路伺候系统)1-5 CHSLBA地址CHS转成LBA磁头数*每磁道扇区数单个柱⾯的扇区总数LBA柱⾯号C单个柱⾯的扇区总数磁头号H每磁道扇区数扇区号S-1即LBA柱⾯号C*(磁头数*每磁道扇区数) 磁头号H * 每磁道扇区数 扇区号S - 1扇区号通常是从1开始的⽽在LBA中地址是从0开始的 • 柱⾯和磁道都是从0开始编号的总柱⾯磁道个数扇区总数等信息在磁盘内部会⾃动维护上层开机的时候会获取到这些参 数。LBA转成CHS柱⾯号CLBA//(磁头数*每磁道扇区数)【就是单个柱⾯的扇区总数】磁头号H(LBA%(磁头数*每磁道扇区数))//每磁道扇区数扇区号S(LBA%每磁道扇区数)1“//”: 表⽰除取整所以从此往后在磁盘使⽤者看来根本就不关⼼CHS地址⽽是直接使⽤LBA地址磁盘内部⾃⼰ 转换。因此从现在开始磁盘就是⼀个元素为扇区的⼀维数组数组的下标就是每⼀个扇区的LBA地址。OS使⽤ 磁盘就可以⽤⼀个数字访问磁盘扇区了。2. 引⼊⽂件系统2-1 引⼊块概念其实硬盘是典型的“块”设备操作系统读取硬盘数据的时候其实是不会⼀个个扇区地读取这样 效率太低⽽是⼀次性连续读取多个扇区即⼀次性读取⼀个”块”block。硬盘的每个分区是被划分为⼀个个的”块”。⼀个”块”的⼤⼩是由格式化的时候确定的并且不可 以更改最常⻅的是4KB即连续⼋个扇区组成⼀个”块”。”块”是⽂件存取的最⼩单位。注意磁盘就是⼀个三维数组我们把它看待成为⼀个⼀维数组数组下标就是LBA每个元素都是扇 区每个扇区都有LBA那么8个扇区⼀个块每⼀个块的地址我们也能算出来。知道LBA块号LBA/8知道块号LAB块号*8n.(n是块内第⼏个扇区)2-2 引⼊分区概念其实磁盘是可以被分成多个分区partition的以Windows观点来看你可能会有⼀块磁盘并且将 它分区成C,D,E盘。那个C,D,E就是分区。分区从实质上说就是对硬盘的⼀种格式化。但是Linux的设备 都是以⽂件形式存在那是怎么分区的呢柱⾯是分区的最⼩单位我们可以利⽤参考柱⾯号码的⽅式来进⾏分区其本质就是设置每个区的起 始柱⾯和结束柱⾯号码。此时我们可以将硬盘上的柱⾯分区进⾏平铺将其想象成⼀个⼤的平 ⾯如下图所⽰注意柱⾯⼤⼩⼀致扇区个位⼀致那么其实只要知道每个分区的起始和结束柱⾯号知道每 ⼀个柱⾯多少个扇区那么该分区多⼤其实和解释LBA是多少也就清楚了.2-3 引⼊inode概念之前我们说过⽂件 数据 属性我们使⽤ls -l的时候看到的除了看到⽂件名还能看到⽂件元数据属性。[rootlocalhost 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命令能够看到更多信息[rootlocalhost 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⼀个⽂件⼀个inodeinode内有⼀个唯⼀ 的标识符叫做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 Groupext2⽂件系统会根据分区的⼤⼩划分为数个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 GDTGroupDescriptorTable块组描述符表描述块组属性信息整个分区分成多个块组就对应有多少个块组描述符。每个块组描 述符存储⼀个块组的描述信息如在这个块组中从哪⾥开始是inodeTable从哪⾥开始是Data Blocks空闲的inode和数据块还有多少个等等。块组描述符在每个块组的开头都有⼀份拷⻉。3-3-3 块位图BlockBitmapBlockBitmap中记录着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_BLOCKS15,就是⽤来进⾏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; /* HurdUID高16位 */ __le16 h_i_gid_high; /* HurdGID高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_count1–_block 数组全部置 0空文件无数据块-i_size0修改块组 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_gidtouch更新 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⼀个新⽂件来看看如何⼯作。[rootlocalhost linux]# touch abc [rootlocalhost linux]# ls -i abc 263466 abc为了说明问题我们将上图简化创建⼀个新⽂件主要有以下4个操作存储属性 内核先找到⼀个空闲的i节点这⾥是263466。内核把⽂件信息记录到其中。存储数据 该⽂件需要存储在三个磁盘块内核找到了三个空闲块300,500800。将内核缓冲区的第⼀块 数据复制到300下⼀块复制到500以此类推。记录分配情况 ⽂件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。添加⽂件名到⽬录 新的⽂件名abc。linux如何在当前的⽬录中记录这个⽂件内核将⼊⼝263466abc添加到 ⽬录⽂件。⽂件名和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路径缓存问题1Linux磁盘中存在真正的⽬录吗答案不存在只有⽂件。只保存⽂件属性⽂件内容问题2访问任何⽂件都要从/⽬录开始进⾏路径解析答案原则上是但是这样太慢所以Linux会缓存历史路径结构问题3Linux⽬录的概念怎么产⽣的答案打开的⽂件是⽬录的话由OS⾃⼰在内存中进⾏路径维护Linux中在内核中维护树状路径结构的内核结构体叫做struct dentrystruct 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. 几个关键特性通俗说负 dentryd_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)也就是硬链接。