linux内核文件一致性之被动一致性

前言

       前一篇博客中我们仔细描述了Linux文件系统的主动一致性,即文件系统对外提供的用于实现文件一致性的接口,应用程序可以调用这些接口同步文件/系统的脏数据和元数据。但诚如前一篇博客中所说,一个成熟的系统不仅应该只有这些由用户控制的同步方式,系统需要提供一些方式来保证文件数据/元数据的一致性。本篇博客我们就详细描述Linux内核中这种被动一致性的实现框架以及部分细节。

思考

       所谓被动一致性是指系统后台存在定期的任务刷新某些文件的脏数据以及元数据。稍加思索知道,这些定期任务应该以内核线程的形式出现,于是,这些后台线程在设计的时候存在如下问题需要解决:

  1. 需要创建多少个内核线程来完成同步任务,根据何种标准来确定线程数量?多线程采用何种架构,所有线程处于同等地位还是存在一个集中管理线程(类似lighthttp架构)?
  2. 多线程如何处理并行的问题?这个问题其实又和如何确定创建的线程数量息息相关。
  3. 内核线程作为被动地刷新脏文件,其执行流必然会和主动刷新并行执行,如何设计一个统一的框架来管理这些任务流地执行?

总体框架

        针对上述思考中的各个问题,Linux内核采取了如下的解决办法:

  1. 创建的针对回写任务的内核线程数由系统中持久存储设备决定,操作系统中有N个存储设备,那么在系统初始化时就会为其创建N个刷新线程。
  2. 关于多线程的架构问题,Linux内核采取了Lighthttp的做法,即系统中存在一个管理线程和多个刷新线程(每个持久存储设备对应一个刷新线程)。管理线程监控设备上的脏页面情况,若设备一段时间内没有产生脏页面,就销毁设备上的刷新线程;若监测到设备上有脏页面需要回写且尚未为该设备创建刷新线程,那么创建刷新线程处理脏页面回写。而刷新线程的任务较为单调,只负责将设备中的脏页面回写至持久存储设备中。
  3. 刷新线程刷新设备上脏页面大致设计如下:
  • 每个设备保存脏文件链表,保存的是该设备上存储的脏文件的inode节点。所谓的回写文件脏页面即回写该inode链表上的某些文件的脏页面。
  • 系统中存在多个回写时机,第一是应用程序主动调用回写接口(fsyncfdatasync以及sync等),第二管理线程周期性地唤醒设备上的回写线程进行回写,第三是某些应用程序/内核任务发现内存不足时要回收部分缓存页面而事先进行脏页面回写,设计一个统一的框架来管理这些回写任务非常有必要。

     

 回写线程总体框架

回写机理

       需要特别注意的一点是:系统为每个设备创建一个回写线程,而不是每个磁盘分区创建一个回写线程。这就导致可能出现如下问题:图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_iob_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表示什么?listdone又分别代表了什么?

4. struct writeback_control

该结构可当做上面所描述回写任务的子任务,即系统会将每次回写任务拆分成多个子任务去处理,原因会在后面仔细说明。

回写流程

       前面我们叙述了与被动(隐式)回写相关的数据结构,接下来我们就要思考回写流程到底该如何设计。

       因为内核对回写采取了单管理线程+多工作线程的框架。因此,回写的流程分为管理线程设计和工作线程流程设计。

管理线程

       对于管理线程来说,其主要工作是监视工作线程的运行状况,根据设备上的脏页面状况调整工作线程的运行,如设备上无脏页面且设备的工作线程已经有一段时间未被激活那么就kill该设备的回写线程,如果设备上有回写页面但尚未创建回写线程,那么为设备创建回写线程并启动线程运行。因此,总结来说,管理线程的主要流程如下:

  • 遍历系统中所有的设备,判断设备目前的状态,如果设备脏inode链表不为空或者设备任务队列不为空且该设备当前尚未创建回写线程,那么为设备创建回写线程;如果设备当前脏inode链表为空且设备的回写线程已经有较长一段时间未活跃,那么就需要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;

}

