首页 > *nix技术, 文件系统 > Linux文件系统的日志功能

Linux文件系统的日志功能

2013年3月17日 发表评论 阅读评论 945 次浏览

所谓原子操作,即要么全部成功,要么全部失败。磁盘硬件可以保证对一个扇区的操作是原子的,也就是说对一个磁盘扇区进行写操作,那么结果只有两种:1,整个扇区被完全修改为新数据;2,整个扇区没有被做任何修改;而不会出现扇区半新半旧的混乱状态(比如前256字节为新数据,后256字节还为旧数据,这里假定一个扇区为512字节)。

如果需要对超过一个扇区的操作进行原子控制,那么就需要更高一级别的软件控制机制。这是必须的,否则将导致磁盘文件系统数据的不一致错误,在Linux上对应的软件控制机制为JBD(Journaling Block Device)。

看一个示例,假定某应用程序需要创建一个文件,那么至少将有如下几步操作:
1,文件系统对空闲inode节点计数进行减1。
2,文件系统初始化磁盘上的一个inode节点。
3,文件系统在对应的父目录上新增一个entry条目。
4,……

如果在仅执行完上面操作中的第一步时,系统奔溃,那么此时就将导致文件系统的数据不一致,即空闲inode节点数减少了,但却没有inode节点被初始化。通过工具fsck可以检测出这种不一致,但需要扫描整个文件系统,如果文件系统非常大,这无疑是非常消耗时间的。因此,一种更好的解决方法是在一开始就要避免出现这种磁盘文件系统数据不一致现象。如何避免?即提供粒度更大的原子操作,也就是本文的主题:Journaling日志记录。

简单点说,使用Journaling就类似于使用便笺(scratch pad),所有操作先在便笺上进行,一旦所有操作都已正确,那么再把它们简单拷贝到对应的目标位置上即可。

对于文件系统而言,所有的元数据(metadata)和实际数据(data)都存储在block块内(备注:sector扇区是磁盘硬件的概念,传统大小为512字节,现在也有4K的;block块是文件系统软件的概念,在进行mkfs时可设置,比如:Usage: mkfs.ext3 … [-b block-size] …)。日志文件系统使用磁盘上一个单独的区域作为便笺,这块便笺区域(后续以journal代指)可以和文件系统(后续以disk代指)在同一个块设备,也可以单独的存在于另一块设备。

日志恢复方案:
在前面的示例中,需要修改三块block,即inode计数块、包含inode节点的块以及包含父目录entry的块。所有对它们的修改先写到journal,然后一个被称为提交记录(commit record)的特殊块将被写到journal,它表示一个原子操作的所有相关块都已被写到journal。
下面是在三种不同的典型场景下,日志文件系统如何做出对应的反应:
1,主机在完成第一步操作之后崩溃。
在这种情况下,当主机重启后进行日志检查时,将会发现一个没有“提交记录”标记的日志,这也就意味着这是一个没有完全完成的操作,因此无需对disk做进一步的任何修改,保持了disk的一致性。

2,主机在“提交记录”写到journal之后崩溃。
在这种情况下,当主机重启后进行日志检查时,将会发现一个具有“提交记录”标记的日志,这也就意味着这是一个完全完成的操作,因此需要把它们写到disk,通过回放这几条操作日志,所有相关块被写到disk的正确位置。

3,主机在“提交记录”写到journal之前崩溃。
这和第1种情况下的处理相同。

日志记录机制保证了文件系统的一致性,而对日志记录的检查时间相对也比较少。

JBD(Journaling Block Device)
Linux的JBD提供了这种便笺机制,针对块设备的任何文件系统都可以使用JBD来保证一致性。下面将介绍Linux 2.6内核下的JBD概念和实现细节。

journal是对单块设备的更新进行日志记录和管理,也就是说先将“对disk的更新操作”以日志的形式记录到journal,然后再实际写到disk。所以,如果这之间因种种原因(比如主机崩溃)而操作中断了,那么在主机重启恢复后能够通过已经记录的日志进行回放并对disk进行再写。这也可以看到,如果数据已经成功写到disk,那么对应的日志就没用了,所以如果journal区域已满,那么会从头再写,即journal区域在逻辑上是一个环状。

一个句柄(handle)代表一个原子更新,与一个句柄相关的所有更改操作必须被原子的执行。对每个原子更新进行单独的刷新(flush)操作不太有效,为了获得更好的性能,JBD将一系列句柄串起来形成一个事务(transaction),而JBD确保事务是原子性的,因此,作为事务子组件的句柄同样也被保证是原子性的。

