前言
研究IO也很久了,一直無法串聯bio和塊設備驅動,只知道bio經過IO調度算法傳遞到塊設備驅動,怎麼過去的,IO調度算法在哪裏發揮作用,一直沒有完全搞明白,查看了很多資料,終於對塊設備驅動有所理解,也打通了bio到塊設備。
一、傳統塊設備
我們先來實現一個基於內存的傳統塊設備驅動。
1.1 初始化一些東西
//暫時使用COMPAQ_SMART2_MAJOR作爲主設備號,防止設備號衝突
#define SIMP_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR
//塊設備名
#define SIMP_BLKDEV_DISKNAME "simp_blkdev"
//用一個數組來模擬一個物理存儲
#define SIMP_BLKDEV_BYTES (16*1024*1024)
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];
static struct request_queue *simp_blkdev_queue;//請求隊列
static struct gendisk *simp_blkdev_disk;//塊設備
struct block_device_operations simp_blkdev_fops = {//塊設備的操作函數
.owner = THIS_MODULE,
};
1.2 加載驅動
整個過程
1.創建request_queue(每個塊設備一個隊列),綁定函數simp_blkdev_do_request
2.創建一個gendisk(每個塊設備就是一個gendisk)
3.將request_queue和gendisk綁定
4.註冊gendisk
static int __init simp_blkdev_init(void)
{
int ret;
//初始化請求隊列
simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);//這個方法將會在1.5仔細分析
simp_blkdev_disk = alloc_disk(1);//申請simp_blkdev_disk
//初始化simp_blkdev_disk
strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME);//設備名
simp_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR;//主設備號
simp_blkdev_disk->first_minor = 0;//副設備號
simp_blkdev_disk->fops = &simp_blkdev_fops;//塊設備操作函數指針
simp_blkdev_disk->queue = simp_blkdev_queue;
//設置塊設備的大小,大小是扇區的數量,一個扇區是512B
set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9);
add_disk(simp_blkdev_disk);//註冊simp_blkdev_disk
return 0;
}
1.3 simp_blkdev_do_request
1.調用調度算法的elv_next_request方法獲得下一個處理的request
2.如果是讀,將simp_blkdev_data拷貝到request.buffer,
3.如果是寫,將request.buffer拷貝到simp_blkdev_data
4.調用end_request通知完成
static void simp_blkdev_do_request(struct request_queue *q)
{
struct request *req;
while ((req = elv_next_request(q)) != NULL) {//根據調度算法獲得下一個request
switch (rq_data_dir(req)) {//判斷讀還是寫
case READ:
memcpy(req->buffer, simp_blkdev_data + (req->sector << 9),
req->current_nr_sectors << 9);
end_request(req, 1);//完成通知
break;
case WRITE:
memcpy(simp_blkdev_data + (req->sector << 9),req->buffer,
req->current_nr_sectors << 9);
end_request(req, 1);//完成通知
break;
default:
/* No default because rq_data_dir(req) is 1 bit */
break;
}
}
1.4 卸載驅動
static void __exit simp_blkdev_exit(void)
{
del_gendisk(simp_blkdev_disk);//註銷simp_blkdev_disk
put_disk(simp_blkdev_disk);//釋放simp_blkdev_disk
blk_cleanup_queue(simp_blkdev_queue);//釋放請求隊列
}
千萬別忘記下面代碼
module_init(simp_blkdev_init);
module_exit(simp_blkdev_exit);
1.5 blk_init_queue
看了上面的代碼,可能還是無法清晰的瞭解request_queue如何串聯bio和塊設備驅動,我們深入看一下
simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);//調用blk_init_queue
struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
{
return blk_init_queue_node(rfn, lock, NUMA_NO_NODE);//跳轉1.5.1
}
EXPORT_SYMBOL(blk_init_queue);
//1.5.1
struct request_queue *
blk_init_queue_node(request_fn_proc *rfn, spinlock_t *lock, int node_id)
{
struct request_queue *q;
q = blk_alloc_queue_node(GFP_KERNEL, node_id, lock);
if (!q)
return NULL;
q->request_fn = rfn;//也就是simp_blkdev_do_request
if (blk_init_allocated_queue(q) < 0) {//轉1.5.2
blk_cleanup_queue(q);
return NULL;
}
return q;
}
EXPORT_SYMBOL(blk_init_queue_node);
//1.5.2
int blk_init_allocated_queue(struct request_queue *q)
{
...
blk_queue_make_request(q, blk_queue_bio);//轉1.5.3
if (elevator_init(q))//初始化IO調度算法
goto out_exit_flush_rq;
return 0;
...
}
EXPORT_SYMBOL(blk_init_allocated_queue);
//1.5.3
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
{
...
q->make_request_fn = mfn;//mfn也就是blk_queue_bio
...
}
EXPORT_SYMBOL(blk_queue_make_request);
static blk_qc_t blk_queue_bio(struct request_queue *q, struct bio *bio)//完成bio如何插入到request_queue
{
//IO調度算法發揮作用的地方
}
整個調用完成之後,會綁定當前塊設備的request_queue兩個重要方法
q->make_request_fn = blk_queue_bio;//linux默認實現
q->request_fn = simp_blkdev_do_request;//驅動自己實現
1.5.1 make_request_fn(struct request_queue *q, struct bio *bio)
submit_bio會調用make_request_fn將bio封裝成request插入到request_queue,默認會使用linux系統實現的blk_queue_bio。如果我們替換make_request_fn,會導致IO調度算法失效,一般不會去改。
1.5.2 request_fn(struct request_queue *q)
這個方法一般是驅動實現,也就是simp_blkdev_do_request,從request_queue中取出合適的request進行處理,一般會調用調度算法的elv_next_request方法,獲得一個推薦的request。
1.5.3 bio-塊設備
通過make_request_fn和request_fn,我們將bio和塊設備驅動串聯起來了。
而且IO調度算法會在這兩個函數發揮作用。
給自己挖了兩個坑
1.整個過程中受到了IO調度算法,IO調度算法如何發揮作用?
2.make_request_fn之後如何觸發request_fn?
二、超高速塊設備
傳統塊設備訪問是通過磁頭,IO調度算法可以優化多個IO請求的時候移動磁頭的順序。
IO調度算法
假如你是圖書管理員,十個人找你借十本書,在圖書館的不同角落,你肯定會選擇一條最短的線路去拿這十本書。其實這就是IO調度算法
超高速塊設備
假如這個圖書館只有一個窗口,借書的人只要說出書名,書就會從窗口飛出來,這樣子還需要什麼管理員,更不需要什麼IO調度算法,這個圖書館就是超高速塊設備。
上面寫的基於內存的塊設備不就是一個超高速塊設備嘛,我們能不能寫一個沒有中間商的驅動
2.1 simp_blkdev_init
我們需要重寫一下init代碼,不調用blk_init_queue。直接用下面的2.1.1和2.1.2的方法。
init之後,我們會將make_request_fn設置成simp_blkdev_make_request
static int __init simp_blkdev_init(void)
{
int ret;
//初始化請求隊列
simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);//2.1.1
//將simp_blkdev_make_request綁定到request_queue的make_request_fn。
blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);//2.1.2
simp_blkdev_disk = alloc_disk(1);//申請simp_blkdev_disk
//初始化simp_blkdev_disk
strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME);//設備名
simp_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR;//設備號
simp_blkdev_disk->first_minor = 0;
simp_blkdev_disk->fops = &simp_blkdev_fops;//塊設備操作函數指針
simp_blkdev_disk->queue = simp_blkdev_queue;
set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9);//設置塊設備的大小,大小是扇區的數量,一個扇區是512B
add_disk(simp_blkdev_disk);//註冊simp_blkdev_disk
return 0;
err_alloc_disk:
blk_cleanup_queue(simp_blkdev_queue);
err_alloc_queue:
return ret;
}
2.2 simp_blkdev_make_request
跳過中間商,直接將simp_blkdev_data拷貝到bio的page,調用bio_endio通知讀寫完成,
從頭到尾request_queue和request就沒有用到
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio) {
struct bio_vec *bvec;
int i;
void *dsk_mem;
//獲得塊設備內存的起始地址,bi_sector代表起始扇區
dsk_mem = simp_blkdev_data + (bio->bi_sector << 9);
bio_for_each_segment(bvec, bio, i) {//遍歷每一個塊
void *iovec_mem;
switch (bio_rw(bio)) {
case READ:
case READA:
//page代表高端內存無法直接訪問,需要通過kmap映射到線性地址
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;//頁數加偏移量獲得對應的內存地址
memcpy(iovec_mem, dsk_mem, bvec->bv_len);//將數據拷貝到內存中
kunmap(bvec->bv_page);//歸還線性地址
break;
case WRITE:
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(dsk_mem, iovec_mem, bvec->bv_len);
kunmap(bvec->bv_page);
break;
default:
printk(KERN_ERR SIMP_BLKDEV_DISKNAME": unknown value of bio_rw: %lu\n", bio_rw(bio));
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);//報錯
#else
bio_endio(bio, -EIO);//報錯
#endif
return 0;
}
dsk_mem += bvec->bv_len;//移動地址
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, bio->bi_size, 0);
#else
bio_endio(bio, 0);
#endif
return 0;
}
2.2 沒有中間商
因爲我們直接把數據的訪問實現在make_request_fn,也就是simp_blkdev_make_request。
這樣子就擺脫了request_queue和IO調度算法。沒有中間商,訪問速度槓槓的。
kernel中的zram設備就是基於內存沒有中間商賺差價的塊設備,代碼很類似,有興趣的可以看一下。
三、總結
經過那麼長時間的學習,捅破層層的窗戶紙,終於把IO打通了,但是文件系統,IO調度算法,每一模塊都是值得我深入仔細研究,真正的挑戰纔剛剛開始。