Bootstrap

Linux内核文件系统-ext2文件系统-磁盘数据结构

建议点击这里查看个人主页上的最新原文

一般的Linux书籍都是先讲解进程和内存相关的知识,但我想先讲解文件系统。

第一,因为我就是做文件系统的,更擅长这一块,其他模块的内容我还要再去好好看看书,毕竟不能误人子弟嘛;第二,是

因为文件系统模块更接近于用户态,是相对比较好理解的内容(当然想深入还是要下大功夫的),由文件系统入手比较适合初学者。

英文全称Extended file system,翻译为扩展文件系统。Linux内核最开始用的是minix文件系统,直到1992年4月,Rémy Card开发了ext文件系统,采用Unix文件系统(UFS)的元数据结构,在linux内核0.96c版中引入。设计上参考了BSD的快速文件系统(Fast File System,简称FFS)。1993年1月0.99版本中ext2合入内核, 2001年11月2.4.15版本中ext3合入内核,2006年10月10日2.6.19版本中ext4合入内核。

相关文档网站:

块组

块组(block group)的内容如下:

超级块组描
述符
数据块位图inode
位图
inode表数据块
1个块k个块1个块1个块n个块m个块

启动扇区和块组:

启动块块组0块组1块组n

对于超级块的存储,ext2的采用了稀疏超级块(sparse superblock)技术,超级块只存储到块组0、块组1和其他ID可以表示为3、5、7的幂的块组中,也就是0、1、3、5、7、9、25、49…

块组中内容的解释:

  • 超级块: 存储文件系统自身元数据
  • 组描述符: 包含所有块组的状态
  • 数据块位图: 每个bit表示对应的数据块是否空闲,1表示占用,0表示空闲
  • inode位图: 每个bit表示对应的inode是否空闲
  • inode表: 块组中的inode
  • 数据块: 文件的有用数据

举个例子,32GB的磁盘整个盘格式化为ext2文件系统,块大小为4KB,1个块大小的数据块位图描述8*4K=32K个数据块,也就是32K*4KB=128MB,大约有32*1024MB/128MB=256个块组。总块数为total,块大小为bsize字节,块组的总数约为total/(8*bsize),套到上面的例子,就是total=32*1024MB/4KB=8192K,块组的总数约为8192K/(8*4K)=256个。bsize越小,块组数越大。

超级块

struct ext2_super_block {
        __le32  s_inodes_count;         /* 索引节点总数 */
        __le32  s_blocks_count;         /* 块总数 */
        __le32  s_r_blocks_count;       /* 保留的块数 */
        __le32  s_free_blocks_count;    /* 空闲块计数器 */
        __le32  s_free_inodes_count;    /* 空闲索引节点计数器 */
        __le32  s_first_data_block;     /* 第一个数据块的块号,总是为1 */
        // 最小 EXT2_MIN_BLOCK_SIZE,最大 EXT2_MAX_BLOCK_SIZE
        __le32  s_log_block_size;       /* 块大小,对数表示,值为0时表示2^0*1024=1024,值为1时表示2^1*1024=2048,值为2时表示2^2*1024=4096 */
        __le32  s_log_frag_size;        /* 片大小 */
        __le32  s_blocks_per_group;     /* 每组中的块数 */
        __le32  s_frags_per_group;      /* 每组中的片数 */
        __le32  s_inodes_per_group;     /* 每组中的索引节点数 */
        __le32  s_mtime;                /* 最后一次挂载时间 */
        __le32  s_wtime;                /* 写时间 */
        __le16  s_mnt_count;            /* 挂载次数 */
        __le16  s_max_mnt_count;        /* 检查之前挂载操作的次数,挂载次数达到这个值后要进行检查 */
        __le16  s_magic;                /* 幻数,EXT2_SUPER_MAGIC */
        __le16  s_state;                /* 状态标志,挂载时为0,正常卸载为1(EXT2_VALID_FS),错误为2(EXT2_ERROR_FS) */
        __le16  s_errors;               /* 检测到错误的行为 */
        __le16  s_minor_rev_level;      /* 次版本号 */
        __le32  s_lastcheck;            /* 最后检查的时间 */
        __le32  s_checkinterval;        /* 检查间隔 */
        __le32  s_creator_os;           /* 在什么操作系统上格式化的 */
        __le32  s_rev_level;            /* Revision level,主版本号 */
        __le16  s_def_resuid;           /* 保留块的默认uid */
        __le16  s_def_resgid;           /* 保留块默认gid */
        /*
         * 这些字段仅适用于 EXT2_DYNAMIC_REV 超级块。
         *
         * 注意: 兼容功能集和不兼容功能集之间的区别在于,
         * 如果内核不知道不兼容功能集中设置的位,
         * 它应该拒绝挂载文件系统。
         *
         * e2fsck 的要求更加严格;如果它不知道
         * 兼容或不兼容功能集中的某个功能,
         * 它必须中止操作,而不是尝试处理
         * 它不理解的东西...
         */
        __le32  s_first_ino;            /* 第一个非保留的索引节点号 */
        __le16   s_inode_size;          /* 磁盘索引节点大小 */
        __le16  s_block_group_nr;       /* 超级块块组号 */
        __le32  s_feature_compat;       /* 兼容特性,查看 EXT2_FEATURE_COMPAT_DIR_PREALLOC 等宏定义 */
        __le32  s_feature_incompat;     /* 非兼容特性 */
        __le32  s_feature_ro_compat;    /* 只读兼容特性 */
        __u8    s_uuid[16];             /* 卷的 128 位 uuid,文件系统标识符 */
        char    s_volume_name[16];      /* 卷名 */
        char    s_last_mounted[64];     /* 最后挂载点文件夹 */
        __le32  s_algorithm_usage_bitmap; /* 压缩 */
        /*
         * 性能提示。只有在 EXT2_COMPAT_PREALLOC 标志开启时,
         * 才应进行目录预分配。
         */
        __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];        /* 填充到块的末尾 */
};

