日志文件系统的目标是避免对整个文件系统进行耗时的一致性检查,ext3日志文件系统的思想是对文件系统进行的任何高级修改都分两步进行。首先,把待写块的一个副本存放在日志中;其次,当发往日志的I/O数据传送完成时(把数据提交到日志后),待写块就被写入文件系统。当发往文件系统的I/O数据传送终止时(把数据提交到文件系统后),日志中的块的副本就被丢弃。
当从系统故障中恢复时,e2fsck程序区分以下两种情况:
1.提交到日志之前系统故障发生:与高级修改相关的块的副本全部丢失或者部分写入日志,e2fsck忽略这种情况,此时文件系统保持一致的状态(故障发生时的请求没有完成)。
2.提交到日志之后系统故障发生:块的副本是有效的,e2fsck通过日志重放,将块的副本进行更新,此时文件系统也能保持一致的状态。从上面的两种情况可以看出,日志文件系统并不能保证数据不丢失(系统崩溃仍然可能造成数据丢失),它只能确保文件系统保持数据与元数据一致的状态。另外,考虑到性能原因,日志文件系统通常不能把所有的块都拷贝到日志中,而只是拷贝元数据块(包括超级块、块组描述符、索引节点块、间接寻址块、数据块位图块、索引节点位图块)。
ext3支持三种日志模式,划分的依据是选择元数据块还是数据块写入日志,以及何时写入日志。
1.日志模式(Journal):文件系统所有数据和元数据的改变都被记入日志。这种模式减少了丢失每个文件的机会,但需要很多额外的磁盘访问。例如:当一个新文件被创建时,它的所有数据块都必须复制一份作为日志记录。这是最安全但最慢的日志模式。
2.预定模式(Ordered):只对文件系统元数据块的改变才记入日志,这样可以确保文件系统的一致性,但是不能保证文件内容的一致性。然而,ext3文件系统把元数据块和相关的数据块进行分组,以便在元数据块写入日志之前写入数据块。这样,就可以减少文件内数据损坏的机会;例如,确保增大文件的任何写访问都完全受日志的保护。这是缺省的ext3日志模式。
3.写回(Writeback):只有对文件系统元数据的改变才被记入日志,对文件数据的更新与元数据记录可以不同步(相对Ordered模式而言),即ext3是支持异步的日志。
ext3日志可以存储到一个文件中,也可存储在单独的设备上。ext3本身不处理日志,而是利用日志块设备(Journaling Block Device,JBD)的通用内核层来处理。
ext3与JBD之间的交互本质上基于三个单元:
1.日志记录:描述日志文件系统一个磁盘块的一次更新。
2.原子操作处理(句柄):包括文件系统的一次高级修改对应的日志记录,通常,修改文件系统的每个系统调用都引起一次单独的院子操作处理。
3.事务:包括几个原子操作处理(句柄)。
事务、日志和句柄的交互
日志记录
日志记录本质上文件系统将要发出的一个低级操作的描述。在某些日志文件系统中,日志记录只包括操作所修改的字节范围及字节在文件系统中的起始位置。JBD层使用的日志记录由低级操作所修改的整个缓冲区组成。因为JBD直接对缓冲区和缓冲区首部进行操作,其性能相当高。
日志记录在日志内部表示为普通的数据块,每个这样的块都与类型为journal_block_tag_t的小标签相关联,该标签存放块在文件系统中的逻辑块号和几个状态标志。
/*The block tag: used to describe a single buffer in the journal*/
typedef struct journal_block_tag_s
{
__be32t_blocknr;/* The on-disk block number */
__be32t_flags;/* See below */
} journal_block_tag_t;
原子操作处理
修改文件系统的任一系统调用通常都被划分为操作磁盘数据结构的一系列低级操作。如用户向文件追加数据的操作,文件系统必须确定文件的最后一个块,如果空间不足,还需要定位一个空闲块,更新适当块组内的数据块位图,存放新块的逻辑块号在文件索引节点或间接寻址块中,写新块的内容,最后更新索引节点的几个字段。
为了防止数据损坏,ext3必须确保每个系统调用以原子的方式进行处理。原子操作处理是对磁盘数据结构的一组低级操作。当从系统故障中恢复时,文件系统确保要么整个高级操作起作用,要么没有一个低级操作起作用。
任何原子操作处理都用类型为handle_t的描述符表示,为了开始一个原子操作,ext3调用journal_start函数,该函数在必要时分配一个新的原子操作处理并把它插入到当前的事务中。因为对磁盘的任何低级操作都可能挂起进程,活动原子操作处理的地址存放在进程描述符的journal_info字段中。为了通知原子操作已经完成,ext3调用journal_stop函数。
struct task_struct {
/* journalling filesystem info */
void *journal_info;
};
typedef struct handle_shandle_t;/* Atomic operation type */
struct handle_s
{
transaction_t*h_transaction;//句柄所属的事务
inth_buffer_credits;//允许弄脏的剩余缓冲区数量
…
};
每个句柄由各种日志操作组成,每个操作都有自身的缓冲头用于保存修改的信息,即使底层的文件系统修改只改变一个比特位,也是如此。JBD层提供journal_dirty_metadata(data)函数,将修改的元数据(数据)块写到日志。
struct journal_head {
struct buffer_head *b_bh; //指向包含操作数据的缓冲头
transaction_t *b_transaction;//日志项所属的事务
struct journal_head *b_tnext, *b_tprev;//链接与某个原子操作相关的所有日志
…..
};
事务
实现日志文件系统时,可以将一个原子操作就作为一个事务来处理,但是这样实现的效率比较低。于是ext3中将若干个原子操作组合成一个事务,对磁盘日志以事务为单位进行管理,以提高读写日志的效率。
事务由transaction_t类型的数据结构表示。
typedef struct transaction_stransaction_t;/* Compound transaction type */
struct transaction_s
{
journal_t*t_journal; //指向事务数据将写向的日志
tid_t t_tid; //事务序列号
/*事务所处状态
enum {
T_RUNNING,//可以向事务添加新的原子句柄
…
T_FLUSH,//正在将日志刷出到磁盘
T_COMMIT, //所有的数据都已经写到磁盘,但仍然需要处理元数据
T_FINISHED //所有的日志项已经安全的写到磁盘
} t_state;
struct journal_head*t_buffers;//指向与该事务相关的缓冲区
unsigned longt_expires; //指定事务必须在物理上写到日志中的时间期限(默认5s)
int t_handle_count;//与事务相关联的句柄数目
};
ext3代码使用一种“检查点”机制,用于检查日志中记载的改变是否已经写入到文件系统。如果已经写入到文件系统,那么日志中的数据就不再需要了,可以删除。
与ext2的超级块相比,ext3增加了几个成员,用于支持日志功能。
struct ext3_sb_info {
…
/* Journaling */
struct inode * s_journal_inode; //如果日志存储在文件中,指向该文件的inode
struct journal_s * s_journal; //指向日志数据结构
unsigned long s_commit_interval; //指定了数据从内存写到日志的频率
struct block_device *journal_bdev; //如果日志存储在单独设备中,指向该设备的描述符
}
参考资料:1.深入理解Linux内核.中国电力出版社.
2.深入Linux内核架构.人民邮电出版社.