Ext4文件系统架构分析(二)
Ext4 文件系统 ioctl功能概述
ioctl.c 源码功能概述
Ext4的ioctl提供给用户以下接口,以方便用户更改文件系统的各种设置和状态:
(1) EXT4_IOC_GETFLAGS: 获取inode的标志位,用户获取当前的inode标志位信息;
(2) EXT4_IOC_SETFLAGS: 设置inode的标志位,用户为inode标志位设置新的信息;
(3) EXT4_IOC_GETVERSION 或EXT4_IOC_GETVERSION_OLD:获取inode->i_generation,用户获取当前inode对应的文件的版本号;
(4) EXT4_IOC_SETVERSION 或EXT4_IOC_SETVERSION_OLD:设置inode->i_generation,用户设置当前inode对应的文件的版本号;
(5) EXT4_IOC_WAIT_FOR_READONLY:用于JBD2调试时使用,未定义编译开关CONFIG_JBD2_DEBUG 则该ioctl命令不可用;
(6) EXT4_IOC_GROUP_EXTEND: 块组扩展,在文件系统的最后一个块组上扩展文件系统的容量;
(7) EXT4_IOC_MOVE_EXT:交换一个文件的指定范围到另一个文件的指定位置;
(8) EXT4_IOC_GROUP_ADD:增加块组扩展文件系统容量;
(9) EXT4_IOC_MIGRATE:将文件数据块的映射方式由间接映射迁移为extents方式;
(10) EXT4_IOC_ALLOC_DA_BLK:强制将延迟分配的所有数据块分配给文件。
ioctl源码分析之设置/获取文件版本号
1. EXT4_IOC_GETVERSION获取文件的版本号
Ext4 的EXT4_IOC_GETVERSION命令用于获取文件的版本号(inode.i_generation),将获取的文件版本号传给ioctl的第三个参数unsigned int arg:
ioctl(fd, EXT4_IOC_GETVERSION, arg )
2. EXT4_IOC_SETVERSION设置文件的版本号
Ext4 的EXT4_IOC_SETVERSION命令用于设置文件的版本号,将要设置的最终版本号作为ioctl的第三个参数unsigned int arg传入ioctl函数中:
ioctl(fd, EXT4_IOC_SETVERSION, arg )
3. 设置文件版本号的操作过程
(1) 首先确定用户对文件具有访问权限且对文件所在的文件系统具有写权限(因为设置inode标志成功会引起元数据更新操作)。并将用户设置的文件版本号(ioctl命令的第三个参数)拷贝到内核空间。
(2) 发起一个日志事务,用于后面提交日志事务时更新inode;
(3) 在日志中建立元数据更新事务,用于更新inode元数据;
(4) 更新inode元数据,设置新的文件版本号,更新inode到磁盘;
(5) 结束日志事务处理;
(6) 结束对文件系统的写操作;
ioctl源码分析之设置/获取Inode标志位
1. EXT4_IOC_GETFLAGS获取inode标志位
Ext4 的EXT4_IOC_GETFLAGS命令用于获取inode已设置的标志信息,将获取的inode标志位的信息的结果传给ioctl的第三个参数unsigned int arg:
ioctl(fd, EXT4_IOC_GETFLAGS, arg )
2. EXT4_IOC_SETFLAGS设置inode标志位
Ext4 的EXT4_IOC_SETFLAGS命令用于设置inode的标志信息,将要设置的最终inode标志位的信息作为ioctl 的第三个参数unsigned int arg传入ioctl函数中:
ioctl(fd, EXT4_IOC_SETFLAGS, arg )
虽然Inode具有大量的标志,但是最终用户可设置的inode标志只有五个,其他标志由内核默认设置。用户可直接设置的5个inode标志是:
#define EXT4_SYNC_FL 0x00000008 /* 同步更新 */
#define EXT4_NOATIME_FL 0x00000080 /* 不更新文件访问时间 */
#define EXT4_APPEND_FL 0x00000020 /* 文件仅能追加写 */
#define EXT4_IMMUTABLE_FL 0x00000010 /* 不可改变的文件 */
#define EXT4_DIRSYNC_FL 0x00010000 /* 目录同步操作(仅目录可用) */
3. 设置inode标志位的操作过程
(1) 首先确定用户对文件具有访问权限且对文件所在的文件系统具有写权限(因为设置inode标志成功会引起元数据更新操作)。并将用户设置的inode标志flags (ioctl命令的第三个参数)拷贝到内核空间。
(2) 获取inode互斥锁,然后判断inode是否是配额文件,如果是配额文件,则不可对其设置inode标志;直接跳出
(3) 接着会对用户要求设置的inode标志flags进行适当的修改:
修改的原因有很多:
1)屏蔽不适合给定类型的inode的标志。比如要求设置的标志中含有与文件类型不匹配的标志(将目录的标志设置给普通文件可不行),则将不匹配的标志清除掉;
2)如果用户要求设置标识中含有EXT4_APPEND_FL 或 EXT4_IMMUTABLE_FL标志,判断是否需要是否具有权限修改JOURNAL_DATA 标志。要检查用户是否有相应的capability。如果不具备,那么也无法设置inode标志;
3)如果inode原有标识中已设置 EXT4_EXTENTS_FL 标志,则不支持清除extents标志,如果用户要求设置的新标识中无使用extents的标志,则退出;
如果原inode中未设置使用extents的标志,而要设置的inode标志中含使用extents的标志,并从设置的inode标志中去掉使用extents的标志,并在随后迁移文件支持extents方式
4)如果用户要求设置的标识中含有EXT4_EOFBLOCKS_FL标志且inode原有标志中没有EXT4_EOFBLOCKS_FL标志,则退出;
如果用户要求设置的标识中无EXT4_EOFBLOCKS_FL标志且inode原有标志中EXT4_EOFBLOCKS_FL标志,则把inode的全部数据块索引信息清除;
5)发起日志事务操作,准备更新元数据块——inode数据。
6)重新设置用户设置的flags:首先,保存用户设置的flags中那些用户可修改的标志,屏蔽掉用户不可修改的标志;然后,加上原有inode中用户不可修改的标志;
7)设置inode标志,更改inode相关信息(inode更改时间),更新inode内容到磁盘;
8)结束日志事务处理;
9)如果新设置了EXT4_JOURNAL_DATA_FL标志,则刷新日志内容,设置日志标志
10) 迁移inode,使其支持extents。用于将用间接映射方式组织的文件数据块用extents的方式组织。
(4) 释放inode互斥锁,结束对文件系统的写操作,返回
ioctl源码分析之扩展EXT4文件系统最后一个块组大小
1. 扩展Ext4文件系统最后一个块组的大小
Ext4 的EXT4_IOC_GROUP_EXTEND命令用于扩展文件系统最后一个块组的大小,它通过扩展文件系统最后一个块组的方式来扩展文件系统的大小。能扩展的最大范围是将文件系统的最后一个块组扩展为一个完整的块组(128MB)。用户可以通过ioctl函数使用Ext4文件系统的Ioctl命令 EXT4_IOC_GROUP_EXTEND 将用户希望扩展后最终的文件系统所具有的(文件系统)数据块的个数给ioctl的第三个参数unsigned int arg:
ioctl(fd, EXT4_IOC_GROUP_EXTEND, arg )
2. EXT4_IOC_GROUP_EXTEND扩展文件系统的限制
利用Ext4 的EXT4_IOC_GROUP_EXTEND命令扩展文件系统最后一个块组的大小的方式来扩展文件系统的大小,它对文件系统的扩展结果受到以下限制:
(1)文件系统所在分区必须要有多余的空闲空间,且这些空间不属于当前文件系统;
(2)该ioctl命令不用于缩小文件系统。也就是要求最终扩展到的数据块个数不能少于文件系统现有数据块个数;
(3)如果用户设置希望扩展后最终的文件系统所具有的(文件系统)数据块的个数(ioctl的第三个参数arg)为0或者与文件系统现有数据块个数相同,则该命令不做任何事而直接返回。也就是既不对文件系统进行扩展,也不报错。
(4)用户指定扩展到的数据块数目不超过最后一个块组成为完整块组时文件系统具有的数据块数目时,扩展能够正常进行;
(5)用户指定扩展到的数据块数目超过最后一个块组成为完整块组时文件系统所具有的数据块个数时,扩展能正常进行,但实际只扩展到使最后一个块组成为完整块组为止;
(6) 用户指定扩展到的数据块数目使得文件系统的大小超过16TB(2^32个数据块)时,则不进行扩展。
3. 扩展文件系统最后一个块组的操作过程
(1) 首先确定用户对文件具有访问权限且对文件所在的文件系统具有写权限(因为设置inode标志成功会引起元数据更新操作)。并将用户指定扩展到的数据块数目(ioctl命令的第三个参数)拷贝到内核空间。
(2) 确认进程对文件系统具有写权限;
(3) 调用ext4_group_extend()函数扩展文件系统的最后一个块组。如果文件系统支持日志,那么要在日志中建立扩展文件系统最后一个块组的日志事务,并设置barrier(作用如同字面意思,barrier之后提交的操作必须要在barrier释放之后才能处理)。日志事务处理完毕后刷新日志并释放设置的barrier。扩展文件系统最后一个块组函数ext4_group_extend()主要执行流程如下:
a) 获取文件系统现有数据块个数以及块组个数;
b) 检查用户设置的文件系统最终扩展到的数据块的数目是否有效。是否有效的依据参见上一小节的(2)、(3)、(4)、(5)、(6)小点的描述;
c) 因为扩展的是最后一个数据块组,所以先要获取当前文件系统的最后一个块组的块组号和最后一个数据块在块组中的偏移offset;
d) 接着要计算扩展最后一个块组成功,实际需要在最后一个块组中增加的数据块的个数。这个时候会按以下步骤处理:
i. 如果步骤c)计算的当前文件系统最后一个数据块的偏移是0,也就意味着最后一个块组已是完整块组,那么无法使用扩展最后一个块组的方法扩展文件系统了,因而报错退出;
ii. 如果步骤c)计算的当前文件系统最后一个数据块的偏移不为0,那么实际最多可加入的数据块的个数为EXT4_BLOCKS_PER_GROUP(sb) – offset,即一个完整块组包含的数据块的个数减去当前文件系统最后一个数据块的在块组中的偏移;
iii. 如果加入的数据块的个数过多,导致数据块个数溢出,则告警退出。这种情况出现在当前的文件系统已经相当大了,差不多是系统能够支持的最大文件系统的情况,此时虽然最后一个块组未完整,但是如果补充使其完整,那么就会导致文件系统数据块个数溢出。(但是这种情况永远不会出现,因为传入的参数不超过32位);
iv. 如果加入的数据块的个数小于允许加入的最大个数,则加入实际的数据块个数;
v. 如果加入的数据块的个数大于允许加入的最大个数,则仅加入允许加入的最大数据块个数;
e) 确定了最终增加的数据块的个数之后呢,接着就要看文件系统所在的设备(分区)大小是否满足要求了。如果设备剩余空间不够,那么也只能告警退出了;
f) 接着发起一个日志事务,这个事务要修改三个数据块(超级块、数据块位图、块组描述符)的内容;
g) 获取超级块sb->s_resize_lock互斥锁。如果获取到互斥锁后,发现当前文件系统的数据块个数已经发生改变,则意味着已经有其他调整大小的程序运行过,那么先前计算的加入数据块的个数什么的都已经无效了,只好打印提示信息退出了,同时释放获取的互斥锁;
h) 获取超级块sb->s_resize_lock互斥锁后发现文件系统数据块个数没有改变,那么就可以开始执行扩展大小的核心操作了。首先修改与超级块相关的日志事务(当然如果修改出错也会告警退出同时释放互斥锁),接着统计扩展后文件系统的数据块的个数,然后标记超级块为脏。这样就完成了超级块的修改,因而可以释放掉超级块sb->s_resize_lock互斥锁。
i) 接下来,就是加入数据块的操作了。调用ext4_add_groupblocks(handle, sb, o_blocks_count, add)将add个数据块加入到文件系统的最后一个块组中。这一步操作成功后,会将文件系统的最后一个块组的块组描述符所在的数据块以及最后一个块组上的数据块位图都标记为脏,然后更新到磁盘。ext4_add_groupblocks()函数的具体操作流程如下:
1. 获取加入的数据块所在的块组号以及加入的数据块起始数据块偏移;
2. 检查加入数据块的个数是否导致块组跨界;
3. 读取块组中的块位图与块组描述符;
4. 判断加入数据块是否会导致出错。因为除了超级块和块组描述符表外,其他元数据的位置不固定,因而可能造成出错。以下情况会导致加入数据块出错:
a) 块组的数据块位图所在的数据块号数位于加入块组的起始物理数据块号与加入后的所有数据块总和之间——块组中无数据块位图;
b) 块组的inode位图所在的数据块号数位于加入块组的起始物理数据块号与加入后的所有数据块总和之间——块组中无inode位图;
c) 加入块组的起始物理数据块号位于inode表中间——inode表不完整;
d) 加入数据块后的最后一个数据块位于inode表中间——inode表不完整。
5. 在位图中加入数据块,然后修改块组描述符;
6. 清除块位图新加入的counts个数据块的标志,使其为空闲可用状态;
7. 锁定块组,更新块组描述符的相关内容,如块组中空闲块个数,块组校验和等。操作成功后解锁块组;
8. 如果设置了flex_bg特性,计算块组所在的flex_bg,进而修改flex_bg描述符信息,也就是空闲数据块的个数;
9. 设置块组需初始化位,重新载入具有新的位图信息的块组描述符元数据。这主要是为了更新内存中块组描述符的信息(更新块组的空闲数据块个数、更新块组中连续2^n个数据块构成的片段的个数);
10. 将数据块位图所在数据块标记为脏;
11. 将块组描述符所在数据块标记为脏。
j) 提交日志事务,结束日志操作。主要是更新前面提到的三个数据块(超级块、数据块位图、块组描述符)的日志提交及更新到磁盘;
k) 更新文件系统的备份元数据(主要是超级块和块组描述符表的备份)。
(4) 结束对文件系统的写操作,返回;
ioctl源码分析之增加块组扩展EXT4文件系大小
1. 增加块组扩展Ext4文件系统
Ext4 的EXT4_IOC_GROUP_ADD命令用于增加块组来扩展文件系统的大小,它通过向文件系统中加入新的块组的方式来扩展文件系统的大小。单次能扩展的最大范围是向文件系统中增加一个完整的块组(128MB,4KB数据块计算)。用户可以通过ioctl函数使用Ext4文件系统的Ioctl命令 EXT4_IOC_GROUP_ADD将用户希望加入的新块组的相关信息struct ext4_new_group_input的地址传给ioctl的第三个参数unsigned int arg(因此用户对当前文件系统必须要有先验知识):
ioctl(fd, EXT4_IOC_GROUP_ADD, arg )
2. EXT4_IOC_GROUP_ADD扩展文件系统的限制
利用Ext4 的EXT4_IOC_GROUP_ADD命令向文件系统中增加新块组的方式来扩展文件系统的大小,它对文件系统的扩展结果受到以下限制:
1. 文件系统层面的限制,这些限制在函数ext4_group_add()中给出:
(1)如果input对应的块组所在的位置为GDT中某个数据块的第一个块组,且文件系统不是以稀疏方式管理元数据,那么不允许增加块组,退出;
(2)若加入块组中包含的数据块个数使得加入后文件系统后数据块个数溢出,则不允许增加块组,退出;
(3)若加入块组后使得加入后文件系统inode个数溢出,则不允许增加块组,退出;
(4)如果文件系统没有预留用于预留块组描述符的inode,或者文件系统预留GDT数据块个数为0,那么系统报无预留GDT数据块无法调整大小的错误。 如果以上都没有问题的话,那么就获取预留块组描述符的inode,准备增加块组。
2.新增加的块组的自身参数导致的限制,由函数verify_group_input()检验这些限制:
(1)新增加的块组的块组号是否有效。新加入的块组号要比系统现有块组号大1,否则块组号无效;
(2)现有文件系统最后一个块组不是完整块组则不允许增加新块组;
(3)新加入的块组中预留数据块超过块组中数据块个数的20%,则不允许增加该新块组;
(4)新加入的块组中没有空闲块,也就是新加入的块组的数据块个数存放的只能是块组的元数据,甚至是仅仅存放元数据的空间都不够,则不允许增加该新块组;
(5)新块组中最后一个数据块的内容读取不了(主要原因是空间不足),则不允许增加该新块组;
(6)新加入的块组指定的块位图不在新块组中,则不允许增加该新块组;
(7)新加入的块组指定的Inode位图不在新块组中,则不允许增加该新块组;
(8)新加入的块组指定的Inode表不在新块组中,则不允许增加该新块组;
(9)新块组的块位图与Inode位图是同一个数据块,则不允许增加该新块组;
(10)新加入的块组指定的块位图在新块组的inode表中,则不允许增加该新块组;
(11)新加入的块组指定的Inode位图在新块组的inode表中,则不允许增加该新块组;
(12)新加入的块组指定的块位图在新块组的GDT中,则不允许增加该新块组;
(13)新加入的块组指定的Inode位图在新块组的GDT中,则不允许增加该新块组;
(14)新加入的块组指定的Inode表在新块组的GDT中,则不允许增加该新块组;
正是由于这些限制的存在,使得增加一个新块组没有像扩展最后一个块组的操作那样简单,所以在增加块组之前必须做够充足的准备,以防止以上限制导致增加块组失败。获取这些信息的最简单方式是使用tune2fs -l 命令:
3. 增加新块组的操作过程
1. 调用函数setup_new_group_blocks()为新增块组创建并初始化数据块位图、inode位图以及Inode表。该函数的执行步骤如下:
(1)获取块组的第一个数据块号,获取块组中预留GDT数据块的个数,获取块组中块组描述符表使用的数据块的个数,这几个数据对块组的数据块位图有影响;
(2)发起一个文件系统超级块更新的事务,后续(增加块组操作成功后)更新超级块,这个超级块更新事务就是调整文件系统大小,因为调整大小需要修改超级块元数据;
(3)获取s_resize_lock互斥锁用于调整文件系统大小;
(4)检查新增加块组是否有效。块组号不等于当前系统拥有的块组个数,表明已经有其他增加块组的程序运行过或者正在运行,因而不能增加相同的块组,要退出;
(5)将新块组中的块位图置零;
(6)如果新的块组中应当含有超级块的备份,则将块位图的第一位置位,标记为已使用;
(7)拷贝所有的GDT数据块到新块组的备份元数据块中,并在块位图中将相应的数据块设置为1;
(8)将新块组中的预留GDT数据块中全部写0填充,并在块位图中将相应的数据块设置为1;
(9)依次在块位图中将块位图所在的块的位置1、将inode位图所在的块的位置1;
(10)初始化inode表。将新块组中的inode表数据块中全部写0填充,然后将该inode表数据块对应的块位图中的位置1,每次这样处理一个inode表数据块;
(11)设置块位图中的其他块(这些块不存在,因而块位图置1);
(12)标记新块组中块位图数据块为脏;
(13)将新块组中inode位图数据块清零;
(14)设置inode位图中的其他inode(这些inode不存在,因而inode位图置1);
(15)标记新块组中inode位图数据块为脏;
(16)释放s_resize_lock互斥锁;
(17)提交日志,结束新块组内元数据更新事务。
2. 添加新块组后至少要修改超级块以及一个GDT数据块. 如果加入块组超出当前的最后一个GDT数据块,则还需要修改inode(预留GDT的inode)和GDT数据块自身.如果加入的块组具有超级块/GDT的备份,则需要更新的元数据更多;
3. 获取s_resize_lock互斥锁,准备更新文件系统超级块等元数据;
4. 判断文件吸引是否已被其他程序调整,如果文件系统已被其他程序调整,则退出;
5. 准备更新超级块;
6. 更新GDT元数据;
7. 至此,已经建立了新的块组,现在要激活它,使其可用;
(1) 最关键的是 sbi->s_groups_count:只要它还包含旧的值,那么就访问不到新块组.因而,首先要为新块组更新用于新块组的所有的描述符元数据;然后更新总的磁盘数据块数目;然后更新块组数目以激活块组;最后更新空闲数据块数目以使得系统可以使用新的磁盘数据块;
a) 为新块组更新块组描述符块;
b) 为新块组在内存中创建块组描述符信息
(2) 接下来,使得新的数据块和inodes有效。在增加文件系统中的块组数目之前先进行这一步,这样一旦块组激活,它上面的所有数据块和inodes都已生效;
a) 获取文件系统最新数据块个数;
b) 获取文件系统最新inode个数;
(3) 更新超级块的s_groups_count元素,即更新全局文件系统域,增加块组个数;
(4) 更新空闲数据块个数,然后更新空闲inode个数;
(5) 如果文件系统支持Flex_bg特性,则更改新块组所在的flex_bg的信息(修改空闲数据块个数、空闲inode个数);
8. 标记超级块为脏;
9. 释放s_resize_lock互斥锁;
10. 提交日志事务更新超级块;
11. 更新文件系统中超级块的拷贝,然后更新文件系统中GDT的拷贝。