组描述符

struct ext2_group_desc
{
        __le32  bg_block_bitmap;        /* 数据块位图所在的块号 */
        __le32  bg_inode_bitmap;        /* inode位图所在的块号 */
        __le32  bg_inode_table;         /* inode表所在的起始块号 */
        __le16  bg_free_blocks_count;   /* 组中空闲块个数 */
        __le16  bg_free_inodes_count;   /* 组中空闲索引节点数 */
        __le16  bg_used_dirs_count;     /* 组中目录数 */
        __le16  bg_pad;
        __le32  bg_reserved[3];
};

inode表

struct ext2_group_descbg_inode_table表示inode表所在的起始块号,磁盘索引节点固定128字节(可以在gdb中打印p sizeof(struct ext2_inode)),1024字节块大小包含8个inode,4096字节块大小包含32个inode。

注意没有索引节点号,因为可以通过计算出来,比如块大小为4096字节,块组中inode位图占用一个块,一个块组的inode个数为4096,索引节点12345在磁盘上的位置可以这样计算12345/4096=3余57,所以在第3个块组(从块组0开始算)中索引节点表中的第57个表项。

/*
 * 磁盘索引节点结构
 */
struct ext2_inode {
        __le16  i_mode;         /* 文件类型和访问权限,查看S_ISREG()等函数 */
        __le16  i_uid;          /* 所有者 Uid 的低 16 位,拥有者id */
        // 文件长度,最高位没使用,最大表示2GB文件,大于2GB文件再使用i_dir_acl字段
        __le32  i_size;         /* 大小(字节) */
        __le32  i_atime;        /* 访问时间 */
        __le32  i_ctime;        /* 索引节点创建时间 */
        __le32  i_mtime;        /* 文件数据最后改变时间 */
        __le32  i_dtime;        /* 删除时间 */
        __le16  i_gid;          /* 组 ID 的低 16 位,用户组id */
        __le16  i_links_count;  /* 硬链接计数 */
        __le32  i_blocks;       /* 数据块数,以512字节为单位 */
        __le32  i_flags;        /* 文件标志 */
        union {
                struct {
                        __le32  l_i_reserved1;
                } linux1;
                struct {
                        __le32  h_i_translator;
                } hurd1;
                struct {
                        __le32  m_i_reserved1;
                } masix1;
        } osd1;                         /* OS dependent 1,特定操作系统信息 */
        // i_block 数据块指针,指向15个块,前12个指向数据,第13个一次间接地址,第14个二次间接地址,第15个三次间接地址
        __le32  i_block[EXT2_N_BLOCKS];/* 指向块的指针 */
        __le32  i_generation;   /* 文件版本,给nfs用的 */
        // i_file_acl 访问控制列表,指向一个存放增强属性的块,其他inode如果增强属性一样,可以共享同一个块
        __le32  i_file_acl;     /* 文件访问控制列表(ACL) */
        __le32  i_dir_acl;      /* 目录访问控制列表 */
        __le32  i_faddr;        /* 片地址 */
        union {
                struct {
                        __u8    l_i_frag;       /* 片编号 */
                        __u8    l_i_fsize;      /* 片大小 */
                        __u16   i_pad1;
                        __le16  l_i_uid_high;   /* 以前是reserved2[0]    */
                        __le16  l_i_gid_high;   /* 以前是reserved2[0] */
                        __u32   l_i_reserved2;
                } linux2;
                struct {
                        __u8    h_i_frag;       /* 片编号 */
                        __u8    h_i_fsize;      /* 片大小 */
                        __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;       /* 片编号 */
                        __u8    m_i_fsize;      /* 片大小 */
                        __u16   m_pad1;
                        __u32   m_i_reserved2[2];
                } masix2;
        } osd2;                         /* 特定文件系统信息 */
};

