ubifs: Why c min_log_bytes should be initialized as c leb_size before in mount operation - 549642238/linux-stable GitHub Wiki

为什么要在mount时对c->min_log_bytes初始化?

ubifs每次提交操作时需要在Flash上预留一段空间记录journal,这段预留空间由c->log_lebs个LEB构成,LEB号连续,逻辑上是一个环形log记录区域。c->lhead_lnum是日志记录开始块号,c->ltail_lnum是日志记录结束块号,c->lhead_offs是日志记录在块内c->lhead_lnum的开始偏移。如图:

从c->ltail_lnum到c->lhead_offs的LEB已经被记录了日志,在提交操作结束之前一直有效,为了防止在commit过程中发生unclean reboot,我们需要把日志写入Flash,下次mount时可以调用ubifs_replay_journal恢复commit,只要commit完成后,Flash上log LEB中的日志内容就不再有效,可以擦除。从c->lhead_offs到c->ltail_lnum的LEB还未被记录日志,可以用于记录新的日志。

       c->ltail_lnum      c->lhead_lnum
            ↓ existed journal ↓

LEB(0)    LEB(1)    LEB(2)    LEB(3)   ...   LEB(c->log_lebs-1)


c->lhead_offs(new journal start from here)
               ↓

XXXXXXXXXXXXXXXX.......................
                LEB3

如果c->lhead_lnum到了LEB(c->log_lebs-1),再记录新的journal时从LEB(0)开始。记录日志前要检查剩下的日志空间必须大于1个LEB size(c->leb_size),以防c->lhead_lnum超越c->ltail_lnum导致日志覆盖,使得提交操作失败。所以在ubifs_add_bud_to_log写日志到log LEB中前要检查if (empty_log_bytes(c) - c->ref_node_alsz < c->min_log_bytes),c->min_log_bytes在每次commit结束后会被置为c->leb_size,保证下次写入的日志在下次提交前不会因为日志满了发生覆盖。

make_reservation:
1. reserve_space -> ubifs_add_bud_to_log             # 写日志到log LEB
2. ubifs_run_commit -> do_commit:
  2.1 ubifs_log_start_commit                         # 提交日志
  2.2 ubifs_log_end_commit                           # 提交结束,回收所有log LEB
  2.3 ubifs_log_post_commit

ubifs_add_bud_to_log和do_commit没有先后顺序,调用do_commit的地方不止make_reservation一处:

如果do_commit发生在ubifs_add_bud_to_log之前,c->min_log_bytes每次在commit结束前会被置为c->leb_size,今后的ubifs_add_bud_to_log不管在commit前执行多少次,都会在log LEB中预留一个LEB size,保证do_commit提交日志成功 [OK]

如果do_commit发生在ubifs_add_bud_to_log之后:

若mount后已经执行过do_commit,c->min_log_bytes每次在commit结束前会被置为c->leb_size,今后的ubifs_add_bud_to_log不管在commit前执行多少次,都会在log LEB中预留一个LEB size,保证do_commit提交日志成功 [OK]

若mount后是第一次do_commit,c->min_log_bytes在mount后未被显示赋值,默认为0,可能多次ubifs_add_bud_to_log执行之后,没有为do_commit预留空间,导致do_commit因为log LEB没有空间支持commit start log而失败 [FAIL]

A. 写入日志到log LEB,写之前检查剩余log LEB空间扣除本次写入日志大小后要预留出至少一个LEB大小,以便ubifs_log_start_commit时使用

int ubifs_add_bud_to_log(struct ubifs_info *c, int jhead, int lnum, int offs)
{
	...
	/* Make sure we have enough space in the log */
	if (empty_log_bytes(c) - c->ref_node_alsz < c->min_log_bytes) {       // 在log LEB中预留一个LEB大小的空间用于ubifs_log_start_commit使用(后文会讲ubifs_log_start_commit会在log LEB写入commit start信息到至多一个LEB)
		dbg_log("not enough log space - %lld, required %d",
			empty_log_bytes(c), c->min_log_bytes);
		ubifs_commit_required(c);
		err = -EAGAIN;
		goto out_unlock;                                              // 如果扣除本次日志大小c->ref_node_alsz后不够c->min_log_bytes,那就可能导致commit失败,所以直接返回-EAGAIN,调用ubifs_run_commit进行一次提交,提交之后就会有足够的可用log LEB写入journal
	}
	...
	if (c->lhead_offs > c->leb_size - c->ref_node_alsz) {                 // 如果当前log LEB剩余空间不够写入c->ref_node_alsz,就使用下一块log LEB写入,块内开始写入偏移量置0(c->lhead_offs = 0)
		c->lhead_lnum = ubifs_next_log_lnum(c, c->lhead_lnum);
		ubifs_assert(c, c->lhead_lnum != c->ltail_lnum);
		c->lhead_offs = 0;
	}

	if (c->lhead_offs == 0) {                                            // 说明使用了新的log LEB,要使用unmap擦除
		/* Must ensure next log LEB has been unmapped */
		err = ubifs_leb_unmap(c, c->lhead_lnum);
		if (err)
			goto out_unlock;
	}
	...
	err = ubifs_write_node(c, ref, UBIFS_REF_NODE_SZ, c->lhead_lnum,    // 写入ref到对应块号为c->lhead_lnum的log LEB,从偏移量为c->lhead_offs的块内位置开始写,UBIFS_REF_NODE_SZ就是c->ref_node_alsz,大小前后一致
			       c->lhead_offs);
	...
	c->lhead_offs += c->ref_node_alsz;                                  // 更新块内日志开始写入的偏移位置
}

B. 提交操作

static int do_commit(struct ubifs_info *c)
{
	...
	err = ubifs_log_start_commit(c, &new_ltail_lnum);
	old_ltail_lnum = c->ltail_lnum;						// 本次提交的journal内容包括从c->ltail_lnum到c->lhead_lnum的log LEB
	err = ubifs_log_end_commit(c, new_ltail_lnum);
	err = ubifs_log_post_commit(c, old_ltail_lnum);
	...
}

B1. 开始日志提交,在可用log LEB中扣除一个LEB大小以记录commit start信息

int ubifs_log_start_commit(struct ubifs_info *c, int *ltail_lnum)
{
	...
	/* Switch to the next log LEB */
	if (c->lhead_offs) {						    // 将日志节点写入新的log LEB,如果当前log LEB已经有内容
		c->lhead_lnum = ubifs_next_log_lnum(c, c->lhead_lnum);      // 获取下一个可用的log LEB
		ubifs_assert(c, c->lhead_lnum != c->ltail_lnum);            // 没有可用的log LEB了,不可能!每次写入log前都会检查,一定会保证预留出c->min_log_bytes(c->leb_size)大小的log LEB空间用作提交操作
		c->lhead_offs = 0;
	}
	/* Must ensure next LEB has been unmapped */
	err = ubifs_leb_unmap(c, c->lhead_lnum);			   // unmap c->lhead_lnum号LEB,之后会后台擦除,可以理解为擦除操作
	len = ALIGN(len, c->min_io_size);				   // 要往log LEB中写入commit start的对齐长度,不会超过一个LEB大小
	err = ubifs_leb_write(c, c->lhead_lnum, cs, 0, len);               // 写入commit start信息到Flash,从c->lhead_lnum号的LEB(写前重新map,获得一块没有内容的新LEB)开始,起始偏移为0,长度为len
	*ltail_lnum = c->lhead_lnum;				          // commit_end执行后,c->ltail_lnum=ltail_lnum,可以清空所有log LEB的内容

	c->lhead_offs += len;					          // 更新c->lhead_offs
	if (c->lhead_offs == c->leb_size) {
		c->lhead_lnum = ubifs_next_log_lnum(c, c->lhead_lnum);
		c->lhead_offs = 0;
	}
	c->min_log_bytes = 0;						  // commit已经开始,journal内容已被写入Flash,没必要再为日志预留Flash空间
	...
}

B2. 提交完成,log LEB保留的journal不再有用,即使现在发生reboot操作

int ubifs_log_end_commit(struct ubifs_info *c, int ltail_lnum)
{
	...
	c->ltail_lnum = ltail_lnum;					  // 日志写入完成,log LEB记录内容无效
	c->min_log_bytes = c->leb_size;					  // 保证下一次commit时为日志内容预留至少一个LEB的Flash空间
	...
	err = ubifs_write_master(c);					  // 将超级块master node写入Flash
	...
}

B3. 提交结束清理工作,清除log LEB的内容

int ubifs_log_post_commit(struct ubifs_info *c, int old_ltail_lnum)
{
	...
	for (lnum = old_ltail_lnum; lnum != c->ltail_lnum;		// 释放这次commit用来存储journal内容的log LEB,ubifs_log_start_commit结束后c->ltail_lnum号LEB一直到c->lhead_lnum的log LEB都被释放
	     lnum = ubifs_next_log_lnum(c, lnum)) {
		dbg_log("unmap log LEB %d", lnum);
		err = ubifs_leb_unmap(c, lnum);
		if (err)
			goto out;
	}
	...
}

那么,当mount ubifs之后,c->min_log_bytes=0,如果进行了多次写入日志操作ubifs_add_bud_to_log,导致没有为commit在log LEB中预留够一个LEB的大小,在执行commit时,就会触发ubifs_log_start_commit -> ubifs_assert(c, c->lhead_lnum != c->ltail_lnum)断言失败:

A. commit前:

c->lhead_lnum  c->ltail_lnum      
      ↓           ↓   existed     ...                journal 

   LEB(0)        LEB(1)    LEB(2)    LEB(3)   ...   LEB(c->log_lebs-1)

c->lhead_offs(new journal start from here)
               ↓

XXXXXXXXXXXXXXXX.......................
                LEB0

B. commit时:

	if (c->lhead_offs) {         // 将日志节点写入新的log LEB,发现当前log LEB已经有内容
		c->lhead_lnum = ubifs_next_log_lnum(c, c->lhead_lnum);   // c->lhead_lnum往前移动一个LEB,等于c->ltail_lnum
		ubifs_assert(c, c->lhead_lnum != c->ltail_lnum);         // 断言失败,如果继续执行下去会发生log overrun(日志覆盖)
		c->lhead_offs = 0;
	}

其他细节:

1. 对c->lhead_lnum、c->lhead_offs、c->ltail_lnum的修改地方都加了c->log_mutex锁保护,除了ubifs_log_start_commit:

首先ubifs_log_end_commit、ubifs_log_post_commit和ubifs_add_bud_to_log两两之间不会并发访问这些变量,受c->log_mutex锁保护;

其次,ubifs_replay_journal、ubifs_fixup_free_space->fixup_free_space和ubifs_consolidate_log对这些变量的修改和访问也不会与do_commit和ubifs_add_bud_to_log操作冲突,因为ubifs_replay_journal和ubifs_consolidate_log发生在mount时,mount结束后才能进行do_commit和ubifs_add_bud_to_log操作,而且ubifs_replay_journal、ubifs_consolidate_log和ubifs_fixup_free_space有严格的先后顺序;

然后ubifs_log_start_commit一定不会和ubifs_add_bud_to_log并发执行,ubifs_log_start_commit前要锁定c->commit_sem,reserve_space -> ubifs_add_bud_to_log前后也要锁定c->commit_sem;

最后ubifs_log_start_commit一定不会和ubifs_log_end_commit、ubifs_log_post_commit中对c->ltail_lnum等的访问造成影响,因为同一个commit中这三者顺序执行。而且不会有两个do_commit并发执行,因为c->cmt_state记录了commit状态,不允许两个正在进行的commit同时运行:

int ubifs_run_commit(struct ubifs_info *c)
{
	...
	if (c->cmt_state == COMMIT_RUNNING_REQUIRED) {
		up_write(&c->commit_sem);
		spin_unlock(&c->cs_lock);
		return wait_for_commit(c);         // 别的任务正在进行commit操作,等他完成
	}
	c->cmt_state = COMMIT_RUNNING_REQUIRED;    // 当前正在进行commit操作
	spin_unlock(&c->cs_lock);

	err = do_commit(c);                        // 执行commit操作
	return err;

out_cmt_unlock:
	up_write(&c->commit_sem);
	...
}
后台线程commit:
static int run_bg_commit(struct ubifs_info *c)
{
	spin_lock(&c->cs_lock);
	if (c->cmt_state != COMMIT_BACKGROUND &&
	    c->cmt_state != COMMIT_REQUIRED)       // 不是这两种状态的都不能在后台进行commit
		goto out;
	spin_unlock(&c->cs_lock);

	down_write(&c->commit_sem);
	...
	return do_commit(c);                       // 执行commit操作
	...
out:
	spin_unlock(&c->cs_lock);
	return 0;
}

2. c->ltail_lnum,c->lhead_lnum在mount结束后初始化值是多少并不重要。如果mount正常返回,不论c->lhead_lnum和c->ltail_lnum的相对位置如何,只要c->min_log_bytes初始化为c->leb_size,都会在ubifs_add_bud_to_log中检查预留空间,保证commit操作有足够的可用log lEB,不会触发日志覆盖。

⚠️ **GitHub.com Fallback** ⚠️