一个事务的最重要的属性是它的状态。当一个事务被提交后,在它的生命周期内将有如下一些状态:
1,Running:事务当前处于活动(live)状态并且可接受新句柄。在整个系统里,只允许存在一个事务处于running状态。
2,Locked:事务当前不可接受任何新句柄,并且存在已持有的句柄未完成。一旦所有已持有的句柄全部完成,事务将进入到下一个状态。
3,Flush:事务包含的所有句柄都已经完成。事务开始把自己写到journal。
4,Commit:整个事务日志(log)已被写到journal。事务开始将一个提交块(commit block)写到journal,以标识事务日志已成功写到journal。
5,Finished:事务已经写入到journal,但事务将保持存在,直到所有块被更新到disk。

事务提交和检查点(Transaction Committing and CheckPointing)
1,将事务刷新到journal并且标记事务为完成状态的过程叫做事务提交(Transaction Committing)。journal只有有限的区域可供控制使用,因此需要对该区域进行重复利用(reuse),即针对已经提交完成的事务,所有相关block块已经被写到disk,因此它们无需再保留在journal中。

2,将已完成事务刷新到disk并回收journal中不再使用的对应空间的过程叫做检查点(Checkpointing)。

实现概要:
Commit:journal_commit_transaction
每一个journaled设备都有一个相关联的kjournald线程,kjournald线程保证Running状态的事务将在一个特定的时间间隔内被提交。事务提交分为八个不同的阶段,分别为:
阶段0:将事务从running状态(T_RUNNING)迁移到locked状态(T_LOCKED),这也就意味着该事务不再接受新句柄。事务将等待已持有的句柄全部完成更新。

阶段1:事务进入到flush状态(T_FLUSH),该事务被标记为journal的当前正在提交(currently committing)的事务。同时,该阶段还将标记当前journal没有处于running状态的事务,因此,新的句柄请求将初始化出一个新的事务。

阶段2:事务的相关buffer将被刷新到disk。首先刷新数据buffer,因为数据buffer不会保存到log区域,因此数据被直接刷新到disk。当接收到所有buffer的I/O完成通知时,该阶段结束。

阶段3:所有数据buffer已经写到disk,但它们对应的元数据(metadata)还处在内存当中。对于元数据,不能直接往disk写,它们需要先被写到log区域,并需要将在disk的实际位置一一对应的记录下来。即:为了进行元数据buffer的刷新,需要一个日志描述块(journal descriptor block),它将以tag的形式保存了journal中元数据buffer与disk实际位置的映射关系。当元数据buffer全部刷新到journal或者日志描述块已满,日志描述块也需被刷新到journal。到现在为止,所有的元数据buffer被写到journal,并且它们所对应的disk位置也以日志描述块的形式记录下来。

阶段4和阶段5:这两个阶段分别等待元数据buffer和日志描述块的I/O完成通知。

阶段6:所有数据和元数据都安全的处在存储设备上,数据处在disk,而元数据处于journal,因此需要将事务标记为已提交状态(committed),因此将分配一个新的日志描述块,其中包含一个标识事务已成功提交的tag标记,该日志描述块将被同步写到journal,至此,事务迁移到已提交状态(T_COMMIT)。

阶段7:如果journal有多个事务存在并且它们尚未刷新到disk,那么将出现阶段7。在本次事务中的一部分元数据buffer可能也是以前事务中的一部分。既然在本次事务中已有了它们的最新拷贝,因此它们无需在以前事务中存在,所以它们将从以前事务中移除。

阶段8:事务被标记为完成状态(T_FINISHED),journal结构被更新从而将能反映出最新事务已经提交。同时,该事务被添加到相应列表以供检查点进行检查。

Checkpointing:
当把journal刷新到disk(比如umount)或启动一个新的句柄时,就将开始一个checkpointing。一个新句柄需要一定数量的buffer,所以在启动一个新的句柄时,就需要执行一个对应的checkpointing,以便释放出journal中的一些空间供新句柄使用,从而也就避免句柄失败。

checkpointing用于对事务中尚未没有写到disk对应位置的元数据buffer进行刷新操作,journal可以有多个checkpointing,而每一个checkpointing可以有多个buffer,对这些buffer的刷新是通过一次批量处理来进行的,一旦所有buffer全部刷新完成,事务将被从journal中删除。

