[操作系统] Ext系列文件系统

引⼊⽂件系统

“块”概念引入

“块”是⽂件系统存取的最⼩单位

其实硬盘是典型的“块”设备,操作系统读取硬盘数据的时候,其实是不会⼀个个扇区地读取,这样效率太低,⽽是⼀次性连续读取多个扇区,即⼀次性读取⼀个块(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]三级间接块指向一个间接块,该块指向多个二级间接块,支持超大文件。

映射过程示例

假设文件系统块大小为 4KBi_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个操作:

  1. 存储属性

内核先找到⼀个空闲的i节点(这⾥是263466)。内核把⽂件信息记录到其中。

  1. 存储数据

该⽂件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第⼀块
数据复制到300,下⼀块复制到500,以此类推。

  1. 记录分配情况

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

  1. 添加⽂件名到⽬录

新的⽂件名abc。linux如何在当前的⽬录中记录这个⽂件?内核将⼊⼝(263466,abc)添加到
⽬录⽂件。⽂件名和inode之间的对应关系将⽂件名和⽂件的内容及属性连接起来。

⽬录与⽂件名

我们访问⽂件,一直⽤的都是⽂件名,没用过inode编号,并且inode也没有存储文件名,那我们是怎么通过文件名访问的呢?⽬录究竟是什么?

⽬录也是⽂件,但是磁盘上没有⽬录的概念,只有⽂件属性+⽂件内容的概念。

⽬录的属性不⽤多说,内容保存的是:⽂件名和Inode号的映射关系

进程有CWD信息,所以我们打开一个目录时,就是路径+打开的文件名。

目录也是文件,按照文件系统的保存方式保存,目录的数据内容存放的是文件名和inode的映射关系。所以inode是文件名本身的属性,文件名和inode的映射关系保存在该文件所属的内容当中,因为映射关系也是数据。

所有在磁盘中保存文件时不会有目录的概念,保存的所有东西都是inode + 数据。

文件系统只认inode,所以如果现在要打开一个文件,文件系统会通过路径 + 文件名的方式来打开,先通过目录来到当前路径,然后再文件名文件的数据内容中查询文件名对应的inode映射关系,得到映射关系后根据得到的inode中数据块编号来得到文件内容。

路径解析

"当我们需要获取某个文件的inode和数据块信息时,必须通过其所在的目录层级逐级查找。我的理解是:

  1. 每个目录的数据块中存储的是其直接子项的文件名与对应inode的映射关系,而非文件内容本身。
  2. 要获取当前文件(例如 /home/user/file.txt)的inode,需要从上一级目录(/home/user)的数据块中查找 file.txt 对应的inode编号。
  3. 上一级目录(如 /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:解析根目录 **/**

  1. 获取根目录的 inode
    系统直接使用根目录的固定 inode(如 inode 2)。
  2. 读取根目录的数据块
    通过 inode 2 中的 数据块指针,找到根目录的数据块。
  3. 查找**** **home** ****条目
    在根目录的数据块中搜索 home 文件名,得到其对应的 inode 编号(例如 inode 100)。

步骤 2:解析 **/home** 目录

  1. 获取**** **/home** ****的 inode
    使用上一步得到的 inode 100
  2. 读取**** **/home** ****的数据块
    通过 inode 100 的数据块指针,找到 /home 目录的数据块。
  3. 查找**** **user** ****条目
    /home 的数据块中搜索 user 文件名,得到其 inode 编号(例如 inode 200)。

步骤 3:解析 **/home/user** 目录

  1. 获取**** **/home/user** ****的 inode
    使用 inode 200
  2. 读取**** **/home/user** ****的数据块
    通过 inode 200 的数据块指针,找到 /home/user 目录的数据块。
  3. 查找**** **file.txt** ****条目
    /home/user 的数据块中搜索 file.txt,得到其 inode 编号(例如 inode 300)。

步骤 4:访问目标文件 **/home/user/file.txt**

  1. 获取**** **file.txt** ****的 inode
    使用 inode 300
  2. 读取文件元数据
    inode 300 中获取文件权限、大小、时间戳等信息。
  3. 权限检查
    系统检查当前进程是否有权限访问该文件(读、写、执行权限)。
  4. 读取文件内容
    通过 inode 300 中的 数据块指针(直接指针、间接指针等),找到文件内容所在的数据块。


在 Linux 文件系统中,目录文件和其内部的文件确实是独立存储的,但它们通过 inode 映射关系层级路径解析 实现关联。以下是详细的步骤说明:

目录文件与普通文件的独立性
  • 目录文件
    是一种特殊文件,其数据块中存储的是 直接子项的文件名 → inode 映射表,而非传统文件内容。
    • 例如,目录 /home 的数据块可能包含条目:
user → inode_200
alice → inode_300
  • 普通文件
    其数据块存储实际内容(如文本、二进制数据),通过自己的 inode 管理元数据和数据块指针。

如何通过目录文件找到其内部的文件?

步骤 1:创建文件时的操作

当在目录 /home 下创建文件 file.txt 时,系统会执行以下操作:

  1. 分配新 inode
    为新文件 file.txt 分配一个 inode(例如 inode_400),存储其元数据(权限、时间戳等)。
  2. 分配数据块
    根据文件内容大小,为 file.txt 分配数据块,并将数据块地址记录在 inode_400 中。
  3. 更新目录文件
    在目录 /home 的数据块中添加一条映射:
file.txt → inode_400

步骤 2:访问文件时的路径解析

当用户访问 /home/file.txt 时:

  1. 解析根目录**** **/**
    • 通过根目录的固定 inode(如 inode_2)找到其数据块。
    • 在数据块中查找 home,得到其 inode 编号(如 inode_100)。
  2. 解析目录**** **/home**
    • 通过 inode_100 找到 /home 的数据块。
    • 在数据块中查找 file.txt,得到其 inode 编号(如 inode_400)。
  3. 访问文件内容
    • 通过 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!"

内核缓存机制

  1. 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>**
  1. 缓存命中流程
    • 路径遍历时优先查询 dentry 缓存
    • 有效命中直接返回已缓存的 dentry 对象
    • 未命中时触发磁盘读取并创建新 dentry

四、性能优化策略

  1. 预读机制(readahead)
    • 目录遍历时预加载后续可能访问的数据块
    • 典型预读窗口:32-128 个数据块(可配置)
  2. 负缓存(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>真正的磁盘效果。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DevKevin

你们的点赞收藏是对我最大的鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值