进入此门的肯定都对journal block device有一定了解,需要对ext3文件系统有了解,多余的就不赘述。

为什么要设计JBD?

普通数据是存在硬盘上的,文件系统也是作为普通数据存在硬盘上,类似如果碰到突然断电的情况,硬盘就可能损坏,硬件损坏,还是要硬件设计保证,软件设计(JBD)就是解决软件错误,断电可能会导致软件错误,举个例子,文件系统相当于常用的压缩文件,普通数据则是其中一个txt中的文字,如果压缩到一半被杀掉,如果txt中的文字损坏,压缩文件仍能解压,只是txt内容不同而已,但如果压缩文件的结构被损坏,很可能解压不来任何文件。而JBD就是防止文件系统的结构数据(元数据)被损坏,它作为一个缓存块先缓存所有的元数据,如果磁盘数据异常后,就从缓存块中恢复。

jbd-oenhan

JBD的具体工作流程:

如上图示,kernel正常读写磁盘,读磁盘直接获取,写磁盘则走两条路,每个IO群(即事务),先写到jbd里面,然后在写磁盘,如果写磁盘被中断,则从jbd恢复,如果jbd被中断,OK,没影响。jbd本身数据存储到磁盘的一个用户态不可见位置,即日志空间,日志空间本身是一个文件系统结构的存储空间,有超级块,组描述符,位图等,估计所有数据系统都是类似结构。

基本原理就不说了,下面就以ext3_mkdir为例,描述jbd工作机制。

首先通过ext3_journal_start获取原子操作handle,(原子操作即操作不可分割的,只有完成态和未开始状态,不会停留在中间态,和atomic_inc不同,atomic加减是限制多线程冲突,handle则是保证完整性),具体细节可以参考ext3_journal_start函数,我对此的理解是,ext3_journal_start对handle进行了初始化,获取当前journal空间的数据,比如,空闲字节的开始位置。

handle = ext3_journal_start(dir, EXT3_DATA_TRANS_BLOCKS(dir->i_sb) +
EXT3_INDEX_EXTRA_TRANS_BLOCKS + 3 +
EXT3_MAXQUOTAS_INIT_BLOCKS(dir->i_sb));

在后面ext3_new_inode函数中见handle传递进入,在ext3_new_inode中申请新inode,需要修改位图,当然还有超级块和组描述符等,下面截取位图的写入作为一个描述:

bitmap_bh = read_inode_bitmap(sb, group);
if (!bitmap_bh)
goto fail;

ino = 0;

