FreeBSD - ext2 文件系统基本数据结构分析(下)

暑假日常
清晨,Douyiya 被急促的脚步声吵醒了,室友 狒狒 因为要参加电子设计大赛,所以早早起床收拾今天所要用到的资料。简单寒暄两句之后,他便急匆匆地走出了寝室。Douyiya 起身张望,又是再熟悉不过的场景: 老马 彻夜未归,阿狗 和 龙哥 还在睡梦中。想到 Nanami 和 Nanase 因为天气太过炎热而回家避暑,Douyiya 也只能轻轻叹了口气,“又是无聊的一天呀!” 随后就约着在计算机爱好者社团结识的朋友 Ling,一块去图书馆分享最近学习的感悟。
Douyiya 的汇报
Ling 是一个说话语速慢慢悠悠,但思维非常敏捷,并且涉猎广泛的技术大佬,所以 Douyiya 也非常喜欢向她请教问题。两人碰面之后互道了早安,随后便找了一个安静的位置坐了下来。“最近阅读了 ext2 文件系统的源码,想跟你分享一下,顺便帮我看看有没有理解上的错误。” Douyiya 笑眯眯地说到。“我之前读过这些代码,你直接开始讲吧。” Douyiya 点了点头:
我们可以认为操作系统管理着一棵巨大的 文件树,无论是目录文件,还是普通文件,都是树上的一个节点。如果用户指定的是文件的绝对路径,操作系统会从根节点逐级查找,直到定位目标文件;如果是相对路径,则从当前目录下开始查找。所以,目录文件必须要包含一些信息,用来表示出它的子文件到底有哪些。ext2 文件系统的设计如下:

/* * Structure of a directory entry */ #defineEXT2FS_MAXNAMLEN255struct ext2fs_direct { uint32_t e2d_ino; /* inode number of entry */ uint16_t e2d_reclen; /* length of this record */ uint16_t e2d_namlen; /* length of string in e2d_name */ char e2d_name[EXT2FS_MAXNAMLEN]; /* name with length<=EXT2FS_MAXNAMLEN */ }; /* * The new version of the directory entry.Since EXT2 structures are * stored in intel byte order, and the name_len field could never be * bigger than 255 chars, it's safe to reclaim the extra byte for the * file_type field. */ struct ext2fs_direct_2 { uint32_t e2d_ino; /* inode number of entry */ uint16_t e2d_reclen; /* length of this record */ uint8_te2d_namlen; /* length of string in e2d_name */ uint8_te2d_type; /* file type */ chare2d_name[EXT2FS_MAXNAMLEN]; /* name with * length<=EXT2FS_MAXNAMLEN */ };

  • e2d_ino: 文件对应的唯一 inode number
  • e2d_name: 存放文件名的字符数组,最大不超过 255 bytes
  • e2d_namlen: 文件名的长度
  • e2d_type: 文件类型,比如普通文件、设备文件、目录、链接文件等等
  • 【FreeBSD - ext2 文件系统基本数据结构分析(下)】e2d_reclen: 与文件名的长度有关,sizeof(uint64_t) + e2d_namelen
    该结构体对应两种状态,在磁盘上和在内存中,操作系统首先需要将数据从磁盘读入内存,使用完成之后再回写到磁盘当中。出于节省空间的考虑,在磁盘上给它分配的存储区域大小是 e2d_reclen,在内存中直接分配 sizeof(struct ext2fs_direct_2) 大小的空间
目录也是文件,操作系统会给它分配 inode 和磁盘块。所以我们只需要将所有目录子文件 对应的 struct ext2fs_direct_2 作为 entry 放到其磁盘块当中,查找的时候通过某种方法遍历这些 entry,就可以知道该目录下存在哪些子文件。如下图所示:
FreeBSD - ext2 文件系统基本数据结构分析(下)
文章图片

因为每个文件的名称长度不可能完全一致,所以磁盘块上的每个 entry 所占用空间的大小也是不同的。上图表示的是 entry 的线性排布方式,所以遍历就是从头开始,一个接一个向后解析。但是随着子文件数量的增长,这种方法所花费的时间会越来越长,影响文件查找效率。为了解决这个问题,开发者们在 ext2 中引入了 hashtree 查找机制。目前还没详细研究,以后再来填坑。。。
每一个 file entry,可能会在末尾还会包含一个 struct ext2fs_direct_tail (并不一定会存在,需要在 ext2 挂载前使能)
struct ext2fs_direct_tail { uint32_t e2dt_reserved_zero1; /* pretend to be unused */ uint16_t e2dt_rec_len; /* 12 - 固定 12 字节大小*/ uint8_te2dt_reserved_zero2; /* zero name length */ uint8_te2dt_reserved_ft; /* 0xDE, fake file type */ uint32_t e2dt_checksum; /* crc32c(uuid+inum+dirblock) */ };

