Linux文件系統從磁盤讀頁面

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


1. 引言
在我前面的博客中詳細分析了Linux頁面緩存的實現機制,包括各種數據結構以及之間的關聯。本篇專欄中我們將會詳細討論文件系統如何從磁盤上讀出一個頁面。
我們知道,文件系統以頁面(page,默認大小4096字節)爲單位緩存文件數據,而早期的Linux中是以buffer head結構組織文件緩存的。每個buffer head數據大小與文件系統塊大小相同,在當前版本操作系統中,page和buffer_head的關係如下圖描述(圖例中的頁幁大小爲4096字節,buffer_head數據大小爲1024字節):
Linux文件系統從磁盤讀頁面 - tracymacding - tracymacding的博客
圖1 page與buffer head關係圖

因爲Linux使用內存緩存文件數據,每次應用程序讀寫文件時首先必然在內存中進行,讀時會首先從內存中查找當前讀頁面是否存在,若頁面不存在或者當前頁面的數據並非出於uptodata狀態,那麼VFS必須啓動一次讀頁面流程。
若文件系統處理流程檢測當前讀頁面不存在(尚未緩存)或者頁面狀態與磁盤不一致,此時需要從磁盤上讀出頁面內容。具體來說,調用具體文件系統的struct address_space_operations中的readpage方法,對於ext2文件系統來說,該方法被實例化爲ext2_readpage,而其又是mpage_readpage的封裝。mpage_readpage對於頁面的讀出會根據頁面中的塊在磁盤中是否連續而不同。具體來說,如果一個頁面中的buffer_head對應的磁盤塊在磁盤上連續,那麼其實該page是無需創建buffer_head與其相關聯,只有當頁面中保存的磁盤塊物理位置不連續,此時才需要創建多個buffer_head並在buffer_head結構中記錄每一個邏輯塊在磁盤中的物理塊號。
mpage_readpage調用do_mpage_readpage完成具體的讀頁面工作。函數的參數一爲需讀出的頁面信息,參數二爲文件邏輯塊至物理磁盤塊的映射方法,因具體文件系統而異。

int mpage_readpage(struct page *page, get_block_t get_block)
{
struct bio *bio = NULL;
sector_t last_block_in_bio = 0;
struct buffer_head map_bh;
unsigned long first_logical_block = 0;

map_bh.b_state = 0;
map_bh.b_size = 0;
bio = do_mpage_readpage(bio, page, 1, &last_block_in_bio,&map_bh, &first_logical_block, get_block);
if (bio)
mpage_bio_submit(READ, bio);
return 0;
}

struct bio是文件系統與底層IO子系統連接件, 文件系統讀/寫頁面其實就是向底層IO子系統發送struct bio請求。關於IO子系統會在別的章節中討論,此處略過。mpage_readpage向調用者傳入了多個參數,“1”表示僅讀入一個頁面。

static struct bio * do_mpage_readpage(struct bio *bio, struct page *page, unsigned nr_pages,
sector_t *last_block_in_bio, struct buffer_head *map_bh,
unsigned long *first_logical_block, get_block_t get_block)
{
struct inode *inode = page->mapping->host;
const unsigned blkbits = inode->i_blkbits;
const unsigned blocks_per_page = PAGE_CACHE_SIZE >> blkbits;
const unsigned blocksize = 1 << blkbits;
sector_t block_in_file;
sector_t last_block;
sector_t last_block_in_file;
sector_t blocks[MAX_BUF_PER_PAGE];
unsigned page_block;
unsigned first_hole = blocks_per_page;
struct block_device *bdev = NULL;
int length;
int fully_mapped = 1;
unsigned nblocks;
unsigned relative_block;

if (page_has_buffers(page))
goto confused;
......
confused:
if (bio)
bio = mpage_bio_submit(READ, bio);
if (!PageUptodate(page))
       block_read_full_page(page, get_block);
else
unlock_page(page);
goto out;
}

該函數的處理流程是異常複雜的,但我們這裏僅僅關注該page已經與buffer_head關聯過的場景,當函數檢測到該條件成立時,轉入confused分支處理。
該分支中檢測page是否是“Uptodata”狀態,如果已經是Uptodata,直接解鎖頁面,那些等待在該頁面的進程可以被喚醒,如果否,那麼只能乖乖地調用block_read_full_page()從磁盤上讀出頁面數據。
這裏需要注意的是,page不處於Uptodata並不意味着page中的所有block都是非Uptodata,可能是隻有某些block是非Uptodata,而部分是Uptodata,因此,block_read_full_page只需讀出那麼非Uptodata的block即可,千萬不可被函數名欺騙了雙眼。

