☰
Linux之文件系统
2026/10/20 13:47:00 网站建设 项目流程

一、文件系统的引入

1.引入块的概念

其实硬盘是典型的“块”设备,操作系统读取硬盘数据的时候,其实不会一个个扇区地读取,这样效率太低,而是一次性读取连续多个扇区,即一次性读取一个“块”(BLock)

硬盘的每个分区是被划分成一个个“块”。一个“块”的大小是由格式化的时候确定的,并且不可以更改,最常见是4Kb,即连续8个扇区组成一个“块”。“块”是文件存取的最小单位。

注意:

  • 磁盘就是一个三维数组,我们把它看待为一个“一维数组”,数组下标就是LBA,每个元素都是扇区,每个扇区都有LBA,那么8个扇区一个块,每个块的地址我们也能算出来。
  • 知道LBA:块号=LBA/8
  • 知道块号:LBA=块号*8+n(n为第几个扇区)

2.“分区”

其实磁盘可以变成多个分区(partition)的,以Windows来看,一块磁盘,被分为c盘,d盘,这个c,d就是分区。分区实质是对硬盘的一种格式化。但是Linux的设备都是以文件形式存在,那是怎么区分呢?

柱面是分区的最小单位,我们可以利用参考柱面号码的方式进行分区,其本质就是设置每个区的起始柱面和结束柱面号码。此时我们可以将硬盘上的柱面(分区)进程平铺,想象成大的平面,如图:

3.引入“inode”(index node)概念

之前我们说文件=属性+内容,我们使用ls -l 的时候看到的除了文件名,还能看到文件元数据(属性)

每行包含 模式 硬链接数 文件所有者 所属组 大小 最后修改时间 文件名

ls -l读取存储在磁盘上的文件信息,然后显示出来。

其实这个信息除了通过这种方式来读取,还有一个stat命令能够看到更多信息

[xhx_home@iZ2zeiwy4tf4rghoyh5d1yZ file_blog]$ stat log.txt File: log.txt Size: 5 Blocks: 8 IO Block: 4096 regular file Device: fd01h/64769d Inode: 868065 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 1001/xhx_home) Gid: ( 1001/xhx_home) Access: 2026-04-23 21:12:19.586261635 +0800 Modify: 2026-04-23 21:12:16.924181829 +0800 Change: 2026-04-23 21:12:16.924181829 +0800 Birth: -

到这我们要思考一个问题,文件数据都存储在“块”中,那么我们很显然,我们还必须找到一个地方存储文件的源信息(属性信息),比如文件的创建者,文件的创建日期,文件的大小等等。这种储存文件元信息的区域叫inode,中文译名“索引结点”。

每一个文件都有对应的inode,里面包含一些与该文件有关的一些信息。下面我们深入了解一下文件系统。

注意:

  • Linux下文件的存储是属性和内容分离存储的。
  • Linux下,保存文件属性的集合叫inode,一个文件一个inode,inode内有一个唯一的标识符,叫做inode号

所以一个文件的属性inode长什么样子呢?

// 简易版 ext2 inode 结构体 struct inode { // 文件元数据 uid_t i_uid; // 所属用户 gid_t i_gid; // 所属组 mode_t i_mode; // 权限+文件类型 off_t i_size; // 文件大小 time_t i_atime; // 访问时间 time_t i_mtime; // 修改时间 time_t i_ctime; // 元数据修改时间 // 硬链接计数 nlink_t i_nlink; // 核心:数据块指针(重点!) // 共15个块指针,存【全局逻辑块号】 uint32_t i_block[15]; /* * i_block[0] ~ i_block[11] :12个直接块 * i_block[12] :一级间接块 * i_block[13] :二级间接块 * i_block[14] :三级间接块 */ // 磁盘占用块数量 uint32_t i_blocks; };

注意!

文件名属性并未纳入到inode数据结构内部

任何文件的内容大小可以不同,但属性大小一定是相同的。

二、ext2文件系统

1.宏观认识

所有的准备工作都已经做完,是时候认识下文件系统了。我们想要在硬盘上存储文件,必须先把硬盘格式化为某种格式的文件系统,才能存储文件。文件系统的目的就是组织和管理硬盘中的文件。ext2文件系统是ext3 ext4等的基础,设计核心保持一致,我们以ext2来演示

