细说jbd(journal-block-device)& 源码分析

ext4 用的日志文件系统变成了 jbd2,本次分析以ext3为主,分析jbd文件系统。

jbd 要解决什么问题

  • 或者说ext2的缺点在哪里,因为ext3与ext2的主要差别就在于ext3在ext2的基础上增加了日志功能。
  • 假设你正在运行一个Linux系统,运行一个程序,在一个ext2分区上不断地读写磁盘文件。突然断电了,或者系统崩溃了,你的心里肯定会咯噔一下:“磁盘分区没坏吧?文件还完整么?”告诉你一个不幸的消息,文件可能不完整了,文件可能已经损坏了,甚至该分区不能再被挂载了。也就是说,意外的系统崩溃,可能会使ext2文件系统处于一个不一致的状态。
  • 假设你的运气好一点,分区仍能被识别,但是重新挂载时,如果发现分区处于不一致状态,那么系统会自动调用fsck程序,尝试将文件系统恢复到一致的状态。那将是一个非常漫长的过程,并且随着磁盘容量的增大,花费的时间也越长,有时需要长达几个小时。这样会极大地影响系统的可用性。
  • 总之,jbd的主要目的不是减少系统崩溃的概率,而是系统正常运行时,尽量使文件系统处于一个一致的状态,以及系统崩溃后,尽可能减少使文件系统重新处于一致性状态的时间。通过减少维护时间,增加系统的可用性。

jbd是如何解决问题

  • 提到一致性,大家会想到数据库里面的事务的概念,事务有四个基本属性
  1. 原子性
    事务必须是原子工作单元;对于其数据修改,要么全部执行,要么全都不执行。
  2. 一致性
    事务在完成时,必须使所有的数据都保持一致状态。
  3. 隔离性
    由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务识别数据时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是第二个事务修改它之后的状态,事务不会识别中间状态的数据。
  4. 持久性
    事务完成之后,他对于系统的影响是永久性的。该修改即使出现系统故障也将一直保持。
  • 文件系统的开发者借用了数据库中事务的思想,将其应用于文件系统上,以期保证对文件系统操作的原子性、隔离性,尽量使文件系统处于一致性。
文件系统某些操作抽象成原子操作
  • 所谓原子操作,就是内部不再分割的操作,该操作要么完全完成,要么根本没有执行,不存在部分完成的状态。
  • 那么,什么样的操作可以看成对文件系统的原子操作呢?往一个磁盘文件中追加写入1MB字节可以看成一个原子操作么?这个操作其实比较大,因为要写1MB的数据,要为文件分配1024个磁盘块,同时还要分配若干个索引块,也会涉及到很多的磁盘块位图、块组块的读写,非常复杂,时间也会比较长,中间出问题的机会就比较多,所以不适宜看做一个原子操作。
  • 那么,什么样的操作可以看成对文件系统的原子操作呢?比如说为文件分配一个磁盘块,就看成一个原子操作就比较合适。分配一个磁盘块,可能需要修改一个inode块、一个磁盘块位图、最多三个间接索引块、块组块、超级块,一共最多7个磁盘块。将分配一个磁盘块看成一个原子操作,意味着上述修改7个磁盘块的操作要么都成功,要么都失败,不可能有第三种状态。
若干个原子操作组成一个事务
  • 实现日志文件系统时,可以将一个原子操作就作为一个事务来处理,但是这样实现的效率比较低。于是ext3将若干个原子操作组合成一个事务,对磁盘日志以事务为单位进行管理,以提高读写日志的效率。
在磁盘上单独划分一个日志空间
  • 日志,在这里指的是磁盘上存储事务数据的那个地方,即若干磁盘块。它可以以一个单独的文件形式存在,也可以由文件系统预留一个inode和一些磁盘块,也可以是单独的磁盘分区。总之就是磁盘上存储事务数据的那个地方。

  • 提到日志时,可能还有另外一种含义,就是它是一种机制,用于管理内存中的缓存区、事务、磁盘日志数据读写等等所有这一切,统称为日志。读者注意根据上下文进行区分。

将内存事务的数据写到日志中
  • 文件系统可以选择定期(每隔5秒,或用户指定的时间间隔)或者立即将内存中的事务数据写到磁盘日志上,以备发生系统崩溃后可以利用日志中的数据恢复,重新使文件系统保持一致的状态。
  • 这个间隔时间的选取,要注意性能的平衡。时间间隔越短,文件系统丢失数据的可能性就越少,一致性的时间点就越新,但是IO负担就越重,很可能就会影响系统的性能。反过来,时间间隔越大,文件系统丢失的数据可能就越多,一致性的时间点就越旧。但是IO负担就比较轻,不太会影响系统的性能。
从日志恢复数据
  • jbd的思想就是原来内核读写磁盘的逻辑保持不变,但是对于影响文件系统一致性的数据块(即元数据块,第四章会详细解释),及时地写到磁盘上的日志空间中去。这样,即使系统崩溃了,也能从日志中恢复数据,确保文件系统的一致性。如错误!未找到引用源。,其中绿色的箭头表示正常的磁盘读写,紫色的箭头表示由jbd将元数据块额外写一份到磁盘日志中,红色箭头表示恢复时,由jbd将日志中的数据写回磁盘的原始位置。

细说jbd(journal-block-device)& 源码分析

概念介绍

buffer_head
  • buffer_head 是内核一个用于管理磁盘缓冲区的数据结构。根据局部性原理,磁盘上的数据进入内存后一般都是存放在磁盘缓冲区中,以备将来重复读写。所以说,一个buffer_head就会对应一个文件系统块,即对应一个磁盘块。(512字节大小块)
元数据块
  • 笼统地,可以将一个文件系统内的块分为两种,一种是对文件系统的一致性有重要影响的、用于文件系统管理的磁盘块,称之为元数据块,包括超级块、磁盘位图块、inode位图块、索引块、块组描述符块等等;另一种是存放文件数据的,称之为数据块。
  • 因为元数据块对文件系统的一致性有至关重要的影响,故jbd主要处理元数据块。当然,ext3的日志可以设置为三种模式,不同的模式中jbd处理的数据也是不一样的,这个下文会详述。ext3磁盘物理布局参考图表 2 ext3磁盘物理布局

细说jbd(journal-block-device)& 源码分析


handle
  • 提到的原子操作,jbd中用handle来表示。一个handle代表针对文件系统的一次原子操作。这个原子操作要么成功,要么失败,不会出现中间状态。在一个handle中,可能会修改若干个缓冲区,即buffer_head.
transaction
  • jbd为了提高效率,将若干个handle组成一个事务,用transaction来表示。对日志读写来说,都是以transaction为单位的。在处理日志数据时,transaction具有原子性,即恢复时,如果一个transaction是完整的,其中包含的数据就可用于文件系统的恢复,否则,忽略不完整的transaction。
journal
  • journal 在英文中有“日志”之意,在jbd中journal既是磁盘上日志空间的代表,又起到管理内存中为日志机制而创建的handle、transaction等数据结构的作用,可以说是整个体脂机制的代表。
commit
  • 所谓提交,就是把内存中transaction中的磁盘缓冲区中的数据写到磁盘的日志空间上。注意,jbd是将缓冲区中的数据另外写一份,写到日志上,原来的kernel将缓冲区写回磁盘的过程并没有改变。
  • 在内存中,transaction是可以有若干个的,而不是只有一个。transaction可分为三种,一种是已经commit到磁盘日志中的,它们正在进行checkpoint操作;第二种是正在将数据提交到日志的transaction;第三种是正在运行的transaction。正在运行的transaction管理随后发生的handle,并在适当时间commit到磁盘日志中。注意正在运行的transaction最多只可能有一个,也可能没有,如果没有,则handle提出请求时,则会按需要创建一个正在运行的transaction。
checkpoint
  • 当一个transaction已经commit,那么,是不是在内存中它就没有用了呢?好像是这样,因为其中的数据已经写到磁盘日志中了。但是实际上不是这样的。主要原因是磁盘日志是个有限的空间,比如说100MB,如果一直提交transaction,很快就会占满,所以日志空间必须复用。
  • 其实与日志提交的同时,kernel也在按照自己以前的方式将数据写回磁盘。试想,如果一个transaction中包含的所有磁盘缓冲区的数据都已写回到磁盘的原来的位置上(不是日志中,而是在磁盘的原来的物理块上),那么,该transaction就没有用了,可以被删除了,该transaction在磁盘日志中的空间就可以被回收,进而重复利用了。
revoke
  • 假设有一个缓冲区,对应着一个磁盘块,内核多次修改该缓冲区,于是磁盘日志中就会有该缓冲区的若干个版本的数据。假设此时要从文件中删除该磁盘块,那么,一旦包含该删除操作的transaction提交,那么,再恢复时,已经存放在磁盘日志中的该磁盘块的若干个版本的数据就不必再恢复了,因为到头来还是要删除的。revoke就是这样一种加速恢复速度的方法。当本transaction包含删除磁盘块操作时,就会在磁盘日志中写一个revoke块,该块中包含<被revoked的块号blocknr,提交的transaction的ID>,表示恢复时,凡是transaction ID小于等于ID的所有写磁盘块blocknr的操作都可以取消了,不必进行了。
recover
  • 加入日志机制后,一旦系统崩溃,重新挂载分区时,就会检查该分区上的日志是否需要恢复。如果需要,则依次将日志空间的数据写回磁盘原始位置,则文件系统又重新处于一致状态了。
kjournald
  • 日志的提交操作是由一个内核线程实现的,该线程称为kjournald。该内核线程平时一直在睡眠,直到有进程主动唤醒它,或者是定时器时间到了(一般为每隔5秒)。被唤醒后它就进行事务的提交操作。

数据结构介绍

  • 以下数据结构的定义在include/linux/jbd.h和include/linux/journal_head.h中。
handle_t 表示一个原子操作
struct handle_s
{
	transaction_t	*h_transaction;	// 本原子操作属于哪个transaction
	int			h_buffer_credits;	// 本原子操作的额度,即可以包含的磁盘块数
	int			h_ref;			// 引用计数
	int			h_err;

	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提交。
// 其余两个好像没有用到。
};
typedef struct handle_s handle_t;	/* Atomic operation type */

  • 注意jbd中数据结构定义一般都采用这种方式,即定义结构时用XXX_s,然后用typedef定义XXX_t。下面不再特别指出了。
  • 乍看这个表示原子操作的结构有些奇怪,它怎么不包含缓冲区呢?其实handle_t的主要目的是顺着它能找到对应的transaction。如果你想把一些缓冲区纳入日志管理,需要另外的步骤。
transaction_t 表示一个事务
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已被逼迫了,有进程在等待它的完成。
};

journal_t
  • journal_t是整个日志机制的代表,既管理者内存中的各种日志相关的数据结构,又管理着磁盘上的日志空间。
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
};

journal_superblock_t
  • 日志超级块在内存中的表现。
/*
 * 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;

journal_head
  • 一个buffer_head对应一个磁盘块,而一个journal_head对应一个buffer_head。日志通过journal_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;
};

journal_head_t
  • 每个块的开头,都有一个起描述作用的结构,定义如下:
/*
 * Standard header for all descriptor blocks:
 */
typedef struct journal_header_s
{
	__be32		h_magic;
	__be32		h_blocktype;
	__be32		h_sequence;
} journal_header_t;
  • 其中,h_magic是一个幻数,如果是一个日志块的描述块,则为JFS_MAGIC_NUMBER,
    #define JFS_MAGIC_NUMBER 0xc03b3998U,否则该块就不是一个日志描述块。
    h_blocktype表示该块的类型,即上述五种块之一。
    h_sequence表示本描述块对应的transaction的序号。

三种日志模式

  • 日志机制的基本原理就是选择与文件系统一致性相关的缓冲区,在适当的时机“偷偷地”写入日志。但是选择什么样的缓冲区,以及在什么时间写入日志,就是所谓的日志模式。日志模式与系统性能息息相关,是个值得仔细研究、平衡的大问题。
  • ext3支持三种日志模式,划分的依据是选择元数据块还是数据块写入日志,以及何时写入日志。
  1. 日志(journal)
    文件系统所有数据块和元数据块的改变都记入日志。 这种模式减少了丢失每个文件所作修改的机会,但是它需要很多额外的磁盘访问。例如,当一个新文件被创建时,它的所有数据块都必须复制一份作为日志记录。这是最安全和最慢的ext3日志模式。
  2. 预定(ordered)
    只对文件系统元数据块的改变才记入日志,这样可以确保文件系统的一致性,但是不能保证文件内容的一致性。然而,ext3文件系统把元数据块和相关的数据块进行分组,以便在元数据块写入日志之前写入数据块。这样,就可以减少文件内数据损坏的机会;例如,确保增大文件的任何写访问都完全受日志的保护。这是缺省的ext3 日志模式。
  3. 写回(writeback)
    只有对文件系统元数据的改变才记入日志,不对数据块进行任何特殊处理。这是在其他日志文件系统发现的方法,也是最快的模式。
    在挂载ext3文件系统时,可通过data=journal等修改日志模式。如果不做特殊说明,下文中将默认介绍预定(ordered)模式,这是ext3默认的日志模式,也是实现最复杂的一种模式。

