Linux IO請求處理流程-bio和request

Linux IO請求處理流程-bio和request

來自專欄 Linux I/O

說明

從這裡開始,我們要深入進入每個IO請求內部,探測它的生命軌跡。

數據結構

與塊設備層IO相關的主要數據結構有以下兩個:

struct bio { sector_t bi_sector; struct bio *bi_next; /* request queue link */ struct block_device *bi_bdev; unsigned long bi_flags; /* status,command,etc */ unsigned long bi_rw; unsigned short bi_vcnt; /* how many bio_vecs */ unsigned short bi_idx; unsigned int bi_phys_segments; ...... // bio完成時的回調函數 bio_end_io_t *bi_end_io; void *bi_private; bio_destructor_t *bi_destructor; struct bio_vec bi_inline_vecs[0]; };struct request { struct list_head queuelist; struct call_single_data csd; struct request_queue *q; unsigned int cmd_flags; enum rq_cmd_type_bits cmd_type; unsigned long atomic_flags; ......}

將linux io相關最重要的兩個數據結構列在這裡,不作過多分析,都很簡單。以後需要再仔細分析吧。

提交bio請求

請求的提交是由上層文件系統發起的,文件系統準備好所需讀寫參數(初始化bio),接下來調用Linux塊設備層提供的submit_bio介面提交讀寫請求:

void submit_bio(int rw, struct bio *bio){ bio->bi_rw |= rw; ...... // 根據bio構造request,接下來的下層主要與request打交道 generic_make_request(bio);}void generic_make_request(struct bio *bio){ struct bio_list bio_list_on_stack; if (!generic_make_request_checks(bio)) return; // 保證每個進程同時只有一個線程在執行make_request_fn // 否則在某些情況下有可能出錯 // 但是為什麼這裡不加鎖呢?加入線程A和B同時進來 // 不是就出錯了嘛? if (current->bio_list) { bio_list_add(current->bio_list, bio); return; } bio_list_init(&bio_list_on_stack); current->bio_list = &bio_list_on_stack; do { // bio->bi_bdev指向bio請求所屬塊設備描述符 // 進而可以找到gendisk,找到其request_queue // 最後調用request_queue->make_request_fn方法 // 這個方法被初始化為__make_request struct request_queue *q = bdev_get_queue(bio->bi_bdev); q->make_request_fn(q, bio); bio = bio_list_pop(current->bio_list); } while (bio); current->bio_list = NULL; /* deactivate */}

注意:到這裡開始出現了OO編程思想:調用了request_queue的make_request_fn()方法,我們不作過多糾結,在後面會仔細梳理request_queue內容。但是我們必須知道,對於scsi磁碟,該request_queue的make_request_fn被初始化為__make_request

bio處理-合併

經過上面的處理,每個bio到達了磁碟設備的request_queue,接下來需要對該bio進行深加工,為什麼需要深加工,提高IO效率,誰叫你是巨人的阿喀琉斯之踵呢?

static int __make_request(struct request_queue *q, struct bio *bio){ struct request *req; int el_ret; spin_lock_irq(q->queue_lock); // 嘗試進行請求合併,將bio合併至請求req中 // 返回值el_ret表明了該bio需要合併的方向:前向合併還是後向合併 el_ret = elv_merge(q, &req, bio); switch (el_ret) { // 可後向合併 case ELEVATOR_BACK_MERGE: ...... // 可前向合併 case ELEVATOR_FRONT_MERGE: // 無法做合併 default: } ......}

這裡的關鍵在於將bio合併至已存在request內,所謂的合併指的是該bio所請求的io是否與當前已有request在物理磁碟塊上連續,如果是,無需分配新的request,直接將該請求添加至已有request,這樣一次便可傳輸更多數據,提升IO效率,這其實也是整個IO系統的核心所在。

根據elv_merge()的判斷結果,可能會出現以下三種情況:

1. 可以後向合併:該bio可以合併至某個request的尾部;

2. 可以前向合併:該bio可以合併至某個request的頭部;

3. 無法合併:該bio無法與任何request進行合併。

我們以「後向合併」和「無法合併」為例闡述具體實現邏輯。

後向合併

case ELEVATOR_BACK_MERGE: BUG_ON(!rq_mergeable(req)); // 檢查該req是否由於硬體限制而無法再進行合併 // elv_merge()只能判斷是否可以進行合併 if (!ll_back_merge_fn(q, req, bio)) break; // 執行後向合併,將bio鏈接到req的bio鏈表的尾部 req->biotail->bi_next = bio; req->biotail = bio; req->__data_len += bytes; req->ioprio = ioprio_best(req->ioprio, prio); if (!blk_rq_cpu_valid(req)) req->cpu = bio->bi_comp_cpu; drive_stat_acct(req, 0); // 尚不知道這個函數到底幹啥? // 目前好像只有CFQ演算法用到了 elv_bio_merged(q, req, bio); // 後向合併完成後檢查req是否與下一個req可以繼續合併 // 如果可以則將兩個request再作一次合併,好複雜 if (!attempt_back_merge(q, req)) // 因為做了request合併,可能需要調整request在具體 // 調度演算法中的位置,對於deadline演算法來說,實現是 // deadline_merged_request() // 它對於前向合併過的request,調整了其在RB樹中的位置 elv_merged_request(q, req, el_ret); goto out;

如果某個request可以做後向合併,那麼:

1. 調用ll_back_merge_fn()判斷是否可以真的合併,因為request內的數據大小可能受限於硬體;

2. 將bio添加到request的尾部,因為是後向合併;

3. 判斷合併後的request是否可以與其他的request再做一次合併,調用了attempt_back_merge

4. 因為request做過合併,可能需要調整其在調度演算法中的位置,調用了elv_merged_request,因為不同的調度演算法可能內部實現不同,因此,內部實現其實是一個介面,由具體調度演算法實現。

無法合併

get_rq: rw_flags = bio_data_dir(bio); if (sync) rw_flags |= REQ_SYNC; // 這裡可能會陷入sleep req = get_request_wait(q, rw_flags, bio); // 根據bio初始化req init_request_from_bio(req, bio); spin_lock_irq(q->queue_lock); if (test_bit(QUEUE_FLAG_SAME_COMP, &q->queue_flags) || bio_flagged(bio, BIO_CPU_AFFINE)) req->cpu = blk_cpu_to_group(smp_processor_id()); if (queue_should_plug(q) && elv_queue_empty(q)) blk_plug_device(q); /* insert the request into the elevator */ drive_stat_acct(req, 1); // 將req添加到調度演算法隊列之中 // 這個函數需要處理判斷req是否可以合併至現有req邏輯 // 因為在get_request_wait返回後,這個bio已經可以合併了 __elv_add_request(q, req, where, 0);

無法合併的bio請求的處理邏輯就相對簡單:為bio分配一個request結構,注意:這裡可能會阻塞。接下來初始化該request,將request添加到queue(__elv_add_request)。

需要注意的一點就是:由於在申請request的時候可能會阻塞,在此期間,其他進程提交的bio可能與本次bio在物理位置上連續,因此在__elv_add_request()內必須判斷該request是否可合併,而不僅僅將其添加到request_queue中就完事。

由於前向合併和後向合併的邏輯極其相似,我們就不再此贅述了,感興趣的讀者可自行分析。

總結

在這裡,我們著重分析了上層請求bio是如何被下發並添加到系統的request中。接下來我們要來仔細分析相關request是如何被下發到具體的物理設備執行。


推薦閱讀:

如何使用特殊許可權:setuid、setgid 和 sticky 位
文件系統
十五. 文件系統一(文件系統的基本概念)
文件基礎
如何在 Ubuntu 上使用 ZFS 文件系統

TAG:Linux | 數據存儲技術 | 文件系統 |