int block_read_full_page(struct page *page, get_block_t *get_block)
{
struct inode *inode = page->mapping->host;
sector_t iblock, lblock;
struct buffer_head *bh, *head, *arr[MAX_BUF_PER_PAGE];
unsigned int blocksize;
int nr, i;
int fully_mapped = 1;

BUG_ON(!PageLocked(page));
blocksize = 1 << inode->i_blkbits;
if (!page_has_buffers(page))
create_empty_buffers(page, blocksize, 0);
head = page_buffers(page);

//block number of a page
iblock = (sector_t)page->index << (PAGE_CACHE_SHIFT - inode->i_blkbits);
//last block number of this file
lblock = (i_size_read(inode)+blocksize-1) >> inode->i_blkbits;
bh = head;
nr = 0;
i = 0;

do {
if (buffer_uptodate(bh))
continue;

if (!buffer_mapped(bh)) {
int err = 0;

fully_mapped = 0;
//current block doesn't exceed file end
if (iblock < lblock) {
WARN_ON(bh->b_size != blocksize);
err = get_block(inode, iblock, bh, 0);
if (err)
SetPageError(page);
}
if (!buffer_mapped(bh)) {
zero_user(page, i * blocksize, blocksize);
if (!err)
set_buffer_uptodate(bh);
continue;
}
/*
* get_block() might have updated the buffer,some file system may do like this
* synchronously
*/
if (buffer_uptodate(bh))
continue;
}
arr[nr++] = bh;
} while (i++, iblock++, (bh = bh->b_this_page) != head);

if (fully_mapped)
SetPageMappedToDisk(page);

if (!nr) {
/*
* All buffers are uptodate - we can set the page uptodate
* as well. But not if get_block() returned an error.
*/
if (!PageError(page))
SetPageUptodate(page);
unlock_page(page);
return 0;
}

/* Stage two: lock the buffers */
for (i = 0; i < nr; i++) {
bh = arr[i];
lock_buffer(bh);
mark_buffer_async_read(bh);
}

/*
* Stage 3: start the IO.  Check for uptodateness
* inside the buffer lock in case another process reading
* the underlying blockdev brought it uptodate (the sct fix).
*/
for (i = 0; i < nr; i++) {
bh = arr[i];
if (buffer_uptodate(bh))
end_buffer_async_read(bh, 1);
else
submit_bh(READ, bh);
}
return 0;
}
這個函數讀頁面的邏輯是極其清晰的:
1. 蒐集判斷該頁面中有多少個block處於非Uptodata狀態,只需判斷buffer_head的標誌位即可,對於非Uptodata狀態的block還需要判斷該block是否已映射,即該邏輯塊是否已經與物理磁盤塊建立了映射關係,如果沒有,那麼說明可能別的進程解除了該block的映射關係(誰?爲什麼會解除一個頁面的某些block的映射關係?),此時需要作一個簡單判斷,如果當前讀的位置並沒有超過文件末尾,那麼,建立映射,否則,將內存block填充0,並設置其爲Uptodata狀態,至於爲什麼會讀超過文件末尾的位置,我想這是因爲,可能有別的進程在本程序讀之前刪除了文件的部分數據,導致文件變小,而讀進程並未感知這種變化。映射完成以後,還需要判斷該block是否處於Uptodata狀態,這是因爲某些文件系統可能在映射期間讀出塊數據。
2.檢查完成頁面中的所有塊狀態以後,如果某些塊處於非Uptodata狀態,那麼需要鎖住這些block(lock_buffer()),並設置讀完成以後的回調函數mark_buffer_async_read().
3.一切就緒後,向底層IO子系統提交每一個待讀出的buffer_head。
在上面的步驟2中,首先鎖定每個非Uptodata的buffer_head,即對每個buffer_head設置一個BH_Locked標誌位,此時其他進程無法再操作該buffer_head對應的塊直到其被解鎖。鎖定以後,調用函數mark_buffer_async_read()來設置讀完成以後的回調函數。
static void mark_buffer_async_read(struct buffer_head *bh)
{
bh->b_end_io = end_buffer_async_read;
set_buffer_async_read(bh);
}
可以看到在改函數中不僅設置了bh完成時的回調函數end_buffer_async_read,同時調用了set_buffer_async_read()爲該buffer_head設置一個標記BH_Async_Read,表示該buffer_head目前正處於讀過程中,那麼,爲什麼需要設置這樣一個標誌位?
要弄清楚這個問題,我們就必須深入到buffer_head完成時的回調函數end_buffer_async_read中:

