天天看點

Linux中page、buffer_head、bio的聯系

在Linux Block IO層,說到關鍵資料結構我想可能就隻有标題中描述的三種了。我們今天就來較長的描述這三種資料結構在檔案系統和塊裝置層扮演的角色以及他們之間的聯系。

PAGE

page在核心中被稱為緩存頁,在檔案系統中扮演最核心的角色。Linux使用記憶體緩存檔案資料,而所有的檔案内容都被分割成page然後通過一定方式組織起來,便于查找。

Linux中page、buffer_head、bio的聯系

page大小固定,目前一般為4KB。一個大檔案的緩存可能會占據很多page。

BUFFER_HEAD

buffer_head顧名思義,表示緩沖區頭部。這個緩沖區緩沖的是磁盤等塊裝置資料,而buffer_head則是描述緩沖區的中繼資料。

前面說過,核心以page為機關管理檔案内容,page典型大小為4KB,而一般塊裝置的通路以扇區為機關,扇區的一般大小為512 byte,當然随着技術的進步,這個大小也在不斷提升。而檔案系統最小邏輯可尋址單元稱為塊。塊的大小要比扇區大,但又比頁小,典型大小為1K。 核心執行磁盤的所有操作是按照塊來操作的。

在如此背景之下,便誕生了buffer_head這樣的資料結構,它是核心page與磁盤上實體資料塊之間的橋梁。一方面,每個page包含多個buffer_head(一般4個),另外一方面,buffer_head中又記錄了底層裝置塊号資訊。這樣,通過page->buffer_head->block就能完成資料的讀寫。

page與buffer_head資料結構之間關系如下圖所示:假設page大小為4KB,而檔案系統塊大小為1KB。

Linux中page、buffer_head、bio的聯系

page通過private字段索引該page的第一個buffer_head,而所有的buffer_head通過b_this_page形成一個單循環連結清單;

buffer_head中的b_data指向緩存檔案的塊資料;

buffer_head内還通過b_page指向其所屬的page(圖中未畫出)

由于buffer_head描述的是檔案系統塊緩存,既然緩存,便存在資料一緻性問題:緩存中的資料可能比磁盤資料落後,緩存中的資料也可能比磁盤資料新。是以,需要一種機制來描述這些狀态。buffer_head中需要一系列的狀态位。

enum bh_state_bits {
    BH_Uptodate, 
    BH_Dirty,
    BH_Lock,
    BH_Req,
    BH_Uptodate_Lock,
    BH_Mapped,
    BH_New,
    BH_Async_Read,
    BH_Async_Write,
    BH_Delay,
    BH_Boundary,
    BH_Write_EIO,
    BH_Unwritten,
    BH_Quiet, 
    BH_Meta,
    BH_Prio,
    BH_Defer_Completion, 
    BH_PrivateStart,
}
           

其中:

BH_Uptodate: 表示緩存資料與磁盤資料一緻

BH_Dirty: 表示緩存資料被更新,有待同步至磁盤上。

這些狀态何時會被更新呢?

以BH_Uptodate為例,當檔案系統向塊裝置層發起一次讀請求時,會注冊一個完成時的回調函數end_buffer_read_sync,在IO層傳回讀取結果後,該函數内根據讀取結果:

  • 如果讀取成功,則說明buffer_head緩沖區中的資料與磁盤上一緻,設定BH_Update。
  • 如果讀取失敗,說明buffer_head緩沖區的資料處于一種不一緻狀态,此時,需要清除buffer_head的BH_Update。
static void __end_buffer_read_notouch(struct buffer_head *bh, int uptodate)
{
    if (uptodate) {
        set_buffer_uptodate(bh);
    } else {
        clear_buffer_uptodate(bh);
    }
    unlock_buffer(bh);
}

void end_buffer_read_sync(struct    
    buffer_head *bh, int uptodate)
{
    __end_buffer_read_notouch(bh, uptodate);
    put_bh(bh);
}
           

BIO

        在 Linux 2.6 版本以前,buffer_head 是 kernel 中非常重要的資料結構,它曾經是 kernel 中 I/O 的基本機關(現在已經是 bio 結構),它曾被用于為一個塊映射一個頁,它被用于描述磁盤塊到實體頁的映射關系,所有的 block I/O 操作也包含在 buffer_head 中。但是這樣也會引起比較大的問題:buffer_head 結構過大(現在已經縮減了很多),用 buffer head 來操作 I/O 資料太複雜,kernel 更喜歡根據 page 來工作(這樣性能也更好);另一個問題是一個大的 buffer_head 常被用來描述單獨的 buffer,而且 buffer 還很可能比一個頁還小,這樣就會造成效率低下;第三個問題是 buffer_head 隻能描述一個 buffer,這樣大塊的 I/O 操作常被分散為很多個 buffer_head,這樣會增加額外占用的空間。是以 2.6 開始的 kernel (實際 2.5 測試版的 kernel 中已經開始引入)使用 bio 結構直接處理 page 和位址空間,而不是 buffer。

        是以,目前的核心在向塊裝置層送出讀寫請求時,都會将buffer_head封裝在bio結構中,而不再使用原來的buffer_head,例如下面這段代碼是ext2檔案系統向磁盤寫資料的實作:

static int submit_bh_wbc(int rw, struct buffer_head *bh,                         
unsigned long bio_flags, struct writeback_control *wbc)
{
    struct bio *bio;           

    if (test_set_buffer_req(bh) && (rw & WRITE))                 
        clear_buffer_write_io_error(bh);

    bio = bio_alloc(GFP_NOIO, ); 
    if (wbc) {                 
        wbc_init_bio(wbc, bio);
        wbc_account_io(wbc, bh->b_page, 
            bh->b_size);
    }

    bio->bi_iter.bi_sector = bh-
        >b_blocknr * (bh->b_size >> );
    bio->bi_bdev = bh->b_bdev; 

    bio_add_page(bio, bh->b_page, bh-
        >b_size, bh_offset(bh));

    bio->bi_end_io = end_bio_bh_io_sync;
    bio->bi_private = bh;
    bio->bi_flags |= bio_flags;
    ......
}
           

繼續閱讀