ext2文件系统将整个分区划分成若干个同样大小的块组(BLock Group),如下图所示,只要能管理一个分区,就可以管理所有分区,也就能管理所有的磁盘文件。

2.Disk

(1)MBR(Master Boot Record 主引导记录)

位于磁盘最开头的512字节扇区,不属于任何分区,包含分区表和引导代码,是BIOS(basic input output system)启动时最先读取的位置,负责找到并加载活动分区的引导扇区。

(2)Partition (分区)

将磁盘划分为多个独立的逻辑区域,每个分区可以独立格式化,安装文件系统,互不干扰。

3.Partition

(1)Boot Sector(引导扇区)

每个分区最开头的扇区,存放该分区的引导代码,负责加载分区上的操作系统内核,是文件系统之前的启动块。

(2)EXT2 File System

文件系统的具体实现

4.File System

(1)BLock Group (块组)

文件系统为了提升性能,将整个分区再划分为多个大小相同的块组,每个块组都有独立的管理结构,减少碎片,提升局部性。

5.BLock Group

(1)Super BLock(超级块)

存放文件系统本身的结构信息,描述整个分区系统信息。记录的信息主要有:BLock和inode总量,未使用的block和inode,一个block和inode大小,最近一次挂载时间,最近一次写入数据时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。

超级块在每个块组的开头都有一份拷贝(第一个块组必须有,其他可以没有)。是为了保证文件系统在磁盘部分扇区出现物理问题还能正常工作,就必须保证文件系统的Super Block信息在这种情况下也能访问,所以一个文件系统的Super Block会在多个块组中去备份,这些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 doesn't * know about, it should refuse to mount the filesystem. * * e2fsck's requirements are more strict; if it doesn't know * about a feature in either the compatible or incompatible * feature set, it must abort and not try to meddle with * things it doesn't 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 */ };

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

块组描述符表,描述块组属性信息,整个分区分成多少个块组,就对应有多少个描述符。每个块组描述符存储一个块组的描述信息,如在这个块组中从哪里开始是inode Table,哪里开始是Data block,空闲的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)Block Bitmap(块位图)

Block Bitmap中记录着Data Block 中哪个数据块已经被占用,哪个数据块没有被占用。

(4)inode Bitmap(inode位图)

每个bit位表示一个inode是否空闲可用

(5)inode Table(i结点表)

存放文件属性 如 文件大小 所有者 最近修改时间等等

当前分组所有inode属性的集合

一个inode管理一个文件

inode编号以分区为单位,整体划分,不可垮分区,所以一个文件不可以垮分区存储。

(6)Data Block

存放文件内容,也就是一个一个的Block。是Block Group里最主要部分

对与普通文件,文件的数据存储在数据块中。

对与目录,该目录下的所有文件名和目录名存储在所在目录的数据块中,除了文件名外,ls -l命令可以看到其他信息保存在该文件的inode中。

Block 号按分区划分,不可夸分区。

6.inode和Data Block 映射

inode内部存在__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */ , EXT2_N_BLOCKS =15,就是用来进行inode和Block映射的。

这样文件=内容+属性,就都能找到了。

// 简化:EXT2 迷你 inode 结构体 #define BLOCK_SIZE 4096 #define DIRECT_PTRS 12 // 12个直接块 #define INDIRECT1 12 // 一级间接 #define INDIRECT2 13 // 二级间接 #define INDIRECT3 14 // 三级间接 // 全局块号:inode 里存的都是【分区全局唯一Block号】 typedef unsigned int block_t; typedef unsigned int inode_no_t; struct inode { // 1. 文件元数据 unsigned int i_mode; // 权限+文件类型 unsigned int i_size; // 文件大小 unsigned int i_nlink; // 硬链接计数 unsigned int i_uid; unsigned int i_gid; // 2. 核心:数据块指针数组 // 全部存:【全局逻辑块号】(跨块组依靠这个) block_t i_block[15]; // 3. 磁盘占用块数量 unsigned int i_blocks; // 时间域省略... };

7.格式化

格式化 = 清空分区 + 给分区装修成标准文件系统结构

裸分区只是一块空白磁盘空间,操作系统没法存文件;

格式化就是:在分区里批量建好 块组、超级块、位图、inode 表、数据块。