jbd基本操作

  • 日志机制的核心是处理磁盘块缓冲区。它是在不影响系统原有的处理缓冲区的逻辑的基础上进行了。
  • 日志的基本操作包括对原子操作的操作,对缓冲区的操作,对日志空间的操作,对日志提交的操作等等。
journal_start
  • journal_start 的主要作用是取得一个原子操作描述符handle_t,如果当前进程已经有一个,则直接返回,否则,需要新创建一个。
  • 同样,该handle_t也必须与一个正在运行的transaction相关联,如果没有正在运行的transaction,则创建一个新的transaction,并将其设置为正在运行的。
  • 参数nblocks是向日志申请nblocks个缓冲区的空间,也表明该原子操作预期将修改 nblocks个缓冲区。
271 handle_t *journal_start(journal_t *journal, int nblocks)
 272 {
 273     handle_t *handle = journal_current_handle();
 274     int err;
279     if (handle) {
			 // 如果当前进程已经有handle_t,则直接返回。
 280         J_ASSERT(handle->h_transaction->t_journal == journal);
 281         handle->h_ref++;
 282         return handle;
 283     }
 284 
		 // 否则,创建一个新的handle_t
 285     handle = new_handle(nblocks);
289     current->journal_info = handle;
          // start_this_handle的主要作用是将该handle与当前正在运行的transaction相关联,
         // 如果没有正在运行的transaction,则创建一个新的,并将其设置为正在运行的。
291     err = start_this_handle(journal, handle);
299     return handle;
 300 }

journal_stop
  • 该函数的主要作用是将该handle与transaction断开链接,调整所属transaction的额度。如果该原子操作时同步的,则设置事务的t_synchronous_commit标志。在事务提交时,会根据该标志决定缓冲区的写方式。
1363 int journal_stop(handle_t *handle)
1364 {
1365     transaction_t *transaction = handle->h_transaction;
1366     journal_t *journal = transaction->t_journal;
		 
// 如果该原子操作时同步的,则设置相应transaction的t_synchronous_commit标志,
// 则在提交事务时同步写出缓冲区。
1436     if (handle->h_sync)
1437         transaction->t_synchronous_commit = 1;

1438     current->journal_info = NULL;
1441     transaction->t_outstanding_credits -= handle->h_buffer_credits;
1442     transaction->t_updates--;
1455     if (handle->h_sync ||
1456             transaction->t_outstanding_credits >
1457                 journal->j_max_transaction_buffers ||
1458             time_after_eq(jiffies, transaction->t_expires)) {
1462         tid_t tid = transaction->t_tid;
			 
// 这里的提交,只是设置journal中的j_commit_request,
// 并唤醒等待的内核线程。
1468         __log_start_commit(journal, transaction->t_tid);

// 如果该原子操作时是同步的,则我们等待事务提交完成
1475         if (handle->h_sync && !(current->flags & PF_MEMALLOC))
1476             err = log_wait_commit(journal, tid);
1480     }
1484     jbd_free_handle(handle);
1485     return err;
1486 }

  • journal_stop()函数调用了__log_start_commit()函数,该函数的主要作用是设置journal的j_commit_request域,标明申请进行提交的事务ID的最大值,并唤醒等待在j_wait_commit等待队列上的内核线程kjournald。
435 int __log_start_commit(journal_t *journal, tid_t target)
436 {       
440     if (!tid_geq(journal->j_commit_request, target)) {
446         journal->j_commit_request = target;
450         wake_up(&journal->j_wait_commit);
451         return 1;
452     }
453     return 0;
454 }

  • log_wait_commit()函数,就不这么简单了,他需要一直等待ID为tid的transaction结束。
535 int log_wait_commit(journal_t *journal, tid_t tid)
536 {
549     while (tid_gt(tid, journal->j_commit_sequence)) {
			// journal->j_commit_sequence保存的是上一次已提交的transaction的ID
// 循环等待ID为tid的transaction结束。
552         wake_up(&journal->j_wait_commit);
553         spin_unlock(&journal->j_state_lock);
554         wait_event(journal->j_wait_done_commit,
555                 !tid_gt(tid, journal->j_commit_sequence));
556         spin_lock(&journal->j_state_lock);
557     }
565 }

journal_get_create_access
  • 取得通过journal_start()获得原子操作描述符后,在修改缓冲区前,我们应该在jbd中先获得该缓冲区的写权限。journal_get_create_access()、journal_get_write_access()和journal_get_undo_access()这三个函数的作用就是在jbd中取得该缓冲区的写权限。注意,这三个函数都是将缓冲区加入transaction的BJ_Reserved队列上,表示这些缓冲区已被该transaction管理,但是并未修改。
  • 元数据缓冲区的内容可能来自磁盘块,也可能是先在内存中产生数据,然后写回到磁盘块上的。典型的是文件的索引块。假设我们在扩展文件,则有可能新分配索引块。那么,对这种新创建的元数据块,必须通过journal_get_create_access()函数取得该缓冲区的写权限。
781 int journal_get_create_access(handle_t *handle, struct buffer_head *bh)
782 {
783     transaction_t *transaction = handle->h_transaction;
784     journal_t *journal = transaction->t_journal;
		
		// 将一个新的jh与bh相关联,即将bh纳入jbd管理。
785     struct journal_head *jh = journal_add_journal_head(bh);

802     jbd_lock_bh_state(bh);
803     spin_lock(&journal->j_list_lock);
812     if (jh->b_transaction == NULL) {
			// 如果该jh以前没有受jbd管理
821         clear_buffer_dirty(jh2bh(jh));
822         jh->b_transaction = transaction;
825         jh->b_modified = 0;
			// 将jh加入transaction的BJ_Reserved 队列,供jbd管理使用
828         __journal_file_buffer(jh, transaction, BJ_Reserved);
829     } else if (jh->b_transaction == journal->j_committing_transaction) {
			// 如果该jh由已经由正在提交的事务管理,
			// 则将jh->b_next_transaction设置为handle的transaction,
			// 意思是jh由当前所属的transaction处理完之后,
// 还要由handle的transnation继续处理。
831         jh->b_modified = 0;
834         jh->b_next_transaction = transaction;
835     }
836     spin_unlock(&journal->j_list_lock);
837     jbd_unlock_bh_state(bh);

		// 取消“取消”操作。
		// 这个有点奇怪吧。第四章第8节提到过revoke的概念。
		// 现在很明显该jh是文件系统需要的了,不应该再被revoke。
847     journal_cancel_revoke(handle, jh);
848     journal_put_journal_head(jh);
849 out:
850     return err;
851 }

  • journal_add_journal_head()函数的作用是将缓冲区bh纳入jbd管理,即给每个bh关联一个journal_head,一般简称jh,然后用jh指向bh。这样,事务中就可以通过管理jh,来管理对应的缓冲区。
1802 struct journal_head *journal_add_journal_head(struct buffer_head *bh)
1803 {
1804     struct journal_head *jh;
1805     struct journal_head *new_jh = NULL;
1806 
1807 repeat:
1808     if (!buffer_jbd(bh)) {
1809         new_jh = journal_alloc_journal_head();
1810         memset(new_jh, 0, sizeof(*new_jh));
1811     }
1812 
1813     jbd_lock_bh_journal_head(bh);
1814     if (buffer_jbd(bh)) {
1815         jh = bh2jh(bh);
1816     } else {
1821         if (!new_jh) {
1822             jbd_unlock_bh_journal_head(bh);
1823             goto repeat;
1824         }
1825 
1826         jh = new_jh;
1827         new_jh = NULL;      /* We consumed it */
1828         set_buffer_jbd(bh);
1829         bh->b_private = jh;

			 // 本函数的主要作用在这里,将jh指向一个bh。
1830         jh->b_bh = bh;
1831         get_bh(bh);
1833     }
1834     jh->b_jcount++;
1835     jbd_unlock_bh_journal_head(bh);
1836     if (new_jh)
1837         journal_free_journal_head(new_jh);
1838     return bh->b_private;
1839 }

journal_get_write_access
  • journal_get_write_access()函数的作用是使jbd取得对bh的写权限。
748 int journal_get_write_access(handle_t *handle, struct buffer_head *bh)
749 {
// 将一个新的jh与bh相关联,即将bh纳入jbd管理。
750     struct journal_head *jh = journal_add_journal_head(bh);
751     int rc;

		// 将jh加入transaction的BJ_Reserved队列,供管理使用
756     rc = do_get_write_access(handle, jh, 0);

757     journal_put_journal_head(jh);
758     return rc;
759 }
do_get_write_access()函数的主要作用是将jh加入transaction的相关队列中。
514 static int
515 do_get_write_access(handle_t *handle, struct journal_head *jh,
516             int force_copy)
517 {
518     struct buffer_head *bh;
519     transaction_t *transaction;
520     journal_t *journal;
521     int error;
522     char *frozen_buffer = NULL;
523     int need_copy = 0;
528     transaction = handle->h_transaction;
529     journal = transaction->t_journal;

534 repeat:
535     bh = jh2bh(jh);
539     lock_buffer(bh);
540     jbd_lock_bh_state(bh);
555     if (buffer_dirty(bh)) {
			// 从数据是否与磁盘一致的角度看,
// 缓冲区可分为dirty与update两种状态。
// 纳入jbd管理后,缓冲区的脏状态将由jbd管理。
560         if (jh->b_transaction) {
568             warn_dirty_buffer(bh);
569         }
576         clear_buffer_dirty(bh);
577         set_buffer_jbddirty(bh);
578     }
579 
580     unlock_buffer(bh);
593     if (jh->b_transaction == transaction ||
594         jh->b_next_transaction == transaction)
			// 如果该jh已经由当前transaction管理了,
			// 则我们不需要做什么了,直接退出即可。
595         goto done;

601     jh->b_modified = 0;
607     if (jh->b_frozen_data) {
			// 否则,该jh尚未由当前的transaction管理,
			// 这样,该jh已经被旧的某个transaction管理了,
			// 设置b_next_transaction的值,
			// 表示旧的transaction处理完本jh之后,
			// 本transaction会继续处理该jh。
610         jh->b_next_transaction = transaction;
				// 因为b_frozen_data已经存在了,则不需要再拷贝了。
611         goto done;
612     }
615 
616     if (jh->b_transaction && jh->b_transaction != transaction) {
// 该缓冲区属于旧的transaction
631         if (jh->b_jlist == BJ_Shadow) {
				// 如果该jh在旧的transaction的BJ_Shadow队列上,
				// 则表示该jh正在被写入到日志中,
				// 我们要等待写操作的完成。
632             DEFINE_WAIT_BIT(wait, &bh->b_state, BH_Unshadow);
633             wait_queue_head_t *wqh;
635             wqh = bit_waitqueue(&bh->b_state, BH_Unshadow);
638             jbd_unlock_bh_state(bh);
640             for ( ; ; ) {
641                 prepare_to_wait(wqh, &wait.wait,
642                         TASK_UNINTERRUPTIBLE);
643                 if (jh->b_jlist != BJ_Shadow)
644                     break;
645                 schedule();
646             }
647             finish_wait(wqh, &wait.wait);
648             goto repeat;
649         }
650 
665         if (jh->b_jlist != BJ_Forget || force_copy) {
				// 该缓冲区属于旧的transaction,并且不在BJ_Forget队列上,
				// 或者force_copy == 1
				// 则我们需要将缓冲区当前的数据冻结起来,备份起来,
// 供旧的transaction使用。
667             if (!frozen_buffer) {
669                 jbd_unlock_bh_state(bh);
670                 frozen_buffer =
671                     jbd_alloc(jh2bh(jh)->b_size,
672                              GFP_NOFS);
673                 if (!frozen_buffer) {
678                     error = -ENOMEM;
679                     jbd_lock_bh_state(bh);
680                     goto done;
681                 }
682                 goto repeat;
683             }
684             jh->b_frozen_data = frozen_buffer;
685             frozen_buffer = NULL;
				// 必须要拷贝,不能含糊
686             need_copy = 1;
687         }
			// 旧的transaction处理完本jh之后,
			// 本transaction会继续处理该jh。
688         jh->b_next_transaction = transaction;
689     }
691 
697     if (!jh->b_transaction) {
// 最后,如果该缓冲区目前没有被journald,
// 我们需要确保在调用者写回磁盘之前journal该缓冲区。
700         jh->b_transaction = transaction;
702         spin_lock(&journal->j_list_lock);
703         __journal_file_buffer(jh, transaction, BJ_Reserved);
704         spin_unlock(&journal->j_list_lock);
705     }
706 
707 done:
708     if (need_copy) {
			// 如果需要将缓冲区目前的数据冻结起来,
			// 就复制一份,放到jh->b_frozen_data中。
709         struct page *page;
710         int offset;
711         char *source;
715         page = jh2bh(jh)->b_page;
716         offset = ((unsigned long) jh2bh(jh)->b_data) & ~PAGE_MASK;
717         source = kmap_atomic(page, KM_USER0);
718         memcpy(jh->b_frozen_data, source+offset, jh2bh(jh)->b_size);
719         kunmap_atomic(source, KM_USER0);
720     }
721     jbd_unlock_bh_state(bh);
		// 同理,取消“取消”操作。
727     journal_cancel_revoke(handle, jh);
734     return error;
735 }

