linux-kernel 2.6源碼閱讀[7]IO請求處理

一. 概述

本節(jié)開頭還是先簡要回顧一下linux IO棧。當(dāng)對文件進行讀寫時,Page Cache或Direct IO層負(fù)責(zé)產(chǎn)生數(shù)據(jù)的IO請求,此時需要IO的數(shù)據(jù)保存在內(nèi)存中,使用struct bio進行描述,隨后映射層負(fù)責(zé)找到數(shù)據(jù)在磁盤中的位置,將此位置告訴通用層,如圖1.1中所示。


圖7.1 linux IO stack

二. 塊設(shè)備數(shù)據(jù)結(jié)構(gòu)

內(nèi)核需要處理各式各樣的塊設(shè)備,通用塊設(shè)備層抽象出所有塊設(shè)備的公共特征,描述了一種邏輯上的塊設(shè)備。通用層有3個關(guān)鍵的數(shù)據(jù)結(jié)構(gòu),他們之間的關(guān)系如圖7.2所示,1)gendisk表示一個真正的物理磁盤,除了自身設(shè)備號、分區(qū)設(shè)備號等信息外,其中最重要的信息是一個分區(qū)表和一個請求隊列,分區(qū)表是一個hd_struct指針數(shù)組;2)hd_struct描述一個分區(qū)的物理信息,包括起始扇區(qū)和總扇區(qū)數(shù)目,以及一些統(tǒng)計信息;3)block_device描述一個邏輯分區(qū)(或稱為文件卷),每個block_device都與/dev中的一個塊設(shè)備文件對應(yīng),因此不管是物理磁盤還是邏輯分區(qū)都有一個block_device,各邏輯分區(qū)的bd_contains指向物理盤的bd_contains,各分區(qū)的bd_disk字段指向gendisk,各分區(qū)的bd_part字段指向其對應(yīng)的分區(qū)信息hd_struct。這部分內(nèi)容基于一些默認(rèn)的常識:1)同一個物理設(shè)備的邏輯分區(qū)的從設(shè)備號依次排布,并且第一個從設(shè)備號代表物理設(shè)備;2)LBA扇區(qū)編碼隊則中,第一個維度是柱面號,既,一段相鄰的扇區(qū)號位于同一柱面。

圖7.2 通用塊層各數(shù)據(jù)結(jié)構(gòu)之間的關(guān)系

以上數(shù)據(jù)結(jié)構(gòu)與驅(qū)動程序密切相關(guān),它們的建立過程不是我們關(guān)心的重點,我們重點理解如何使用它們。但是有一點需要知道,當(dāng)一個塊設(shè)備文件打開時,open系統(tǒng)調(diào)用默認(rèn)使用blkdev_open函數(shù),在該函數(shù)中調(diào)用rescan_partitions來探測磁盤的分區(qū)情況,并建立圖7.2所示的數(shù)據(jù)結(jié)構(gòu),rescan_partitions創(chuàng)建一個新分區(qū)使用的函數(shù)是add_partition。

三. 處理IO請求

3.1 請求的數(shù)據(jù)結(jié)構(gòu)

磁盤處理讀寫請求的模型是:當(dāng)CPU發(fā)起讀寫請求之后,由DMA負(fù)責(zé)異步完成該請求,一次請求的數(shù)據(jù)在磁盤上必須連續(xù),但在內(nèi)存中卻可以分散到各個頁中,將一個頁中的數(shù)據(jù)作為一個段。用bio_vec結(jié)構(gòu)來表示一個段,該結(jié)構(gòu)包含頁的struct page以及數(shù)據(jù)在頁中的起始和長度,所以每次請求都使用一個bio結(jié)構(gòu)來表示,它記錄了一次請求中內(nèi)存中數(shù)據(jù)在磁盤上的位置,其核心成員包括bi_sector記錄請求的起始扇區(qū);bi_io_vec記錄bio_vec鏈表;bi_flags記錄讀寫命令。

@ /include/linux/boi.h
struct bio {
    sector_t        bi_sector;  //該請求的起始扇區(qū)
    struct bio      *bi_next;   // 同一請求中的bio鏈
    struct block_device *bi_bdev;
    unsigned long       bi_flags;   // status, command, etc
    unsigned long       bi_rw;      //請求的讀寫方向 READ/WRITE,

    unsigned short      bi_vcnt;    //該bio分散在多少頁中
    unsigned short      bi_idx; // current index into bvl_vec 
    struct bio_vec      *bi_io_vec; //bio_vec鏈表,核心結(jié)構(gòu)
    atomic_t        bi_cnt;     /* pin count */
    void            *bi_private;
};

