引⼊⽂件系统
“块”概念引入
“块”是⽂件系统存取的最⼩单位
其实硬盘是典型的“块”设备,操作系统读取硬盘数据的时候,其实是不会⼀个个扇区地读取,这样效率太低,⽽是⼀次性连续读取多个扇区,即⼀次性读取⼀个块(block)。
硬盘的每个分区是被划分为⼀个个的”块”。⼀个”块”的⼤⼩是由格式化的时候确定的,可以在格式化之前进行调整设置,最常⻅的是4KB,即连续⼋个扇区组成⼀个 ”块”。
磁盘就是⼀个三维数组,我们把它看待成为⼀个"⼀维数组",数组下标就是LBA,每个元素都是扇区
每个扇区都有LBA,那么8个扇区⼀个块,每⼀个块的地址我们也能算出来。
- 知道LBA:块号 = LBA / 8
- 知道块号:LAB=块号 × 8 + n. (n是块内第⼏个扇区)
“分区”概念引入
磁盘可以被分成多个分区(partition),以Windows观点来看,你有⼀块磁盘并且将它分区成C,D,E盘。分盘的操作实际上就是分区。分区从实质上说就是对硬盘的⼀种格式化。但是Linux的设备都是以⽂件形式存在,如何“分区”呢?
柱⾯是分区的最⼩单位,我们可以利⽤参考柱⾯号码的⽅式来进⾏分区,其本质就是设置每个区的起始柱⾯和结束柱⾯号码。 此时我们可以将硬盘上的柱⾯(分区)进⾏平铺,将其想象成⼀个⼤的平⾯,如下图所示:
每个柱⾯的⼤⼩⼀致,扇区数量⼀致,那么其实只要知道每个分区的起始和结束柱⾯号,知道每⼀个柱⾯多少个扇区,那么该分区多⼤,LBA是多少也就清楚了。
“inode”概念引入
⽂件=数据+属性 ,使⽤<font style="color:rgb(31,35,41);">ls -l</font>
的时候看到的除了看到⽂件名,还看到⽂件元数据(属性)。
[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列:
- 模式
- 硬链接数
- ⽂件所有者
- 组
- ⼤⼩
- 最后修改时间
- ⽂件名
<font style="color:rgb(31,35,41);">ls -l</font>
读取存储在磁盘上的⽂件信息,然后显⽰出来
到这我们要思考⼀个问题,⽂件数据都储存在”块”中,那么很显然,我们还必须找到⼀个地⽅储存⽂件的元信息(属性信息),⽐如⽂件的创建者、⽂件的创建⽇期、⽂件的⼤⼩等等。这种储存⽂件元信息的区域就叫做inode,中⽂译名为”索引节点”。
每⼀个⽂件都有对应的inode,⾥⾯包含了与该⽂件有关的⼀些信息。下文我们深入了解文件系统时进行详细讲解。
- Linux下⽂件的存储是属性和内容分离存储的
- Linux下,保存⽂件属性的集合叫做inode,⼀个⽂件,⼀个inode,inode内有⼀个唯⼀的标识符,叫做inode号
/*
* 磁盘上 inode 的结构体
*/
struct ext2_inode {
__le16 i_mode; /* 文件模式 */
__le16 i_uid; /* 所有者UID的低16位 */
__le32 i_size; /* 文件大小(字节) */
__le32 i_atime; /* 访问时间 */
__le32 i_ctime; /* 创建时间 */
__le32 i_mtime; /* 修改时间 */
__le32 i_dtime; /* 删除时间 */
__le16 i_gid; /* 组ID的低16位 */
__le16 i_links_count; /* 硬链接数 */
__le32 i_blocks; /* 文件占用的块数 */
__le32 i_flags; /* 文件标志 */
union {
struct {
__le32 l_i_reserved1; /* Linux专用保留字段 */
} linux1;
struct {
__le32 h_i_translator; /* Hurd专用翻译器字段 */
} hurd1;
struct {
__le32 m_i_reserved1; /* Masix专用保留字段 */
} masix1;
} osd1; /* 操作系统依赖字段1 */
__le32 i_block[EXT2_N_BLOCKS]; /* 指向数据块的指针 */
__le32 i_generation; /* 文件版本号(用于NFS) */
__le32 i_file_acl; /* 文件访问控制列表(ACL) */
__le32 i_dir_acl; /* 目录访问控制列表(ACL) */
__le32 i_faddr; /* 片段地址 */
union {
struct {
__u8 l_i_frag; /* 片段编号 */
__u8 l_i_fsize; /* 片段大小 */
__u16 i_pad1; /* 填充字段 */
__le16 l_i_uid_high; /* 高16位UID(保留2[0]) */
__le16 l_i_gid_high; /* 高16位GID(保留2[0]) */
__u32 l_i_reserved2; /* 预留字段 */
} linux2;
struct {
__u8 h_i_frag; /* 片段编号 */
__u8 h_i_fsize; /* 片段大小 */
__le16 h_i_mode_high;/* 高16位文件模式 */
__le16 h_i_uid_high; /* 高16位UID */
__le16 h_i_gid_high; /* 高16位GID */
__le32 h_i_author; /* 作者信息 */
} hurd2;
struct {
__u8 m_i_frag; /* 片段编号 */
__u8 m_i_fsize; /* 片段大小 */
__u16 m_pad1; /* 填充字段 */
__u32 m_i_reserved2[2]; /* 预留字段 */
} masix2;
} osd2; /* 操作系统依赖字段2 */
} __attribute__((packed));
/*
* 数据块相关常量
*/
#define EXT2_NDIR_BLOCKS 12 /* 直接块指针数量 */
#define EXT2_IND_BLOCK 12 /* 一级间接块指针 */
#define EXT2_DIND_BLOCK 13 /* 二级间接块指针 */
#define EXT2_TIND_BLOCK 14 /* 三级间接块指针 */
#define EXT2_N_BLOCKS 15 /* 总块指针数量 */
可以得知:
- ⽂件名属性并未纳⼊到
<font style="color:rgb(31,35,41);">inode</font>
数据结构内部<font style="color:rgb(31,35,41);">inode</font>
的⼤⼩⼀般是128字节或者256,我们后⾯统⼀128字节- 任何⽂件的内容⼤⼩可以不同,但是属性⼤⼩⼀定是相同的,因为
<font style="color:rgb(31,35,41);">inode</font>
是结构体,结构体大小固定- 任何文件又有相同的属性类型,但是内容不一样
疑问
我们已经知道硬盘是典型的“块”设备,操作系统读取硬盘数据的时候,读取的基本单位是”块”。“块”⼜是硬盘的每个分区下的结构,那“块”是怎么在分区上进行管理的呢?怎么找到“块”呢?<font style="color:rgb(31,35,41);">inode</font>
,⼜是如何放置管理的呢?
⽂件系统进行组织管理!
ext2 ⽂件系统
宏观
我们想要在硬盘上储⽂件,必须先把硬盘格式化为某种格式的⽂件系统,才能存储⽂件。⽂件系统的⽬的就是组织和管理硬盘中的⽂件。在Linux 系统中,最常⻅的是 ext2 系列的⽂件系统。其早期版本为 ext2,后来⼜发展出 ext3 和 ext4。 ext3 和 ext4 虽然对 ext2 进⾏了增强,但是其核⼼设计并没有发⽣变化。
ext2⽂件系统将整个分区划分成若⼲个同样⼤⼩的块组 (Block Group),如下图所⽰。只要能管理⼀个分区就能管理所有分区,也就能管理所有磁盘⽂件。
比如磁盘有300GB,分区成若干个30GB的分区,后通过<font style="color:rgb(31,35,41);">Boot Sector</font>
启动分区,然后通过分组来管理整个分区。这样一直向下管理,通过分治的思想进行管理。
上图中启动块(Boot Block/Sector)的⼤⼩是确定的,为1KB,由PC标准规定,用来引导加载,任何⽂件系统都不能修改启动块。启动块之后才是ext2⽂件系统的开始。
文件系统的载体是分区,一个分区一个文件系统。
Block Group
ext2⽂件系统会根据分区的⼤⼩划分为数个Block Group。⽽每个Block Group都有着相同的结构组
成。块组的基本单位都是“块”,大小是4KB。
块组内部构成
Data Block
文件由内容和属性组成。
数据区:存放⽂件内容,也就是⼀个⼀个的Block。根据不同的⽂件类型有以下⼏种情况
- 对于普通⽂件,⽂件的数据存储在数据块中。
- 对于⽬录,该⽬录下的所有⽂件名和⽬录名存储在所在⽬录的数据块中,除了⽂件名外,ls -l命令
- 看到的其它信息保存在该⽂件的inode中。
- Block 号按照分区划分,不可跨分区。
- block可以跨组保存,编号是全局的,如果块组装不下block,会在其他块组存放,通过inode根据block的编号就可以找到存储块。
i 节点表(Inode Table)
- 存放⽂件属性 如 ⽂件⼤⼩,所有者,最近修改时间等
- 当前分组所有Inode属性的集合
- inode编号以分区为单位,整体划分,不可跨分区
单位仍然是数据块,inode大小固定,保存各种属性,并且文件名不在inode中保存,文件名字段可能会影响inode大小固定。当加载到内存的时候按照数据块为单位,一个inode为128字节,所以一个数据库可以存储32个inode,当加载其中一个会将整个数据块全部加载到内存。当打开其他文件的时候如果inode已经在之前加载的数据块中存在就会跳过加载i节点表这一步。
所以在同一分区内部,inode和块的编号都是唯一的。
超级块(Super Block)
存放⽂件系统本⾝的结构信息,描述整个分区的⽂件系统信息。记录的信息主要有:bolck 和 inode的总量,未使⽤的block和inode的数量,⼀个block和inode的⼤⼩,最近⼀次挂载的时间,最近⼀次写⼊数据的时间,最近⼀次检验磁盘的时间等其他⽂件系统的相关信息。Super Block的信息被破坏,可以说整个⽂件系统结构就被破坏了。
超级块在每个块组的开头都有⼀份拷⻉(第⼀个块组必须有,后⾯的块组可以没有)。为了保证⽂ 件系统在磁盘部分扇区出现物理问题的情况下还能正常⼯作,就必须保证⽂件系统的super block信息在这种情况下也能正常访问。所以⼀个⽂件系统的super block会在多个block group中进⾏备份,这些super block区域的数据保持⼀致。
/*
* EXT2 文件系统超级块结构体
*/
struct ext2_super_block {
// inode 和块的总数
__le32 s_inodes_count; /* inode 数量 */
__le32 s_blocks_count; /* 块数量 */
__le32 s_r_blocks_count; /* 保留块数量 */
__le32 s_free_blocks_count; /* 空闲块数量 */
__le32 s_free_inodes_count; /* 空闲 inode 数量 */
// 块和片段的相关信息
__le32 s_first_data_block; /* 第一个数据块编号 */
__le32 s_log_block_size; /* 块大小的对数(2的幂) */
__le32 s_log_frag_size; /* 片段大小的对数(2的幂) */
__le32 s_blocks_per_group; /* 每个块组的块数量 */
__le32 s_frags_per_group; /* 每个块组的片段数量 */
__le32 s_inodes_per_group; /* 每个块组的 inode 数量 */
// 时间戳
__le32 s_mtime; /* 文件系统挂载时间 */
__le32 s_wtime; /* 文件系统写入时间 */
__le16 s_mnt_count; /* 挂载次数 */
__le16 s_max_mnt_count; /* 最大挂载次数 */
// 文件系统状态和错误处理
__le16 s_magic; /* 魔术数字(签名) */
__le16 s_state; /* 文件系统状态 */
__le16 s_errors; /* 检测到错误时的行为 */
__le16 s_minor_rev_level; /* 次版本号 */
// 检查相关
__le32 s_lastcheck; /* 上次检查时间 */
__le32 s_checkinterval; /* 最大检查间隔时间 */
// 操作系统和修订信息
__le32 s_creator_os; /* 创建文件系统的操作系统 */
__le32 s_rev_level; /* 修订级别 */
// 预留块和 inode 的默认 UID/GID
__le16 s_def_resuid; /* 预留块的默认用户 ID */
__le16 s_def_resgid; /* 预留块的默认组 ID */
// 动态修订超级块专用字段
__le32 s_first_ino; /* 第一个非预留 inode 编号 */
__le16 s_inode_size; /* inode 结构的大小 */
__le16 s_block_group_nr; /* 当前超级块所在的块组编号 */
// 特性集
__le32 s_feature_compat; /* 兼容特性集 */
__le32 s_feature_incompat; /* 不兼容特性集 */
__le32 s_feature_ro_compat; /* 只读兼容特性集 */
// UUID 和卷信息
__u8 s_uuid[16]; /* 卷的 128 位 UUID */
char s_volume_name[16]; /* 卷名称 */
char s_last_mounted[64]; /* 上次挂载的目录路径 */
// 压缩和其他性能提示
__le32 s_algorithm_usage_bitmap;/* 压缩算法使用位图 */
__u8 s_prealloc_blocks; /* 尝试预分配的块数量 */
__u8 s_prealloc_dir_blocks; /* 尝试为目录预分配的块数量 */
__u16 s_padding1; /* 填充字节,用于对齐下一个字段 */
// 日志相关(仅当 EXT3_FEATURE_COMPAT_HAS_JOURNAL 设置时有效)
__u8 s_journal_uuid[16]; /* 日志超级块的 UUID */
__u32 s_journal_inum; /* 日志文件的 inode 编号 */
__u32 s_journal_dev; /* 日志文件所在设备的编号 */
__u32 s_last_orphan; /* 待删除 inode 列表的起始位置 */
// 哈希树相关
__u32 s_hash_seed[4]; /* HTREE 哈希种子 */
__u8 s_def_hash_version; /* 默认哈希版本 */
__u8 s_reserved_char_pad;
__u16 s_reserved_word_pad;
// 默认挂载选项和其他元数据
__le32 s_default_mount_opts;
__le32 s_first_meta_bg; /* 第一个元数据块组编号 */
__u32 s_reserved[190]; /* 填充至超级块末尾 */
};
格式化的本质就是写入文件系统的管理信息,将超级块信息写入,将每个分组的GDT和位图都置零。
GDT(Group Descriptor Table)
块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储⼀个块组的描述信息,如在这个块组中从哪⾥开始是inode Table,从哪⾥开始是Data Blocks,空闲的inode和数据块还有多少个等等。块组描述符在每个块组的开头都有⼀份拷⻉。
超级块是整个分区的文件系统信息,GDT是当前块组的信息。
/*
* 块组描述符结构体
* 描述了EXT2文件系统中一个块组的元数据信息
*/
struct ext2_group_desc {
__le32 bg_block_bitmap; /* 块位图块编号:指向该块组中用于记录块使用状态的位图块 */
__le32 bg_inode_bitmap; /* inode位图块编号:指向该块组中用于记录inode使用状态的位图块 */
__le32 bg_inode_table; /* inode表块编号:指向该块组中存储inode信息的块 */
__le16 bg_free_blocks_count; /* 空闲块数量:该块组中当前空闲的块的数量 */
__le16 bg_free_inodes_count; /* 空闲inode数量:该块组中当前空闲的inode的数量 */
__le16 bg_used_dirs_count; /* 已使用目录数量:该块组中当前已使用的目录数量 */
__le16 bg_pad; /* 填充字段:用于字节对齐,确保结构体大小为32字节 */
__le32 bg_reserved[3]; /* 保留字段:为将来扩展预留的空间 */
};
块位图(Block Bitmap)
Block Bitmap中记录着Data Block中哪个数据块已经被占⽤,哪个数据块没有被占⽤,占用置1,未占用置0.
inode位图(Inode Bitmap)
每个bit表示⼀个inode是否空闲可⽤。可用置1,不可用置0。
往磁盘下载文件时会比卸载要慢,因为下载时是确实将文件数据等信息向Data block拷贝,但是删除文件,实际上是将块位图和inode位图和文件相关的位置0。
置0后对应的inode和块上的数据对操作系统来说就是乱码,毫无意义,当再次使用这些空间的时候将其覆盖即可。
inode和datablock映射(弱化)
inode
源码:
struct inode {
// 文件系统基础信息
umode_t i_mode; // 文件类型和访问权限 (e.g. S_IFREG, S_IFDIR)
kuid_t i_uid; // 文件所有者用户ID
kgid_t i_gid; // 文件所有者组ID
unsigned long i_ino; // Inode 编号 (唯一标识)
dev_t i_rdev; // 如果是设备文件,存储设备号
// 时间戳相关
struct timespec64 i_atime; // 最后访问时间
struct timespec64 i_mtime; // 最后修改时间
struct timespec64 i_ctime; // Inode状态最后改变时间
// 文件大小与块管理
loff_t i_size; // 文件大小(字节)
unsigned int i_blkbits; // 块大小位数(如 12 表示 4KB 块)
blkcnt_t i_blocks; // 文件占用的磁盘块数
// 链接与引用计数
unsigned short i_nlink; // 硬链接计数
atomic_t i_count; // 引用计数(防止被释放)
// 文件系统关键指针
const struct inode_operations *i_op; // Inode操作函数表(如创建/删除文件)
const struct file_operations *i_fop; // 文件操作函数表(如open/read/write)
struct super_block *i_sb; // 指向所属超级块的指针
// 地址空间管理
struct address_space *i_mapping; // 管理页面缓存和文件映射
// 文件系统特定数据(通过union节省内存)
union {
struct ext4_inode_info ext4_i; // ext4文件系统的特有数据
struct xfs_inode xfs_i; // XFS文件系统的特有数据
struct btrfs_inode btrfs_i; // Btrfs文件系统的特有数据
// ... 其他文件系统的特有结构
};
// 同步与锁机制
struct rw_semaphore i_rwsem; // 读写信号量(保护inode元数据)
// 安全相关
void *i_security; // 安全模块(如SELinux)的私有数据
// 其他重要成员
unsigned long i_state; // Inode状态标志(如I_DIRTY, I_SYNC)
unsigned int i_flags; // 文件系统无关的标志(如S_NOATIME)
void *i_private; // 文件系统或驱动私有数据
};
查看inode编号:
[fz@VM-20-14-centos ~]$ ls -li
total 24
787918 drwxrwxr-x 5 fz fz 4096 Jan 20 09:53 classtest
787512 drwxrwxr-x 3 fz fz 4096 Sep 11 20:54 Gitee
926514 drwxrwxr-x 3 fz fz 4096 Jan 9 22:20 Github_Repository
658003 -rw-rw-r-- 1 fz fz 827 Nov 13 22:09 install.sh
792625 drwxrwxr-x 7 fz fz 4096 Jan 21 23:32 linux
657737 -rw-rw-r-- 1 fz fz 207 Jan 15 12:14 Makefile
通过inode存储的信息我们就可以得到文件的所有内容:
- 怎么知道inode在哪个分组:inode编号跨组
- 怎么知道inode在分组中哪个位置:所有的大小固定,可以通过除运算和模运算得到位置
inode源码中struct ext4_inode_info ext4_i;
结构体简化版代码如下:
struct ext4_inode_info {
// 继承自通用 inode 结构(通过 container_of 关联)
struct inode vfs_inode; // 内嵌的通用 inode 结构
// 数据块管理
__le32 i_data[EXT4_N_BLOCKS]; // 传统间接块指针或 Extent 树根
struct ext4_extent_header i_extent_root; // Extent 树的根节点(若启用 Extents)
struct rb_root i_extent_tree; // Extent 树的内存中表示
ext4_lblk_t i_prealloc_block; // 预分配块的起始逻辑块号
ext4_lblk_t i_prealloc_count; // 预分配的连续块数量
// 时间戳扩展(纳秒级精度)
struct timespec64 i_crtime; // 文件创建时间(ext4 特有)
// 加密与安全
struct ext4_crypt_info *i_crypt_info; // 加密上下文(e.g., fscrypt)
// 日志与原子操作
unsigned int i_sync_tid; // 提交事务ID(用于原子写)
unsigned int i_datasync_tid;
// 大文件支持(64位特性)
__u32 i_extra_isize; // 扩展的 inode 大小
__u32 i_inode_osd; // 操作系统依赖字段
// 内联数据(Inline Data)
void *i_inline_data; // 内联数据指针(小文件直接存储在 inode 中)
size_t i_inline_size; // 内联数据大小
// 其他标志与状态
unsigned long i_flags; // ext4 特有的标志(如 EXT4_XXX_FL)
unsigned int i_delalloc_reserved_flag;
// 预分配空间管理
struct ext4_prealloc_space *i_prealloc_list;
// 扩展属性(xattr)
void *i_xattr; // 扩展属性存储块
};
i_data[EXT4_N_BLOCKS];
用来存储inode对应文件数据的数据块的编号,具体存储规则在datablock映射中讲解。
datablock映射
__le32 i_block[EXT2_N_BLOCKS]
(EXT2_N_BLOCKS = 15
)是 ext2/ext3 文件系统中 inode 用于管理文件数据块的核心结构。它的设计目标是实现 inode 与数据块的映射,从而让文件系统能够通过 inode 找到文件的所有内容。
文件 = 内容 + 属性
- 属性:存储在 inode 的元数据中(如权限
i_mode
、时间戳i_atime
、大小i_size
等)。 - 内容:存储在磁盘的数据块(block)中,通过
i_block
数组建立逻辑块号到物理块号的映射。
i_block[15]
的作用
i_block
数组的 15 个元素分为 直接块指针 和 间接块指针,用于支持不同大小的文件:
索引范围 | 类型 | 说明 |
---|---|---|
i_block[0-11] | 直接块 | 直接指向文件数据块的物理块号,适合小文件(最多 12 个块)。 |
i_block[12] | 一级间接块 | 指向一个间接块(该块存储直接块指针),支持中等文件。 |
i_block[13] | 二级间接块 | 指向一个间接块,该块指向多个一级间接块,支持较大文件。 |
i_block[14] | 三级间接块 | 指向一个间接块,该块指向多个二级间接块,支持超大文件。 |
映射过程示例
假设文件系统块大小为 4KB(i_blkbits = 12
),则:
场景 1:小文件(直接块)
- 文件大小 ≤ 12 块 × 4KB = 48KB。
- 所有数据块的物理块号直接存储在
i_block[0-11]
中。 - 无需间接块,访问速度快。
场景 2:中等文件(一级间接块)
- 文件大小 ≤ 12×4KB + (1间接块 × 1024指针/块) ×4KB = 48KB + 4MB = 4.05MB。
i_block[12]
指向一个间接块,该块存储 1024 个直接块指针(每个指针 4 字节,4KB 块可存 1024 个指针)。- 访问逻辑块号 ≥12 时,需先读取一级间接块,再找到对应的直接块指针。
场景 3:大文件(二级间接块)
- 文件大小 ≤ 12×4KB + 1×4MB + 1×1024×4MB = 4.05MB + 4GB ≈ 4.004GB。
i_block[13]
指向一个二级间接块,该块存储 1024 个一级间接块指针。- 访问逻辑块号时,需遍历二级间接块 → 一级间接块 → 直接块指针。
场景 4:超大文件(三级间接块)
- 文件大小 ≤ 12×4KB + 1×4MB + 1×4GB + 1×1024×4GB ≈ 4.004GB + 4TB ≈ 4.004TB。
i_block[14]
指向一个三级间接块,该块存储 1024 个二级间接块指针。- 访问逻辑块号时,需遍历三级间接块 → 二级间接块 → 一级间接块 → 直接块指针。
为什么需要多级间接块?
- 节省 inode 空间:直接存储所有块指针会占用大量 inode 空间(尤其是大文件)。
- 动态扩展:文件大小增长时按需分配间接块,避免预分配浪费。
- 兼容性:ext2/ext3 的设计需要兼容早期文件系统。
文件内容与属性的关联
- 属性:通过 inode 的元数据(如
i_mode
,i_size
)描述文件的基本信息。 - 内容:通过
i_block
数组的指针链,逐级找到所有数据块,最终拼接成完整的文件内容。
思考
知道inode号的情况下,在指定分区怎么得到文件内容和属性
- 分区之后的格式化操作,就是对分区进⾏分组,在每个分组中写⼊超级块、GDT、BlockBitmap、InodeBitmap等管理信息,这些管理信息统称: ⽂件系统
- 只要知道⽂件的inode号,就能在指定分区中确定是哪⼀个分组,进⽽在哪⼀个分组确定是哪⼀个inode
- 拿到inode⽂件属性和内容就全部都有了
对⽂件进⾏增、删、查、改是在做什么?
在已知文件inode号且指定分区的情况下,对文件的增、删、查、改操作本质上是通过inode直接操作文件元数据与数据块,具体过程如下:
增(创建文件)
- 分配inode:在分区的inode位图中查找空闲inode,标记为已使用。
- 分配数据块:根据文件大小,从数据块位图中分配空闲块,并建立inode与数据块的映射(直接/间接指针或Extents)。
- 更新目录:在父目录中添加文件名与inode号的映射关系。
删(删除文件)
- 释放inode:将inode位图中对应位标记为未使用。
- 释放数据块:根据inode中的块指针,释放数据块并更新数据块位图。
- 删除目录项:在父目录中移除文件名与inode号的映射。
- 注:实际数据未被擦除,仅标记为可覆盖。
查(查找文件)
- 定位inode:通过inode号直接访问分区的inode表,获取文件元数据(权限、大小等)。
- 读取内容:根据inode中的块指针(直接/间接或Extents)找到数据块并读取内容。
改(修改文件)
- 扩展内容:若需新增数据,分配新数据块并更新inode中的块指针。
- 修改内容:直接修改现有数据块内容,若涉及块位置变化则调整指针。
- 更新元数据:修改inode中的时间戳、大小等属性。
关键点
- 分区内唯一性:inode号仅在分区内唯一,操作前需确保分区已挂载。
- 绕过文件名:直接通过inode号操作文件,无需依赖目录中的文件名映射(适用于特殊文件名场景)。
- 效率优化:增删操作通过位图快速分配/释放资源,改查操作通过指针链或Extents减少磁盘I/O。
新文件创建时文件系统操作示例
下⾯,通过touch⼀个新⽂件来看看如何⼯作。
创建⼀个新⽂件主要有以下4个操作:
- 存储属性
内核先找到⼀个空闲的i节点(这⾥是263466)。内核把⽂件信息记录到其中。
- 存储数据
该⽂件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第⼀块
数据复制到300,下⼀块复制到500,以此类推。
- 记录分配情况
⽂件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。
- 添加⽂件名到⽬录
新的⽂件名abc。linux如何在当前的⽬录中记录这个⽂件?内核将⼊⼝(263466,abc)添加到
⽬录⽂件。⽂件名和inode之间的对应关系将⽂件名和⽂件的内容及属性连接起来。
⽬录与⽂件名
我们访问⽂件,一直⽤的都是⽂件名,没用过inode编号,并且inode也没有存储文件名,那我们是怎么通过文件名访问的呢?⽬录究竟是什么?
⽬录也是⽂件,但是磁盘上没有⽬录的概念,只有⽂件属性+⽂件内容的概念。
⽬录的属性不⽤多说,内容保存的是:⽂件名和Inode号的映射关系
进程有CWD信息,所以我们打开一个目录时,就是路径+打开的文件名。
目录也是文件,按照文件系统的保存方式保存,目录的数据内容存放的是文件名和inode的映射关系。所以inode是文件名本身的属性,文件名和inode的映射关系保存在该文件所属的内容当中,因为映射关系也是数据。
所有在磁盘中保存文件时不会有目录的概念,保存的所有东西都是inode + 数据。
文件系统只认inode,所以如果现在要打开一个文件,文件系统会通过路径 + 文件名的方式来打开,先通过目录来到当前路径,然后再文件名文件的数据内容中查询文件名对应的inode映射关系,得到映射关系后根据得到的inode中数据块编号来得到文件内容。
路径解析
"当我们需要获取某个文件的inode和数据块信息时,必须通过其所在的目录层级逐级查找。我的理解是:
- 每个目录的数据块中存储的是其直接子项的文件名与对应inode的映射关系,而非文件内容本身。
- 要获取当前文件(例如
/home/user/file.txt
)的inode,需要从上一级目录(/home/user
)的数据块中查找file.txt
对应的inode编号。 - 上一级目录(如
/home/user
)的inode和数据块信息,又需要通过更上一级目录(如/home
)的数据块解析得到,依此类推直到根目录。
疑问点:
- 这种逐级依赖的解析方式是否意味着,文件的inode信息本质上是通过父目录的层级关系间接确定的?
- 目录的数据块是否仅存储直接子项的映射,而不包含更深层级的路径信息?
- 如果路径中存在符号链接或挂载点,这种解析过程会如何变化?"
以下是 Linux 文件系统中打开一个文件时路径解析的 详细过程,以路径 /home/user/file.txt
为例:
路径解析的起点
- 根目录的固定 inode:
文件系统根目录 (/
) 的 inode 编号是固定的(通常为inode 2
),这是解析所有绝对路径的起点。 - 当前工作目录的 inode:
如果是相对路径(如documents/report.doc
),则从进程的当前工作目录(通过pwd
命令查看)的 inode 开始解析。
逐级解析路径
假设访问绝对路径 /home/user/file.txt
,系统会按以下步骤解析:
步骤 1:解析根目录 **/**
- 获取根目录的 inode:
系统直接使用根目录的固定 inode(如inode 2
)。 - 读取根目录的数据块:
通过inode 2
中的数据块指针
,找到根目录的数据块。 - 查找****
**home**
****条目:
在根目录的数据块中搜索home
文件名,得到其对应的 inode 编号(例如inode 100
)。
步骤 2:解析 **/home**
目录
- 获取****
**/home**
****的 inode:
使用上一步得到的inode 100
。 - 读取****
**/home**
****的数据块:
通过inode 100
的数据块指针,找到/home
目录的数据块。 - 查找****
**user**
****条目:
在/home
的数据块中搜索user
文件名,得到其 inode 编号(例如inode 200
)。
步骤 3:解析 **/home/user**
目录
- 获取****
**/home/user**
****的 inode:
使用inode 200
。 - 读取****
**/home/user**
****的数据块:
通过inode 200
的数据块指针,找到/home/user
目录的数据块。 - 查找****
**file.txt**
****条目:
在/home/user
的数据块中搜索file.txt
,得到其 inode 编号(例如inode 300
)。
步骤 4:访问目标文件 **/home/user/file.txt**
- 获取****
**file.txt**
****的 inode:
使用inode 300
。 - 读取文件元数据:
从inode 300
中获取文件权限、大小、时间戳等信息。 - 权限检查:
系统检查当前进程是否有权限访问该文件(读、写、执行权限)。 - 读取文件内容:
通过inode 300
中的数据块指针
(直接指针、间接指针等),找到文件内容所在的数据块。
在 Linux 文件系统中,目录文件和其内部的文件确实是独立存储的,但它们通过 inode 映射关系 和 层级路径解析 实现关联。以下是详细的步骤说明:
目录文件与普通文件的独立性
- 目录文件:
是一种特殊文件,其数据块中存储的是 直接子项的文件名 → inode 映射表,而非传统文件内容。- 例如,目录
/home
的数据块可能包含条目:
- 例如,目录
user → inode_200
alice → inode_300
- 普通文件:
其数据块存储实际内容(如文本、二进制数据),通过自己的 inode 管理元数据和数据块指针。
如何通过目录文件找到其内部的文件?
步骤 1:创建文件时的操作
当在目录 /home
下创建文件 file.txt
时,系统会执行以下操作:
- 分配新 inode:
为新文件file.txt
分配一个 inode(例如inode_400
),存储其元数据(权限、时间戳等)。 - 分配数据块:
根据文件内容大小,为file.txt
分配数据块,并将数据块地址记录在inode_400
中。 - 更新目录文件:
在目录/home
的数据块中添加一条映射:
file.txt → inode_400
步骤 2:访问文件时的路径解析
当用户访问 /home/file.txt
时:
- 解析根目录****
**/**
:- 通过根目录的固定 inode(如
inode_2
)找到其数据块。 - 在数据块中查找
home
,得到其 inode 编号(如inode_100
)。
- 通过根目录的固定 inode(如
- 解析目录****
**/home**
:- 通过
inode_100
找到/home
的数据块。 - 在数据块中查找
file.txt
,得到其 inode 编号(如inode_400
)。
- 通过
- 访问文件内容:
- 通过
inode_400
找到文件的数据块指针,读取实际内容。
- 通过
关键设计:文件名与数据的解耦
- 文件名仅存在于目录中:
文件名本身不存储在文件的 inode 或数据块中,而是由父目录的条目维护。- 允许同一文件有多个文件名(硬链接),只需在多个目录中添加相同 inode 的条目。
- inode 是唯一标识:
文件的实际数据通过 inode 定位,与文件名无关。删除文件时,本质是删除目录中的条目,而非立即擦除数据。
示例:目录与文件的关联结构
文件系统结构:
/
├── home/ (inode_100)
│ ├── user/ (inode_200)
│ └── file.txt (inode_400)
└── etc/ (inode_300)
数据块内容:
- 根目录(inode_2)的数据块:
| home → inode_100 | etc → inode_300 |
- /home 目录(inode_100)的数据块:
| user → inode_200 | file.txt → inode_400 |
- /home/file.txt(inode_400)的数据块:
"Hello, World!"
内核缓存机制
- dentry 缓存系统
struct dentry {
struct hlist_node d_hash; // 哈希表链接
struct dentry *d_parent; // 父目录
struct qstr d_name; // 文件名
struct inode *d_inode; // 关联inode
unsigned char d_iname[DNAME_INLINE_LEN]; // 短名内联存储
// ... 其他字段(引用计数、状态标志等)
};
- **<font style="color:rgba(0, 0, 0, 0.9);">数据结构:</font>**
- **<font style="color:rgba(0, 0, 0, 0.9);">缓存组织:</font>**
* **<font style="color:rgba(0, 0, 0, 0.9);">哈希表加速查找(dentry_hashtable)</font>**
* **<font style="color:rgba(0, 0, 0, 0.9);">LRU 链表管理缓存淘汰(dentry_unused)</font>**
- 缓存命中流程
- 路径遍历时优先查询 dentry 缓存
- 有效命中直接返回已缓存的 dentry 对象
- 未命中时触发磁盘读取并创建新 dentry
四、性能优化策略
- 预读机制(readahead)
- 目录遍历时预加载后续可能访问的数据块
- 典型预读窗口:32-128 个数据块(可配置)
- 负缓存(Negative Caching)
- 缓存不存在的文件查询结果
- 有效避免重复磁盘查找
- 典型缓存时间:30-60 秒(依赖具体实现)
挂载分区
我们已经能够根据inode号在指定分区找⽂件了,也已经能根据⽬录⽂件内容,找指定的inode了,在指定的分区内,我们可以为所欲为了。
问题:inode不是不能跨分区吗?Linux不是可以有多个分区吗?我怎么知道我在哪⼀个分区???
磁盘在分区后进行格式化,格式化后的磁盘分区仍然不能直接使用。分区需要和一个目录进行关联,通过进入这个目录,就相当于进入分区。这个关联过程就是挂载。
所以我们通过路径最前面的目录就可以知道当前在哪个分区。云服务只有一个磁盘,因为这个磁盘没有分区所以只有一个分区。这个分区挂载到了/
文件夹,所以当我们在这个分区内操作时,pwd
的前缀就是/
。如果还有其他分区,那么第一个目录就是其他分区挂载的目录名。
实验
# 使用 dd 命令创建一个 5MB 的磁盘镜像文件 disk.img
$ dd if=/dev/zero of=./disk.img bs=1M count=5
# 使用 mkfs.ext4 将 disk.img 格式化为 ext4 文件系统
$ mkfs.ext4 disk.img
# 在 /mnt 目录下创建一个名为 mydisk 的目录,作为挂载点
$ mkdir /mnt/mydisk
# 使用 df -h 查看当前挂载的文件系统信息
$ 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
# 将 disk.img 挂载到 /mnt/mydisk 目录
$ sudo mount -t ext4 ./disk.img /mnt/mydisk/
# 再次使用 df -h 查看挂载的文件系统信息,确认 /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
/dev/loop0 4.9M 24K 4.5M 1% /mnt/mydisk
# 使用 umount 卸载 /mnt/mydisk 目录
$ sudo umount /mnt/mydisk
# 最后使用 df -h 查看挂载的文件系统信息,确认 /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
<font style="color:rgb(31,35,41);">/dev/loop0</font>
在Linux系统中代表第⼀个循环设备(loop device)。循环设备,也被称为回环设备或者loopback设备,是⼀种伪设备(pseudo-device),它允许将⽂件作为块设备(block device)来使⽤。这种机制使得可以将⽂件(⽐如ISO镜像⽂件)挂载(mount)为⽂件系统,就像它们是物理硬盘分区或者外部存储设备⼀样。因此可以用来模拟
<font style="color:rgb(31,35,41);">/dev/vda1</font>
真正的磁盘效果。