journal_get_undo_access
  • 这个函数是处理一种特殊的元数据块的----磁盘块位图。
  • 磁盘块位图是文件系统用于记录磁盘块使用情况的一种结构,块中的每一个位表示相应的磁盘块是否被占用。如果空闲,则为0,否则为1。磁盘快位图之所以特殊,在于一个磁盘块不能被两个文件同时占用,他要么是空闲的,要么在同一时刻只能被一个文件占用。对这种元数据块的修改,要取得undo权限,为什么呢?
  • 假设handle1中,删除了一个数据块b1,则对应bitmap1中的位被清掉,这个操作属于transaction1.此时,再进行磁盘块的分配和释放,则我们必须要知道bitmap1是否已被提交到日志中了。因为,如果bitmap1已经被提交到日志中,则表示handle1已经确实完成了,即使现在发生崩溃,删除b1的操作也可以是重现的。但是如果bitmap1没有被提交到日志中,则表示handle并没有完成,那么,你说此时数据块b1是已经被删除了还是没有被删除?从物理的角度看b1并没有被删除,因为实际上磁盘块位图并没有被改变。
  • 此时,如果重新分配磁盘块b1,我们必须等待,直到t1提交完成,以保证handle1的可恢复性。
  • 因此,我们从磁盘块位图中分配磁盘块时,只可以分配在缓冲区中和日志中该位都为0的磁盘块。为此jbd在取得磁盘块位图缓冲区的写权限是,必须将缓冲区当前的内容考本一份,以备分配磁盘块时使用。
  • journal_get_undo_access()与journal_get_write_access()函数基本类似,但是注意在调用do_get_write_access()函数时最后一个参数是1,表示force_copy为真,表示一定要将缓冲区当前的数据冻结起来。
878 int journal_get_undo_access(handle_t *handle, struct buffer_head *bh)
879 {
880     int err;
881     struct journal_head *jh = journal_add_journal_head(bh);
882     char *committed_data = NULL;
883 
891     err = do_get_write_access(handle, jh, 1);
		// 将缓冲区当前的数据拷贝一份,放到jh->b_committed_data中,
		// 作为将来分配磁盘块的依据。
895 repeat:
896     if (!jh->b_committed_data) {
897         committed_data = jbd_alloc(jh2bh(jh)->b_size, GFP_NOFS);
898         if (!committed_data) {
901             err = -ENOMEM;
902             goto out;
903         }
904     }
905 
906     jbd_lock_bh_state(bh);
907     if (!jh->b_committed_data) {
911         if (!committed_data) {
912             jbd_unlock_bh_state(bh);
913             goto repeat;
914         }
915 
916         jh->b_committed_data = committed_data;
917         committed_data = NULL;
918         memcpy(jh->b_committed_data, bh->b_data, bh->b_size);
919     }
920     jbd_unlock_bh_state(bh);
921 out:
922     journal_put_journal_head(jh);
925     return err;
926 }

我们接着看看ext3中是如何判断一个磁盘块是否可以被分配的。
fs/ext3/balloc.c。
判断一个磁盘块是否可以被分配的逻辑是这样的:

  1. 先看内存中磁盘块位图中该块对应位的值,如果为1,表示不可分配;
    (注意,此时虽然内存中该位为1,可能磁盘上该位为0,但是,没说的,无论如何不分配这样的块。)
  2. 如果内存中磁盘块位图中该块对应位的值为0,再检查b_committed_data中该块对应位的值。如果为0,表示可以分配;如果为1,表示不可分配!
    ext3_test_allocatable()函数返回值:
    返回0:第nr位已经被置为1,表示不可分配。
    返回1:第nr位没有被置为1,表示可以分配。
704 static int ext3_test_allocatable(ext3_grpblk_t nr, struct buffer_head *bh)
705 {
706     int ret;
707     struct journal_head *jh = bh2jh(bh);
708 
709     if (ext3_test_bit(nr, bh->b_data))
710         return 0;
711 
712     jbd_lock_bh_state(bh);
713     if (!jh->b_committed_data)
714         ret = 1;
715     else
716         ret = !ext3_test_bit(nr, jh->b_committed_data);
717     jbd_unlock_bh_state(bh);
718     return ret;
719 }

journal_dirty_data
  • jbd 在取得缓冲区的写权限后,文件系统就可以修改改缓冲区的内容了。修改完毕,文件系统需要调用一个函数,来通知jbd该缓冲区的修改已经完成。journal_dirty_data()和journal_dirty_metadata()函数就是作通知作用的。
  • journal_dirty_data()的作用就是通知jbd一个数据块缓冲区的修改已经完成。这样在ordered模式中,可以确保在提交transaction之前。将相关联的数据块缓冲区都写会磁盘原始位置。
945 int journal_dirty_data(handle_t *handle, struct buffer_head *bh)
946 {
947     journal_t *journal = handle->h_transaction->t_journal;
949     struct journal_head *jh;
		 // 将一个新的jh与bh相关联,即将bh纳入jbd管理。
955     jh = journal_add_journal_head(bh);
985     jbd_lock_bh_state(bh);
 986     spin_lock(&journal->j_list_lock);
 987 
994     if (jh->b_transaction) {
			 // 该jh已经由某个transaction管理了
996         if (jh->b_transaction != handle->h_transaction) {
				 // 如果该jh是由旧的transaction管理的
1034             if (jh->b_jlist != BJ_None &&
1035                     jh->b_jlist != BJ_SyncData &&
1036                     jh->b_jlist != BJ_Locked) {
					 // 一个数据块缓冲区,在jbd控制下正常写回磁盘时,
					 // 会依次经历BJ_SyncData、BJ_Locked、BJ_None三种状态。
					 // 如果该jh不在上述三个队列上,则表示jbd没有管理该jh。
1038                 goto no_journal;
1039             }
1040 
1047             if (buffer_dirty(bh)) {
					 // 如果该缓冲区已经是脏的了,则我们需要同步地写回磁盘。
1048                 get_bh(bh);
1049                 spin_unlock(&journal->j_list_lock);
1050                 jbd_unlock_bh_state(bh);
1051                 need_brelse = 1;
1052                 sync_dirty_buffer(bh);
1053                 jbd_lock_bh_state(bh);
1054                 spin_lock(&journal->j_list_lock);
1062             }
1074             if (jh->b_transaction != NULL) {
				 // 此时,经过1047-1062行的代码,该缓冲区已经是update的了。
			     // 故可以脱离原来的transaction,由handle->h_transaction进行管理了。
1076                 __journal_temp_unlink_buffer(jh);
1081                 jh->b_transaction = handle->h_transaction;
1082             }
1085         }
1092         if (jh->b_jlist != BJ_SyncData && jh->b_jlist != BJ_Locked) {
				 // 数据块缓冲区只能有三种状态BJ_SyncData、BJ_Locked、BJ_None,
				 // 这里只能是BJ_None了,
				 // 而journal_dirty_data()需要将缓冲区链入BJ_SyncData队列。
1095             __journal_temp_unlink_buffer(jh);
1096             jh->b_transaction = handle->h_transaction;
1098             __journal_file_buffer(jh, handle->h_transaction,
1099                         BJ_SyncData);
1100         }
1101     } else {	
			 // 这个else对应的是994行的if
1103         __journal_file_buffer(jh, handle->h_transaction, BJ_SyncData);
1104     }
1105 no_journal:
1106     spin_unlock(&journal->j_list_lock);
1107     jbd_unlock_bh_state(bh);
1108     if (need_brelse) {
1110         __brelse(bh);
1111     }
1113     journal_put_journal_head(jh);
1114     return ret;
1115 }

journal_dirty_metadata
  • journal_dirty_metadata()的作用是通知jbd一个元数据块缓冲区的修改已经完成。
1136 int journal_dirty_metadata(handle_t *handle, struct buffer_head *bh)
1137 {
1138     transaction_t *transaction = handle->h_transaction;
1139     journal_t *journal = transaction->t_journal;
1140     struct journal_head *jh = bh2jh(bh);
1147     jbd_lock_bh_state(bh);
1159 
1167     if (jh->b_transaction == transaction && jh->b_jlist == BJ_Metadata) {
1169         J_ASSERT_JH(jh, jh->b_transaction ==
1170                     journal->j_running_transaction);
			 // 如果该jh已经由当前正在运行的transaction管理了,
// 则本函数不需要做什么了。
1171         goto out_unlock_bh;
1172     }
1173 
1174     set_buffer_jbddirty(bh);
1175 
1182     if (jh->b_transaction != transaction) {
			 // 该jh在正在提交的transaction上
1184         J_ASSERT_JH(jh, jh->b_transaction ==
1185                     journal->j_committing_transaction);
1186         J_ASSERT_JH(jh, jh->b_next_transaction == transaction);
			 // 从这两个断言来看,jh->b_next_transaction已经设置好了,
			 // 则正在提交的transaction完成后,本transaction会继续处理的。
1189         goto out_unlock_bh;
1190     }
1196     spin_lock(&journal->j_list_lock);
		 // 将jh链入BJ_Metadata队列。
		 // 注意,本缓冲区以前可能通过journal_get_XXX_access()加入了BJ_Reserved队列,这里要从原队列上移除,然后加入BJ_Metadata队列。
1197     __journal_file_buffer(jh, handle->h_transaction, BJ_Metadata);
1198     spin_unlock(&journal->j_list_lock);
1199 out_unlock_bh:
1200     jbd_unlock_bh_state(bh);
1201 out:
1202     JBUFFER_TRACE(jh, "exit");
1203     return 0;
1204 }

journal_forget
  • 以索引块缓冲区为例。如果一个原子操作在运行过程中,要分配一个新的索引块,则它就先调用journal_get_write_access()函数取得写权限,然后修改这个索引块缓冲区,然后调用journal_dirty_metadata()将该缓冲区设为脏。但是,如果不幸,该原子操作后边运行出错了,需要将之前的修改全部取消,则需要调用journal_forget()函数使jbd“忘记”该缓冲区。