static void end_buffer_async_read(struct buffer_head *bh, int uptodate)
{
unsigned long flags;
struct buffer_head *first;
struct buffer_head *tmp;
struct page *page;
int page_uptodate = 1;

BUG_ON(!buffer_async_read(bh));

page = bh->b_page;
if (uptodate) {
set_buffer_uptodate(bh);
} else {
clear_buffer_uptodate(bh);
if (!quiet_error(bh))
buffer_io_error(bh);
SetPageError(page);
}

/*
* Be _very_ careful from here on. Bad things can happen if
* two buffer heads end IO at almost the same time and both
* decide that the page is now completely done.
*/
//serialize page's buffer_head's callback function
first = page_buffers(page);
local_irq_save(flags);
bit_spin_lock(BH_Uptodate_Lock, &first->b_state);
clear_buffer_async_read(bh);
//now the buffer is Uptodata,we can unlock it
unlock_buffer(bh);
tmp = bh;
do {
if (!buffer_uptodate(tmp))
page_uptodate = 0;
if (buffer_async_read(tmp)) {
BUG_ON(!buffer_locked(tmp));
goto still_busy;
}
tmp = tmp->b_this_page;
} while (tmp != bh);
bit_spin_unlock(BH_Uptodate_Lock, &first->b_state);
local_irq_restore(flags);

/*
* If none of the buffers had errors and they are all
* uptodate then we can set the page uptodate.
*/
if (page_uptodate && !PageError(page))
SetPageUptodate(page);
unlock_page(page);
return;

still_busy:
bit_spin_unlock(BH_Uptodate_Lock, &first->b_state);
local_irq_restore(flags);
return;
}
當某個block被讀出後,最終會進入該完成函數,傳入的uptodata表明本次讀是否正確,如果讀正確,那麼設置該buffer_head狀態爲BH_Uptodata,然後進入一個串行化處理流程,之所以要串行化是爲了以下考慮:
假如pageX的兩個頁面A和B都被提交至IO子系統進行讀,當他們完成以後都會進入該完成函數,在函數中會檢查頁面中的其他block狀態,因此,需要串行化,避免多個函數同時處理page這個臨界資源。
明白了這個問題,我們上述的問題也就比較容易理解了,每個block在讀完成以後都會在完成函數中判斷block所在頁面其餘的block狀態,閱讀上述代碼可知,完成函數的處理流程是首先將buffer_head的Uptodata置位,然後獲取串行處理的鎖,在獲取鎖之後解鎖buffer_head並清除BH_Async_Read標誌。現在我們來考慮如果不設置BH_Async_Read的處理流程:
如果沒有BH_Async_Read這類標誌,那麼每個buffer_head唯一設置的標誌位就是BH_Locked。那完成函數的處理流程可如下圖描述:
1. 設置buffer_head BH_Uptodata;
2. 獲取串行化鎖;
3. 對buffer_head解鎖;
4. 遍歷buffer_head所屬page的所有block,檢查其是否鎖定,若鎖定跳轉至步驟6;
5. 根據上述遍歷結果決定是否將page設置爲PG_Uptodata,解鎖page;
6. 釋放串行化鎖。

對於pageX中的塊A和B,A和B在讀之前都被加了鎖,即設置了BH_Locked,同時pagex也被加鎖,設置標誌位PG_Locked,此時假如A讀完成,進入完成函數,函數中首先設置A狀態爲BH_Uptodata,然後獲取串行鎖,假設獲取成功,完成函數中遍歷pageX其他的block,發現B被鎖定,那麼A就認定此時B可能正被鎖定讀寫,因此,根據上述流程,此時尚不能解鎖頁面pageX,但A的BH_Locked已被清除。
假如此時某個線程T鎖定了A,而正好B讀完成進入完成函數end_buffer_async_read(),它完成固定工作後獲取串行鎖遍歷pageX所有block,發現A此時被鎖定,那麼直接釋放串行鎖並返回。
此時便產生問題了,被加在pageX上的鎖PG_Locked永遠沒有機會得到釋放,形成死鎖。
因此,問題出現在一個BH_Locked並不能滿足buffer_head的所有應用情況,需要爲其增加一個標誌位來標誌是否正在進行Read/Write。因此,添加BH_Async_Read便可解決上述問題,此時處理流程變爲:
1. 設置buffer_head BH_Uptodata;
2. 獲取串行化鎖;
3. 清除buffer_head的BH_Async_Read標誌;
4. 對buffer_head解鎖;
5. 遍歷buffer_head所屬page的所有block,檢查其標誌位BH_Async_Read是否設置,若鎖定跳轉至步驟7,此時頁面尚有塊正在進行IO,不可解鎖;
6. 根據上述遍歷結果決定是否將page設置爲PG_Uptodata,解鎖page;
7. 釋放串行化鎖。


發佈了4 篇原創文章 · 獲贊 4 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章