分区之后的格式化操作,就是对分区进行分组,在每个分组中写入Super Block,GDT,Block Bitmap等等的管理信息,这些管理信息统称为文件系统。

只有知道文件的inode号,就可以在指定分区中确定是哪一个分组,进而在分组内找见是哪一个inode,拿到inode文件属性和内容就都有了

8.样例

下面,我们touch一个新文件来解释一下是如何工作

[xhx_home@iZ2zeiwy4tf4rghoyh5d1yZ file_blog]$ touch abc [xhx_home@iZ2zeiwy4tf4rghoyh5d1yZ file_blog]$ ls -i abc 868055 abc

创建一个新文件主要有这些操作:

存储属性:

内核先找到一个空闲的inode节点(内核把文件信息记录其中)。

存储数据:

该文件需要存储在三个磁盘块,内核找到了三个空闲块:300 500 800.将内核缓冲区的第一块数据复制到300,第二块复制到500,以此类推,对应块的Block Bitmap值由0变1

记录分配情况:

文件内容按300,500,800存放,内核在inode上的磁盘分布区(inode Table)记录了上述块列表。

添加文件名到目录:

内核将入口(868055 abc)添加到其目录文件。文件名和inode之间的对应关系将文件名和文件内容及属性连接起来。

文件名和inode的映射:

在我们访问文件的时候,使用文件名,实际操作系统将文件名和inode号进行映射,所以我们使用文件名访问文件,其实就是使用inode号来访问。

DIR* dir= opendir(argv[1]); struct dirent* entry; while((entry=readdir(dir))!=NULL){ printf("filename:%s inode:%lu\n",entry->d_name,(unsigned long)entry->d_ino); } closedir(dir); return 0; //dirent 记录磁盘上目录文件的文件名+inode号 //DIR 遍历目录 和FILE类似

所以,访问文件,必须打开当前目录,根据文件名获得对应inode号,然后进行文件访问

所以,访问文件必须要知道当前工作目录,本质是必须能打开当前工作目录文件,查看目录文件的内容!

9.路径解析

当前工作目录的文件打开也需要上一级目录inode,所以我们要把路径的所有目录全部解析出来,出口是‘/’根目录。

任何文件都有路径,访问目标文件,比如/home/a/b/c都要从根目录开始,依次打开每一个目录,根据目录名,依次访问每个目录下指定的目录,直到访问到c,这个过程就叫做Linux的路径解析。

跟目录的inode无须查找,开机之后就必须知道

路径:

访问文件,都是指令/工具进行访问,本质是进程访问,进程有CWD!进程提供路径。

open文件,提供了路径

磁盘文件系统中,新建目录文件,都在系统指定的目录新建,这不就有了天然路径了吗

系统+用户共同构造Linux路径结构

10.路径缓存

问题1:Linux磁盘中,存在真正的⽬录吗?

答案:不存在,只有⽂件。只保存⽂件属性+⽂件内容

问题2:访问任何⽂件,都要从/⽬录开始进⾏路径解析?

答案:原则上是,但是这样太慢,所以Linux会缓存历史路径结构

问题2:Linux⽬录的概念,怎么产⽣的?

答案:打开的⽂件是⽬录的话,由OS⾃⼰在内存中进⾏路径维护

Linux中,在内核中维护树状路径结构的内核结构体叫做:struct 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结构,包括普通文件。这样所有被打开的文件,就可以在内存中形成整个树形结构。

内核读取目录后在内存创建dentry+内存inode

整个树形节点也同时会隶属于LRU(least recently used,最近最少使用)结构中,进行结点淘汰

整个树形结点也同时属于Hash,方便快速查找。

更重要的是,这个树型结构,整体构成了Linux的路径缓存结构,打开访问任何文件,都先在这棵树下根据路径查找,找到就返回inode属性和内容,没找到就从磁盘加载新路径,添加dentry结构,缓冲新路径。

11.挂载分区

我们已经能够根据inode号在指定分区找文件了,也已经能根据目录文件内容,找到指定的inode了,在我们指定的分区,我们可以为所欲为。可是:

inode不是不能夸分区吗?Linux不是可以有多个分区吗?我怎么知道我在哪一个分区??

分区写入文件系统,无法直接使用,需要和指定的目录关联,进行挂载才能使用

所以,可以根据访问目标文件的“路径前缀”准确判断我在哪一个分区。

三、文件系统总结

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询