1234 int journal_forget (handle_t *handle, struct buffer_head *bh)
1235 {
1236     transaction_t *transaction = handle->h_transaction;
1237     journal_t *journal = transaction->t_journal;
1238     struct journal_head *jh;
1239     int drop_reserve = 0;
1240     int err = 0;
1241     int was_modified = 0;
1242 
1245     jbd_lock_bh_state(bh);
1246     spin_lock(&journal->j_list_lock);
1250     jh = bh2jh(bh);
1261     was_modified = jh->b_modified;
1267     jh->b_modified = 0;
1268 
1269     if (jh->b_transaction == handle->h_transaction) {
        	 // 如果该缓冲区属于该handle所属的transaction
        	 // 立即从本transaction中删除即可。
1275         clear_buffer_dirty(bh);
1276         clear_buffer_jbddirty(bh);
1284         if (was_modified)
1285             drop_reserve = 1;
1299         if (jh->b_cp_transaction) {
       			 // 如果该缓冲区在transaction的checkpoint队列上,
            	 // 则重新将缓冲区加入BJ_Forget队列,
            	 // 这样,本transaction提交时,
// 才会根据该bh是否为脏将其链入本transaction的checkpoint队列上。
1300             __journal_temp_unlink_buffer(jh);
1301             __journal_file_buffer(jh, transaction, BJ_Forget);
1302         } else {
				 // 如果该缓冲区不在transaction的checkpoint队列上,
           		 // 则直接删除
1303             __journal_unfile_buffer(jh);
1304             journal_remove_journal_head(bh);
1312         }
1313     } else if (jh->b_transaction) {
        	 // 如果该缓冲区属于以前的transaction,
        	 // 则我们不能丢掉它
1314         J_ASSERT_JH(jh, (jh->b_transaction ==
1315                  journal->j_committing_transaction));
1322         if (jh->b_next_transaction) {
1323             J_ASSERT(jh->b_next_transaction == transaction);
				 // jh->b_next_transaction设置为NULL,
// 表示旧的transaction处理完该jh之后,
// 没有transaction要处理它了。
1324             jh->b_next_transaction = NULL;
1330             if (was_modified)
1331                 drop_reserve = 1;
1332         }
1333     }
1339 drop:
1340     if (drop_reserve) {
			 // 既然要使jbd“忘记”该缓冲区,
// 那么该缓冲区在handle中占用的额度可以回收了
1342         handle->h_buffer_credits++;
1343     }
1344     return err;
1345 }

journal_revoke
  • jbd在内存中设置了两个hash表用于管理被revoked的缓冲区。
    struct journal_s
    {
    ……
    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]; // 指向两个revoke hash table
    }
    jbd初始化时,要为创建两个hash表。
272 /* Initialise the revoke table for a given journal to a given size. */
273 int journal_init_revoke(journal_t *journal, int hash_size)
274 {
		// 创建第一个hash表
278     journal->j_revoke_table[0] = journal_init_revoke_table(hash_size);
279     if (!journal->j_revoke_table[0])
280         goto fail0;
281 
		// 创建第二个hash表
282     journal->j_revoke_table[1] = journal_init_revoke_table(hash_size);
283     if (!journal->j_revoke_table[1])
284         goto fail1;
285 
		// 将journal当前使用的revoke hash 表设置为第二个。
286     journal->j_revoke = journal->j_revoke_table[1];
287 
288     spin_lock_init(&journal->j_revoke_lock);
289 
290     return 0;        
291 
292 fail1:
293     journal_destroy_revoke_table(journal->j_revoke_table[0]);
294 fail0:
295     return -ENOMEM;
296 }

journal_init_revoke_table()函数会创建给定大小的一个hash表,每个hash表在内存中都由一个jbd_revoke_table_s结构表示。
/* The revoke table is just a simple hash table of revoke records. */
struct jbd_revoke_table_s
{
int hash_size;
int hash_shift;
struct list_head *hash_table;
};

  • 其中hash_size表示该hash表的大小,而2hash_shift = hash_size。而hash_table则指向一个list_head结构数组,即hash表。从逻辑上看,这个hash表中保存的数据是jbd_revoke_record_s结构,但实际上,只是hash表中保存的数据是jbd_revoke_record_s. hash。

struct jbd_revoke_record_s
{
struct list_head hash;
tid_t sequence; /* Used for recovery only */
unsigned int blocknr;
};

  • 其中hash是用于将本结构链入revoke hash表的,sequence是调用revoke的transaction的ID,blocknr是磁盘块号。
228 static struct jbd_revoke_table_s *journal_init_revoke_table(int hash_size)
229 {
230     int shift = 0;
231     int tmp = hash_size;
232     struct jbd_revoke_table_s *table;
233 
234     table = kmem_cache_alloc(revoke_table_cache, GFP_KERNEL);
235     if (!table)
236         goto out;
237 
		// 计算hash_shift
238     while((tmp >>= 1UL) != 0UL)
239         shift++;
240 
241     table->hash_size = hash_size;
242     table->hash_shift = shift;
		// 创建hash表
243     table->hash_table =
244         kmalloc(hash_size * sizeof(struct list_head), GFP_KERNEL);
245     if (!table->hash_table) {
246         kmem_cache_free(revoke_table_cache, table);
247         table = NULL;
248         goto out;
249     }
250 
		// 初始化hash表
251     for (tmp = 0; tmp < hash_size; tmp++)
252         INIT_LIST_HEAD(&table->hash_table[tmp]);
253 
254 out:
255     return table;
256 }

在include/linux/jbd.h中,
962 #define JOURNAL_REVOKE_DEFAULT_HASH 256
所以每个hash表都有256项,每项都是一个用struct list_head链起来的双向链表。
细说jbd(journal-block-device)& 源码分析


  • 其中每个红色部分都是一个jbd_revoke_record_s结构。
335 int journal_revoke(handle_t *handle, unsigned int blocknr,
336            struct buffer_head *bh_in)
337 {
338     struct buffer_head *bh = NULL;
339     journal_t *journal;
340     struct block_device *bdev;
346 
347     journal = handle->h_transaction->t_journal;
352 
353     bdev = journal->j_fs_dev;
354     bh = bh_in;
355 
356     if (!bh) {
			// 如果bh==NULL,则根据blocknr查找对应的缓冲区
357         bh = __find_get_block(bdev, blocknr, journal->j_blocksize);
360     }
386     if (bh) {
387         if (!J_EXPECT_BH(bh, !buffer_revoked(bh),
388                  "inconsistent data on disk")) {
389             if (!bh_in)
390                 brelse(bh);
391             return -EIO;
392         }
393         set_buffer_revoked(bh);
394         set_buffer_revokevalid(bh);
395         if (bh_in) {
397             journal_forget(handle, bh_in);
398         } else {
400             __brelse(bh);
401         }
402     }
403 	// 将<blocknr, tid>加入hash表。
405     err = insert_revoke_hash(journal, blocknr,
406                 handle->h_transaction->t_tid);
408     return err;
409 }

  • 为什么jbd要设置两个hash表呢?这要从提交revoke记录说起。当一个正在运行的transaction要提交时,与之相对应的revoke hash 表也要提交。要提交revoke hash 表,必须把其中的数据冻结起来,不再被改动。此时,为了能使jbd能够继续接收revoke记录,则需为journal 设置另一个hash 表。所以jbd设置了两个hash 表,供journal交替使用。
  • fs/jbd/revoke.c中还包含以下若干与revoke机制相关的函数,都比较简单,读者可以自行阅读。
    insert_revoke_hash
    find_revoke_record
    journal_destroy_revoke_caches
    journal_init_revoke_caches
    journal_init_revoke_table
    journal_destroy_revoke_table
    journal_init_revoke
    journal_destroy_revoke
    journal_cancel_revoke
    journal_switch_revoke_table
    journal_write_revoke_records
    write_one_revoke_record
    flush_descriptor
    journal_set_revoke
    journal_test_revoke
    journal_clear_revoke
journal_extend
  • 每个原子操作handle在创建时,会为之分配若干额度。但是如果后便的运行中发现还需要更多的额度,则可通过journal_extend()函数增加某个handle的额度。
322 int journal_extend(handle_t *handle, int nblocks)
323 {
324     transaction_t *transaction = handle->h_transaction;
325     journal_t *journal = transaction->t_journal;
326     int result;
327     int wanted;
334 
335     spin_lock(&journal->j_state_lock);
336 
344     spin_lock(&transaction->t_handle_lock);
345     wanted = transaction->t_outstanding_credits + nblocks;
346 
347     if (wanted > journal->j_max_transaction_buffers) {
// j_max_transaction_buffers表示在一次transnation提交中允许提交的最多个缓冲区个数。
// 如果wanted > journal->j_max_transaction_buffers,表示要求的太多了,
// 本transaction不能满足。
350         goto unlock;
351     }
352 
353     if (wanted > __log_space_left(journal)) {
			// 如果请求的额度大于日志剩余的空间,则出错。
356         goto unlock;
357     }
358 
		// 给handle和相应的transaction增加额度。
359     handle->h_buffer_credits += nblocks;
360     transaction->t_outstanding_credits += nblocks;
361     result = 0;
362 
364 unlock:
365     spin_unlock(&transaction->t_handle_lock);
366 error_out:
367     spin_unlock(&journal->j_state_lock);
368 out:
369     return result;
370 }

元数据缓冲区处理流程
  • 当文件系统吧一个元数据块纳入jbd管理时,处理流程需要5步:
  1. 创建一个原子操作描述符
    调用journal_start()函数
  2. 取得在jbd中的写权限
    这个针对不同的元数据缓冲区,调用的函数是不同的:
    新创建的:journal_get_create_access()、
    文件系统中已经存在的: journal_get_write_access()、
    磁盘块位图:journal_get_undo_access()
  3. 修改缓冲区的数据
    这里文件系统可以根据自己的需要随意修改
  4. 将缓冲区设置为脏
    调用journal_dirty_metadata()函数。
  5. 关闭原子操作符
    调用journal_stop()函数
  • 我们举一个ext3中的实际的例子来说明一下文件系统是如何处理元数据缓冲区的。注意,ext3对jbd的各个函数都进行了一定的封装,如journal_dirty_metadata()封装成了ext3_journal_dirty_metadata(),这里不再特殊解释了。
  • 以在一个目录中创建一个新文件为例吧。
    ext3中关于目录节点的操作由ext3_dir_inode_operations结构来描述。
    fs/ext3/namei.c
2451 const struct inode_operations ext3_dir_inode_operations = {
2452     .create     = ext3_create,
2453     .lookup     = ext3_lookup,
2454     .link       = ext3_link,
2455     .unlink     = ext3_unlink,
2456     .symlink    = ext3_symlink,
2457     .mkdir      = ext3_mkdir,
2458     .rmdir      = ext3_rmdir,
2459     .mknod      = ext3_mknod,
2460     .rename     = ext3_rename,
2461     .setattr    = ext3_setattr,
2468     .check_acl  = ext3_check_acl,
2469 };

其中ext3_create()是负责创建一个新文件的。所谓创建一个新文件,实现时主要做三件事情:

  1. 在文件系统中创建一个代表该文件的inode;
  2. 在父目录中分配一个新的目录项,用于指向新创建的文件。
  3. 将目录中的新目录项与inode相关联。
1692 static int ext3_create (struct inode * dir, struct dentry * dentry, int mode,
1693         struct nameidata *nd)
1694 {
		// 参数说明:
		 // dir是父目录的inode,
		 // dentry是新创建的目录项,但是尚未与一个磁盘上的inode相关联。
1695     handle_t *handle;
1696     struct inode * inode;
1697     int err, retries = 0;
1700 
1701 retry:
		 // 取得原子操作描述符,对应第1步
1702     handle = ext3_journal_start(dir, EXT3_DATA_TRANS_BLOCKS(dir->i_sb) +
1703                     EXT3_INDEX_EXTRA_TRANS_BLOCKS + 3 +
1704                     EXT3_MAXQUOTAS_INIT_BLOCKS(dir->i_sb));
		 // 在目录dir所在的块组中分配一个新的inode,
		 // 即在块组的inode位图中寻找一个为0的位,然后标记为1
1711     inode = ext3_new_inode (handle, dir, mode);
1712     err = PTR_ERR(inode);
1713     if (!IS_ERR(inode)) {
			 // 新分配的indoe是一个普通文件,设置文件操作。
1714         inode->i_op = &ext3_file_inode_operations;
1715         inode->i_fop = &ext3_file_operations;
1716         ext3_set_aops(inode);
			 // 将新分配的inode与目录项关联起来,这样通过目录就可查找到文件。
1717         err = ext3_add_nondir(handle, dentry, inode);
1718     }
		 // 关闭原子操作描述符,对应第5步
1719     ext3_journal_stop(handle);
1722     return err;
1723 }
ext3_new_inode()函数比较复杂,这里只把与jbd相关的主干部分列出来。
419 struct inode *ext3_new_inode(handle_t *handle, struct inode * dir, int mode)
420 {
		……
        // 先分配一个内存inode
439     sb = dir->i_sb;
440     inode = new_inode(sb);
443     ei = EXT3_I(inode);
444
    // 445-454行,找到父目录所在的块组
		……

		// 尝试在块组的inode位图中分配一个未使用的磁盘inode
459     for (i = 0; i < sbi->s_groups_count; i++) {
460         err = -EIO;
461 
			// 获取块组描述符
462         gdp = ext3_get_group_desc(sb, group, &bh2);
463         if (!gdp)
464             goto fail;

			// 获取块组inode位图
467         bitmap_bh = read_inode_bitmap(sb, group);
471         ino = 0;
472 
473 repeat_in_this_group:
			// 在块组inode位图中查找第一个未使用的磁盘inode。
474         ino = ext3_find_next_zero_bit((unsigned long *)
475                 bitmap_bh->b_data, EXT3_INODES_PER_GROUP(sb), ino);
476         if (ino < EXT3_INODES_PER_GROUP(sb)) {
				// 找到了,我们要修改块组磁盘块位图了,
				// 先取得写权限,对应第2步
479             err = ext3_journal_get_write_access(handle, bitmap_bh);

				// 将块组磁盘块位图相应位置1,表示分配该磁盘inode。对应第3步
483             if (!ext3_set_bit_atomic(sb_bgl_lock(sbi, group),
484                         ino, bitmap_bh->b_data)) {

					// 既然我们修改了元数据块缓冲区,则我们要把它置为脏。
// 对应第4步
488                 err = ext3_journal_dirty_metadata(handle,
489                                 bitmap_bh);
492                 goto got;
493             }
499         }
510     }
514 got:
515     ino += group * EXT3_INODES_PER_GROUP(sb) + 1;
		
		// 下面要修改块组描述符了,这也是一个元数据块缓冲区,
		// 先取得写权限。
525     err = ext3_journal_get_write_access(handle, bh2);
526     if (err) goto fail;
527     spin_lock(sb_bgl_lock(sbi, group));
528     le16_add_cpu(&gdp->bg_free_inodes_count, -1);
529     if (S_ISDIR(mode)) {
530         le16_add_cpu(&gdp->bg_used_dirs_count, 1);
531     }
532     spin_unlock(sb_bgl_lock(sbi, group));
		
// 既然我们修改了元数据块缓冲区,则我们要把它置为脏。
534     err = ext3_journal_dirty_metadata(handle, bh2);

		// 下面是一些内存inode初始化的代码,我们简单略过
		……
		
		// 既然我们修改了磁盘inode(创建当然也是一种修改),
// 我们将磁盘inode对应的内存缓冲区设置为脏,以便在适当时机写回磁盘。
// 注意这里也将原子操作描述符hanlde传入了,因为磁盘inode也是一个元数据块,
// 故仍然会进行上述5步操作中的2-4步。
// 因为过程与本函数前面的修改磁盘块位图和块组描述符很类似,这里略过。
603     err = ext3_mark_inode_dirty(handle, inode);
631 }

数据缓冲区处理流程
  • 在data=ordered模式中,ext3保证在元数据块写入日志之前写入数据块。这样transaction也需要对数据块缓冲区进行管理。对数据块缓冲区的管理比较简单,不需要在修改缓冲区之前先取得写权限,直接调用journal_dirty_data()函数即可。
  • 我们也通过一个ext3中的例子,看看文件系统事如何处理数据块缓冲区的。
  • 内核对文件内容进行读写时,是以页面为单位的。每个页面会对若干个磁盘块缓冲区,及对应若干个磁盘块。ext3_ordered_write_end()函数是在内核修改文件内容之后调动的,用以进行写操作的最后的处理。

fs/ext3/inode.c

1288 static int ext3_ordered_write_end(struct file *file,
1289                 struct address_space *mapping,
1290                 loff_t pos, unsigned len, unsigned copied,
1291                 struct page *page, void *fsdata)
1292 {
		 // 取得一个原子操作描述符
1293     handle_t *handle = ext3_journal_current_handle();
1294     struct inode *inode = file->f_mapping->host;
1295     unsigned from, to;
1296     int ret = 0, ret2;
1297 
1298     copied = block_write_end(file, mapping, pos, len, copied, page, fsdata);
		 
		 // 对该页的每个页面,调用一次journal_dirty_data_fn()函数。
1302     ret = walk_page_buffers(handle, page_buffers(page),
1303         from, to, NULL, journal_dirty_data_fn);
1304 
		 // 关闭原子操作描述符
1313     ret2 = ext3_journal_stop(handle);
1322 }

  • walk_page_buffers()函数也在fs/ext3/inode.c。
1092 static int walk_page_buffers(   handle_t *handle,
1093                 struct buffer_head *head,
1094                 unsigned from,
1095                 unsigned to,
1096                 int *partial,
1097                 int (*fn)(  handle_t *handle,
1098                         struct buffer_head *bh))
1099 {
1100     struct buffer_head *bh;
1101     unsigned block_start, block_end;
1102     unsigned blocksize = head->b_size;
1103     int err, ret = 0;
1104     struct buffer_head *next;
1105     
1106     for (   bh = head, block_start = 0;
1107         ret == 0 && (bh != head || !block_start);
1108         block_start = block_end, bh = next)
1109     {
1110         next = bh->b_this_page;
1111         block_end = block_start + blocksize;
1112         if (block_end <= from || block_start >= to) {
1113             if (partial && !buffer_uptodate(bh))
1114                 *partial = 1;
1115             continue;
1116         }
1117         err = (*fn)(handle, bh);
1118         if (!ret)
1119             ret = err;
1120     }
1121     return ret;
1122 }

  • journal_dirty_data_fn()函数也在fs/ext3/inode.c
1244 static int journal_dirty_data_fn(handle_t *handle, struct buffer_head *bh)
1245 {
1250     if (buffer_mapped(bh) && buffer_uptodate(bh))
1251         return ext3_journal_dirty_data(handle, bh);
1252     return 0;
1253 }

1234 int ext3_journal_dirty_data(handle_t *handle, struct buffer_head *bh)
1235 {
1236     int err = journal_dirty_data(handle, bh);
1240     return err;
1241 }

提交事务kjournald----- 时刻准备着

  • 对文件系统来说,对元数据块缓冲区和数据库块缓冲区的处理在之前已经完成任务。剩下的逻辑文件系统交给,jbd全权负责。
  • 有一个内核线程,它的唯一作用就是将事务的数据提交到日志中。每隔固定间隔(一般5秒)会醒来,选择合适的事务进行提交。我们也可以主动唤醒该线程进行事务提交。
  • 我们将kjournald()中与睡眠相关的代码删掉,就可以很清楚地看到它的逻辑。

fs/jbd/journal.c

115 static int kjournald(void *arg)
 116 {
 117     journal_t *journal = arg;
 118     transaction_t *transaction;
124     setup_timer(&journal->j_commit_timer, commit_timeout,
 125             (unsigned long)current);
 126 
 127     /* Record that the journal thread is running */
 128     journal->j_task = current;
 129     wake_up(&journal->j_wait_done_commit);
 130 
137     spin_lock(&journal->j_state_lock);
138 
 139 loop:
146     if (journal->j_commit_sequence != journal->j_commit_request) {
			 // j_commit_sequence表示已经提交的最新的transaction的ID,
			 // j_commit_request表示申请进行提交的transaction的ID,
			 // 如果两者不等,我们进行一次提交。
 148         spin_unlock(&journal->j_state_lock);
 149         del_timer_sync(&journal->j_commit_timer);
		     // 进行事务的提交,详见第十章。
 150         journal_commit_transaction(journal);
 151         spin_lock(&journal->j_state_lock);
 152         goto loop;
 153     }
 154 
 204 end_loop:
205     spin_unlock(&journal->j_state_lock);
 206     del_timer_sync(&journal->j_commit_timer);
 207     journal->j_task = NULL;
 208     wake_up(&journal->j_wait_done_commit);
 209     jbd_debug(1, "Journal thread exiting.\n");
 210     return 0;
 211 }

提交事务

journal_commit_transaction

细说jbd(journal-block-device)& 源码分析

  • journal_commit_transaction()函数,长达600多行,是事务提交的主要函数。
302 void journal_commit_transaction(journal_t *journal)
303 {
304     transaction_t *commit_transaction;
305     struct journal_head *jh, *new_jh, *descriptor;
306     struct buffer_head **wbuf = journal->j_wbuf;
307     int bufs;
308     int flags;
309     int err;
310     unsigned int blocknr;
311     ktime_t start_time;
312     u64 commit_time;
313     char *tagp = NULL;
314     journal_header_t *header;
315     journal_block_tag_t *tag = NULL;
316     int space_left = 0;
317     int first_tag = 0;
318     int tag_flag;
319     int i;
320     int write_op = WRITE;

		// 将当前正在运行的transaction设置为正在提交的transaction,然后进行提交。
344     commit_transaction = journal->j_running_transaction;
349 
350     spin_lock(&journal->j_state_lock);
		// 将transaction的状态设置为T_LOCKED,
// 表示不能再向其中加入新的原子操作了。
351     commit_transaction->t_state = T_LOCKED;
358     if (commit_transaction->t_synchronous_commit)
359         write_op = WRITE_SYNC_PLUG;
360     spin_lock(&commit_transaction->t_handle_lock);
361     while (commit_transaction->t_updates) {
			// 因为此时,可能仍有handle在修改此transaction,
// 故361-374行等所有的handle结束。
374     }
375     spin_unlock(&commit_transaction->t_handle_lock);
396     while (commit_transaction->t_reserved_list) {
			// t_reserved_list队列上的缓冲区是本transaction已管理的、但是未修改的缓冲区。既然未修改,则不必提交。
397         jh = commit_transaction->t_reserved_list;
			// 将该jh从本transaction中移除。
411         journal_refile_buffer(journal, jh);
412     }

419     spin_lock(&journal->j_list_lock);
		// 先遍历journal中所有已提交的事务,依次处理其中的checkpoint队列
420     __journal_clean_checkpoint_list(journal);
421     spin_unlock(&journal->j_list_lock);
422 
		// 提交阶段1
423     jbd_debug (3, "JBD: commit phase 1\n");
424 
		// 切换一下revoke hash table。
		// 这样就不会发生该表以便被提交,一边被修改的情况。
428     journal_switch_revoke_table(journal);
429 
		//将transaction的状态设置为T_FLUSH,
430     commit_transaction->t_state = T_FLUSH;
431     journal->j_committing_transaction = commit_transaction;
432     journal->j_running_transaction = NULL;
433     start_time = ktime_get();
434     commit_transaction->t_log_start = journal->j_head;
435     wake_up(&journal->j_wait_transaction_locked);
436     spin_unlock(&journal->j_state_lock);
437 
// 提交阶段2
// 将与本transaction相关联的数据块缓冲区先写回磁盘,
// ordered模式就是这样实现的。
// 这些数据块缓冲区原来在BJ_SyncData队列上,
// journal_submit_data_buffers()会将它们移动到BJ_Locked队列上。
438     jbd_debug (3, "JBD: commit phase 2\n");
444     err = journal_submit_data_buffers(journal, commit_transaction,
445                       write_op);
446 
		// 等待数据块缓冲区写入完成。
450     spin_lock(&journal->j_list_lock);
451     while (commit_transaction->t_locked_list) {
			// 等待bh写回完成       
			……
			// 将jh从transaction中删除
480         if (buffer_jbd(bh) && bh2jh(bh) == jh &&
481             jh->b_transaction == commit_transaction &&
482             jh->b_jlist == BJ_Locked) {
483             __journal_unfile_buffer(jh);
484             jbd_unlock_bh_state(bh);
485             journal_remove_journal_head(bh);
486             put_bh(bh);
487         } 
492     }
493     spin_unlock(&journal->j_list_lock);

505 	// 将revoke hash table中的记录写到日志中。
		// 这就是取消块。
506     journal_write_revoke_records(journal, commit_transaction, write_op);
507 
// 提交阶段3
// 将元数据块缓冲区写到日志中
516     jbd_debug (3, "JBD: commit phase 3\n");

523     spin_lock(&journal->j_state_lock);
// 将transaction的状态设置为T_COMMIT,
524     commit_transaction->t_state = T_COMMIT;
525     spin_unlock(&journal->j_state_lock);

530     descriptor = NULL;
531     bufs = 0;
532     while (commit_transaction->t_buffers) {
			// 532-685将BJ_Metadata队列(即t_buffers)中的元数据缓冲区写到日志中。
			// 这里只是启动所有缓冲区的写操作,第四阶段会等待写操作完成。
536         jh = commit_transaction->t_buffers;
556 		
			// 元数据块缓冲区写入日志的格式是:描述符块、数据块、提交块
			// 这里的descriptor表示一个描述符块。
557         if (!descriptor) {
558             struct buffer_head *bh;
				// 从日志空间新分配一个块,用作描述符块
564             descriptor = journal_get_descriptor_buffer(journal);
570             bh = jh2bh(descriptor);
573             header = (journal_header_t *)&bh->b_data[0];
				// 设置描述符块
574             header->h_magic     = cpu_to_be32(JFS_MAGIC_NUMBER);
575             header->h_blocktype = cpu_to_be32(JFS_DESCRIPTOR_BLOCK);
576             header->h_sequence  = cpu_to_be32(commit_transaction->t_tid);
				
// tagp用于指示新的journal_block_tag_t的起点
578             tagp = &bh->b_data[sizeof(journal_header_t)];
579             space_left = bh->b_size - sizeof(journal_header_t);
580             first_tag = 1;
581             set_buffer_jwrite(bh);
582             set_buffer_dirty(bh);
				// wbuf[]数组记录要写到日志的所有缓冲区。
583             wbuf[bufs++] = bh;
584 
				// 将描述符块的缓冲区加入BJ_LogCt链表,下面会写出到日志。
588             journal_file_buffer(descriptor, commit_transaction,
589                     BJ_LogCtl);
590         }
591 
			// 计算日志中下一个空间块的块号,保存在blocknr中。
594         err = journal_next_log_block(journal, &blocknr);
608         commit_transaction->t_outstanding_credits--;
609 
613         atomic_inc(&jh2bh(jh)->b_count);

			// journal_write_metadata_buffer()函数从字面意思上看是写一个元数据缓冲区,
			// 但是实际上它并未完成真正的写操作。
			// 它主要的作用是根据已存在的元数据块缓冲区(jh对应的那个),
// 创建一个新的缓冲区(new_jh对应的那个),供jbd写入到日志时使用。
// jh此时会从BJ_Metadata队列上移动到BJ_Shadow队列上,
// 而new_jh则会链入到BJ_IO队列上。
			// journal_write_metadata_buffer()函数也会处理转义问题。
627         flags = journal_write_metadata_buffer(commit_transaction,
628                               jh, &new_jh, blocknr);
629         set_bit(BH_JWrite, &jh2bh(new_jh)->b_state);

			// 记录下来,后面一起写出。
630         wbuf[bufs++] = jh2bh(new_jh);
				
			// 635-652,将刚写出的缓冲区信息记录在描述符块中。
635         tag_flag = 0;
636         if (flags & 1)
637             tag_flag |= JFS_FLAG_ESCAPE;
638         if (!first_tag)
639             tag_flag |= JFS_FLAG_SAME_UUID;
640 
641         tag = (journal_block_tag_t *) tagp;
			// 记录块号和标志。
642         tag->t_blocknr = cpu_to_be32(jh2bh(jh)->b_blocknr);
643         tag->t_flags = cpu_to_be32(tag_flag);
644         tagp += sizeof(journal_block_tag_t);
645         space_left -= sizeof(journal_block_tag_t);
646 
647         if (first_tag) {
				// 第一个journal_block_tag_t会记录一个UUID
648             memcpy (tagp, journal->j_uuid, 16);
649             tagp += 16;
650             space_left -= 16;
651             first_tag = 0;
652         }

			// 如果达到了journal一次允许写出的缓冲区个数,
			// 或者BJ_Metadata队列已经为空,
			// 或者描述符块已被journal_block_tag_t记录填满了
			// 则需进行提交
657         if (bufs == journal->j_wbufsize ||
658             commit_transaction->t_buffers == NULL ||
659             space_left < sizeof(journal_block_tag_t) + 16) {
				// 最后一个journal_block_tag_t要设置JFS_FLAG_LAST_TAG标志。
667             tag->t_flags |= cpu_to_be32(JFS_FLAG_LAST_TAG);
668 
669 start_journal_io:
670             for (i = 0; i < bufs; i++) {
					// 启动写操作。
671                 struct buffer_head *bh = wbuf[i];
672                 lock_buffer(bh);
673                 clear_buffer_dirty(bh);
674                 set_buffer_uptodate(bh);
675                 bh->b_end_io = journal_end_buffer_io_sync;
676                 submit_bh(write_op, bh);
677             }
678             cond_resched();
679 
				// 如果描述符块已被journal_block_tag_t记录填满了,则重新分配一个。
682             descriptor = NULL;
683             bufs = 0;
684         }
685     }
// 提交阶段4
// 等待元数据块缓冲区写入日志。
698     jbd_debug(3, "JBD: commit phase 4\n");
704 wait_for_iobuf:
705     while (commit_transaction->t_iobuf_list != NULL) {
			// 705-753循环,等待每个元数据块缓冲区写入日志。
706         struct buffer_head *bh;
707 
708         jh = commit_transaction->t_iobuf_list->b_tprev;
709         bh = jh2bh(jh);
710         if (buffer_locked(bh)) {
711             wait_on_buffer(bh);
712             goto wait_for_iobuf;
713         }
714         if (cond_resched())
715             goto wait_for_iobuf;
716 
720         clear_buffer_jwrite(bh);
723         journal_unfile_buffer(journal, jh);
730         journal_put_journal_head(jh);
731         __brelse(bh);
733         free_buffer_head(bh);
734 
735         /* We also have to unlock and free the corresponding
736                    shadowed buffer */
			// BJ_IO队列上的缓冲区与BJ_Shadow队列上的缓冲区是一一对应的。
			// 每处理完一个BJ_IO队列上的缓冲区,都会将其从transaction中删除,
			// 并且要将在BJ_Shadow队列上对应的缓冲区移到BJ_Forget队列上。
			// (后便checkpoint还要处理BJ_Forget队列)
737         jh = commit_transaction->t_shadow_list->b_tprev;
738         bh = jh2bh(jh);
739         clear_bit(BH_JWrite, &bh->b_state);
747         journal_file_buffer(jh, commit_transaction, BJ_Forget);
750         wake_up_bit(&bh->b_state, BH_Unshadow);
752         __brelse(bh);
753     }

// 提交阶段5
// 等待取消块和描述符块写入日志。
// 取消块和描述符块都可视为jbd的控制块,都在BJ_LogCtl队列上。
// 参考上面的588行。
757     jbd_debug(3, "JBD: commit phase 5\n");
760  wait_for_ctlbuf:
761     while (commit_transaction->t_log_list != NULL) {
// 等待每一个控制块都写入日志
762         struct buffer_head *bh;
763 
764         jh = commit_transaction->t_log_list->b_tprev;
765         bh = jh2bh(jh);
766         if (buffer_locked(bh)) {
767             wait_on_buffer(bh);
768             goto wait_for_ctlbuf;
769         }
770         if (cond_resched())
771             goto wait_for_ctlbuf;
772 
773         if (unlikely(!buffer_uptodate(bh)))
774             err = -EIO;

			// 控制块已经写到日志中了,则可以从transaction中删除了。
777         clear_buffer_jwrite(bh);
778         journal_unfile_buffer(journal, jh);
779         journal_put_journal_head(jh);
780         __brelse(bh);       /* One for getblk */
782     }

// 提交阶段6
// 描述符块和数据块都写到日志中了,
// 现在我们写一个提交块。
787     jbd_debug(3, "JBD: commit phase 6\n");
788 
790     spin_lock(&journal->j_state_lock);
		//将transaction的状态设置为T_COMMIT_RECORD
792     commit_transaction->t_state = T_COMMIT_RECORD;
793     spin_unlock(&journal->j_state_lock);
794 
		// 将提交块同步地写到日志中。
795     if (journal_write_commit_record(journal, commit_transaction))
796         err = -EIO;

// 提交阶段7
806     jbd_debug(3, "JBD: commit phase 7\n");
807 	// 确保下列队列都为空了!
808     J_ASSERT(commit_transaction->t_sync_datalist == NULL);
809     J_ASSERT(commit_transaction->t_buffers == NULL);
810     J_ASSERT(commit_transaction->t_checkpoint_list == NULL);
811     J_ASSERT(commit_transaction->t_iobuf_list == NULL);
812     J_ASSERT(commit_transaction->t_shadow_list == NULL);
813     J_ASSERT(commit_transaction->t_log_list == NULL);
814 
815 restart_loop:
820     spin_lock(&journal->j_list_lock);
821     while (commit_transaction->t_forget) {
			// 这里实现checkpoint机制的地方。
			// 此时,对元数据块缓冲区而言,
// 或者它已被内核按照原有的方式写回到磁盘的原始位置(update状态),
// 那么直接从transaction中删除即可;
			// 或者未被内核写回到磁盘的原始位置(dirty状态),
// 这样我们就要把它加入到本transaction德checkpoint队列上,
// 本函数的420行会处理该队列。
822         transaction_t *cp_transaction;
823         struct buffer_head *bh;
824 
825         jh = commit_transaction->t_forget;
826         spin_unlock(&journal->j_list_lock);
827         bh = jh2bh(jh);
828         jbd_lock_bh_state(bh);

854         spin_lock(&journal->j_list_lock);
855         cp_transaction = jh->b_cp_transaction;
856         if (cp_transaction) {
				// 如果该jh已经在旧的transaction的checkpoint队列上了,
				// 则从旧的transaction的checkpoint队列上删除,
				// 因为我们会把它加入到本transaction的checkpoint队列上。
858             __journal_remove_checkpoint(jh);
859         }

880         if (buffer_jbddirty(bh)) {
				// 如果该缓冲区仍为脏,则加入到本transaction的checkpoint队列上。
882             __journal_insert_checkpoint(jh, commit_transaction);
				// 该jh除了在本transaction的checkpoint队列上之外,
// 不需要在其他队列上了。
// 从transaction的队列上移除。
886             __journal_refile_buffer(jh);
887             jbd_unlock_bh_state(bh);
888         } else {
				// 该缓冲区已处于update状态,
				// 直接从transaction中删除即可。
898             __journal_refile_buffer(jh);
899             if (!jh->b_transaction) {
900                 jbd_unlock_bh_state(bh);
902                 journal_remove_journal_head(bh);
903                 release_buffer_page(bh);
904             } else
905                 jbd_unlock_bh_state(bh);
906         }
907         cond_resched_lock(&journal->j_list_lock);
908     }
909     spin_unlock(&journal->j_list_lock);


916     spin_lock(&journal->j_state_lock);
917     spin_lock(&journal->j_list_lock);

		// 如果因为加锁过程中BJ_Forget队列上又有缓冲区了,
		// 转到815行继续处理
922     if (commit_transaction->t_forget) {
923         spin_unlock(&journal->j_list_lock);
924         spin_unlock(&journal->j_state_lock);
925         goto restart_loop;	// 815行
926     }
927 
// 提交阶段8
// 事务提交已经完成了,
// 将本事务加入journal->j_checkpoint_transactions队列
930     jbd_debug(3, "JBD: commit phase 8\n");
		//将transaction的状态设置为T_FINISHED。
934     commit_transaction->t_state = T_FINISHED;
935     J_ASSERT(commit_transaction == journal->j_committing_transaction);
936     journal->j_commit_sequence = commit_transaction->t_tid;
937     journal->j_committing_transaction = NULL;
938     commit_time = ktime_to_ns(ktime_sub(ktime_get(), start_time));
939 
944     if (likely(journal->j_average_commit_time))
945         journal->j_average_commit_time = (commit_time*3 +
946                 journal->j_average_commit_time) / 4;
947     else
948         journal->j_average_commit_time = commit_time;
949 
950     spin_unlock(&journal->j_state_lock);
951 
952     if (commit_transaction->t_checkpoint_list == NULL &&
953         commit_transaction->t_checkpoint_io_list == NULL) {
			// 本事务的checkpoint已经为空,则本事务可从内存中直接删除了。
954         __journal_drop_transaction(journal, commit_transaction);
955     } else {
			// 头插法
956         if (journal->j_checkpoint_transactions == NULL) {
957             journal->j_checkpoint_transactions = commit_transaction;
958             commit_transaction->t_cpnext = commit_transaction;
959             commit_transaction->t_cpprev = commit_transaction;
960         } else {
961             commit_transaction->t_cpnext =
962                 journal->j_checkpoint_transactions;
963             commit_transaction->t_cpprev =
964                 commit_transaction->t_cpnext->t_cpprev;
965             commit_transaction->t_cpnext->t_cpprev =
966                 commit_transaction;
967             commit_transaction->t_cpprev->t_cpnext =
968                 commit_transaction;
969         }
970     }
971     spin_unlock(&journal->j_list_lock);
976     wake_up(&journal->j_wait_done_commit);
977 }

