前一篇博客中我们仔细描述了Linux文件系统的主动一致性,即文件系统对外提供的用于实现文件一致性的接口,应用程序可以调用这些接口同步文件/系统的脏数据和元数据。但诚如前一篇博客中所说,一个成熟的系统不仅应该只有这些由用户控制的同步方式,系统需要提供一些方式来保证文件数据/元数据的一致性。本篇博客我们就详细描述Linux内核中这种被动一致性的实现框架以及部分细节。
所谓被动一致性是指系统后台存在定期的任务刷新某些文件的脏数据以及元数据。稍加思索知道,这些定期任务应该以内核线程的形式出现,于是,这些后台线程在设计的时候存在如下问题需要解决:
针对上述思考中的各个问题,Linux内核采取了如下的解决办法:
图1 回写线程总体框架
图2 回写机理
需要特别注意的一点是:系统为每个设备创建一个回写线程,而不是每个磁盘分区创建一个回写线程。这就导致可能出现如下问题:图2中的脏inode链表中的inode可能并不属于同一个文件系统,因为每个文件系统可能会建立在设备的一个分区之上。
为了实现相关我们上面构思的回写框架,内核中必须设计一些相关的数据结构,以下部分我们就主要阐述与回写相关的一些数据结构,我们重点放在思考为何必须这些数据结构。
首先,上面描述中我们知道,必须为每个设备创建相关的脏inode链表以及刷新线程,这些信息必须都记录在设备信息中,因此,设备信息中必须增加额外的成员变量以记录这些信息。同时,对于刷新线程部分,我们除了记录刷新线程的task结构外,还必须记录与该刷新线程相关的一些控制信息,如为了实现周期性地回写,必须记录上次回写时间等,也可以将脏inode链表记录在该结构体之中。
另外,为了实现周期性回写和释放缓存而导致的回写,可为每次回写构造一个任务,发起回写的本质是构造这样一个任务,回写的执行者只是执行这样的任务,当然,发起者需要根据其回写的意图(如数据完整性回写、周期性任务回写、释放缓存页面而进行的回写)设置任务的参数,执行者根据任务的参数决定任务的处理过程,结构相当清晰。为了增加这样一个任务数据结构,必须在设备中添加一个任务队列,以记录调用者发起的所有任务。
因此,根据上面的思考,我们总结出Linux内核中为了回写而引入的数据结构。
1. struct backing_dev_info
2. struct bdi_writeback
3. struct wb_writeback_work
4. struct writeback_control
1. struct backing_dev_info
系统中每个设备均对应这样一个结构体,该结构体最初是为了设备预读而设计的,但内核后来对其扩充,增加了设备回写相关的成员变量。与设备脏文件回写的相关成员如下列举:
struct backing_dev_info {
struct list_head bdi_list;
.........
struct bdi_writeback wb;
spinlock_t wb_lock;
struct list_head work_list;
.........
};
系统提供bdi_list将所有设备的bdi结构串联成链表,便于统一管理;wb是该设备对应的回写线程的数据结构,会在下面仔细描述;work_list是设备上所有任务的链表,发起回写的调用者只是构造一个回写任务挂入该链表即可;wb_lock是保护任务链表的锁。
2. struct bdi_writeback
前面我们说过,每个设备均为其创建一个写回线程,每个写回线程不仅需要记录创建的进程结构,还需要记录线程的上次刷新时间以及脏inode链表等,因此,内核中为此抽象出的数据结构为struct bdi_writeback。
struct bdi_writeback {
struct backing_dev_info *bdi; /* our parent bdi */
unsigned int nr;
unsigned long last_old_flush; /* last old data flush */
unsigned long last_active; /* last time bdi thread was active */
struct task_struct *task; /* writeback thread */
struct timer_list wakeup_timer; /* used for delayed bdi thread wakeup */
struct list_head b_dirty; /* dirty inodes */
struct list_head b_io; /* parked for writeback */
struct list_head b_more_io; /* parked for more writeback */
};
成员bdi指向设备结构体,last_old_flush记录上次刷新的时间,这是用于周期性回写之用,last_active记录回写线程的上次活动时间,该成员可用于销毁长时间不活跃的回写线程。task是刷新线程的进程结构,b_dirty是脏inode链表,每当一个文件被弄脏时,都会将其inode添加至所在设备的b_dirty链表中,至于b_io和b_more_io链表的作用在后面将仔细描述吧。
3. struct wb_writeback_work
我们前面说过,回写的过程实质上就是发起者构造一个回写任务,交给回写执行者去处理。这个任务就详细描述了本次回写请求的具体参数,内核中每个回写任务的具体参数如下描述:
struct wb_writeback_work {
long nr_pages;
struct super_block *sb;
enum writeback_sync_modes sync_mode;
unsigned int for_kupdate:1;
unsigned int range_cyclic:1;
unsigned int for_background:1;
struct list_head list; /* pending work list */
struct completion *done; /* set if the caller waits */
};
nr_pages表示调用者指示本次回写任务需要回写的脏页面数。sb表示调用者是否指定需要回写设备上属于哪个文件系统的脏页面,因为前面我们说过,每个设备可能会被划分成多个分区以支持多个文件系统,sb如果没有被赋值,则由回写线程决定回写哪个文件系统上的脏页面。sync_mode代表本次回写任务同步策略,WB_SYNC_NONE代表什么?WB_SYNC_ALL又代表什么?for_kupdate代表本回写任务是不是由于周期性回写而发起的,range_cyclic表示什么?for_background表示什么?list和done又分别代表了什么?
4. struct writeback_control
该结构可当做上面所描述回写任务的子任务,即系统会将每次回写任务拆分成多个子任务去处理,原因会在后面仔细说明。
前面我们叙述了与被动(隐式)回写相关的数据结构,接下来我们就要思考回写流程到底该如何设计。
因为内核对回写采取了单管理线程+多工作线程的框架。因此,回写的流程分为管理线程设计和工作线程流程设计。
对于管理线程来说,其主要工作是监视工作线程的运行状况,根据设备上的脏页面状况调整工作线程的运行,如设备上无脏页面且设备的工作线程已经有一段时间未被激活那么就kill该设备的回写线程,如果设备上有回写页面但尚未创建回写线程,那么为设备创建回写线程并启动线程运行。因此,总结来说,管理线程的主要流程如下:
static int bdi_forker_thread(void *ptr)
{
struct bdi_writeback *me = ptr;
current->flags |= PF_FLUSHER | PF_SWAPWRITE;
set_freezable();
/*
* Our parent may run at a different priority, just set us to normal
*/
set_user_nice(current, 0);
//线程运行在一个大的循环之中
for (;;) {
struct task_struct *task = NULL;
struct backing_dev_info *bdi;
enum {
NO_ACTION, /* Nothing to do */
FORK_THREAD, /* Fork bdi thread */
KILL_THREAD, /* Kill inactive bdi thread */
} action = NO_ACTION;
/*
* Temporary measure, we want to make sure we don't see
* dirty data on the default backing_dev_info
*/
/*
**如果当前设备上也有脏的inode或者有回写任务,那么处理,但一般来说,控制线程对应的设备上并不会产生脏inode或者回写任务
*/
if (wb_has_dirty_io(me) || !list_empty(&me->bdi->work_list)) {
del_timer(&me->wakeup_timer);
wb_do_writeback(me, 0);
}
spin_lock_bh(&bdi_lock);
set_current_state(TASK_INTERRUPTIBLE);
list_for_each_entry(bdi, &bdi_list, bdi_list) {
bool have_dirty_io;
if (!bdi_cap_writeback_dirty(bdi) ||
bdi_cap_flush_forker(bdi))
continue;
WARN(!test_bit(BDI_registered, &bdi->state),"bdi %p/%s is not registered!\n", bdi, bdi->name);
have_dirty_io = !list_empty(&bdi->work_list) || wb_has_dirty_io(&bdi->wb);
/*
* 若设备上有任务需要回写并且尚未创建回写线程
*/
if (!bdi->wb.task && have_dirty_io) {
/*
* 为设备设置Pending标志位,这样其他线程如果想要移除该设备,必须等在该标志位上
*/
set_bit(BDI_pending, &bdi->state);
action = FORK_THREAD;
break;
}
spin_lock(&bdi->wb_lock);
/*
*如果设备没有任务且长时间尚未处于活跃状态,那么Kill设备的回写线程,如果它存在的话
*这里对设备加wb_lock是为了保证在此过程中没有其他的线程对向该设备发送回写任务并且唤醒该回写线程
*/
if (bdi->wb.task && !have_dirty_io && time_after(jiffies, bdi->wb.last_active + bdi_longest_inactive())) {
task = bdi->wb.task;
bdi->wb.task = NULL;
spin_unlock(&bdi->wb_lock);
set_bit(BDI_pending, &bdi->state);
action = KILL_THREAD;
break;
}
spin_unlock(&bdi->wb_lock);
}
spin_unlock_bh(&bdi_lock);
/* Keep working if default bdi still has things to do */
if (!list_empty(&me->bdi->work_list))
__set_current_state(TASK_RUNNING);
switch (action) {
case FORK_THREAD:
__set_current_state(TASK_RUNNING);
task = kthread_create(bdi_writeback_thread, &bdi->wb,
"flush-%s", dev_name(bdi->dev));
if (IS_ERR(task)) {
/*
*如果为设备创建回写线程失败,那么管理线程亲自操刀,回写设备上的任务
*/
bdi_flush_io(bdi);
} else {
spin_lock_bh(&bdi->wb_lock);
bdi->wb.task = task;
spin_unlock_bh(&bdi->wb_lock);
wake_up_process(task);
}
break;
case KILL_THREAD:
__set_current_state(TASK_RUNNING);
kthread_stop(task);
break;
case NO_ACTION:
if (!wb_has_dirty_io(me) || !dirty_writeback_interval)
/*
* 如果对设备遍历了一圈发现没有设备上需要进行任何的处理,那么好吧,我们尽量睡眠更长的时间
*这样可以更省电
*/
schedule_timeout(bdi_longest_inactive());
else
schedule_timeout(msecs_to_jiffies(dirty_writeback_interval * 10));
try_to_freeze();
/* Back to the main loop */
continue;
}
/*任务做完以后,清除Pending标志位,这样其它想要移除该设备的线程便可以继续处理了*/
clear_bit(BDI_pending, &bdi->state);
smp_mb__after_clear_bit();
wake_up_bit(&bdi->state, BDI_pending);
}
return 0;
}
相较于管理线程,每个设备的工作线程的设计更为复杂,因为它要完成具体的工作,而且工作量还比较繁重,让我们首先来仔细思考工作线程有哪些任务需要处理:
对于上述问题,我们大概设计的工作线程方案如下:
图3 工作线程总体结构
Linux回写线程的主体部分的代码为:
int bdi_writeback_thread(void *data)
{
struct bdi_writeback *wb = data;
struct backing_dev_info *bdi = wb->bdi;
long pages_written;
current->flags |= PF_FLUSHER | PF_SWAPWRITE;
set_freezable();
wb->last_active = jiffies;
/*
* Our parent may run at a different priority, just set us to normal
*/
set_user_nice(current, 0);
trace_writeback_thread_start(bdi);
//判断我们该回写线程是否应该结束
while (!kthread_should_stop()) {
/*
* Remove own delayed wake-up timer, since we are already awake
* and we'll take care of the preriodic write-back.
*/
del_timer(&wb->wakeup_timer);
//在wb_do_writeback中处理设备所有的任务以及周期性回写任务
//其中参数2代表是否进行同步回写,0代表异步回写,即不等写完即可返回
pages_written = wb_do_writeback(wb, 0);
trace_writeback_pages_written(pages_written);
//记录上次活跃时间
if (pages_written)
wb->last_active = jiffies;
//接下来可能要进入睡眠了,提前设置线程的状态
//在睡眠之前,我们需要判断,如果设备当前还有任务或者该线程被管理者叫停,那么不进入睡眠,而是进行下一轮的循环
set_current_state(TASK_INTERRUPTIBLE);
if (!list_empty(&bdi->work_list) || kthread_should_stop()) {
__set_current_state(TASK_RUNNING);
continue;
}
//根据设备任务状态决定睡眠时间
if (wb_has_dirty_io(wb) && dirty_writeback_interval)
schedule_timeout(msecs_to_jiffies(dirty_writeback_interval * 10));
else {
/*
* We have nothing to do, so can go sleep without any
* timeout and save power. When a work is queued or
* something is made dirty - we will be woken up.
*/
schedule();
}
try_to_freeze();
}
/* Flush any work that raced with us exiting */
if (!list_empty(&bdi->work_list))
wb_do_writeback(wb, 1);
trace_writeback_thread_stop(bdi);
return 0;
}
long wb_do_writeback(struct bdi_writeback *wb, int force_wait)
{
struct backing_dev_info *bdi = wb->bdi;
struct wb_writeback_work *work;
long wrote = 0;
set_bit(BDI_writeback_running, &wb->bdi->state);
//遍历设备任务链表上的所有任务,依次处理
while ((work = get_next_work_item(bdi)) != NULL) {
//若回写线程决定采用同步写,那么对每个任务都必须设置一个同步标志位
if (force_wait)
work->sync_mode = WB_SYNC_ALL;
trace_writeback_exec(bdi, work);
//对每个任务,调用wb_writeback()进行回写的真正过程
wrote += wb_writeback(wb, work);
//如果调用者等待在自己发起的任务上,那么任务完成了就必须告知调用者
if (work->done)
complete(work->done);
else
kfree(work);
}
//所有的任务完成以后,处理周期性回写
wrote += wb_check_old_data_flush(wb);
clear_bit(BDI_writeback_running, &wb->bdi->state);
return wrote;
}
static long wb_check_old_data_flush(struct bdi_writeback *wb)
{
unsigned long expired;
long nr_pages;
/*
* When set to zero, disable periodic writeback
*/
if (!dirty_writeback_interval)
return 0;
//若周期性回写时机尚未来到,那么直接返回
expired = wb->last_old_flush + msecs_to_jiffies(dirty_writeback_interval * 10);
if (time_before(jiffies, expired))
return 0;
//记录本次回写时间
wb->last_old_flush = jiffies;
nr_pages = global_page_state(NR_FILE_DIRTY) + global_page_state(NR_UNSTABLE_NFS) + (inodes_stat.nr_inodes - inodes_stat.nr_unused);
//为本次周期性回写构造一个回写任务
if (nr_pages) {
struct wb_writeback_work work = {
.nr_pages = nr_pages,
.sync_mode = WB_SYNC_NONE,
//设置该标志位表示这是用于定期更新目的的回写任务
.for_kupdate = 1,
//更新操作是逐个检查地址空间的页面,当检查到最后一个时
//需要回绕到地址空间的起始位置从头再来
//此时需要将该标志位置为1
.range_cyclic = 1,
};
//调用函数进行真正地回写
return wb_writeback(wb, &work);
}
return 0;
}
周期性回写的本质也是构造一个回写任务,并调用统一的wb_writeback()函数来处理本次回写任务。
搞清楚了整体框架,接下来我们需要关注的就是每次的回写任务的处理流程的细节了。
真正地弄清楚处理回写任务之前,我们尚需弄清楚如下几个问题:
弄清楚了上面4点问题,整个回写框架也就呼之欲出,主要实现位于wb_writeback()函数中,函数流程如下图所示:
该函数每次处理一个任务(work),在实现的时候,会将每个任务分解成多个子任务,每个子任务完成特定数量的脏页面回写(目前是1024)。通过一个大循环来控制任务的完成进度,代表每个大任务的结构体是struct wb_writeback_work,而代表每个子任务的数据结构是struct writeback_control。为什么要将大任务分解成子任务呢?我想还是效率的原因,因为在回写文件脏页面的时候,会对全局的inode_lock加锁(为什么需要加这把锁?),直到本次回写任务完成,如果对每个任务采取一次性回写的策略,那么这把锁加的时间可能会很长,系统中其他的地方可能也会等着加这把锁,那么就会因为回写而影响了其他地方的使用,因此,将大任务拆分成多个小的任务体现了高效和公平性。
对于每一个子任务的处理也较为复杂,我们需要考虑每个子任务的完成状况,因为并不是每个子任务都能按照设定正确地写完本次任务。因此,对于没有正确完成的子任务需要作出判断,接下来我们需要仔细描述任务处理的大循环过程:
回写大大致逻辑就如上面所述,接下来就要看每个子任务的处理流程。
我们前面说过,每个大任务中有个域用以指示是否回写属于特定文件系统(即特定超级块)的脏页面,对于设置与否会调用不同的函数,我们首先来考察设定了的情况下的处理流程,此时调用的函数为__writeback_inodes_sb()。
static void __writeback_inodes_sb(struct super_block *sb,
struct bdi_writeback *wb, struct writeback_control *wbc)
{
WARN_ON(!rwsem_is_locked(&sb->s_umount));
spin_lock(&inode_lock);
if (!wbc->for_kupdate || list_empty(&wb->b_io))
queue_io(wb, wbc->older_than_this);
writeback_sb_inodes(sb, wb, wbc, true);
spin_unlock(&inode_lock);
}
前面我们说过,每个设备关联了三个链表,回写主要集中在io链表上,我们同样说过,会在合适的时候将另外两个链表(dirty和more_io链表)中的脏inode转移至io链表,当然并非转移所有的脏inode,而是more_io上所有的脏inode和dirty链表上的部分脏inode。至于转移的时机,有个判断的标准:!wbc->for_kupdate || list_empty(&wb->b_io),只要不是用于周期性回写或者io链表为空,那么即开始转移,调用函数queue_io()。
接下来,便是开始真正的回写了,调用函数writeback_sb_inodes(),其最后一个参数为true,表示只回写属于该文件系统(sb标识)的脏inode。我们会在后面仔细描述该函数的实现。
考察完指定刷新文件系统脏inode的情况,我们看如果上层任务不指定文件系统情况下的脏inode的刷新。此时调用的函数为writeback_inodes_wb()。
void writeback_inodes_wb(struct bdi_writeback *wb,
struct writeback_control *wbc)
{
int ret = 0;
if (!wbc->wb_start)
wbc->wb_start = jiffies; /* livelock avoidance */
spin_lock(&inode_lock);
if (!wbc->for_kupdate || list_empty(&wb->b_io))
queue_io(wb, wbc->older_than_this);
while (!list_empty(&wb->b_io)) {
//其实是取wb->b_io链表上的最后一个inode,因为
//该链表上的inode是按照修改时间为顺序被链接起来的
//取最后一个inode是将修改时间最久的inode脏页面回写
struct inode *inode = list_entry(wb->b_io.prev, struct inode, i_list);
struct super_block *sb = inode->i_sb;
if (!pin_sb_for_writeback(sb)) {
requeue_io(inode);
continue;
}
ret = writeback_sb_inodes(sb, wb, wbc, false);
drop_super(sb);
if (ret)
break;
}
spin_unlock(&inode_lock);
}
阅读代码可以发现,在经过了一系列的准备工作后,它也调用了函数writeback_inodes_wb()来完成脏inode的回写,只是其最后一个参数为false,表示并非一定回写属于该文件系统(以sb标识)的脏inode。所做的准备工作如下:
比较有意思的是该函数判断writeback_inodes_wb()的返回值,根据该返回值决定接下来是否继续从io链表上取脏inode进行回写,如果返回1,表示不应继续回写,而返回0则意味着还要继续回写,因此就有了if(ret) break的判断。
接下来,我们尝试阅读writeback_inodes_wb()的代码,看看系统到底如何回写脏inode。
static int writeback_sb_inodes(struct super_block *sb, struct bdi_writeback *wb,
struct writeback_control *wbc, bool only_this_sb)
{
//如果wb->b_io链表为空,那么直接返回1,告诉调用者无需继续回写
while (!list_empty(&wb->b_io)) {
long pages_skipped;
//取b_io链表的最后一个节点
struct inode *inode = list_entry(wb->b_io.prev, struct inode, i_list);
//如果当前回写的inode不属于调用者指定的super_block
if (inode->i_sb != sb) {
//如果设置了标志位表明必须回写该super_block,
//那么必须放弃该inode,将其重新加入bdi的dirty链表中
if (only_this_sb) {
/*
* We only want to write back data for this
* superblock, move all inodes not belonging
* to it back onto the dirty list.
*/
redirty_tail(inode);
continue;
}
/*
* The inode belongs to a different superblock.
* Bounce back to the caller to unpin this and
* pin the next superblock.
*/
//如果该inode并非属于指定超级块,而且调用者也
//没有指定必须回写该超级块的脏inode,那么此时返回0,告知调用者去回写
//io链表上的下一个脏inode。
return 0;
}
//如果该inode刚刚被创建或者即将被销毁,那么将其重新放入more_io链表,等待下次被回写
if (inode->i_state & (I_NEW | I_WILL_FREE)) {
requeue_io(inode);
continue;
}
/*
* Was this inode dirtied after sync_sb_inodes was called?
* This keeps sync from extra jobs and livelock.
*/
//为了防止活锁,只回写wbc->wb_start时间点之前被弄脏的inode
//返回1告知调用者已经回写到回写开始时间点之后被弄脏的inode了,可以停止回写了
if (inode_dirtied_after(inode, wbc->wb_start))
return 1;
BUG_ON(inode->i_state & I_FREEING);
__iget(inode);
pages_skipped = wbc->pages_skipped;
//将inode上的脏页面回写
writeback_single_inode(inode, wbc);
if (wbc->pages_skipped != pages_skipped) {
/*
* writeback is not making progress due to locked
* buffers. Skip this inode for now.
*/
redirty_tail(inode);
}
spin_unlock(&inode_lock);
iput(inode);
cond_resched();
spin_lock(&inode_lock);
if (wbc->nr_to_write <= 0) {
wbc->more_io = 1;
return 1;
}
if (!list_empty(&wb->b_more_io))
wbc->more_io = 1;
}
/* b_io is empty */
return 1;
}
这段代码的逻辑理解起来不难,主要是在一个大的循环体中依次从io链表中取出脏inode,判断inode是否可以进行回写,如果可以,调用writeback_single_inode(inode, wbc),否则,将其添加到某个链表中。不能进行回写的原因有很多,因不同原因导致的无法回写其处理方法也不同,简单罗列,有如下:
除上述三种状况之外,其余的io链表上的脏inode均可进行回写,具体来说调用函数writeback_single_inode,但在回写完成时需要作如下判断:
上面分析了整个函数的大致逻辑,分析的过程中我们会产生这样几个问题:
对于问题1,我们暂时还没有一个较为合理的解答。对于问题2,我是这样考虑的:wbc->more_io是为了告诉上层当前more_io链表中是否还有脏inode尚未处理而设置的,不放让我们看看它的设置时机:1是在本次回写控制中的待回写页面数已全部完成,这时会向调用者返回1,并设置more_io;2是more_io链表中非空,表明当前仍然有脏inode可被处理。观察更高层调用者的行为,她会判断wbc->more_io标志位,如果为1意味着回写控制中的页面尚未全部完成但more_io被置位1,此时继续下一次循环继续回写,而如果回写控制的页面尚未全部完成同时more_io没被置位,说明底层告诉高层调用者,已无脏inode可回写,直接返回吧。
至于writeback_single_inode已经在我的前一篇博客中有了较为详细的描述,不再赘言。