这个结构体是做什么用的呢? 其实就是为了对磁盘块中的数据进行校验。数据在磁盘和内容之间流动的时候,可能会存在一些异常操作导致数据损坏。这时操作系统就可以计算 file entry 的 crc32 并与读取出的字段进行对比。如果一致,则可以证明数据没有被损坏。
实际调试中没有用使能该属性,仅仅是从代码逻辑推测 checksum 对应的是磁盘块中的 entry,而不是整个磁盘块。如果有使用到还是实际验证一下为好
目录并不是一成不变的,可能在用户使用的过程中会出现频繁的文件创建和删除,磁盘块中的 file entry 也会随之申请和释放。那操作系统如何确定磁盘块中是否包含有足够的空间用于存放新创建的 file entry? 这就涉及到另外一个数据结构 struct ext2fs_searchslot
enum slotstatus { NONE, COMPACT, FOUND }; struct ext2fs_searchslot { enum slotstatus slotstatus; doff_tslotoffset; /* offset of area with free space */ intslotsize; /* size of area at slotoffset */ intslotfreespace; /* amount of space free in slot */ intslotneeded; /* sizeof the entry we are seeking */};

操作系统会利用 struct inode 中的
int32_ti_count; /* Size of free slot in directory. */ doff_ti_endoff; /* End of useful stuff in directory. */ doff_ti_diroff; /* Offset in dir, where we found last entry. */ doff_ti_offset; /* Offset of free space in directory. */

几个字段,查找符合新创建 file entry 要求的可用空间,并将基本信息保存在 struct ext2fs_searchslot 返回给文件系统上层调用函数,这样就获取到了一个可用的 “slot”。
用户有时候会对文件的访问权限有一些特殊的需求,正常情况下就是设置文件的 owner / group / other 的可读可写可执行等等,更特殊一点就是 SUID / SGID 等。但假如用于想要对某个具体的用户设置更加特殊的访问权限,那么这种方式就难以胜任了。所以,针对这样的需求,ext2 文件系统就引入了文件的 扩展属性 机制。设计思想就是为文件单独分配一个或者多个磁盘块,并依照 POSIX 中的一些通用方法将某个文件特有的属性信息放到里边。操作系统在处理文件操作时,就会同时检查基本属性和扩展属性,判断该用户时候是否可以访问此文件。
/* * Ext4 extent tail with csum */ struct ext4_extent_tail { uint32_t et_checksum; /* crc32c(uuid+inum+extent_block) */ }; /* * Ext4 file system extent on disk. */ struct ext4_extent { uint32_t e_blk; /* first logical block */ uint16_t e_len; /* number of blocks */ uint16_t e_start_hi; /* high 16 bits of physical block */ uint32_t e_start_lo; /* low 32 bits of physical block */ }; /* * Extent index on disk. */ struct ext4_extent_index { uint32_t ei_blk; /* indexes logical blocks */ uint32_t ei_leaf_lo; /* points to physical block of the * next level */ uint16_t ei_leaf_hi; /* high 16 bits of physical block */ uint16_t ei_unused; }; /* * Extent tree header. */ struct ext4_extent_header { uint16_t eh_magic; /* magic number: 0xf30a */ uint16_t eh_ecount; /* number of valid entries */ uint16_t eh_max; /* capacity of store in entries */ uint16_t eh_depth; /* the depth of extent tree */ uint32_t eh_gen; /* generation of extent tree */ }; /* * Save cached extent. */ struct ext4_extent_cache { daddr_tec_start; /* extent start */ uint32_t ec_blk; /* logical block */ uint32_t ec_len; uint32_t ec_type; }; /* * Save path to some extent. */ struct ext4_extent_path { int index_count; uint16_t ep_depth; uint64_t ep_blk; char *ep_data; struct ext4_extent *ep_ext; struct ext4_extent_index *ep_index; struct ext4_extent_header *ep_header; };

然后扩展属性中最常见的就是 访问控制链表 (ACL,Access Control List)。我们可以认为它是扩展属性的子集,会对文件访问属性进行更加细致的设置
struct ext2_acl_entry { int16_tae_tag; int16_tae_perm; int32_tae_id; }; struct ext2_acl_entry_short { int16_tae_tag; int16_tae_perm; }; struct ext2_acl_header { int32_ta_version; };

