目录
0 Ext4的日志模式
ext4 支持根据用户需求采用多种模式的日志记录。例如,ext4 支持 Writeback 模式,它仅记录元数据;或 Ordered 模式,它记录元数据,但写为元数据的数据是从日志中写入的;或 Journal 模式(最可靠的模式),它同时记录元数据和数据。注意,虽然 Journal 模式是确保文件系统一致的最佳选择,但它也是最慢的,因为所有数据都要经过日志。
默认的时候是ordered模式。
ordered和writeback只把元数据写到日志中,而journal把元数据和数据全都写到日志中。
ordered有顺序得把数据直接写回,writeback无顺序。
所以,如果不研究Journal模式,下面的内容就无意义了:
Journal模式调用的顺序如下:ext4_load_journal -> ext4_get_dev_journal -> jbd2_journal_init_dev -> jbd2_stats_proc_init -> jbd2_seq_info_fops -> jbd2_seq_info_open -> jbd2_seq_info_ops -> jbd2_seq_info_show
1 日志系统的作用
原子性是操作的一种属性,标明这个操作要么完全成功,要么完全失败,不会处于中间状态。磁盘可以保证扇区级的原子性。这意味着写一个扇区的操作,要么完全成功,要么根本没写。不过,当一个操作涉及到多个扇区的时候,就需要高层机制了。这种机制应该确保全部的扇区修改都是原子性的。如果不能做到原子性的话将导致数据的不一致性。
Ext4文件系统创建文件的时候,分配一定数量的inode数量给创建的文件,第二步初始化磁盘上的inode,并为文件的父目录添加一个对应于新文件的条目。如果在上述操作进行到一半的时候计算机崩溃了,不一致性就被引入到文件系统当中了——可用的inode数量减少了,但磁盘上inode的初始化可能还没有进行。即inode虽然已经分配,但未能被使用,而且以后也不可能在创建新文件的时候被引用,造成空间上的浪费。
要解决这种问题可以有两种方式:
第一:扫描文件系统
fsck (filesystem consistency check)。扫描很大的文件系统时,一致性检查可能需要相当长的时间(可能高达数小时)来检查并修复这些不一致问题。
第二:JBD日志系统
日志就是为这些操作提供原子性的一种手段。对于操作系统而言,所有的元数据和数据都存储在文件系统所在的块设备上。日志可以是同一个块设备的一部分,或者是一个单独的设备。一个日志文件系统首先记录所有在日志中的操作。一旦这些作为一个原子操作的操作们都被记录到日志之中了,它们才会被一起写到实际的块设备中。在后文中,“磁盘(disk)”将指代“真实的块设备”,而“日志(journal)”将指代“日志区域”。
2 日志系统工作场景
当第一个块被写入日志的时候机器崩溃了。在这种情况下,当计算机重启之后检查日志,它会发现一个有没有提交的操作。这就标明可能有一个未完成操作。那么,既然现在还没有对磁盘进行修改操作,也就保持数据一致性。
当提交记录被写入日志的时候机器崩溃。在这种情况下,机器重新启动并检查日志,它会发现一个操作和它的提交记录正在那里。提交记录指明有一个完成了的操作可以被写入到磁盘之中。所有的属于这个操作的块都会依据日志被写到他们在磁盘中的实际位置。
3 JDB日志块设备
日志(journal)是管理一个块设备的更新的内部记录(log)。正如上文提到的,更新首先会放到日志之中,然后反射到它们在磁盘上的真实位置。日志区域被当作一个环状链表来管子。也就是说,当日志记满的时候会重用之前用过的区域。handle 代表一个原子更新。需要被原子地完成的全部一组改写被提取出来引用为一个 handle。JBD 将一组 handle 打包为一个事务(transaction),并将事务一次写入日志。JBD 保障事务本身是原子性的。这样,作为事务的组成部分的 handle 们自然也是原子性的。
3.1 JBD2的数据结构
3.1.1 buffer_head
buffer_head 是内核一个用于管理磁盘缓冲区的数据结构。根据局部性原理,磁盘上的数据进入内存后一般都是存放在磁盘缓冲区中,以备将来重复读写。所以说,一个buffer_head就会对应一个文件系统块,即对应一个磁盘块。(512字节大小块)
一个transaction可以有多个buffer_head,即多份缓冲。buffer_head用于跟踪从磁盘加载到内存的块数据
struct journal_head {
struct buffer_head *b_bh;
int b_jcount;
unsigned b_jlist;
// 本journal_head在transaction_t的哪个链表上
unsigned b_modified;
// 标志该缓冲区是否以被当前正在运行的transaction修改过
char *b_frozen_data;
// 当jbd遇到需要转义的块时,
// 将buffer_head指向的缓冲区数据拷贝出来,冻结起来,供写入日志使用。
char *b_committed_data;
// 目的是防止重新写未提交的删除操作
// 含有未提交的删除信息的元数据块(磁盘块位图)的一份拷贝,
// 因此随后的分配操作可以避免覆盖未提交的删除信息。
// 也就是说随后的分配操作使用的时b_committed_data中的数据,
// 因此不会影响到写入日志中的数据。
transaction_t *b_transaction;
// 指向所属的transaction
transaction_t *b_next_transaction;
// 当有一个transaction正在提交本缓冲区,
// 但是另一个transaction要修改本元数据缓冲区的数据,
// 该指针就指向第二个缓冲区。
/*
* Doubly-linked list of buffers on a transaction's data, metadata or
* forget queue. [t_list_lock] [jbd_lock_bh_state()]
*/
struct journal_head *b_tnext, *b_tprev;
transaction_t *b_cp_transaction;
// 指向checkpoint本缓冲区的transaction。
// 只有脏的缓冲区可以被checkpointed。
struct journal_head *b_cpnext, *b_cpprev;
// 在旧的transaction_t被checkpointed之前必须被刷新的缓冲区双向链表。
/* Trigger type */
struct jbd2_buffer_trigger_type *b_triggers;
struct jbd2_buffer_trigger_type *b_frozen_triggers;
};
3.1.2 handle
提到的原子操作,jbd中用handle来表示。一个handle代表针对文件系统的一次原子操作。这个原子操作要么成功,要么失败,不会出现中间状态。在一个handle中,可能会修改若干个缓冲区,即buffer_head。
/**
* struct handle_s - this is the concrete type associated with handle_t.
* @h_transaction: Which compound transaction is this update a part of?
* @h_buffer_credits: Number of remaining buffers we are allowed to dirty.
* @h_ref: Reference count on this handle
* @h_err: Field for caller's use to track errors through large fs operations
* @h_sync: flag for sync-on-close
* @h_jdata: flag to force data journaling
* @h_aborted: flag indicating fatal error on handle
* @h_lockdep_map: lockdep info for debugging lock problems
*/
struct handle_s
{
/* Which compound transaction is this update a part of? */
transaction_t *h_transaction; // 本原子操作属于哪个transaction
/* Number of remaining buffers we are allowed to dirty: */
int h_buffer_credits; // 本原子操作的额度,即可以包含的磁盘块数
/* Reference count on this handle */
int h_ref; // 引用计数
/* Field for caller's use to track errors through large fs */
/* operations */
int h_err;
/* Flags [no locking] */
unsigned int h_sync: 1; /* sync-on-close */
unsigned int h_jdata: 1; /* force data journaling */
unsigned int h_aborted: 1; /* fatal error on handle */
// h_sync表示同步,意思是处理完该原子操作后,立即将所属的transaction提交。
#ifdef CONFIG_DEBUG_LOCK_ALLOC
3.1.3 transaction
struct transaction_s
{
journal_t *t_journal; // 指向所属的jounal
tid_t t_tid; // 本事务的序号
/*
* Transaction's current state
* [no locking - only kjournald alters this]
* [j_list_lock] guards transition of a transaction into T_FINISHED
* state and subsequent call of __journal_drop_transaction()
* FIXME: needs barriers
* KLUDGE: [use j_state_lock]
*/
enum {
T_RUNNING,
T_LOCKED,
T_FLUSH,
T_COMMIT,
T_COMMIT_RECORD,
T_FINISHED
} t_state; // 事务的状态
unsigned int t_log_start;
// log中本transaction_t从日志中哪个块开始
int t_nr_buffers;
// 本transaction_t中缓冲区的个数
struct journal_head *t_reserved_list;
// 被本transaction保留,但是并未修改的缓冲区组成的双向循环队列。
struct journal_head *t_locked_list;
// 由提交时所有正在被写出的、被锁住的数据缓冲区组成的双向循环链表。
struct journal_head *t_buffers;
// 元数据块缓冲区链表
// 这里面可都是宝贵的元数据啊,对文件系统的一致性至关重要!
struct journal_head *t_sync_datalist;
// 本transaction_t被提交之前,
// 需要被刷新到磁盘上的数据块(非元数据块)组成的双向链表。
// 因为在ordered模式,我们要保证先刷新数据块,再刷新元数据块。
struct journal_head *t_forget;
// 被遗忘的缓冲区的链表。
// 当本transaction提交后,可以un-checkpointed的缓冲区。
// 这种情况是这样:
// 一个缓冲区正在被checkpointed,但是后来又调用journal_forget(),
// 此时以前的checkpointed项就没有用了。
// 此时需要在这里记录下来这个缓冲区,
// 然后un-checkpointed这个缓冲区。
struct journal_head *t_checkpoint_list;
// 本transaction_t可被checkpointed之前,
// 需要被刷新到磁盘上的所有缓冲区组成的双向链表。
// 这里面应该只包括元数据缓冲区。
struct journal_head *t_checkpoint_io_list;
// checkpointing时,已提交进行IO操作的所有缓冲区组成的链表。
struct journal_head *t_iobuf_list;
// 进行临时性IO的元数据缓冲区的双向链表。
struct journal_head *t_shadow_list;
// 被日志IO复制(拷贝)过的元数据缓冲区组成的双向循环链表。
// t_iobuf_list 上的缓冲区始终与t_shadow_list上的缓冲区一一对应。
// 实际上,当一个元数据块缓冲区要被写到日志中时,数据会被复制一份,
// 放到新的缓冲区中。
// 新缓冲区会进入t_iobuf_list队列,
// 而原来的缓冲区会进入t_shadow_list队列。
struct journal_head *t_log_list;
// 正在写入log的起控制作用的缓冲区组成的链表。
spinlock_t t_handle_lock;
// 保护handle的锁
int t_updates;
// 与本transaction相关联的外部更新的次数
// 实际上是正在使用本transaction的handle的数量
// 当journal_start时,t_updates++
// 当journal_stop时,t_updates--
// t_updates == 0,表示没有handle正在使用该transaction,
// 此时transaction处于一种可提交状态!
int t_outstanding_credits;
// 本事务预留的额度
transaction_t *t_cpnext, *t_cpprev;
// 用于在checkpoint队列上组成链表
unsigned long t_expires;
ktime_t t_start_time;
int t_handle_count;
// 本transaction_t有多少个handle_t
unsigned int t_synchronous_commit:1;
// 本transaction已被逼迫了,有进程在等待它的完成。
};
3.1.4 checkpoint
用于重复使用,清除无效日志。
3.1.5 kjournald
日志的提交操作是由一个内核线程实现的,该线程称为kjournald,Kjournald 线程保证运行中的事务会在一个特定间隔后被提交。
3.1.6 Journal
struct journal_s
{
unsigned long j_flags; // journal的状态
int j_errno;
struct buffer_head *j_sb_buffer; // 指向日志超级块缓冲区
journal_superblock_t *j_superblock;
int j_format_version;
spinlock_t j_state_lock;
int j_barrier_count;
// 有多少个进程正在等待创建一个barrier lock
// 这个变量是由j_state_lock来保护的。
struct mutex j_barrier;
// 互斥锁
transaction_t *j_running_transaction;
// 指向正在运行的transaction
transaction_t *j_committing_transaction;
// 指向正在提交的transaction
transaction_t *j_checkpoint_transactions;
// 仍在等待进行checkpoint操作的所有事务组成的循环队列
// 一旦一个transaction执行checkpoint完成,则从此队列删除。
// 第一项是最旧的transaction,以此类推。
wait_queue_head_t j_wait_transaction_locked;
// 等待一个已上锁的transaction_t开始提交,
// 或者一个barrier 锁被释放。
wait_queue_head_t j_wait_logspace;
// 等待checkpointing完成以释放日志空间的等待队列。
wait_queue_head_t j_wait_done_commit;
//等待提交完成的等待队列
wait_queue_head_t j_wait_checkpoint;
wait_queue_head_t j_wait_commit;
// 等待进行提交的的等待队列
wait_queue_head_t j_wait_updates;
// 等待handle完成的等待队列
struct mutex j_checkpoint_mutex;
// 保护checkpoint队列的互斥锁。
unsigned int j_head;
// journal中第一个未使用的块
unsigned int j_tail;
// journal中仍在使用的最旧的块号
// 这个值为0,则整个journal是空的。
unsigned int j_free;
unsigned int j_first;
unsigned int j_last;
// 这两个是文件系统格式化以后就保存到超级块中的不变的量。
// 日志块的范围[j_first, j_last)
// 来自于journal_superblock_t
struct block_device *j_dev;
int j_blocksize;
unsigned int j_blk_offset;
// 本journal相对与设备的块偏移量
struct block_device *j_fs_dev;
unsigned int j_maxlen;
// 磁盘上journal的最大块数
spinlock_t j_list_lock;
struct inode *j_inode;
tid_t j_tail_sequence;
// 日志中最旧的事务的序号
tid_t j_transaction_sequence;
// 下一个授权的事务的顺序号
tid_t j_commit_sequence;
// 最近提交的transaction的顺序号
tid_t j_commit_request;
// 最近相申请提交的transaction的编号。
// 如果一个transaction想提交,则把自己的编号赋值给j_commit_request,
// 然后kjournald会择机进行处理。
__u8 j_uuid[16];
struct task_struct *j_task;
// 本journal指向的内核线程
int j_max_transaction_buffers;
// 一次提交允许的最多的元数据缓冲区块数
unsigned long j_commit_interval;
struct timer_list j_commit_timer;
// 用于唤醒提交日志的内核线程的定时器
spinlock_t j_revoke_lock;
// 保护revoke 哈希表
struct jbd_revoke_table_s *j_revoke;
// 指向journal正在使用的revoke hash table
struct jbd_revoke_table_s *j_revoke_table[2];
struct buffer_head **j_wbuf;
// 指向描述符块页面
int j_wbufsize;
// 一个描述符块中可以记录的块数
pid_t j_last_sync_writer;
u64 j_average_commit_time;
void *j_private;
// 指向ext3的superblock
};
3.1.7 journal_superblock
/*
* The journal superblock. All fields are in big-endian byte order.
*/
typedef struct journal_superblock_s
{
journal_header_t s_header; // 用于表示本块是一个超级块
__be32 s_blocksize; /* journal device blocksize */
// journal所在设备的块大小
__be32 s_maxlen; /* total blocks in journal file */
// 日志的长度,即包含多少个块
__be32 s_first; /* first block of log information */
// 日志中的开始块号,
// 注意日志相当于一个文件,
// 这里提到的开始块号是文件中的逻辑块号,
// 而不是磁盘的物理块号。
// 初始化时置为1,因为超级块本身占用了逻辑块0。
// 注意s_maxlen和s_first是在格式化时确定的,
// 以后就不会改变了。
__be32 s_sequence; /* first commit ID expected in log */
// 日志中第一个期待的commit ID
// 就是指该值应该是日志中最旧的一个事务的ID
__be32 s_start; /* blocknr of start of log */
// 日志开始的块号
// s_start为0表示不需要恢复
// 因为日志空间需要重复使用,相当于一个环形结构,
// s_start表示本次有效日志块的起点
__be32 s_errno;
// 注意:下列各域只有在superblock v2中才有效
/* Remaining fields are only valid in a version-2 superblock */
__be32 s_feature_compat; /* compatible feature set */
__be32 s_feature_incompat; /* incompatible feature set */
__be32 s_feature_ro_compat; /* readonly-compatible feature set */
__u8 s_uuid[16]; /* 128-bit uuid for journal */
__be32 s_nr_users; /* Nr of filesystems sharing log */
__be32 s_dynsuper; /* Blocknr of dynamic superblock copy*/
__be32 s_max_transaction; /* Limit of journal blocks per trans.*/
__be32 s_max_trans_data; /* Limit of data blocks per trans. */
__u32 s_padding[44];
__u8 s_users[16*48]; /* ids of all fs'es sharing the log */
} journal_superblock_t;
该内核线程平时一直在睡眠,直到有进程主动唤醒它,或者是定时器时间到了(一般为每隔5秒)。被唤醒后它就进行事务的提交操作。
三个过程:
写入日志叫transaction commiting,提交到磁盘叫commit,提交完成日志声明空间擦除(释放日志的过程)可重新使用叫checkpointing。
3.2 事务的状态
事务最重要的属性是它的状态(state)。当事务正在提交时,它的生命周期经历了下面的一系列状态。
- 运行(running):可以接收handler的状态叫运行。
- 锁定(locked):不接收handler但handler还没完全放进日志叫锁定。
- 写入(flush):接收完handler正在写入日志叫写入。
- 提交(commit):事务写入日志后,以原子单位写入磁盘块中叫提交。
- 完成(finished):事务完整的写到日志种种之后,它会留在那直到所有的块都被更新到磁盘上的实际位置。
3.3 事务提交的阶段
对应的函数:[journal_commit_transaction(journal object)]
Kjournald 线程保证运行中的事务会在一个特定间隔后被提交。事务提交的代码分成了如下八个不同阶段。简单来说要对几个目标进行操作:事务,事务的缓冲,事务的元数据。事务和事务的元数据都要通过日志,只有缓冲是直接写入磁盘。
具体分析几个阶段:
阶段0:将事务的状态从运行(T_RUNNING)变为锁定(T_LOCKED),这样,这个事务就不能再添加新的 handle 了。事务会等待所有的 handle 都完成的。当事务初始化的时候,会预留一部分缓冲区。在这一阶段,有些缓冲区可能没有使用,就被释放了。到这里,所有事务都完成了之后,事务就已经准备好了可以被提交了。
阶段1:事务进入到写入状态(T_FLUSH)。事务被标记为正在向日志提交的事。这一阶段中,也会标记该日志没有运行状态的事务存在;这样,新的 handles 请求将会导致新的事务的初始化。
阶段2:事务实际使用的缓冲们被写入到磁盘上。
阶段3:这时所有的数据缓冲都已经写入到磁盘上了,但它们的元数据还在内存之中。日志描述块以标签(tag)的形式存储日志中的每个元数据缓存到它的实际位置的映射。
阶段4 和阶段5:这两个阶段分别在等元数据缓冲和日志描述符的 I/O 描述通告。一旦收到 I/O 完成消息,内存链表中对应的缓冲就可以释放掉了。
阶段6:现在所有数据和元数据都已经安全地保存着 了,数据就在它们的实际位置,而元数据在日志中。现在,事务要被标记为已提交,这样就表明所有的更新都妥善保存在日志之中了。因此,日志描述块再次分配。 一个标签会被写入,以示事务已经被成功提交,这个块是同步写入到到日志之中的。这之后,事务便进入了已提交状态,T_COMMIT。
阶段7:当很多事务被写入到日志中但还没有写入到磁盘中的时候。当前事务中的一些元数据缓存可能是一些之前的事务的一部分。这就不需要保存之前的事务了,因为我们的当前提交的事务已经有了更新的版本了。这些缓冲于是就被从老的事务中删除了。
阶段8:事务被标记为完成状态,T_FINISHED。日志结构被更新以将当前事务标记为最新提交的事务。同时也将自己标到要被 checkpoint 的事务列表中去。
3.4 释放日志空间Checkpointing
释放日志空间的过程。
一个日志区域可以有多个checkpointing事务,每个 checkpointing 事务都可以有多个缓冲。
3.5 日志的恢复
对应的函数:[journal_recover(journal object)]
修复分三个阶段进行:
- PASS_SCAN: 发现日志记录的尾部。
- PASS_REVOKE: 为日志记录准备一串要被撤销的块。
- PASS_REPLAY: 未被撤销的块以确保磁盘一致性的顺序被重新写入(重放)。
如果在修复过程中系统再次崩溃的话不会造成任何破坏。同样的日志可以在下次用 来重新恢复,在恢复的过程中,无关的操作不会被进行。
4 JBD的操作函数
4.1 journal_start
journal_start 的主要作用是取得一个原子操作描述符handle_t,如果当前进程已经有一个,则直接返回,否则,需要新创建一个。
4.2 journal_stop
该函数的主要作用是将该handle与transaction断开链接,调整所属transaction的额度。如果该原子操作时同步的,则设置事务的t_synchronous_commit标志。在事务提交时,会根据该标志决定缓冲区的写方式。
4.3 journal_get_create_access
取得通过journal_start()获得原子操作描述符后,在修改缓冲区前,我们应该在jbd中先获得该缓冲区的写权限。journal_get_create_access()、journal_get_write_access()和journal_get_undo_access()这三个函数的作用就是在jbd中取得该缓冲区的写权限。
4.4 journal_get_write_access
journal_get_write_access()函数的作用是使jbd取得对bh的写权限
4.5 journal_dirty_data
jbd 在取得缓冲区的写权限后,文件系统就可以修改改缓冲区的内容了。
4.6 journal_forget
以索引块缓冲区为例。如果一个原子操作在运行过程中,要分配一个新的索引块,则它就先调用journal_get_write_access()函数取得写权限,然后修改这个索引块缓冲区,然后调用journal_dirty_metadata()将该缓冲区设为脏。但是,如果不幸,该原子操作后边运行出错了,需要将之前的修改全部取消,则需要调用journal_forget()函数使jbd“忘记”该缓冲区。
5 关闭ext4 的Journal
# 关闭ext4的journal功能
tune2fs -O has_journal /dev/sdb1
# 检查磁盘完整性
e2fsck -f /dev/sdb1
# 查看是否关闭成功
dumpe2fs /dev/sdb1 | grep 'Filesystem features'
root@henry_fordham ➜ ~ dumpe2fs /dev/sda1 | grep 'Filesystem features'
dumpe2fs 1.44.1 (24-Mar-2018)
Filesystem features: ext_attr resize_inode dir_index filetype sparse_super large_file
来源:CSDN
作者:一个姓雪的小哥哥
链接:https://blog.csdn.net/qq_32473685/article/details/103585546