通用層會對上層發(fā)來的請求進行合并和排序,多個相鄰的bio會被合并到一個struct request中,struct request也是向設(shè)備驅(qū)動程序發(fā)送IO請求的單位。上一節(jié)提到,每個gendisk有一個請求鏈表,該設(shè)備的所有未處理請求都被放到該鏈表上,queuelist字段負(fù)責(zé)這個鏈表。nr_sectors表示提交扇區(qū)的總數(shù)目,current_nr_sectors表示當(dāng)前bio的扇區(qū)數(shù)目,hard_sector、 hard_cur_sectors與結(jié)構(gòu)中沒有hard_前綴的對應(yīng)成員語義相同,通常兩組變量的值相同,但在使用RAID時可能會有差別。bio鏈表的鏈表頭記錄在struct bio *bio中,鏈表尾記錄在struct bio *biotail中。最后, 所有等待該數(shù)據(jù)的進程在struct completion *waiting上掛起。

@ /include/linux/blkdev.h
struct request {
    struct list_head queuelist; 
    unsigned long flags;          // 請求的命令
    sector_t sector;              // 下一個要提交的扇區(qū)號
    unsigned long nr_sectors;    //提交扇區(qū)的總數(shù)目
    unsigned int current_nr_sectors; //當(dāng)前bio的扇區(qū)數(shù)目

    struct bio *bio;                      // bio鏈表頭
    struct bio *biotail;                 // bio鏈表尾
    void *elevator_private;         // 用于IO調(diào)度算法
    struct gendisk *rq_disk;       // 請求所屬的gendisk

    int tag;
    char *buffer;
    request_queue_t *q;
    struct request_list *rl;
    struct completion *waiting;  // 等待該數(shù)據(jù)的進程在此掛起
    unsigned int timeout;
};

3.2 請求入隊

在開始后兩節(jié)的討論之前,需要明確幾個概念:1)請求隊列有兩個狀態(tài),阻塞狀態(tài)(plug)下隊列只進不出,只能向隊列提交新的請求,但新的請求不會得到執(zhí)行,排空狀態(tài)(drain)下,隊列只出不進,此時驅(qū)動程序正在處理隊列中已有的請求,但是新的請求不能提交,需要阻塞等待;2)請求是按照邏輯分區(qū)而不是物理磁盤提交的,所以提交入隊分為兩個階段,第一個階段為邏輯分區(qū)中的提交函數(shù),主要檢查請求是否位于該分區(qū)內(nèi),并將邏輯扇區(qū)號轉(zhuǎn)換為物理扇區(qū)號,第二階段為物理設(shè)備的提交函數(shù),負(fù)責(zé)真正的請求提交。
submit_bio負(fù)責(zé)根據(jù)傳遞的bio實例創(chuàng)建一個新請求,它實際上調(diào)用generic_make_request完成核心功能。該函數(shù)首先檢查請求的扇區(qū)是否位于該分區(qū)內(nèi),隨后檢查隊列是否處于排空狀態(tài)(drain),如果是則阻塞請求的提交。最后,將邏輯分區(qū)中的扇區(qū)號轉(zhuǎn)換成物理設(shè)備中的扇區(qū)號,調(diào)用物理設(shè)備的make_request_fn完成真正的提交。

@ /drivers/block/ll_rw_blk.c
void generic_make_request(struct bio *bio)
{
    // ......判斷請求的扇區(qū)是否位于該分區(qū)內(nèi)
    do {
        q = bdev_get_queue(bio->bi_bdev);
end_io:
        bio_endio(bio, bio->bi_size, -EIO);
        break;
        // 如果該隊列處于排空(drain)狀態(tài),則該進程需要阻塞等待
        block_wait_queue_running(q);
        // 將邏輯分區(qū)中的扇區(qū)號轉(zhuǎn)換成物理設(shè)備中的扇區(qū)號
        blk_partition_remap(bio);
        //調(diào)用物理設(shè)備的make_request_fn
        ret = q->make_request_fn(q, bio);
    } while (ret);
}

__make_request函數(shù)負(fù)責(zé)將新的bio請求插入請求隊列中,該函數(shù)首先檢查是否為空,若為空則代表之前的請求已經(jīng)處理完成,將隊列設(shè)為阻塞狀態(tài)接收新的請求。隨后嘗試將新請求合并到原有請求上,如果合并成功就可以準(zhǔn)備退出該函數(shù)。如果合并不超過,則準(zhǔn)備構(gòu)建一個請求,先獲取新請求需要的存儲空間,然后為新請求賦值,最后調(diào)用add_request將新請求添加到調(diào)度隊列中,該功能由調(diào)度算法提供,新請求可能不直接插入調(diào)度隊列,而是先插入調(diào)度算法的私有隊列中,等到該請求可以執(zhí)行時再插入調(diào)度隊列。