由于本人在新设计的文件系统中没有使用到文件的扩展属性(操作系统本身只有一个特权用户),所以这部分代码并没有在实践中进行检验。小伙伴们如果用到了,也是要亲自调试一下的。有问题也可以留言,一起探讨,互相进步
我们平时使用的过程中一般不会设置过多的额外属性,所以分配给文件用来存放扩展属性的磁盘块基本上一个就够了,这些属性 entry 就可以在磁盘块中以线性方式排布,上述数部分据结构就用不到了。假如真出现了一个文件需要包含特别多扩展属性的情况,那文件系统就会以 二叉平衡搜索树 对它们进行排布和分配磁盘块。(细节还没去研究,以后有时间再来填坑...)
FreeBSD - ext2 文件系统基本数据结构分析(下)
文章图片

上图只是简单示意图,具体还是要参考 ext2fs disk layout
看完上述这些数据结构之后,我们就要考虑它们在磁盘中到底是如何分布的。现有设计如下:
FreeBSD - ext2 文件系统基本数据结构分析(下)
文章图片

可以看到,整个磁盘好像是逻辑上被分成了许多部分。这里就要引入 ext2 文件系统中另外一个重要设计,块组。目前机械磁盘对于数据的访问还是通过改变磁头位置的方式实现的,此过程必然会或多或少的花费一些时间。那就要想办法把这种时间上的浪费降到最低。理想情况就是文件数据是完全连续存储的,这样只要从数据起始位置连续读取磁盘块即可。虽然不能完全实现上述情形,但可以将同一个文件的数据存储在距离尽可能近的、顺序排布的磁盘块中。开发者同时结合了磁盘的硬件特性,就在 ext2 文件系统中添加了块组结构,将整个磁盘分为多个组进行管理,为的就是让操作系统能够以最高效率实现对文件的访问。
/* ext2 file system block group descriptor */ struct ext2_gd { uint32_t ext2bgd_b_bitmap; /* blocks bitmap block */ uint32_t ext2bgd_i_bitmap; /* inodes bitmap block */ uint32_t ext2bgd_i_tables; /* inodes table block*/ uint16_t ext2bgd_nbfree; /* number of free blocks */ uint16_t ext2bgd_nifree; /* number of free inodes */ uint16_t ext2bgd_ndirs; /* number of directories */ uint16_t ext4bgd_flags; /* block group flags */ uint32_t ext4bgd_x_bitmap; /* snapshot exclusion bitmap loc. */ uint16_t ext4bgd_b_bmap_csum; /* block bitmap checksum */ uint16_t ext4bgd_i_bmap_csum; /* inode bitmap checksum */ uint16_t ext4bgd_i_unused; /* unused inode count */ uint16_t ext4bgd_csum; /* group descriptor checksum */ uint32_t ext4bgd_b_bitmap_hi; /* high bits of blocks bitmap block */ uint32_t ext4bgd_i_bitmap_hi; /* high bits of inodes bitmap block */ uint32_t ext4bgd_i_tables_hi; /* high bits of inodes table block */ uint16_t ext4bgd_nbfree_hi; /* high bits of number of free blocks */ uint16_t ext4bgd_nifree_hi; /* high bits of number of free inodes */ uint16_t ext4bgd_ndirs_hi; /* high bits of number of directories */ uint16_t ext4bgd_i_unused_hi; /* high bits of unused inode count */ uint32_t ext4bgd_x_bitmap_hi; /* high bits of snapshot exclusion */ uint16_t ext4bgd_b_bmap_csum_hi; /* high bits of block bitmap checksum */ uint16_t ext4bgd_i_bmap_csum_hi; /* high bits of inode bitmap checksum */ uint32_t ext4bgd_reserved; };

块组跟超级快的内容非常类似,只不过超级快是管理整个磁盘,而块组只是管理当前组。所以,文件系统给文件分配磁盘块时,会优先考虑当前块组中存在的空闲块。假如被全部占用,才会去其他块组中寻找可用块。
未完待续
Ling 听完了 Douyiya 的讲解,连连点头:“基本上没什么大问题。不过目前只是看了这些吗,函数的代码有没有看?” “昂,,还没有,最近花了点时间练习钢琴,杰伦的《晴天》,哈哈哈!”
Ling 听完笑着说道:“不错不错!那我感觉你后面可以先读inode 和磁盘块分配相关实现代码,这两部分是文件系统最基础的功能之一。” Douyiya 笑着回答到:“好,我知道了。”

    推荐阅读