Linux塊設備驅動初級教程

前言

研究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_queuemake_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調度算法,每一模塊都是值得我深入仔細研究,真正的挑戰纔剛剛開始。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章