1. 前言
文件系统可能在任何两次写入之间崩溃或者断电,在一些必须更新两个磁盘上结构的特定操作场景下,如果其中一个首先到达磁盘,此时发生了崩溃,磁盘上的结构就会处于不一致的状态。崩溃后,系统重启并尝试重新挂载文件系统,如何确保磁盘上的结构保持在合理的状态,成为所有文件系统需要解决的问题。
古老的解决方案是通过文件系统检查程序(file system checker),即fsck。该方案的思想是并不妨碍或者阻止磁盘不一致状态的产生,而是在崩溃后重启时检查并修复不一致状态。fsck是UNIX系统下的工具,用于查找并修复磁盘的不一致状态。fsck会通过检查超级块的合理性、扫描inode等一系列操作来确保当前磁盘的一致性。构建有效工作的fsck通常需要复杂的文件系统知识,而且对于大容量磁盘,扫描整个磁盘可能需要数小时。
更加通用的一种解决方案是日志记录(journaling,write-ahead logging),该解决方案的思想是在磁盘中开辟一块空间用于记录日志,通过每次写入时增加一点记录写入内容的开销,阻止不一致状态的产生。许多现代文件系统如ext3、ext4、NTFS都使用该种解决方案。其中ext4确保数据一致性主要分为以下几步:
数据写入。
首先将数据写入磁盘中的最终位置。日志元数据写入。
将本次写入事务的开始数据块和元数据写入日志。日志提交。
将事物提交数据块写入日志。元数据写入磁盘。
将元数据更新的内容写入磁盘最终位置。释放。
在日志的超级块中将事务标记为空闲。
其中先写入被指对象(本次写入的数据),然后写入指针对象(inode元数据),可以保证inode中的数据块地址永远不会指向垃圾数据。
2. 日志记录块设备(jbd)
jbd是Linux内核中的通用块设备日志记录层,它独立于文件系统,ext3,ext4和OCFS2都在使用JBD,其中jbd2随着ext4文件系统在2006诞生。在Linux内核版本4.14中,jbd2共包含六个源代码文件,位于目录/fs/jbd2/下。
- journal
journal 在英文中有“日志”之意,在jbd中journal既是磁盘上日志空间的代表,又起到管理内存中为日志机制而创建的handle、transaction等数据结构的作用,可以说是整个机制的代表。
- transaction
jbd为了提高效率,将若干个handle组成一个事务,用transaction来表示。对日志读写来说,都是以transaction为单位的。在处理日志数据时,transaction具有原子性,即恢复时,如果一个transaction是完整的,其中包含的数据就可用于文件系统的恢复,否则,忽略不完整的transaction。
- commit
所谓提交,就是把内存中transaction中的磁盘缓冲区中的数据写到磁盘的日志空间上。注意,jbd是将缓冲区中的数据另外写一份,写到日志上,原来的kernel将缓冲区写回磁盘的过程并没有改变。
在内存中,transaction是可以有若干个的,而不是只有一个。transaction可分为三种,一种是已经commit到磁盘日志中的,它们正在进行checkpoint操作;第二种是正在将数据提交到日志的transaction;第三种是正在运行的transaction。正在运行的transaction管理随后发生的handle,并在适当时间commit到磁盘日志中。注意正在运行的transaction最多只可能有一个,也可能没有,如果没有,则handle提出请求时,则会按需要创建一个正在运行的transaction。
- checkpoint
当一个transaction已经commit,那么,是不是在内存中它就没有用了呢?好像是这样,因为其中的数据已经写到磁盘日志中了。但是实际上不是这样的。主要原因是磁盘日志是个有限的空间,比如说100MB,如果一直提交transaction,很快就会占满,所以日志空间必须复用。
其实与日志提交的同时,kernel也在按照自己以前的方式将数据写回磁盘。试想,如果一个transaction中包含的所有磁盘缓冲区的数据都已写回到磁盘的原来的位置上(不是日志中,而是在磁盘的原来的物理块上),那么,该transaction就没有用了,可以被删除了,该transaction在磁盘日志中的空间就可以被回收,进而重复利用了。
- revoke
假设有一个缓冲区,对应着一个磁盘块,内核多次修改该缓冲区,于是磁盘日志中就会有该缓冲区的若干个版本的数据。假设此时要从文件中删除该磁盘块,那么,一旦包含该删除操作的transaction提交,那么,再恢复时,已经存放在磁盘日志中的该磁盘块的若干个版本的数据就不必再恢复了,因为到头来还是要删除的。revoke就是这样一种加速恢复速度的方法。当本transaction包含删除磁盘块操作时,就会在磁盘日志中写一个revoke块,该块中包含<被revoked的块号blocknr,提交的transaction的ID>,表示恢复时,凡是transaction ID小于等于ID的所有写磁盘块blocknr的操作都可以取消了,不必进行了。
- recover
加入日志机制后,一旦系统崩溃,重新挂载分区时,就会检查该分区上的日志是否需要恢复。如果需要,则依次将日志空间的数据写回磁盘原始位置,则文件系统又重新处于一致状态了。
3. 核心代码
打开journal.c文件,可以看到module_init(journal_init); module_exit(journal_exit);函数,应该是基于内核模块编写,来看它的初始化函数journal_init和卸载函数journal_exit。
1 | static int __init journal_init(void) |
BUILD_BUG_ON宏定义检查了日志超级块(struct journal_superblock_s)的大小是否为1024,还不清楚什么情况下会不是1024。日志超级块保存了日志系统的静态属性信息和动态信息。
1 | /* |
接下来journal_init_caches();初始化了日志系统需要的缓存。成功后,使用jbd2_create_jbd_stats_proc_entry();函数在proc文件系统下创建了jbds文件夹。这里使用了条件编译,在proc文件系统存在的情况下才使用proc_mkdir创建文件夹,否则执行空的while循环。
1 |
|
4. 日志模式
ext4支持三种日志模式,划分的依据是选择元数据块还是数据块写入日志,以及何时写入日志。
- 日志(journal)
文件系统所有数据块和元数据块的改变都记入日志。 这种模式减少了丢失每个文件所作修改的机会,但是它需要很多额外的磁盘访问。例如,当一个新文件被创建时,它的所有数据块都必须复制一份作为日志记录。这是最安全和最慢的ext4日志模式。
- 预定(ordered)
只对文件系统元数据块的改变才记入日志,这样可以确保文件系统的一致性,但是不能保证文件内容的一致性。然而,ext4文件系统把元数据块和相关的数据块进行分组,以便在元数据块写入日志之前写入数据块。这样,就可以减少文件内数据损坏的机会;例如,确保增大文件的任何写访问都完全受日志的保护。这是缺省的ext4日志模式。
- 写回(writeback)
只有对文件系统元数据的改变才记入日志,不对数据块进行任何特殊处理。这是在其他日志文件系统发现的方法,也是最快的模式。
在挂载ext4文件系统时,可通过data=journal等修改日志模式。
5. 日志系统初始化
ext4通过内核函数ext4_fill_super (fs/ext4/super.c)对每个分区的超级块进行初始化。
1 | /* |
ext4_fill_super函数调用了ext4_load_journal函数装载日志系统,装载的方式分为目录和分区,ext4可以使用这两种方式来存储日志。
1 | static int ext4_load_journal(struct super_block *sb, |
函数ext4_get_journal用于初始化日志目录。
1 | static journal_t *ext4_get_journal(struct super_block *sb, |
其中函数jbd2_journal_init_inode用于初始化日志inode,函数被定义在fs/jbd2/journal.c。
1 | journal_t *jbd2_journal_init_inode(struct inode *inode) |
调用jbd2_stats_proc_init函数初始化proc文件系统下jbd2目录中的文件。
1 | //初始化proc文件系统下/proc/fs/jbd2目录中的文件 |
使用proc_create_data创建了一个info文件,并且赋值了文件操作函数集jbd2_seq_info_fops给info文件。
jbd2_seq_info_fops定义了对文件的操作函数,如下:
1 | static const struct file_operations jbd2_seq_info_fops = { |
查看jbd2_seq_info_open函数,
1 | static int jbd2_seq_info_open(struct inode *inode, struct file *file) |
打开操作函数seq_open(file, &jbd2_seq_info_ops)会初始化file结构体,然后将jbd2_seq_info_ops操作函数集赋值给file。
1 | static const struct seq_operations jbd2_seq_info_ops = { |
jbd2_seq_info_show(journal.c/833)这个函数的主要功能就是把日志的统计信息写到打开文件的private_data指向的seq_file里。
1 | static int jbd2_seq_info_show(struct seq_file *seq, void *v) |
我们可以在proc下查看写入的信息如下:
那么文件系统如何通过cat读出这些信息的呢?
上面提到的jbd2_seq_info_fops 文件操作集中有seq_read函数,使用cat读取的时候会调用该函数。
1 | static const struct file_operations jbd2_seq_info_fops = { |
这个函数从一个序列文件读出数据到用户空间。
1 | /* |