Linux 塊裝置驅動分析(一)
Linux 塊裝置驅動分析(二)
Linux 塊裝置驅動分析(三)
塊IO請求的處理過程
頁高速緩存(page cache)是Linux核心實作磁盤緩存,它主要用來減少對磁盤的I/O操作。具體地講,是通過把磁盤中的資料緩存到實體記憶體中,把對磁盤的通路變為對實體記憶體的通路。
如讀取一個檔案,首先會先檢查你讀的那一部分檔案資料是否在高速緩存中,如果在,則放棄通路磁盤,而直接從記憶體中讀取,如果不在,則去磁盤中讀取。
對于寫操作,應用如果是以非SYNC方式寫的話,寫的資料也隻是進記憶體,然後由核心幫忙在适當的時機回寫進硬碟。
記憶體的頁最終是要轉化為硬碟裡面真實要讀寫的位置的。在Linux裡面,用于描述硬碟裡面要真實操作的位置與page cache頁的映射關系的資料結構是bio,定義如下:
struct bio {
struct bio *bi_next;
struct block_device *bi_bdev; //塊裝置
struct bvec_iter bi_iter; //磁盤位置資訊等
unsigned short bi_vcnt; //bio_vec數組的大小
struct bio_vec *bi_io_vec; //bio_vec數組
......
};
struct bvec_iter {
sector_t bi_sector; //塊I/O操作在磁盤上的起始扇區
unsigned int bi_size; //還沒有傳輸的位元組數
unsigned int bi_idx; //bio_vec數組的目前索引
unsigned int bi_bvec_done; //bi_io_vec[bi_idx]完成的位元組數
}
struct bio_vec {
struct page *bv_page; //該bio_vec對應的page描述符
unsigned int bv_len; //資料大小
unsigned int bv_offset; //資料在頁面中的偏移
};
每個bio對應的硬碟裡面一塊連續的位置 ,可能對應着page cache的多頁,或者一頁,是以它裡面會有一個bio_vec數組。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIiclRnblN2XjlGcjAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL0EkeNFTSU5kMRpHW3BjMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLwQDN2IDMyIjM0EzMwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
檔案系統構造好bio之後,調用通用塊層提供的submit_bio函數,向通用塊層送出I/O請求。bio進入通用塊層後,将會由I/O排程層進行合并等操作,所謂I/O請求合并就是将程序内或者程序間産生的在實體位址上連續的多個IO請求合并成單個IO請求一并處理,進而提升IO請求的處理效率。
比如有bio1(對應磁盤A的第100-101塊,寫操作)和bio2(對應磁盤A的第102-103塊,寫操作),那麼這兩個bio是可以合并為一個request。
submit_bio函數定義如下:
blk_qc_t submit_bio(struct bio *bio)
{
......
//generic_make_request函數會調用request_queue的make_request_fn成員函數,制造請求
return generic_make_request(bio)
}
塊裝置有個請求隊列:
struct gendisk {
......
struct request_queue *queue; //請求隊列
......
};
struct request_queue {
......
struct elevator_queue *elevator; // I/O電梯排程器
......
request_fn_proc *request_fn; //處理request的回調函數
make_request_fn *make_request_fn; //make_request_fn回調函數
......
};
顯然,請求隊列的make_request_fn回調函數是由塊裝置驅動設定的。一般,驅動程式會調用blk_init_queue函數申請并初始化一個請求隊列,其make_request_fn回調函數會設定成blk_queue_bio。我們就分析這個函數:
static blk_qc_t blk_queue_bio(struct request_queue *q, struct bio *bio)
{
struct blk_plug *plug;
int el_ret, where = ELEVATOR_INSERT_SORT;
struct request *req;
unsigned int request_count = 0;
if (!blk_queue_nomerges(q)) {
//嘗試與程序本地的plug隊列裡的request合并
if (blk_attempt_plug_merge(q, bio, &request_count, NULL))
return BLK_QC_T_NONE;
} else
request_count = blk_plug_queued_count(q);
/* request_count儲存着程序本地的plug隊列的request數目 */
spin_lock_irq(q->queue_lock);
/* 不能合并到plug隊列裡的request,則嘗試合并到I/O電梯排程器(request_queue->elevator_queue)
的排程隊列
*/
el_ret = elv_merge(q, &req, bio);
......
get_rq:
//都不能合并的話,産生一個新的request
req = get_request(q, bio->bi_opf, bio, GFP_NOIO);
......
//通過bio初始化這個新的request
init_request_from_bio(req, bio);
......
plug = current->plug;
if (plug) {
if (!request_count)
trace_block_plug(q);
else {
/* 如果程序本地的plug隊列的request數目達到BLK_MAX_REQUEST_COUNT(16)個
則把程序本地的plug隊列的request洩洪到I/O電梯排程器的排程隊列
*/
if (request_count >= BLK_MAX_REQUEST_COUNT) {
blk_flush_plug_list(plug, false); //洩洪
trace_block_plug(q);
}
}
//把新的request插入到程序本地的plug隊列
list_add_tail(&req->queuelist, &plug->list);
blk_account_io_start(req, true);
} else {
......
}
return BLK_QC_T_NONE;
}
來個整體的流程圖:
電梯排序
當各個程序本地的plug list裡面的request滿了,就要洩洪,以排山倒海之勢進入的,不是最終的裝置驅動,而是I/O電梯排程器。
進入排程器進行電梯排程,其實目的有:
- 進一步的合并request
- 把request對硬碟的通路變得順序化
在請求隊列裡有一成員elevator:
struct request_queue {
......
struct elevator_queue *elevator; //IO排程器
......
};
struct elevator_queue
{
struct elevator_type *type;
void *elevator_data;
......
DECLARE_HASHTABLE(hash, ELV_HASH_BITS);
};
struct elevator_type
{
......
struct elevator_ops ops; //排程器的操作集
......
};
struct elevator_ops
{
......
//排程器出口函數,用于将排程器内的IO請求派發給裝置驅動
elevator_dispatch_fn *elevator_dispatch_fn;
//排程器入口函數,用于向排程器添加IO請求
elevator_add_req_fn *elevator_add_req_fn;
......
};
核心提供了3個I/O電梯排程器,Noop、Deadline和CFQ排程器,預設的排程器是CFQ。CFQ排程器為系統内的所有任務配置設定均勻的I/O帶寬, 提供一個公平的工作環境, 在多媒體應用中, 能保證音、 視訊及時從磁盤中讀取資料。
可以通過類似如下的指令, 改變一個裝置的排程器:
echo SCHEDULER > /sys/block/DEVICE/queue/scheduler
接下來就來分析blk_flush_plug_list函數:
void blk_flush_plug_list(struct blk_plug *plug, bool from_schedule)
{
struct request_queue *q;
unsigned long flags;
struct request *rq;
LIST_HEAD(list);
unsigned int depth;
......
list_splice_init(&plug->list, &list);
//對蓄流連結清單中的requset進行排序,以扇區大小進行排序
list_sort(NULL, &list, plug_rq_cmp);
......
local_irq_save(flags);
//循環的取出蓄流連結清單中的requset
while (!list_empty(&list)) {
rq = list_entry_rq(list.next);
list_del_init(&rq->queuelist);
......
//向排程器中添加IO請求
if (rq->cmd_flags & (REQ_PREFLUSH | REQ_FUA))
__elv_add_request(q, rq, ELEVATOR_INSERT_FLUSH);
else
__elv_add_request(q, rq, ELEVATOR_INSERT_SORT_MERGE);
depth++;
}
......
if (q)
//調用request_queue->request_fn函數處理請求
queue_unplugged(q, depth, from_schedule);
local_irq_restore(flags);
}
__elv_add_request:
void __elv_add_request(struct request_queue *q, struct request *rq, int where)
{
rq->q = q;
......
switch (where) {
......
case ELEVATOR_INSERT_SORT_MERGE:
//嘗試與排程隊列中的request合并,不能合并則落入ELEVATOR_INSERT_SORT分支
if (elv_attempt_insert_merge(q, rq))
break;
case ELEVATOR_INSERT_SORT:
......
if (rq_mergeable(rq)) {
elv_rqhash_add(q, rq);
if (!q->last_merge)
q->last_merge = rq;
}
//調用elevator_ops->elevator_add_req_fn函數将新來的request插入到排程隊列合适的位置
q->elevator->type->ops.elevator_add_req_fn(q, rq);
break;
......
}
request_queue的request_fn成員函數由驅動程式提供,該函數的主要工作是,從請求隊列中提取出請求,然後進行硬體上的資料傳輸,傳輸完資料後,調用__blk_end_request_all函數報告請求處理完成。下面給出一個request_fn示例:
static void do_request(struct request_queue *q)
{
struct request *req;
struct bio *bio;
//從request_queue提取request
while ((req = blk_fetch_request(q)) != NULL)
{
if(req->cmd_type != REQ_TYPE_FS) //請求類型不是來自fs
{
printk(KERN_ALERT"Skip non-fs request\n");
__blk_end_request_all(req, -EIO); //報告出錯
break;
}
__rq_for_each_bio(bio, req) //周遊request的bio
ramblk_xfer_bio(bio); //處理單個bio
//報告請求處理完成
__blk_end_request_all(req, 0);
}
}
提取請求的函數為blk_fetch_request:
struct request *blk_fetch_request(struct request_queue *q)
{
struct request *rq;
/* 傳回下一個要處理的請求(由I/O排程器決定),如果沒有請求則傳回NULL
request_queue->queue_head連結清單不空,則從該連結清單取出request,如果空,則會
調用elevator_ops->elevator_dispatch_fn函數,讓I/O排程器派發request到queue_head連結清單
*/
rq = blk_peek_request(q);
if (rq)
blk_start_request(rq); //啟動請求,将請求從請求隊列中移除
return rq;
}
總結下blk_flush_plug_list函數的處理流程: