为什么要在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; // 更新块内日志开始写入的偏移位置
}
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)断言失败:
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
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);
...
}
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,不会触发日志覆盖。