工作线程

       相较于管理线程,每个设备的工作线程的设计更为复杂,因为它要完成具体的工作,而且工作量还比较繁重,让我们首先来仔细思考工作线程有哪些任务需要处理:

  1. 需要处理设备任务链表上的任务,需要确定的问题是每次处理多少个任务?
  2. 需要处理脏inode链表上的inode,需要考虑的问题是何时以及如何处理,是对脏inodes也构造一个任务添加到设备的任务链表上?
  3. 如何处理周期性的回写?周期性回写到底回写哪些脏文件?是脏inode链表上的脏页面吗?
  4. 每一次的回写又该如何设计?

       对于上述问题,我们大概设计的工作线程方案如下:

  • 首先,工作线程位于一个大的循环体之中,一般来说,该循环应该设计成一个无限死循环,直到被某些条件触发(如外界主动去停止该线程,就像管理线程做的那样),在循环体中,调用一个特定函数去处理回写,当然处理完成回写以后需要决定当前线程是否需要被调度,进入休眠状态,这就需要根据当前设备的任务状态了,进入休眠是为了节电,避免设备空闲时无谓的空转。
  • 循环中调用一个函数处理回写, 该函数需要处理设备任务链表上的所有任务,因此该函数本身也是一个循环体,每次循环取出链表上的一个任务直到链表为空。对于每个任务,调用特定函数处理该任务,在设备链表上的所有任务处理完成以后,我们来检查周期性的回写时机是否已经来到。
  • 接下来我们考虑一个回写任务该如何完成。回写任务以特定数据结构描述,记录任务信息,因此我们需要做的就是根据任务信息去决定回写哪些脏页面。因为每个任务仅仅指定了需要回写的脏页面数以及回写的类型,并没有指定需要回写哪些文件的脏页面,因此,这个决定需要由回写函数来决定,当然,这时候我们就联想到了设备的脏inode链表,自然,我们回写位于该链表上的脏inode。这样,设备脏inode链表便和回写任务联系起来,回写任务指定需要回写多少页面,而这些页面自然就是位于脏inode链表上的文件脏页面。
  • 每一次的回写,我们知道回写任务指定的回写页面数,然后我们从设备脏inode链表中依次取出脏inode,如果可以,将其脏页面进行回写并进行统计判断所需回写页面数是否已经达到任务中的要求,如果达到,返回即可。
  • 对于周期性的回写任务,我们可简化设计,直接构造一个回写任务,指定回写页面数以及回写类型即可,然后便可调用与任务处理相同的接口去进行回写即可,这样既统一又可极大简化工作量。自然,周期性回写的也是位于设备脏inode链表上的文件脏页面。

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()函数来处理本次回写任务。

   

       搞清楚了整体框架,接下来我们需要关注的就是每次的回写任务的处理流程的细节了。 

       真正地弄清楚处理回写任务之前,我们尚需弄清楚如下几个问题:

  1. 如何区分周期性回写构造的任务和其他的任务,周期性回写任务回写的页面哪些范围(字节为单位)和其他任务不同,周期性回写任务的回写范围应该是承接上一次任务,而其余任务的回写页面范围应该是从0到文件大小;
  2. 回写哪些脏inode,我们前面说过,每个设备的脏inode链表上保存的脏inode可能来自不同的文件系统。我们在回写的时候是否需要区分来自不同的文件系统的inode呢?每次回写是否只回写属于某个文件系统的inode呢还是不加区分?对于这个问题,每个任务的数据结构中均有一个成员记录超级块信息,如果该成员被设置,则表明本次回写任务只回写属于该文件系统的inode,否则不区分到底写哪个文件系统的脏inode。如周期性回写的任务可能并不会指定回写属于哪个文件系统的脏页面,而其他的任务可能会指定回写哪个文件系统的脏inode
  3. 还有一个很重要的问题就是活锁问题:我们要刷新脏链表上的inode脏页面,一方面,回写线程不停地回写,但上层可能源源不断地产生脏inode,这样就导致了回写线程不停陷入工作状态,而在回写工作状态时可能占用某些锁,如果久久不能释放这些锁,可能导致其余的需要该锁的进程无法响应,出现所谓的"活锁"。因此,为了避免出现该问题,我们可设计如下一个解决方案:开始回写时记录一个开始时间,接下来的回写我们只回写在该时间之前被弄脏的inode,这就有效地避免了活锁问题;
  4. 真的一个脏inode链表就足够了吗?也许系统负载很轻的时候足够回答是肯定的,但假如系统负载很重,很多任务都在向某个设备上写文件,不可避免地时时刻刻地需要向脏链表中添加脏inode,而回写线程也要不停地轮询该链表,脏inode链表就成了临界资源,不停地被加锁,解锁,如果某一方占用的时间过长,极有可能是回写线程,那么别的进程就必须等待,等待,等待。。。面对这个问题,怎么办?简单,将一个脏链表拆分为二,外界依然可见的是脏inode链表,而回写线程只回写另外一个链表,暂时称作IO链表上的脏inode。在某个时候,比如开始回写的时候我们将脏inode链表上的某些脏inode搬到IO链表,这样极大减轻了脏inode链表的负担。在Linux内核中,除了上述两个链表外,还设计了第三个链表,成为more_io链表,让我们来思考下为什么会产生这样一个链表:回写IO链表上的脏inode的时候,某些脏inode由于被其他进程占用而可能不能立即被回写,此时我们当然无法等待它变得可用再去回写,于是,将其添加到more_io链表中,在某一个时刻再将这些more_io链表上的脏inode转移到IO链表中,便又可得到处理了,相当优美高效,同时它避免了只有两个链表存在的一个效率问题:dirty链表上的脏inode是按照弄脏时间排序的,而如果一个inode无法得到回写如果直接将其放入dirty链表上,势必要查找其应该的插入位置,效率低下,而对于more_io链表,因为我们从io链表上取出脏inode的时候也是按照时间先后去取的,因此,我们的处理也必然是有先后秩序的,因此,当前的脏inode若无法处理,那么只需将其插入到more_io链表的头部就可以保证more_io链表上的脏inode依然在弄脏时间上有序,相比两个链表的做法,效率高出很多。

       弄清楚了上面4点问题,整个回写框架也就呼之欲出,主要实现位于wb_writeback()函数中,函数流程如下图所示:

 

    该函数每次处理一个任务(work),在实现的时候,会将每个任务分解成多个子任务,每个子任务完成特定数量的脏页面回写(目前是1024)。通过一个大循环来控制任务的完成进度,代表每个大任务的结构体是struct wb_writeback_work,而代表每个子任务的数据结构是struct writeback_control。为什么要将大任务分解成子任务呢?我想还是效率的原因,因为在回写文件脏页面的时候,会对全局的inode_lock加锁(为什么需要加这把锁?),直到本次回写任务完成,如果对每个任务采取一次性回写的策略,那么这把锁加的时间可能会很长,系统中其他的地方可能也会等着加这把锁,那么就会因为回写而影响了其他地方的使用,因此,将大任务拆分成多个小的任务体现了高效和公平性。

    对于每一个子任务的处理也较为复杂,我们需要考虑每个子任务的完成状况,因为并不是每个子任务都能按照设定正确地写完本次任务。因此,对于没有正确完成的子任务需要作出判断,接下来我们需要仔细描述任务处理的大循环过程:

  1. 大任务是否已经完成,如果是,转步骤9
  2. 构造一个子任务,设置子任务待回写页面数MAX_WRITEBACK_PAGES
  3. 回写子任务中的页面;
  4. 更新大任务中的待回写页面数;
  5. 判断3中子任务回写的页面数,如果子任务中的页面数全部回写,那么转步骤1,继续下一次循环;
  6. 进入步骤6说明本次子任务没有写回预先设置好的页面数,那么判断当前设备是否还有脏页面,如果没有了,转步骤9
  7. 进入步骤7说明本次子任务没有写回预先设置的页面数但设备上还有脏页面,本次子任务写了预先设置的部分脏页面,那么转步骤1,继续下一次循环;
  8. 进入步骤8说明本次子任务没有写回任何的脏页面,那么怎么办?等,在more_io链表上等一个脏inode可以被回写为止,接下来转步骤1
  9. 返回已写回脏页面数。

    回写大大致逻辑就如上面所述,接下来就要看每个子任务的处理流程。

    我们前面说过,每个大任务中有个域用以指示是否回写属于特定文件系统(即特定超级块)的脏页面,对于设置与否会调用不同的函数,我们首先来考察设定了的情况下的处理流程,此时调用的函数为__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链表上,我们同样说过,会在合适的时候将另外两个链表(dirtymore_io链表)中的脏inode转移至io链表,当然并非转移所有的脏inode,而是more_io上所有的脏inodedirty链表上的部分脏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。所做的准备工作如下:

  • 如有必要,转移脏inodeio链表,判断条件同上;
  • io链表上取出最后一个脏inode,因为其一定是最早被弄脏的;
  • 判断取出的inode所在的文件系统此时是否可回写,如果不可,重新将inode添加到more_io链表上;

    比较有意思的是该函数判断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,将其重新加入bdidirty链表中

            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并不属于参数中指定的文件系统,有两种处理办法:1.如果参数中指定必须回写属于某个文件系统的脏inode,那么通过redirty_tail将该inode重新弄脏(redirty_tail()会修改inode弄脏的时间并将其添加到dirty链表的头部),继续下一次循环,2.如果参数中并未指定一定得回写属于某个文件系统的脏inode,那么直接向调用者返回0,让调用者重新选择另外一个inode所属的文件系统,这样做我想是为了保证最早被弄脏的inode一定最先得到处理;
  • 如果inode的状态为I_NEW或者I_WILL_FREE,表明该inode当前是无需回写的,处理办法:调用requeue_io将该inode添加到more_io链表的头部;
  • 如果inode_dirtied_after判断出该inode被弄脏的时间位于本次回写开始之后,处理办法:向调用者返回1,表明本次回写过程可以结束。