@ /drivers/block/ll_rw_blk.c
static int __make_request(request_queue_t *q, struct bio *bio)
{
again:    // 檢查隊列是否為空,若空,將隊列設(shè)為阻塞狀態(tài)
    if (elv_queue_empty(q)) {
        blk_plug_device(q);
        goto get_rq;
    }
    //嘗試將新請求合并到原有請求上
    el_ret = elv_merge(q, &req, bio);
    switch (el_ret) {
        case ELEVATOR_BACK_MERGE:
        case ELEVATOR_FRONT_MERGE:
        case ELEVATOR_NO_MERGE:
    }

get_rq:    // 獲取新請求需要的存儲空間
    if (freereq) {
        req = freereq;
        freereq = NULL;
    } else {
        if ((freereq = get_request(q, rw, GFP_ATOMIC)) == NULL) {
            freereq = get_request_wait(q, rw);
        }
        goto again;
    }
    // 為新請求賦值
    req->hard_sector = req->sector = sector;
    // 將新請求添加到調(diào)度隊列及調(diào)度算法的私有隊列中
    add_request(q, req);
out:
    if (bio_sync(bio))
        __generic_unplug_device(q);
    return 0;
end_io:
    bio_endio(bio, nr_sectors << 9, err);
    return 0;
}

3.3 請求出隊

前文提到,為了請求的重排序與合并,隊列有兩個狀態(tài),阻塞狀態(tài)(plug)和排空狀態(tài)(drain)。當(dāng)隊列處于阻塞狀態(tài)(plug)時,需要定時的切換到排空狀態(tài)(drain)排空請求。有兩個條件可以出發(fā)狀態(tài)切換:1)時間,進入阻塞狀態(tài)時,blk_plug_device函數(shù)會設(shè)置一個時鐘,其默認(rèn)值為3ms,時鐘到時后,隊列將自動切換到排空狀態(tài)(drain)2)已積攢請求的數(shù)量,如果當(dāng)前讀寫請求的數(shù)目達到unplug_thresh指定的閾值,則切換狀態(tài),使等待的請求得到處理。

三. 處理IO請求

IO調(diào)度程序又稱為電梯算法,所有這類算法的目的都是減少磁盤的尋道時間(因為磁盤的尋道是機械運動,速度非常慢),其名稱來源于Linux 0.11中使用的Linus電梯算法,該算法向電梯搭載乘客的順序一樣處理IO請求,該算法可以最大化磁盤的吞吐,但會帶來嚴(yán)重的餓死問題。針對該問題,2.6版本中,在Linus電梯算法的基礎(chǔ)上提出了4中電梯算法。

名稱 描述 備注
deadline (最終期限) Linus電梯算法的改進版,增加了DDL,每個請求在DDL內(nèi)會被響應(yīng)。除調(diào)度隊列外,還有4個輔助隊列,其中讀寫各兩個,分別按FIFO和塊號排序。請求一開始不直接放入調(diào)度隊列中,而是放入輔助隊列中,等到排空請求時,再將輔助隊列中的數(shù)據(jù)移動到調(diào)度隊列并且執(zhí)行。一般情況下,從塊號排序的輔助隊列中選取請求,這種情況下與Linus電梯算法類似,但如果有請求的計時器超時,則從FIFO隊列中取出請求。由于讀請求對性能的影響大,所以讀請求的超時時限較長。 2.5.0成為默認(rèn),5.0被刪除
as(預(yù)測) DDL調(diào)度算法的改進版,在讀操作發(fā)出后等待6ms,期間如果有追加的讀請求,這樣做的好處是,進程在得到一個數(shù)據(jù)塊之后很有可能繼續(xù)請求下一個數(shù)據(jù)塊。 2.6.0成為默認(rèn),2.6.33被刪除
cfq(完全公平) 一共有64個輔助隊列,用PID來hash索引每個進程對應(yīng)隊列,同一給定進程的請求,總是在同一隊列中處理。時間片會分配到每個隊列,內(nèi)核使用一個輪轉(zhuǎn)算法來處理各個隊列。 2.6.18成為默認(rèn),5.0被刪除
noop(不排序) 只合并,不排序。適用于flash等介質(zhì)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容