Linux內核文件一致性之被動一致性

http://tracymacding.blog.163.com/blog/static/212869299201302172851251/



前言

       前一篇博客中我們仔細描述了Linux文件系統的主動一致性,即文件系統對外提供的用於實現文件一致性的接口,應用程序可以調用這些接口同步文件/系統的髒數據和元數據。但誠如前一篇博客中所說,一個成熟的系統不僅應該只有這些由用戶控制的同步方式,系統需要提供一些方式來保證文件數據/元數據的一致性。本篇博客我們就詳細描述Linux內核中這種被動一致性的實現框架以及部分細節。

思考

       所謂被動一致性是指系統後臺存在定期的任務刷新某些文件的髒數據以及元數據。稍加思索知道,這些定期任務可以以內核線程的形式出現,於是,這些後臺線程在設計的時候存在如下問題需要解決:

  1. 需要創建多少個內核線程來完成同步任務,根據何種標準來確定線程數量?多線程採用何種架構,所有線程處於同等地位還是存在一個集中管理線程(類似lighthttp架構)?
  2. 多線程如何處理並行的問題?這個問題其實又和如何確定創建的線程數量息息相關。
  3. 內核線程作爲被動地刷新髒文件,其執行流必然會和主動刷新並行執行,如何設計一個統一的框架來管理這些任務流地執行?

總體框架

        針對上述思考中的各個問題,Linux內核採取瞭如下的解決辦法:

  1. 創建的針對回寫任務的內核線程數由系統中持久存儲設備決定,操作系統中有N個存儲設備,那麼在系統初始化時就會爲其創建N個刷新線程。
  2. 關於多線程的架構問題,Linux內核採取了Lighthttp的做法,即系統中存在一個管理線程和多個刷新線程(每個持久存儲設備對應一個刷新線程)。管理線程監控設備上的髒頁面情況,若設備一段時間內沒有產生髒頁面,就銷燬設備上的刷新線程;若監測到設備上有髒頁面需要回寫且尚未爲該設備創建刷新線程,那麼創建刷新線程處理髒頁面回寫。而刷新線程的任務較爲單調,只負責將設備中的髒頁面回寫至持久存儲設備中。
  3. 刷新線程刷新設備上髒頁面大致設計如下:
  • 每個設備保存髒文件鏈表,保存的是該設備上存儲的髒文件的inode節點。所謂的回寫文件髒頁面即回寫該inode鏈表上的某些文件的髒頁面。
  • 系統中存在多個回寫時機,第一是應用程序主動調用回寫接口(fsyncfdatasync以及sync等),第二管理線程週期性地喚醒設備上的回寫線程進行回寫,第三是某些應用程序/內核任務發現內存不足時要回收部分緩存頁面而事先進行髒頁面回寫,設計一個統一的框架來管理這些回寫任務非常有必要。

     

 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_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已經在我的前一篇博客中有了較爲詳細的描述,不再贅言。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章