i_file_acl指向一个存放增强属性的块,其他inode如果增强属性一样,可以共享同一个块,系统调用setxattr()lsetxattr()fsetxattr()设置文件增强属性,getxattr()lgetxattr()fgetxattr()返回文件增强属性,listxattr()llistxattr()flistxattr()列出文件所有增强属性。这些系统调用是通过 chacl()setfacl()getfacl()调用的。没有正式成为POSIX标准。

struct ext2_xattr_entry {
        __u8    e_name_len;     /* 名称长度 */
        __u8    e_name_index;   /* 属性名称索引 */
        __le16  e_value_offs;   /* 值在磁盘块中的偏移量 */
        __le32  e_value_block;  /* 属性存储的磁盘块 (n/i) */
        __le32  e_value_size;   /* 属性值的大小 */
        __le32  e_hash;         /* 名称和值的哈希值 */
        char    e_name[];       /* 属性名称,可变数组/柔性数组/零长度数组 */
};

各种文件类型的存储

文件类型如下:

#define FT_UNKNOWN      0 // 未知
#define FT_REG_FILE     1 // 常规文件
#define FT_DIR          2 // 目录
#define FT_CHRDEV       3 // 字符设备
#define FT_BLKDEV       4 // 块设备
#define FT_FIFO         5 // 命名管道
#define FT_SOCK         6 // 套接字
#define FT_SYMLINK      7 // 符号链接
                         
#define FT_MAX          8 // 类型总数

常规文件刚创建时是空的,不需要数据块,可以用truncate()open()系统调用清空,如输入命令> filename

设备文件、管道、套接字所有信息都存放在inode中。

符号链接名小于60个字符就放到struct ext2_inodei_block数组中(15个4字节),如果大于60个字符就存到单独数据块中。

最后重点讲一下目录的存储,数据块包含ext2_dir_entry_2结构:

/*
 * 目录项的新版本。由于EXT2结构以英特尔字节顺序存储,并且name_len字段永远不可能大于255个字符,因此可以安全地将额外的一个字节重新分配给file_type字段。
 */
struct ext2_dir_entry_2 {
        __le32  inode;                  /* 索引节点号 */
        __le16  rec_len;                /* 目录项长度,总是4的倍数 */
        __u8    name_len;               /* 文件名长度 */
        __u8    file_type;              // 文件类型,struct ext2_dir_entry中没有
        char    name[];                 /* 文件名,最大EXT2_NAME_LEN (255)字节 */
};

我们举个例子,刚格式化完ext2,然后创建目录mkdir dir,创建文件touch file、创建软链接ln -s file link

                      file_type--+
                                 |
                    name_len--+  |
                              |  |
  address     inode   rec_len |  |   name
          +--+--+--+--|--+--|--|--|--+--+--+--+
        0 |      2    |  12 | 1| 2| . \0 \0 \0|
          +--+--+--+--|--+--|--|--|--+--+--+--+
       12 |      2    |  12 | 2| 2| .  . \0 \0|
          +--+--+--+--|--+--|--|--|--+--+--+--+--+--+--+--+--+--+--+--+
       24 |      11   |  20 |10| 2| l  o  s  t  +  f  o  u  n  d \0 \0|
          +--+--+--+--|--+--|--|--|--+--+--+--+--+--+--+--+--+--+--+--+
       44 |    15809  |  12 | 3| 2| d  i  r \0|
          +--+--+--+--|--+--|--|--|--+--+--+--+
       56 |      12   |  12 | 4| 1| f  i  l  e|
          +--+--+--+--|--+--|--|--|--+--+--+--+
       68 |      13   |  12 | 4| 7| l  i  n  k|
          +--+--+--+--|--+--|--|--|--+--+--+--+

如果删除dir,就会变成以下样子,删除的目录inode改为0,然后前一项的rec_len加上12

                      file_type--+
                                 |
                    name_len--+  |
                              |  |
  address     inode   rec_len |  |   name
          +--+--+--+--|--+--|--|--|--+--+--+--+
        0 |      2    |  12 | 1| 2| . \0 \0 \0|
          +--+--+--+--|--+--|--|--|--+--+--+--+
       12 |      2    |  12 | 2| 2| .  . \0 \0|
          +--+--+--+--|--+--|--|--|--+--+--+--+--+--+--+--+--+--+--+--+
       24 |      11   |  32 |10| 2| l  o  s  t  +  f  o  u  n  d \0 \0|
          +--+--+--+--|--+--|--|--|--+--+--+--+--+--+--+--+--+--+--+--+
       44 |      0    |  12 | 3| 2| d  i  r \0|
          +--+--+--+--|--+--|--|--|--+--+--+--+
       56 |      12   |  12 | 4| 1| f  i  l  e|
          +--+--+--+--|--+--|--|--|--+--+--+--+
       68 |      13   |  12 | 4| 7| l  i  n  k|
          +--+--+--+--|--+--|--|--|--+--+--+--+
;