8.6 代码:日志
在系统调用中一个典型的日志使用就像这样:
begin_op();
...
bp = bread(...);
bp->data[...] = ...;
log_write(bp);
...
end_op();
begin_op
(kernel/log.c:126)等待直到日志系统当前未处于提交中,并且直到有足够的未被占用的日志空间来保存此调用的写入。log.outstanding
统计预定了日志空间的系统调用数;为此保留的总空间为log.outstanding
乘以MAXOPBLOCKS
。递增log.outstanding
会预定空间并防止在此系统调用期间发生提交。代码保守地假设每个系统调用最多可以写入MAXOPBLOCKS
个不同的块。
log_write
(kernel/log.c:214)充当bwrite
的代理。它将块的扇区号记录在内存中,在磁盘上的日志中预定一个槽位,并调用bpin
将缓存固定在block cache中,以防止block cache将其逐出。
注:固定在block cache是指在缓存不足需要考虑替换时,不会将这个block换出,因为事务具有原子性:假设块45被写入,将其换出的话需要写入磁盘中文件系统对应的位置,而日志系统要求所有内存必须都存入日志,最后才能写入文件系统。
bpin
是通过增加引用计数防止块被换出的,之后需要再调用bunpin
在提交之前,块必须留在缓存中:在提交之前,缓存的副本是修改的唯一记录;只有在提交后才能将其写入磁盘上的位置;同一事务中的其他读取必须看到修改。log_write
会注意到在单个事务中多次写入一个块的情况,并在日志中为该块分配相同的槽位。这种优化通常称为合并(absorption)。例如,包含多个文件inode的磁盘块在一个事务中被多次写入是很常见的。通过将多个磁盘写入合并到一个磁盘中,文件系统可以节省日志空间并实现更好的性能,因为只有一个磁盘块副本必须写入磁盘。
注:日志需要写入磁盘,以便重启后读取,但日志头块和日志数据块也会在block cache中有一个副本
end_op
(kernel/log.c:146)首先减少未完成系统调用的计数。如果计数现在为零,则通过调用commit()
提交当前事务。这一过程分为四个阶段。write_log()
(kernel/log.c:178)将事务中修改的每个块从缓冲区缓存复制到磁盘上日志槽位中。write_head()
(kernel/log.c:102)将头块写入磁盘:这是提交点,写入后的崩溃将导致从日志恢复重演事务的写入操作。install_trans
(kernel/log.c:69)从日志中读取每个块,并将其写入文件系统中的适当位置。最后,end_op
写入计数为零的日志头;这必须在下一个事务开始写入日志块之前发生,以便崩溃不会导致使用一个事务的头块和后续事务的日志块进行恢复。
recover_from_log
(kernel/log.c:116)是由initlog
(kernel/log.c:55)调用的,而它又是在第一个用户进程运行(kernel/proc.c:539)之前的引导期间由fsinit
(kernel/fs.c:42)调用的。它读取日志头,如果头中指示日志包含已提交的事务,则模拟end_op
的操作。
日志的一个示例使用发生在filewrite
(kernel/file.c:135)中。事务如下所示:
begin_op();
ilock(f->ip);
r = writei(f->ip, ...);
iunlock(f->ip);
end_op();
这段代码被包装在一个循环中,该循环一次将大的写操作分解为几个扇区的单个事务,以避免日志溢出。作为此事务的一部分,对writei
的调用写入许多块:文件的inode、一个或多个位图块以及一些数据块。