对通用日志层JBD的理解

2370阅读 0评论2015-01-07 njupt_lege
分类:LINUX

在看JBD代码的过程中自问自答了很多问题,多少次自以为吃透了某个问题,但是,后面被推翻,又不停的看代码找答案,其实看代码的过程就是领悟作者真实想法的过程,只有
对每句代码都吃透才算是理解作者的真实意图.JBD的代码涉及到文件系统,虚拟内存,脏数据回写等多个子系统,比较复杂,但是其设计巧妙,逻辑严谨,阅读领悟的过程也是一个享受的过程,值得细读.

Q:没有日志保护的文件系统究竟会产生哪些不一致??
A:ext文件系统主要有6大元数据,超级块,块组,inode表,inode位图,数据块位图,间接索引数据块,文件系统的不一致是指元数据中存储的信息与磁盘实际的内容不同,内存中元数据的回写顺序是不可预期的,主要的不一致有:
超级块中的可用块数目,块组中的可用块数目,磁盘上实际的可用块数目都不同.
两个文件使用了相同的磁盘块
列表目录时能够发现文件,但是使用stat()读取到文件的信息为非预期信息.

Q:是否存在属于不同事务的进程同时修改同一个数据块位图的情况?
A:不存在,一个日志环境中(一个文件系统对应一个日志环境)只有运行的事务才能修改元数据,且任意时刻运行的事务和正在提交的事务都只能有一个(进入检查点的事务可以有多个).但是,存在属于同一个事务的多个进程竞争修改数据块位图的情况.

Q:JBD如何解决事务在提交的过程中其元数据buffer被其他事务修改的问题?
A:事务的核心是保证原子操作的完整性,一个事务必然涉及到多个buffer,事务从锁定(不接受新的原子操作)到提交完毕(回写日志空间完成)的过程比较长,这期间会有其他进程启动新事务,新事务可能会修改当前提交事务的buffer,为了保证性能,新事务不能等待正在提交的事务完成后再修改buffer.JBD采取写时复制的方式,新事务在要修改buffer以前,如果发现buffer属于正在提交的事务,将buffer的内容复制备份后,再执行文件系统操作.正在提交的事务回写buffer的数据到日志空间的过程中,如果发现buffer有备份的数据那么回写备份的数据,确保回写到日志空间的数据不受当前运行事务的影响,从而保证了事务所涉及buffer数据的一致性.这个写时复制与进程页表项的写时复制刚好相反,当前运行的事务复制出来的数据是给正在提交的事务用,而不是它自己用.

Q:当前运行的事务提交时,为什么必须等待原子操作完毕后才能启动新事务?
A:执行jounal_commit_transaction()提交当前运行的事务时,必须等待当前事务所有的原子操作完毕后才将当前运行的事务切换为正在提交的事务,即此时新的运行事务才能够启动,等待原子操作完毕的做法对文件系统性能肯定有影响.采用这种做法的原因还是为了保证事务的完整性,如果在jounal_commit_transaction()函数的一开始就做正在运行的事务和提交事务的切换,切换后,提交的事务还是需要等待原子操作的完毕,假设某个原子操作是需要释放100个数据块,在释放到第50个的时候,位图buffer被当前运行的事务修改,需要对位图buffer做写时复制,备份当前位图buffer的数据,当前提交的事务回写元数据到日志空间时,只能回写备份后的数据,但是备份后的数据是中间状态数据,无法保证事务数据的一致性.从另一方面来说,等待原子操作完毕才启动新事务也是遵循buffer不能被两个事务同时修改的原则.

Q:如何解决元数据buffer在做检查点的过程中被其他事务修改的问题?
A:提交完毕的事务会进入到检查点链表,事务通过检查点后即可将占用的日志空间和内存空间释放,事务通过检查点的过程无非就是将事务所涉及到的脏buffer都回写到磁盘(元数据在磁盘空间中的真正位置不是在日志空间的位置).通过检查点时,需要保证回写到磁盘空间的内容和日志空间中记录的内容是相同的,如果发现buffer在提交完毕后???

Q:JBD如何解决掉电时ext2可能出现的两个文件引用了相同磁盘块的问题?
考虑下面的场景:
1.进程1对文件1做截断操作,释放了数据块block1
2.进程2对文件2做扩展操作,恰好分配了数据块block1
3.元数据回写时,在回写了文件2的元数据后,系统掉电
4.文件系统重新挂载时,出现文件1和文件2使用相同的数据块!
JBD通过对数据块位图buffer增加committed_data的方式来解决这个问题,committed_data用于存放位图buffer已经提交到日志空间中的数据,分配数据块时,不仅要求位图buffer的bit位为0,还要求对应的committed_data中相应的位为0.释放数据块时,将位图buffer的bit置0,同时将committed_data相应的bit置1,禁止分配刚刚释放的数据,等待位图buffer回写到日志空间完毕后,将回写到日志空间的数据切换为committed_data,让先前释放的数据块可以被重新分配.JBD数据分配的原则:释放的数据块对应的位图buffer必须提交到日志空间后,释放的数据块才能被重新使用.
buffer,committed_data,forzen_data切换示意图:


Q:JBD如何解决日志恢复时回放间接数据块损坏文件的问题?
考虑下面的场景:
1.事务1中数据块block1被用做间接数据块,事务1提交,block1作为元数据块被写入到日志空间.
2.在紧随的事务2中,block1被释放,事务2提交.
3.在事务3中,block1并被重新分配用作文件foo的数据块
4.系统掉电
5.文件系统重新挂载,事务1被回放,block1的日志数据被回写到磁盘,文件foo的数据被破坏.
JBD采用撤销块(revoke block)的方式来解决这个问题,在事务2中,block1被释放后,会将它作为撤销块记录到日志空间,文件系统挂载做事务回放时,如果日志块的块号被记录为撤销块,则不回写磁盘.

Q:JBD中buffer_head的jbddirty与dirty的转换?
1.bh从BJ_Reserved链表转移到BJ_metadata链表后,置jbddirty标志
journal_dirty_metadata()
 set_buffer_jbddirty(bh);

2.bh回写到日志空间完毕,加入到checkpoint链表后,如果当前运行的事务没有涉及此bh,bh由jbddirty转化为dirty,此时bh才被回写子系统接管,在适当的时候回写到磁盘真正的位置
if (buffer_jbddirty(bh)) {
 __journal_refile_buffer()
  if (test_clear_buffer_jbddirty(bh))
   mark_buffer_dirty(bh); 

3.bh回写到日志空间完毕,加入到checkpoint链表后,如果当前运行的事务涉及到了此bh,bh仍保持jbddirty状态不变,暂时不将bh交给回写子系统
if (buffer_jbddirty(bh)) {
 __journal_refile_buffer()
  was_dirty = test_clear_buffer_jbddirty(bh);
  if (was_dirty)
   set_buffer_jbddirty(bh);

Q:数据块位图journal_head如果属于运行的事务,那么b_committed_data一定不为空?
A:是.
数据块位图主要有下面的使用场景:
1.调用journal_get_undo_access()获取位图的写权限时,位图不属于正在提交的事务,此时,b_frozen_data赋值为空,b_committed_data赋值为当前位图buffer的拷贝.在随后的文件系统操作中,因为位图不属于任何事务,b_committed_data不会被重新赋值,直到事务提交.
2.调用journal_get_undo_access()获取位图的写权限时,位图属于正在提交的事务,此时,b_frozen_data赋值为当前位图buffer的拷贝.在随后的文件系统操作中,正在提交的事务随时可能将位图buffer回写日志空间完毕,对b_committed_data重新赋值,但是此时b_frozen_data不为空,b_committed_data重新赋值为b_frozen_data后,仍然不为空,

Q:ext3修改元数据后为什么不直接调用mark_buffer_dirty()?
A:ext2源码中19个mark_buffer_dirty()在ext3中大部分都消失了,ext3修改了元数据后,并不立即调用mark_buffer_dirty()将元数据块标记为脏,而是调用ext3_journal_dirty_metadata()将元数据buffer加入到当前运行事务的元数据链表中,置位jbddirty,由JBD来管理buffer和buffer的状态,等到buffer回写日志空间完毕后,buffer的状态会由jbddirty转化为dirty,回写子系统会自动接管buffer,做到记录日志的整个过程中,JBD对回写子系统是透明的.

Q:JBD的实现中遵循的规则?
1.buffer不能被多个事务同时修改.
2.在当前事务中释放的数据块不能在当前事务中重新使用,必须等事务提交完毕后才能使用.
3.事务对不属于自己的buffer不能修改其状态和在链表中的位置.
4.buffer在JBD中的脏状态由jbddirty标识,在VM中的脏状态由dirty标识,jbddirty和dirty两个标志不能同时置位.

Q:journal_get_create_access()与journal_get_write_access()的区别?
A:journal_get_create_access()管理的元数据jh是目录的数据块和文件的间接数据块buffer,这些buffer只能被单个进程独享.journal_get_write_access()管理的元数据buffer是超级块,块组,inode表,inode位图和数据块位图buffer,这些buffer可以在同一个事务中被多个进程修改.

Q:事务开始,结束,提交,通过检查点释放的全过程?
几个关键的变量:
journal->j_head   
日志空间的写指针,指向日志空间下一个可用的块号.事务提交的过程中,每使用一个数据块时journal->j_head++
journal->j_tail   
日志空间的释放指针,指向日志空间仍在使用的最后一个块号,事务通过检查点后journal->j_tail向前移动,[journal->j_tail,journal->j_head]定义了当前日志空间已使用的数据块区间
transaction->t_log_start 
事务在日志空间中开始的块号.事务开始提交时,执行transaction->t_log_start=journal->j_head
journal->j_commit_sequence  
最近提交事务的ID号.事务提交完毕后,ID号被赋值给journal->j_commit_sequence
journal->j_tail_sequence  
日志恢复时需要重放的第一个事务(最老的事务)的ID或下一个通过检查点的事务ID

1.事务开始,如果journal中当前运行的事务为空,启动一个新事务
journal_start()
 handle = new_handle(nblocks);
 start_this_handle()
  new_transaction = kzalloc()   
  transaction->t_state = T_RUNNING;
  transaction->t_tid = journal->j_transaction_sequence++; //事务ID向上增长
  transaction->t_expires = jiffies + journal->j_commit_interval;
  add_timer(&journal->j_commit_timer);//设置定时器,5秒后超时提交

  journal->j_running_transaction = transaction;//新事务作为当前运行的事务

  handle->h_transaction = transaction;
  transaction->t_updates++; //记录当前运行事务涉及的原子操作的个数
  transaction->t_handle_count++;
2.事务结束,递减当前事务正在运行的原子操作的个数,如果所有的原子操作都结束,唤醒提交事务的线程
journal_stop()
 current->handle = NULL;
 transaction->t_updates--;
 if (!transaction->t_updates) {
  wake_up(&journal->j_wait_updates);
  if (journal->j_barrier_count)
   wake_up(&journal->j_wait_transaction_locked);
 }

3.事务提交,将当前运行的事务修改过的数据块数据和元数据写入到日志空间
journal_commit_transaction()
 commit_transaction = journal->j_running_transaction;
 while (commit_transaction->t_updates) {//等待包含的原子操作执行完毕
  DEFINE_WAIT(wait);
  prepare_to_wait()
 }
 //删除当前事务还没有修改过的jh,这些jh的共同特点是jh->b_next_transaction为空
 //执行journal_refile_buffer()后,他们将不属于任何事务
 while (commit_transaction->t_reserved_list) {
  journal_refile_buffer(journal, jh);
 }
 journal_switch_revoke_table(journal);//切换撤销表
 //当前运行的事务切换为正在提交的事务,新的文件系统原子操作又可以继续
 journal->j_committing_transaction = commit_transaction;
 journal->j_running_transaction = NULL;
 //t_log_start赋值为日志空间下一个可用的数据块
 commit_transaction->t_log_start = journal->j_head;

 journal_submit_data_buffers();//日志模式为ordered时,回写数据块

 journal_write_revoke_records()//写入撤销块

 //写入描述符块
 while (commit_transaction->t_buffers) {
  journal_write_metadata_buffer()
 }

 wait_for_iobuf://等待元数据块写入完毕
 while (commit_transaction->t_iobuf_list != NULL) {
 }

 journal_write_commit_record()//写入提交块

 //所有的jh现在都转移到forget链表,
 while (commit_transaction->t_forget) {
  //将buffer加入到本事务的checkpoint链表
  if (buffer_jbddirty(bh)) {
   __journal_insert_checkpoint(jh, commit_transaction);
 }

 commit_transaction->t_state = T_FINISHED;//提交完毕
 //修改journal的已提交事务ID为当前事务的ID
 journal->j_commit_sequence = commit_transaction->t_tid;
 journal->j_committing_transaction = NULL;
 //将commit_transaction插入到journal的checkpoint链表尾部
 journal->j_checkpoint_transactions = commit_transaction;
4.事务通过检查点
log_do_checkpoint()
 transaction = journal->j_checkpoint_transactions;//处理journal检查点链表中最老的一个事务
 this_tid = transaction->t_tid;  
 while (!retry && transaction->t_checkpoint_list) {
  //回写最老事务的检查点链表中的脏块
  //等待脏块回写完毕后,jh将会从checkpoint链表中移除,
  //移除后,如果checkpoint链表为空,那么事务会自动从journal的checkpoint链表中移除并释放,事务通过检查点
  __wait_cp_io()
   __journal_remove_checkpoint()
    if (transaction->t_checkpoint_list == NULL &&
        transaction->t_checkpoint_io_list == NULL){
        __journal_drop_transaction()
     //从journal的checkpoint链表中移除,通过检查点
     kfree(transaction);
    }
 cleanup_journal_tail()//对已经通过检查点的事务释放其占用的日志空间
  //最老的事务通过检查点后,次老进入检查点通道的事务(还未通过检查点)
  first_tid = journal->j_checkpoint_transactions->t_tid;
  blocknr = journal->j_checkpoint_transactions->t_log_start;
  //区间[journal->j_tail,blocknr]的日志块都可以释放
  freed = blocknr - journal->j_tail;
  journal->j_free += freed;//释放了freed个日志块,增加日志中可用的日志块数目
  journal->j_tail_sequence = first_tid;//记录下一次需要释放的事务ID
  journal->j_tail = blocknr;//记录下一次需要释放的日志块号,日志已使用区间向前移动
  //日志空间变化了,记录到日志超级块中
  journal_update_superblock(journal, 1);
   //记录在日志空间中有效的最老的一个事务的ID和此ID的开始块号,
   //journal_recover()时,根据这两个值开始日志恢复
   sb->s_sequence = cpu_to_be32(journal->j_tail_sequence);
   sb->s_start    = cpu_to_be32(journal->j_tail);
   sync_dirty_buffer(journal->j_sb_buffer);

参考资料:
journal block device (jbd)源代码分析 潘卫平

上一篇:对unmap_underlying_metadata()的理解
下一篇:Mmap Internals