repeat_in_this_group:
ino = ext3_find_next_zero_bit((unsigned long *)
bitmap_bh->b_data, EXT3_INODES_PER_GROUP(sb), ino);
if (ino < EXT3_INODES_PER_GROUP(sb)) {

BUFFER_TRACE(bitmap_bh, "get_write_access");
err = ext3_journal_get_write_access(handle, bitmap_bh);
if (err)
goto fail;

if (!ext3_set_bit_atomic(sb_bgl_lock(sbi, group),
ino, bitmap_bh->b_data)) {
/* we won it */BUFFER_TRACE(bitmap_bh,
"call ext3_journal_dirty_metadata");
err = ext3_journal_dirty_metadata(handle,
bitmap_bh);
if (err)
goto fail;
goto got;
}

通过read_inode_bitmap获取位图数据bitmap_bh,用ext3_find_next_zero_bit算出空闲ino位置,用ext3_journal_get_write_access获取日志的写权限,更多的是将handle加入事务transaction管理,或者说将bitmap_bh加入到journal管理中,然后才开始进行具体的数据修改,也就是ext3_set_bit_atomic修改位图,修改完成使用ext3_journal_dirty_metadata标记为脏,即告诉journal本次handle操作结束,可以进行提交了。

ext3_new_inode下的组描述符也是类似,包括后面的目录项修改都是如此,也不赘述了。

需要提到的是,此处标记为脏的是元数据,非元数据使用ext3_journal_dirty_data函数,在ext3里面,如果发现当前数据是脏页,则直接进行刷新到磁盘,原因在注释中有描述。

/*
 * This buffer may be undergoing writeout in commit.  We
 * can't return from here and let the caller dirty it
 * again because that can cause the write-out loop in
 * commit to never terminate.
 */if (buffer_dirty(bh)) {
get_bh(bh);
spin_unlock(&journal->j_list_lock);
jbd_unlock_bh_state(bh);
need_brelse = 1;
sync_dirty_buffer(bh);
jbd_lock_bh_state(bh);
spin_lock(&journal->j_list_lock);
/* Since we dropped the lock... */if (!buffer_mapped(bh)) {
JBUFFER_TRACE(jh, "buffer got unmapped");
goto no_journal;
}
/* The buffer may become locked again at any
   time if it is redirtied */}

至此,一个使用journal的标准写入过程结束,后续的就是提交了。

jbd有常驻线程kjournald负责提交transaction,kjournald线程每个ext系列的分区分一个,主要部分通过调用journal_commit_transaction完成。需要插播一下,如果编译内核的时候打开CONFIG_JBD_DEBUG或者CONFIG_JBD2_DEBUG开关,就可以根据jbd-debug跟踪jbd的执行过程,有更直接的感觉,在代码实现上就是jbd_debug函数。

具体流程我建议打开debug开关后,对比着看,具体代码不梳理了,直接上图:

journal_commit_transaction-oenhan

jbd前面所有设计都是为了此时的提交,需要留意的是此时设计的普通数据在元数据前进行提交,来保证ordered执行顺序。另外在之前写文件流程中提到ext3_ordered_write_end,中调用walk_page_buffers中journal_dirty_data_fn标记普通数据为脏,会将已脏的数据先用sync_dirty_buffer刷磁盘一下,可以对比参看。

最后则是出问题之后日志进行恢复:

journal恢复是在mount挂载磁盘的时候,ext3_fill_super()一直调用到journal_recover,判断是否进行日志恢复也是如下判断。

if (!sb->s_start) {
jbd_debug(1, "No recovery required, last transaction %dn",
  be32_to_cpu(sb->s_sequence));
journal->j_transaction_sequence = be32_to_cpu(sb->s_sequence) + 1;
return 0;
}

即根据日志的超级块s_start参数是否为0判断。

整个恢复过程有3部分组成,都是调用do_one_pass,只是传参不同,第一步获取recovery_info信息,journal的起点和终点,journal是一个循环利用的环状存储介质。第二步获取REVOKE块,第三步PASS_REPLAY则根据描述符块将日志信息写到磁盘上。

另外提一下在工作中碰到一个案例:内核在写文件的时候发生了多次复位,根据内核黑匣子记录的信息,看到journal_bmap获取信息为0,日志被__journal_abort_soft中断 了,再写journal出现了panic。当时看以为bmap出现异常,中间读取有问题,后来把journal日志块倒出来看,对应的一个间接索引块里面全为0,在普通文件中是正常的,称为文件的洞,而日志则是格式化一开始就全分配了,而且顺序读取利用不应产生文件的洞。具体原因再也没找到,但是发现fsck不支持修改journal出现洞的问题,导致重复复位,后来找到社区高版本fsck比对一下,改了一个补丁,勉强算解决了问题。

以上都是开胃小菜,更多的请读代码,文章描述不细致的地方请参考jdb代码分析


journal block device代码分析来自于OenHan

链接为:https://oenhan.com/ext3-jbd-journal

5 thoughts on “journal block device代码分析”

  1. 在data=ordered情况下,是文件数据先被写入,然后写入日志,此时日志包含了新文件的元数据, 然后提交日志,最后,如果是 delay allocate,过短时间真正的元数据才被写到磁盘上。
    我的理解

    1. @JET 脏的元数据有两部分,journal的部分由journal_write_metadata_buffer构建了new_jh。
      wbuf[bufs++] = jh2bh(new_jh)
      struct buffer_head *bh = wbuf[i];
      bh->b_end_io = journal_end_buffer_io_sync;
      _submit_bh(write_op, bh, 1 << BIO_SNAP_STABLE); 也就是journal部分立即提交给了bio调度层。 文件系统的元数据部分的及时性此刻要求就没有了,可以交给系统进行刷新。

      1. @OENHAN 这里的意思是说journal每次操作后都会提交一次bio那这个bio是异步还是同步的?
        还有ext3文件的fsync操作具体是怎样做的?
        我理解是先将journal flush到盘上,然后再对对应的每个记录分别更新对应位置的元数据(假设只journal元数据),是这样的吗?
        谢谢!

        1. @ARTHUR 先将journal flush到盘上,然后再对对应的每个记录分别更新对应位置的元数据,是这样的

发表回复