Recovery:journal_recover
当系统从崩溃中重启后,一旦它发现log区域不空,这意味着上次系统异常关闭没有进行umount操作或上一次umount操作没有成功进行,此时,将进行recovery操作。
recovery操作分为三个阶段:
1,PASS_SCAN:寻找log结尾处。
2,PASS_REVOKE:对已废弃的块进行记录。
3,PASS_REPLAY:对未废弃的块进行回放。

先解释一下什么是“已废弃的块”,假设有这么一个场景:
1,更新disk上的一个元数据块A(假设是文件a的inode信息),其内容将被拷贝到journal(假设名为日志L)。
2,在后续文件系统的使用过程中,块A被释放了(比如文件a被删除了)。
3,在后续文件系统的使用过程中,块A用于存放其它数据(比如文件b的实际内容)。

当系统崩溃并进行根据日志做恢复时,那么相对日志L而言,块A就是一个已废弃的块,我们不能根据日志L对块A进行回放重写,否则将导致文件b的实际内容被已删除的文件的inode信息覆盖。当然,是这样的情况:
3,在后续文件系统的使用过程中,块A用于存放其它数据(比如文件b的inode信息),其内容将被拷贝到journal(假设名为日志L’)。
那么当系统崩溃并进行根据日志做恢复时,那么相对日志L’而言,块A需要被回放,因为它的版本更新。

进行recovery操作时,所有有用的信息都来之journal,但是journal本身的确切状态也不可预知,比如最后一个事务可能处于checkpointing状态,或committing状态,不过一个running状态的事务不会出现在journal内,因为它仅处于内存中。

对于committing状态的事务,因为其已经进行的更新操作多半只是所有更新操作中的一部分,所以我们必须从头开始再完整执行一次该事务的所有相关操作。
在PASS_SCAN阶段,会寻找到log中的最后一个log条目。
每一个事务可能都涉及到一些的废弃块,因此在PASS_REVOKE阶段会把所有这些废弃块以哈希表(hash table)的形式记录下来。
在最后一个阶段,对每一个可能的块进行回放重写,不过在此之前需要先通过第二中的哈希表进行废弃检查。在哈希表内,如果没有找到,那么可以安全回放;如果有找到,那么只有版本更新的日志才需回放。

在整个Recovery过程中,恢复操作都不会修改磁盘上的journal数据,因此如果在Recovery过程中,系统再次崩溃也不会影响到重启后的再次恢复操作。

PS:
对于ext3/4文件系统而言,支持三种日志模式:
1,writeback:对metadata元数据进行日志,提供最好的性能。
2,ordered:对元数据进行日志,与writeback的区别在于它在把metadata元数据写到disk之前,会先把相关data数据写到disk。
3,journal:对metadata元数据和data数据都进行日志,即所有数据先写到journal,然后再写到最终的disk,这能提高最高的安全性。

而本文可以说是基于writeback模式来进行描述的。

完全参考:
1,http://kerneltrap.org/node/6741

2,http://mkatiyar.blogspot.com/2011/07/journal-jbd-revoke-mechanism.html

其它资料:
3,https://www.kernel.org/doc/Documentation/filesystems/ext4.txt
4,http://xanpeng.github.com/linux/2012/09/01/jbd-jbd2.html

转载请保留地址:http://lenky.info/archives/2013/03/17/2242http://lenky.info/?p=2242


备注:如无特殊说明,文章内容均出自Lenky个人的真实理解而并非存心妄自揣测来故意愚人耳目。由于个人水平有限,虽力求内容正确无误,但仍然难免出错,请勿见怪,如果可以则请留言告之,并欢迎来讨论。另外值得说明的是,Lenky的部分文章以及部分内容参考借鉴了网络上各位网友的热心分享,特别是一些带有完全参考的文章,其后附带的链接内容也许更直接、更丰富,而我只是做了一下归纳&转述,在此也一并表示感谢。关于本站的所有技术文章,欢迎转载,但请遵从CC创作共享协议,而一些私人性质较强的心情随笔,建议不要转载。

法律:根据最新颁布的《信息网络传播权保护条例》,如果您认为本文章的任何内容侵犯了您的权利,请以或书面等方式告知,本站将及时删除相关内容或链接。

分类: *nix技术, 文件系统 标签: , , ,
  1. 本文目前尚无任何评论.
  1. 本文目前尚无任何 trackbacks 和 pingbacks.