__journal_clean_checkpoint_list
  • journal_commit_transaction()函数中调用的以下几个函数仍需要仔细分析。

  • __journal_clean_checkpoint_list()函数的作用是处理journal->j_checkpoint_transactions队列,如果某个transaction的checkpoint队列上的缓冲区已经处于update状态,则从该transaction的checkpoint队列上删除;如果某个transaction的checkpoint队列已经为空,则从journal->j_checkpoint_transactions队列上删除。
    fs/jbd/checkpoint.c

579 int __journal_clean_checkpoint_list(journal_t *journal)
580 {
581     transaction_t *transaction, *last_transaction, *next_transaction;
582     int ret = 0;
583     int released;
584 
585     transaction = journal->j_checkpoint_transactions;
586     if (!transaction)
587         goto out;
588 
589     last_transaction = transaction->t_cpprev;
590     next_transaction = transaction;
591     do {
			// 循环处理journal中的每个事务
592         transaction = next_transaction;
593         next_transaction = transaction->t_cpnext;
			// 处理一个事务的checkpoint队列。
594         ret += journal_clean_one_cp_list(transaction->
595                 t_checkpoint_list, &released);
601         if (need_resched())
602             goto out;
603         if (released)
604             continue;
610         ret += journal_clean_one_cp_list(transaction->
611                 t_checkpoint_io_list, &released);
612         if (need_resched())
613             goto out;
614     } while (transaction != last_transaction);
615 out:
616     return ret;
617 }
journal_clean_one_cp_list()函数的作用是处理一个transaction中的checkpoint队列。
531 static int journal_clean_one_cp_list(struct journal_head *jh, int *released)
532 {           
533     struct journal_head *last_jh;
534     struct journal_head *next_jh = jh;
535     int ret, freed = 0;
536 
537     *released = 0;
538     if (!jh)
539         return 0;
540         
541     last_jh = jh->b_cpprev;
542     do {
			// 循环处理该checkpoint队列上的每一个缓冲区
543         jh = next_jh;
544         next_jh = jh->b_cpnext;
546         if (jbd_trylock_bh_state(jh2bh(jh))) {
				// 处理一个缓冲区
547             ret = __try_to_free_cp_buf(jh);
555         }
564     } while (jh != last_jh);
565 
566     return freed;
567 }

91 static int __try_to_free_cp_buf(struct journal_head *jh)
 92 {       
 93     int ret = 0;
 94     struct buffer_head *bh = jh2bh(jh);
96     if (jh->b_jlist == BJ_None && !buffer_locked(bh) &&
 97         !buffer_dirty(bh) && !buffer_write_io_error(bh)) {
			// 如果该缓冲区不是脏,即为update状态,则立即从checkpoint队列上删除。
			// 从此,该jh与本transaction再无关系。
99         ret = __journal_remove_checkpoint(jh) + 1;
100         jbd_unlock_bh_state(bh);
101         journal_remove_journal_head(bh);
103         __brelse(bh);
104     } else {
105         jbd_unlock_bh_state(bh);
106     }
107     return ret;
108 }

journal_submit_data_buffers
  • journal_submit_data_buffers()函数将与本transaction相关联的数据块缓冲区先写回磁盘,ordered模式就是这样实现的。这些数据块缓冲区原来在BJ_SyncData队列上,现在会移动到BJ_Locked队列上。
  • 本函数只是提出了写缓冲区的申请,但是并不保证缓冲区同步地写回磁盘。

fs/jdb/commit.c

189 static int journal_submit_data_buffers(journal_t *journal,
190                        transaction_t *commit_transaction,
191                        int write_op)
192 {
193     struct journal_head *jh;
194     struct buffer_head *bh;
195     int locked;
196     int bufs = 0;
197     struct buffer_head **wbuf = journal->j_wbuf;
198     int err = 0;
208 write_out_data:
209     cond_resched();
210     spin_lock(&journal->j_list_lock);
211 
212     while (commit_transaction->t_sync_datalist) {
			// 依次处理每个缓冲区
213         jh = commit_transaction->t_sync_datalist;
214         bh = jh2bh(jh);
215         locked = 0;
243         if (!buffer_jbd(bh) || bh2jh(bh) != jh
244             || jh->b_transaction != commit_transaction
245             || jh->b_jlist != BJ_SyncData) {
				// 该缓冲区已经由别人写回磁盘了,
				// 那太幸运了
246             jbd_unlock_bh_state(bh);
247             if (locked)
248                 unlock_buffer(bh);
249             BUFFER_TRACE(bh, "already cleaned up");
250             release_data_buffer(bh);
251             continue;
252         }
253         if (locked && test_clear_buffer_dirty(bh)) {
				// 如果该缓冲区仍是脏的,则需要我们写回磁盘了
				// 在数组中记录该缓冲区。
255             wbuf[bufs++] = bh;
// 将缓冲区移动到BJ_Locked队列上。
256             __journal_file_buffer(jh, commit_transaction,
257                         BJ_Locked);
258             jbd_unlock_bh_state(bh);
259             if (bufs == journal->j_wbufsize) {
					// 如果缓冲区个数已累积到jouranl一次写操作允许的最大值,
					// 则提交写操作。
260                 spin_unlock(&journal->j_list_lock);
261                 journal_do_submit_data(wbuf, bufs, write_op);
262                 bufs = 0;
263                 goto write_out_data;
264             }
265         } else if (!locked && buffer_locked(bh)) {
266             __journal_file_buffer(jh, commit_transaction,
267                         BJ_Locked);
268             jbd_unlock_bh_state(bh);
269             put_bh(bh);
270         } else {
				// 该缓冲区已经写回磁盘了
272             if (unlikely(!buffer_uptodate(bh)))
273                 err = -EIO;
				// 从本transaction中删除即可。
274             __journal_unfile_buffer(jh);
275             jbd_unlock_bh_state(bh);
276             if (locked)
277                 unlock_buffer(bh);
278             journal_remove_journal_head(bh);
281             put_bh(bh);
282             release_data_buffer(bh);
283         }
289     }
290     spin_unlock(&journal->j_list_lock);
291     journal_do_submit_data(wbuf, bufs, write_op);
292 
293     return err;
294 }
journal_do_submit_data()函数只是提交写操作而已。
174 static void journal_do_submit_data(struct buffer_head **wbuf, int bufs,
175                    int write_op)
176 {
177     int i;
178 
179     for (i = 0; i < bufs; i++) {
180         wbuf[i]->b_end_io = end_buffer_write_sync;
181         /* We use-up our safety reference in submit_bh() */
182         submit_bh(write_op, wbuf[i]);
183     }
184 }
185

journal_write_revoke_records
  • journal_write_revoke_records()函数的作用是将取消块的细腻写到日志中。
  • fs/jbd/revoke.c
503 void journal_write_revoke_records(journal_t *journal,
504                   transaction_t *transaction, int write_op)
505 {       
506     struct journal_head *descriptor;
507     struct jbd_revoke_record_s *record;
508     struct jbd_revoke_table_s *revoke;
509     struct list_head *hash_list;
510     int i, offset, count;
511         
512     descriptor = NULL;
513     offset = 0;
514     count = 0;

517     revoke = journal->j_revoke == journal->j_revoke_table[0] ?
518         journal->j_revoke_table[1] : journal->j_revoke_table[0];
519     
520     for (i = 0; i < revoke->hash_size; i++) {
			// 循环处理hash表的每一个链表。
521         hash_list = &revoke->hash_table[i];
522 
523         while (!list_empty(hash_list)) {
				// 循环处理每一个记录
524             record = (struct jbd_revoke_record_s *)
525                 hash_list->next;
				// “写”一个revoke记录。
				// 其实不是真正的写,只是记录在取消块缓冲区中。
526             write_one_revoke_record(journal, transaction,
527                         &descriptor, &offset,
528                         record, write_op);
529             count++;
530             list_del(&record->hash);
531             kmem_cache_free(revoke_record_cache, record);
532         }
533     }
534     if (descriptor)
535         flush_descriptor(journal, descriptor, offset, write_op);
537 }

  • write_one_revoke_record()函数的作用是将一个revoke记录写到取消块缓冲区中。
544 static void write_one_revoke_record(journal_t *journal,
545                     transaction_t *transaction,
546                     struct journal_head **descriptorp,
547                     int *offsetp,
548                     struct jbd_revoke_record_s *record,
549                     int write_op)
550 {
551     struct journal_head *descriptor;
552     int offset;
553     journal_header_t *header;
562     descriptor = *descriptorp;
563     offset = *offsetp;
566     if (descriptor) {
567         if (offset == journal->j_blocksize) {
				// 如果取消块缓冲区已经写满了,
				// 则刷新该缓冲区。
568             flush_descriptor(journal, descriptor, offset, write_op);
569             descriptor = NULL;
570         }
571     }
572 
573     if (!descriptor) {
			// 如果尚未分配取消块缓冲区,
// 则从日志中分配一个未使用的块作为取消块缓冲区。
574         descriptor = journal_get_descriptor_buffer(journal);
575         if (!descriptor)
576             return;
			// 设置取消块 
577         header = (journal_header_t *) &jh2bh(descriptor)->b_data[0];
578         header->h_magic     = cpu_to_be32(JFS_MAGIC_NUMBER);
579         header->h_blocktype = cpu_to_be32(JFS_REVOKE_BLOCK);
580         header->h_sequence  = cpu_to_be32(transaction->t_tid);
			
			// 取消块也是jbd的控制块。
584         journal_file_buffer(descriptor, transaction, BJ_LogCtl);
585 
586         offset = sizeof(journal_revoke_header_t);
587         *descriptorp = descriptor;
588     }
589 
// 取消块中记录什么?
// 只需记录块号即可!
590     * ((__be32 *)(&jh2bh(descriptor)->b_data[offset])) =
591         cpu_to_be32(record->blocknr);
592     offset += 4;
593     *offsetp = offset;
594 }
flush_descriptor()函数的作用是启动该取消块的写操作。
603 static void flush_descriptor(journal_t *journal, 
604                  struct journal_head *descriptor,
605                  int offset, int write_op)
606 {   
607     journal_revoke_header_t *header;
608     struct buffer_head *bh = jh2bh(descriptor);
615     header = (journal_revoke_header_t *) jh2bh(descriptor)->b_data;
616     header->r_count = cpu_to_be32(offset);
617     set_buffer_jwrite(bh);
619     set_buffer_dirty(bh);
620     ll_rw_block((write_op == WRITE) ? SWRITE : SWRITE_SYNC_PLUG, 1, &bh);
621 }   

journal_write_metadata_buffer
  • journal_write_metadata_buffer()函数的作用是将一个元数据块缓冲区写到日志中。采用的方式是根据输入的缓冲区jh_in,新创建一个数据与jh_in中的数据相同,但是指向日志中的某一个块的缓冲区。
  • fs/jbd/journal.c