除上述三种状况之外,其余的io链表上的脏inode均可进行回写,具体来说调用函数writeback_single_inode,但在回写完成时需要作如下判断:

  • 本次回写中是否忽略了某些页面,可能是由于页面正被locked无法立即回写,如果忽略了,那么必须重新要将该inode弄脏并添加到dirty链表中,调用函数redirty_tail
  • 判断回写控制设定的回写页面是否已全部完成,如果是,那么将wbc->more_io设置为1,并向调用者返回1,表明本次回写可结束;如果尚未全部完成,那么必须得进行下一次循环,在重新循环之前还要判断more_io链表是否为空,如果不为空,设置wbc->more_io=1

上面分析了整个函数的大致逻辑,分析的过程中我们会产生这样几个问题:

  1. 代码中会根据不同的情况将inode添加到dirty链表或者more_io链表,到底何时该将其添加到dirty链表,何时该将其添加到more_io链表?
  2. 代码中有时候会将wbc->more_io设置为1,为何?这个标志位到底是要告诉调用者什么信息?

对于问题1,我们暂时还没有一个较为合理的解答。对于问题2,我是这样考虑的:wbc->more_io是为了告诉上层当前more_io链表中是否还有脏inode尚未处理而设置的,不放让我们看看它的设置时机:1是在本次回写控制中的待回写页面数已全部完成,这时会向调用者返回1,并设置more_io2more_io链表中非空,表明当前仍然有脏inode可被处理。观察更高层调用者的行为,她会判断wbc->more_io标志位,如果为1意味着回写控制中的页面尚未全部完成但more_io被置位1,此时继续下一次循环继续回写,而如果回写控制的页面尚未全部完成同时more_io没被置位,说明底层告诉高层调用者,已无脏inode可回写,直接返回吧。

至于writeback_single_inode已经在我的前一篇博客中有了较为详细的描述,不再赘言。

你可能感兴趣的:(vfs)