深度拆解 Linux Ext 系列文件系统:从硬件底层到软硬链接全流程

张开发
2026/4/6 3:50:49 15 分钟阅读

分享文章

深度拆解 Linux Ext 系列文件系统:从硬件底层到软硬链接全流程
目录一、理解硬件1.1磁盘、服务器、机柜、机房1.2磁盘的结构1.3磁盘的逻辑结构1.3.1理解过程1.3.2真实过程1.4CHS LBA地址2、引入文件系统2.1引入“块”概念2.2引入“分区”概念2.3引入“inode”概念三、ext2文件系统3.1宏观认识3.2Block Group3.3块内部构成3.3.1超级块Super Block3.3.2GDTGroup Descriptor Table3.3.3 块位图Block Bitmap3.3.4inode位图Inode Bitmap3.3.5 i节点表(Inode Table)3.3.6Data Block3.4inode和datablock映射3.5目录与文件名3.6路径解析3.7路径缓存3.8挂载分区3.9总结四、软硬链接4.1硬链接4.2软链接我们今天所讨论的就是没有被打开的文件一、理解硬件1.1磁盘、服务器、机柜、机房机械磁盘是计算机中唯⼀的⼀个机械设备磁盘——外设慢容量大价格便宜1.2磁盘的结构扇区是从磁盘读出和写入信息的最小单位通常大小为512 字节。这意味着如果系统需要修改1比特位的数据都需要把那一个扇区的数据全部读取到内存里。磁头每个盘片⼀般有上下两面分别对应1个磁头共2个磁头。传动臂上的磁头是共进退的。磁道磁道是从盘片外圈往内圈编号0磁道1磁道...靠近主轴的同心圆用于停靠磁头不存储数据。柱面磁道构成柱面数量上等同于磁道个数。所以磁盘写入时是向柱面进行批量写入。圆盘就是盘片的数量。如何定位一个扇区先定位磁头header在确定磁头访问哪⼀个柱面(磁道)cylinder最后定位⼀个扇区(sector)。这就叫做CHS地址定位。CHS寻址对早期的磁盘非常有效知道用哪个磁头读取哪个柱面上的第几扇区就可以读到数据了。 但是CHS模式支持的硬盘容量有限因为系统用8bit来存储磁头地址用10bit来存储柱面地 址用6bit来存储扇区地址而⼀个扇区共有512Byte这样使用CHS寻址⼀块硬盘最大容量为256*1024*63*512B8064MB(1MB1048576B)若按1MB1000000B来算就是 8.4GB磁盘容量 磁头数×磁道(柱面)数×每道扇区数×每扇区字节数1.3磁盘的逻辑结构1.3.1理解过程磁带上面可以存储数据我们可以把磁带“拉直”形成线性结构那么磁盘本质上虽然是硬质的但是逻辑上我们可以把磁盘想象成为卷在⼀起的磁带那么磁盘的逻辑存储结构我们也可以类似于这样每⼀个扇区就有了⼀个线性地址(其实就是数组下标)这种地址叫做LBA1.3.2真实过程柱面是⼀个逻辑上的概念其实就是每一面上相同半径的磁道逻辑上构成柱面。 所以磁盘物理上分了很多面但是在我们看来逻辑上磁盘整体是由“柱面”卷起来的。像一个山楂卷一样。整个磁盘不就是多张⼆维的扇区数组表(三维数组) 所以寻址⼀个扇区先找到哪⼀个柱面(Cylinder),在确定柱面内哪⼀个磁道(其实就是磁头位置 Head)在确定扇区Sector所以就有了 CHS 。我们之前学过C/C的数组在我们看来其实全部都是⼀维数组1.4CHS LBA地址所以每⼀个扇区都有⼀个下标我们叫做 LBA(Logical Block Address) 地址,其实就是线性地址。所以怎么计算得到这个LBA地址呢OS只需要使用LBA就可以了LBA地址转成CHS地址CHS如何转换成为LBA地址。谁做啊磁盘自己来做固件(硬件电路伺服系统)CHS转成LBA磁头数*每磁道扇区数 单个柱面的扇区总数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 的时候看到的除了看到文件名还能看到文件元 数据属性。文件信息和文件内容是分开存储的。文件数据都储存在”块”中那么很显然我们还必须找到⼀个地方储存文件的元信息属性信息比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做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又是如何放置的呢让我们来详细了解一下文件系统⽂件系统就是为了组织管理这些的三、ext2文件系统3.1宏观认识所有的准备工作都已经做完是时候认识下文件系统了。我们想要在硬盘上储文件必须先把硬盘格式化为某种格式的文件系统才能存储文件。文件系统的目的就是组织和管理硬盘中的文件。在Linux系统中最常见的是ext2系列的文件系统。其早期版本为ext2后来又发展出ext3和ext4。 ext3和ext4虽然对ext2进行了增强但是其核心设计并没有发⽣变化我们仍是以较老的ext2作为 演示对象。 ext2文件系统将整个分区划分成若干个同样大小的块组(Block Group)如下图所示。只要能管理⼀个分区就能管理所有分区也就能管理所有磁盘文件。上图中启动块Boot Block/Sector的大小是确定的为1KB由PC标准规定用来存储磁盘分区信息和启动信息任何文件系统都不能修改启动块。启动块之后才是ext2文件系统的开始。3.2Block Groupext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组 成。3.3块内部构成3.3.1超级块Super Block存放文件系统本身的结构信息描述整个分区的文件系统信息。记录的信息主要有bolck和inode的总量未使用的block和inode的数量⼀个block和inode的大小最近⼀次挂载的时间最近⼀次写入数据的时间最近⼀次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏可以说整个文件系统结构就被破坏了。特别注意超级块在每个块组的开头都有⼀份拷贝第⼀个块组必须有后⾯的块组可以没有。为了保证文件系统在磁盘部分扇区出现物理问题的情况下还能正常工作就必须保证文件系统的super block信息在这种情况下也能正常访问。所以⼀个文件系统的super block会在多个block group中进行备份 这些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 doesnt * know about, it should refuse to mount the filesystem. * * e2fscks requirements are more strict; if it doesnt know * about a feature in either the compatible or incompatible * feature set, it must abort and not try to meddle with * things it doesnt 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.2GDTGroup Descriptor Table块组描述符表描述块组属性信息整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储⼀个块组的描述信息如在这个块组中从哪里开始是inode Table从那里开始是Data Blocks空闲的inode和数据块还有多少个等等。块组描述符在每个块组的开头都有⼀份拷贝。// 磁盘级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 BitmapBlock Bitmap中记录着Data Block中哪个数据块已经被占用哪个数据块没有被占用3.3.4inode位图Inode Bitmap每个bit表示⼀个inode是否空闲可用。3.3.5 i节点表(Inode Table)存放文件属性如文件大小所有者最近修改时间等当前分组所有Inode属性的集合inode编号以分区为单位整体划分不可跨分区3.3.6Data Block数据区存放文件内容也就是⼀个⼀个的Block。根据不同的文件类型有以下几种情况对于普通文件文件的数据存储在数据块中对于目录该目录下的所有文件名和目录名存储在所在目录的数据块中除了文件名外ls-l命令看到的其它信息保存在该文件的inode中Block号按照分区划分不可跨分区3.4inode和datablock映射inode内部存在,就是用来来进行inode和block映射的前12个直接指向后面是间接__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */ EXT2_N_BLOCKS 153.5目录与文件名我们在上面了解到文件名属性并未纳入到inode数据结构内部还有所有文件名和目录名存储在所在目录的数据块中而且我们访问文件都是用的文件名没用过inode号啊目录是文件吗如何理解目录是文件但是磁盘上没有目录的概念只有文件属性文件内容的概念。目录的属性不用多说内容保存的是文件名和Inode号的映射关系。所以访问文件必须打开当前目录必须能打开当前⼯作目录文件查看目录文件的内容根据文件名获得对应的inode号然后进行文件访问3.6路径解析打开当前工作目录文件查看当前工作目录文件的内容?当前工作目录不也是文件吗我们访问当前工作目录不也是只知道当前工作目录的文件名吗要访问它不也得知道当前工作目录的inode 吗所以也要打开当前工作目录的上级目录但上级目录不也是目录吗不还是上面的问题吗所以类似递归需要把路径中所有的目录全部解析出口是/根目录。而实际上任何文件都有路径访问目标文件比如/home/whb/code/test/test/test.c都要从根目录开始依次打开每⼀个目录根据目录名依次访问每个目录下指定的目录直到访问到test.c。这个过程叫做Linux路径解析。根目录固定文件名inode号无需查找系统开机之后就必须知道。路径谁提供用户访问文件都是指令/工具访问本质是进程访问进程有CWD进程提供路径。3.7路径缓存Linux磁盘中存在真正的目录吗不存在只有文件。只保存文件属性文件内容。我们访问任何文件都要在根目录下进行路径解析吗这就是一直在做磁盘IO效率十分低下所以OS在进行解析的时候会把我们的历史访问的所有目录路径形成一颗多叉树进行保存。这就是Linux系统树状目录结构。Linux目录的概念怎么产生的打开的文件是目录的话由OS自己在内存中进行路径维护。Linux中在内核中维护树状路径结构的内核结构体叫做struct dentrystruct 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结构包括普通文件。这样所有被打开的文件就可以在内存中形成整个树形结构整个树形节点也同时会隶属于LRU(Least Recently Used最近最少使用)结构中进行节点淘汰整个树形节点也同时会隶属于Hash方便快速查找更重要的是这个树形结构整体构成了Linux的路径缓存结构打开访问任何文件都在先在这 棵树下根据路径进行查找找到就返回属性inode和内容没找到就从磁盘加载路径添加dentry 结构缓存新路径3.8挂载分区我们已经能够根据inode号在指定分区找文件了也已经能根据目录文件内容找指定的inode了在指定的分区内我们可以为所欲为了。可是 问题inode不是不能跨分区吗Linux不是可以有多个分区吗我怎么知道我在哪⼀个分区分区写入文件系统无法直接使用需要和指定的目录关联进行挂载才能使用。所以可以根据访问目标文件的路径前缀挂载在哪个目录准确判断我在哪⼀个分区。$ dd if/dev/zero of./disk.img bs1M count5 #制作⼀个⼤的磁盘块就当做⼀个分区 $ mkfs.ext4 disk.img # 格式化写⼊⽂件系统 $ mkdir /mnt/mydisk # 建⽴空⽬录 $ df -h # 查看可以使⽤的分区Filesystem Size Used Avail Use% Mounted on udev 956M 0 956M 0% /dev tmpfs 198M 724K 197M 1% /run /dev/vda1 50G 20G 28G 42% / tmpfs 986M 0 986M 0% /dev/shm tmpfs 5.0M 0 5.0M 0% /run/lock tmpfs 986M 0 986M 0% /sys/fs/cgroup tmpfs 198M 0 198M 0% /run/user/0 tmpfs 198M 0 198M 0% /run/user/1002$ sudo mount -t ext4 ./disk.img /mnt/mydisk/ # 将分区挂载到指定的⽬录 $ df -hFilesystem Size Used Avail Use% Mounted on udev 956M 0 956M 0% /dev tmpfs 198M 724K 197M 1% /run /dev/vda1 50G 20G 28G 42% / tmpfs 986M 0 986M 0% /dev/shm tmpfs 5.0M 0 5.0M 0% /run/lock tmpfs 986M 0 986M 0% /sys/fs/cgroup tmpfs 198M 0 198M 0% /run/user/0 tmpfs 198M 0 198M 0% /run/user/1002 /dev/loop0 4.9M 24K 4.5M 1% /mnt/mydisk$ sudo umount /mnt/mydisk # 卸载分区 whbbite:/mnt$ df -hFilesystem Size Used Avail Use% Mounted on udev 956M 0 956M 0% /dev tmpfs 198M 724K 197M 1% /run /dev/vda1 50G 20G 28G 42% / tmpfs 986M 0 986M 0% /dev/shm tmpfs 5.0M 0 5.0M 0% /run/lock tmpfs 986M 0 986M 0% /sys/fs/cgroup tmpfs 198M 0 198M 0% /run/user/0 tmpfs 198M 0 198M 0% /run/user/1002/dev/loop0 在Linux系统中代表第⼀个循环设备loop device。循环设备也被称为回环设备或者loopback设备是⼀种伪设备pseudo-device它允许将文件作为块设备 block device来使用。这种机制使得可以将文件比如SO镜像文件挂载mount为 文件系统就像它们是物理硬盘分区或者外部存储设备⼀样。3.9总结四、软硬链接4.1硬链接我们看到真正找到磁盘上文件的并不是文件名而是inode。其实在linux中可以让多个文件名对应于同⼀个inode。硬链接只能给普通文件建立Linux系统不支持给目录建立硬链接但.和..本质就是对目录的硬链接因为如果允许用户自己给目录建立硬链接就容易造成下图这种路径环的问题.和..名字特殊做特殊处理即可4.2软链接硬链接是通过inode引用另外⼀个文件软链接是通过名字引用另外⼀个文件但实际上新的文件和被引用的文件的inode不同应⽤常见上可以想象成⼀个快捷方式。在shell中的做法

更多文章