277 int journal_write_metadata_buffer(transaction_t *transaction,
 278                   struct journal_head  *jh_in,
 279                   struct journal_head **jh_out,
 280                   unsigned int blocknr)
 281 {
 282     int need_copy_out = 0;
 283     int done_copy_out = 0;
 284     int do_escape = 0;
 285     char *mapped_data;
 286     struct buffer_head *new_bh;
 287     struct journal_head *new_jh;
 288     struct page *new_page;
 289     unsigned int new_offset;
 290     struct buffer_head *bh_in = jh2bh(jh_in);
 291     journal_t *journal = transaction->t_journal;
303 
		 // 新创建一个缓冲区结构new_bh
 304     new_bh = alloc_buffer_head(GFP_NOFS|__GFP_NOFAIL);
306     new_bh->b_state = 0;
 307     init_buffer(new_bh, NULL, NULL);
 308     atomic_set(&new_bh->b_count, 1);
 309     new_jh = journal_add_journal_head(new_bh);  /* This sleeps */
315     jbd_lock_bh_state(bh_in);
 316 repeat:
 317     if (jh_in->b_frozen_data) {
 318         done_copy_out = 1;
 319         new_page = virt_to_page(jh_in->b_frozen_data);
 320         new_offset = offset_in_page(jh_in->b_frozen_data);
 321     } else {
 322         new_page = jh2bh(jh_in)->b_page;
 323         new_offset = offset_in_page(jh2bh(jh_in)->b_data);
 324     }
 325 
 326     mapped_data = kmap_atomic(new_page, KM_USER0);
	
		// 判断是否需要转义
330     if (*((__be32 *)(mapped_data + new_offset)) ==
 331                 cpu_to_be32(JFS_MAGIC_NUMBER)) {
 332         need_copy_out = 1;
 333         do_escape = 1;
 334     }
 335     kunmap_atomic(mapped_data, KM_USER0);

340     if (need_copy_out && !done_copy_out) {
 341         char *tmp;
 342 
 343         jbd_unlock_bh_state(bh_in);
 344         tmp = jbd_alloc(bh_in->b_size, GFP_NOFS);
 345         jbd_lock_bh_state(bh_in);
 346         if (jh_in->b_frozen_data) {
 347             jbd_free(tmp, bh_in->b_size);
 348             goto repeat;
 349         }
 350 
 351         jh_in->b_frozen_data = tmp;
 352         mapped_data = kmap_atomic(new_page, KM_USER0);
 353         memcpy(tmp, mapped_data + new_offset, jh2bh(jh_in)->b_size);
 354         kunmap_atomic(mapped_data, KM_USER0);
 355 
 356         new_page = virt_to_page(tmp);
 357         new_offset = offset_in_page(tmp);
 358         done_copy_out = 1;
 359     }

365     if (do_escape) {
			 // 处理转义
 366         mapped_data = kmap_atomic(new_page, KM_USER0);
 367         *((unsigned int *)(mapped_data + new_offset)) = 0;
 368         kunmap_atomic(mapped_data, KM_USER0);
 369     }
 370 
 371     set_bh_page(new_bh, new_page, new_offset);
 372     new_jh->b_transaction = NULL;
 373     new_bh->b_size = jh2bh(jh_in)->b_size;
 374     new_bh->b_bdev = transaction->t_journal->j_dev;
		 // 注意,这个blocknr是从日志中分配的一个磁盘块
 375     new_bh->b_blocknr = blocknr;
 376     set_buffer_mapped(new_bh);
 377     set_buffer_dirty(new_bh);
 378 
 379     *jh_out = new_jh;
387     spin_lock(&journal->j_list_lock);
		 // 将原来的元数据块缓冲区加入到BJ_Shadow队列
 388     __journal_file_buffer(jh_in, transaction, BJ_Shadow);
 389     spin_unlock(&journal->j_list_lock);
 390     jbd_unlock_bh_state(bh_in);
 391 
 392	 // 将新创建的缓冲区加入到BJ_IO队列
 393     journal_file_buffer(new_jh, transaction, BJ_IO);
 394 
 395     return do_escape | (done_copy_out << 1);
 396 }

journal_write_commit_record
  • journal_write_commit_record()函数的作用是写一个提交块。
  • fs/jbd/commit.c
115 static int journal_write_commit_record(journal_t *journal,
116                     transaction_t *commit_transaction)
117 {
118     struct journal_head *descriptor;
119     struct buffer_head *bh;
120     journal_header_t *header;
121     int ret;
122     int barrier_done = 0;
		
		// 从日志中分配一个块,作为提交块。
127     descriptor = journal_get_descriptor_buffer(journal);
131     bh = jh2bh(descriptor);
132 
		// 设置提交块信息
133     header = (journal_header_t *)(bh->b_data);
134     header->h_magic = cpu_to_be32(JFS_MAGIC_NUMBER);
135     header->h_blocktype = cpu_to_be32(JFS_COMMIT_BLOCK);
136     header->h_sequence = cpu_to_be32(commit_transaction->t_tid);
137 
139     set_buffer_dirty(bh);
		// 同步将提交块写到日志中
144     ret = sync_dirty_buffer(bh);
168     put_bh(bh);     /* One for getblk() */
169     journal_put_journal_head(descriptor);
170 
171     return (ret == -EIO);
172 }

数据块缓冲区状态转移图

细说jbd(journal-block-device)& 源码分析


元数据块缓冲区状态转移图

细说jbd(journal-block-device)& 源码分析

日志恢复

准备工作

细说jbd(journal-block-device)& 源码分析

journal_recover 函数
  • journal_recover()函数的主要作用分三个阶段进行日志的恢复操作。

fs/jbd/recovery.c

223 int journal_recover(journal_t *journal)
224 {
225     int         err, err2;
226     journal_superblock_t *  sb;
227 
228     struct recovery_info    info;
229 
230     memset(&info, 0, sizeof(info));
231     sb = journal->j_superblock;
238 
239     if (!sb->s_start) {
			// 如果文件系统是被正常卸载的,则不需要恢复。
			// 递增j_transaction_sequence,使整个日志无效。
242         journal->j_transaction_sequence = be32_to_cpu(sb->s_sequence) + 1;
243         return 0;
244     }
245 
		// 恢复步骤1:PASS_SCAN
		// 这个步骤主要的作用是找到日志的起点和终点,
		// 注意日志空间可看做一个环形结构。
246     err = do_one_pass(journal, &info, PASS_SCAN);

// 恢复步骤2:PASS_REVOKE
		// 这个步骤主要的作用找到revoke块,并把信息读入内存的revoke hash table。
247     if (!err)
248         err = do_one_pass(journal, &info, PASS_REVOKE);

// 恢复步骤3:PASS_REPLAY
		// 这个步骤主要的作用是根据描述符块的指示,
		// 将日志中的数据块写回到磁盘的原始位置上。
249     if (!err)
250         err = do_one_pass(journal, &info, PASS_REPLAY);

		// 恢复完成,递增j_transaction_sequence,使整个日志无效。
260     journal->j_transaction_sequence = ++info.end_transaction;
261 
		// 清空内存中的revoke hash table。
262     journal_clear_revoke(journal);
		// 同步日志所在的设备。
263     err2 = sync_blockdev(journal->j_fs_dev);
266 
267     return err;
268 }

  • 三个步骤实际上调用的都是一个函数do_one_pass,只不过参数不同罢了。
    首先看一个结构recovery_info,它是在恢复过程中保留日志信息的。
    fs/jbd/recovery.c
29 struct recovery_info
 30 {   
 31     tid_t       start_transaction;
 32     tid_t       end_transaction;
 33     
 34     int     nr_replays; 
 35     int     nr_revokes;
 36     int     nr_revoke_hits;
 37 };

恢复步骤1: PASS_SCAN
  • fs/jbd/recovery.c

312 static int do_one_pass(journal_t *journal,
313             struct recovery_info *info, enum passtype pass)
314 {
315     unsigned int        first_commit_ID, next_commit_ID;
316     unsigned int        next_log_block;
317     int         err, success = 0;
318     journal_superblock_t *  sb;
319     journal_header_t *  tmp;
320     struct buffer_head *    bh;
321     unsigned int        sequence;
322     int         blocktype;
335     sb = journal->j_superblock;
336     next_commit_ID = be32_to_cpu(sb->s_sequence);
		
		// next_log_block表示要在日志中读的下一个块的块号
337     next_log_block = be32_to_cpu(sb->s_start);
338 
339     first_commit_ID = next_commit_ID;
340     if (pass == PASS_SCAN)
341         info->start_transaction = first_commit_ID;

352     while (1) {
			// 遍历所有的日志块
353         int         flags;
354         char *          tagp;
355         journal_block_tag_t *   tag;
356         struct buffer_head *    obh;
357         struct buffer_head *    nbh;
			
			// 读入日志中的下一个块
377         err = jread(&bh, journal, next_log_block);
381         next_log_block++;
			
			// 注意日志是个环形结构
382         wrap(journal, next_log_block);
390         tmp = (journal_header_t *)bh->b_data;
391 
392         if (tmp->h_magic != cpu_to_be32(JFS_MAGIC_NUMBER)) {
				// 如果该块不是日志的描述块,则说明已经处理完了,退出
393             brelse(bh);
394             break;
395         }
396 
397         blocktype = be32_to_cpu(tmp->h_blocktype);
398         sequence = be32_to_cpu(tmp->h_sequence);
402         if (sequence != next_commit_ID) {
				// 如果序号不对,退出
403             brelse(bh);
404             break;
405         }

			// 根据描述块的类型,进行相应的处理
411         switch(blocktype) {
412         case JFS_DESCRIPTOR_BLOCK:
				// 描述符块
416             if (pass != PASS_REPLAY) {
					// 我们现在在PASS_SCAN中,
					// count_tags()函数会计算该描述符块中一共描述了对少个数据块的对应关系,现在直接跳过数据块即可。
417                 next_log_block +=
418                     count_tags(bh, journal->j_blocksize);
					
419                 wrap(journal, next_log_block);
420                 brelse(bh);
421                 continue;
422             }

510         case JFS_COMMIT_BLOCK:
				// 提交块
514             brelse(bh);
515             next_commit_ID++;
516             continue;
517 
518         case JFS_REVOKE_BLOCK:
				// 取消块,只在取消步骤中处理。
521             if (pass != PASS_REVOKE) {
522                 brelse(bh);
523                 continue;
524             }

533         default:
				// 不是以上的四种块,完成。
536             brelse(bh);
537             goto done;
538         }
539     }
540 
541  done:
549     if (pass == PASS_SCAN)
550         info->end_transaction = next_commit_ID;
551     else {
554         if (info->end_transaction != next_commit_ID) {
558             if (!success)
559                 success = -EIO;
560         }
561     }
562 
563     return success;
567 }

恢复步骤2:PASS_REVOKE
恢复步骤3:PASS_REPLAY

开始动真格的了。日志机制终于要起作用了!前边一切一切的准备,只为了今天的replay。

  • replay是什么意思呢?就是系统意外崩溃时,有一部分元数据块已经写到日志中了,但是尚未写回到磁盘的原始位置上。replay的作用就是将日志中的元数据块再重新写回到磁盘的原始位置上。
恢复后的设置工作
  • journal_reset()函数是在恢复完成后调用的,主要是更新日志超级块中的数据,以及初始化journal中的数据。
    fs/jbd/journal.c
881 static int journal_reset(journal_t *journal)
 882 {       
 883     journal_superblock_t *sb = journal->j_superblock;
 884     unsigned int first, last;
 885 
 886     first = be32_to_cpu(sb->s_first);
 887     last = be32_to_cpu(sb->s_maxlen);
 888     if (first + JFS_MIN_JOURNAL_BLOCKS > last + 1) {
891         journal_fail_superblock(journal);
 892         return -EINVAL;
 893     }
 894 
 895     journal->j_first = first;
 896     journal->j_last = last;
 897 
 898     journal->j_head = first;
 899     journal->j_tail = first;
 900     journal->j_free = last - first;
 901 
 902     journal->j_tail_sequence = journal->j_transaction_sequence;
 903     journal->j_commit_sequence = journal->j_transaction_sequence - 1;
 904     journal->j_commit_request = journal->j_commit_sequence;
 905 
 906     journal->j_max_transaction_buffers = journal->j_maxlen / 4;
 907 
		 // 更新磁盘上日志中的超级块的信息
909     journal_update_superblock(journal, 1);
		
		 // 启动kjournald内核线程,我们现在可以接受原子操作了。
 910     return journal_start_thread(journal);
 911 }
journal_update_superblock()函数的作用是更新日志超级块中的数据,然后同步地写回到磁盘中。
992 void journal_update_superblock(journal_t *journal, int wait)
 993 {
 994     journal_superblock_t *sb = journal->j_superblock;
 995     struct buffer_head *bh = journal->j_sb_buffer;
1012 
1013     spin_lock(&journal->j_state_lock);
1016 
		 // 更新日志超级块中的数据
1017     sb->s_sequence = cpu_to_be32(journal->j_tail_sequence);
1018     sb->s_start    = cpu_to_be32(journal->j_tail);
1019     sb->s_errno    = cpu_to_be32(journal->j_errno);
1020     spin_unlock(&journal->j_state_lock);
1021 
1023     mark_buffer_dirty(bh);
		 // 注意此时wait==1,同步地将日志超级块写到磁盘上。
1024     if (wait)
1025         sync_dirty_buffer(bh);
1026     else
1027         ll_rw_block(SWRITE, 1, &bh);
1040 }


  • 你可能还会有一个问题,如果恰好恢复时系统又崩溃了,那么正在进行的恢复操作又被打断了,那么会产生什么问题么?答案是不会的。因为如果恢复操作被打断了,journal_reset()函数就不会被调用了,于是日志中超级块的信息与恢复前仍一样。再次挂载该文件系统时,会又一次根据日志超级块中的信息在进行一遍恢复操作。

END

参考文档下载地址:https://download.****.net/download/qq_22613757/10826281