最近在研究塊設備驅動的編寫,看了趙磊大牛的《寫一個塊設備驅動》,受益匪淺,雖然能看懂里面說的,但動手寫寫代碼還是能加深理解的,下面實現的ramdisk寫的很簡單,如果有錯誤,歡迎大牛們指正哈!
分配一塊內存區存放ram disk數據
為了簡單,我直接靜態分配了一個大小為16MB的內存區,當然對於編寫驅動來說這個空間有點大。不過就是為了簡單嘛,可以理解。
#define SIMP_BLKDEV_BYTES (16 * 1024 * 1024) unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];
分配一個請求隊列
可以通過函數blk_alloc_queue分配一個默認的請求隊列,用該方法生成的請求對面沒有設置默認的IO調度器。如果調用blk_init_queue函數分配一個請求隊列,會設置默認的IO調度器。因為是編寫ram disk,不需要訪問外部設備,所以不需要使用IO調度器,故使用blk_alloc_queue來分配一個請求隊列。
simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);
設置自己的make_request_fn函數
blk_alloc_queue分配的請求隊列中make_request_fn是沒有被賦值的,這也導致了前面說的不會使用默認的IO調度器,那么我們就必須自己實現這個函數,因為上層代碼向請求隊列發生請求時都是通過這個函數來完成的。因為我們使用內存來模擬塊設備,所以其實連請求隊列都不需要,上面分配它僅僅為了讓上層代碼能夠使用請求隊列中的make_request_fn函數,否則上層代碼會不知道去哪里調用make_request_fn。
對於上層代碼發出的請求,可以直接用make_request_fn函數來完成請求並直接將結果返回給上層的代碼。具體如下:
static void simp_blkdev_make_request(struct request_queue *q, struct bio *bio) { struct bio_vec *bvec; int i; void *dsk_mem; ... dsk_mem = simp_blkdev_data + (bio->bi_sector << 9); /* 遍歷 bio 中所有的 bvec */ bio_for_each_segment(bvec, bio, i) { void *iovec_mem; switch(bio_rw(bio)) { case READ: case READA: /* 讀和預讀都進行同樣的處理 */ /* 用 kmalloc 將請求頁映射到非線性映射區域進行 * 訪問,這種方法主要是為了兼容高端內存, * (bvec->bv_page 可能源於高端內存) */ 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)); bio_endio(bio, -EIO); return ; } dsk_mem += bvec->bv_len; } bio_endio(bio, 0); }
上面代碼之間通過判斷bio中的請求類型來判斷具體的操作。bio_endio是用來返回給調用者make_request_fn執行結果的。有了上面自定義的make_request_fn,我們還要把該函數的地址賦值給請求隊列中的make_request_fn,這樣上層代碼就可以使用我們自定義的這個函數了,這個可以通過函數blk_queue_make_request來完成,該函數的作用就是為請求隊列綁定make_request_fn方法。
從上面的代碼中可以看出,我們直接在make_request_fn中完成了上層代碼的請求,但通常的方法make_request_fn僅僅是更加上層的請求生成request結構,並將該request插入到請求隊列中,再由請求隊列中的request_fn來完成請求隊列中的請求。為什么要把上層的請求先插入到請求隊列中,而不是像我們上面那樣直接處理請求呢?原因是這些請求大多數都是涉及到慢速的磁盤操作,緩存這些請求到請求隊列中有利於合並相鄰的請求和排序請求(IO調度程序要做的事情),這樣有利於減少磁盤尋道的時間,大家都知道磁盤的尋道時間是非常慢的。而在這里由於我們涉及的只是內存操作,所有就沒有必須用這么復雜的機制了,直接像處理字符設備請求那樣,來一個請求就處理一個。
分配一個gendisk
每個塊設備都對應一個gendisk實例,這里也不例外,我們必須把為內存分配一個gendisk結構來把內存模擬為一個塊設備。可以直接調用函數alloc_disk來分配一個gendisk結構。現在的問題是給設備分配一個什么設備號,可以可以隨便選一個呢?顯然是不行的,由於很多的設備號linux已經欲分配給了特定的設備,如果恰好選擇了當前系統正在使用的設備號作為我們的設備號,那很顯然最后會找不到我們的驅動程序,所以我們必須選一個系統一般都不用的設備號,打開linux/include/linux/major.h文件,我們會發現COMPAQ_SMART2_MAJOR到COMPAQ_SMART2_MAJOR7有8個之多的設備號,並且貌似這個設備號很少使用,所以我們就可以選着它了。有了gendisk結構,接下來就是根據我們的需要初始化這個結構了,具體如下:
/* 分配一個 gendisk 結構 */ simp_blkdev_disk = alloc_disk(1); /* 填充 gendisk 主要結構成員 */ 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);
simp_blkdev_disk->disk_name設置磁盤的名字,這里我設置為cc,這個名字最后會作為設備文件名字出現在/dev目錄中,方便我們最后讀寫這個設備文件。
simp_blkdev_disk->major是分配給我們這個虛擬設備的設備號,我選的是COMPAQ_SMART2_MAJOR,當然也可以選擇其他的。simp_blkdev_disk->first_mino表示分配給設備的第一個從設備號,一般都是從0開始。simp_blkdev_disk->fops是根具體設備相關的底層函數集,由於我們用的是內存,所以這個只有簡單的使用下面定義就行:
struct block_device_operations simp_blkdev_fops = { .owner = THIS_MODULE, };
simp_blkdev_disk->queue就是前面創建的請求隊列,set_capacity設置我們虛擬的這個設備的大小,以扇區為單位。
到這里一切都准備就緒了,最后只要使用add_disk函數向內核注冊我們虛擬的塊設備就行了。
測試
為了運行上面寫得ram disk代碼,最好的辦法就是使用模塊了。將上面的代碼封裝到模塊中很簡單,把創建請求隊列和gendisk的代碼都放在模塊初始化代碼中,把釋放請求隊列和gendisk的代碼放在模塊退出代碼中。
static void __exit simp_blkdev_exit(void) { del_gendisk(simp_blkdev_disk); /* 刪除 gendisk 結構 */ put_disk(simp_blkdev_disk); /* 釋放一個該對象的引用 */ blk_cleanup_queue(simp_blkdev_queue); /* 清理請求隊列 */ }
一切都准備就緒了,現在直接編譯模塊,將模塊插入到內核中就可以了。在將模塊插入到內核后,可以查看系統消息:
再查看/dev目錄下,我們可以看到目錄下多了cc這個文件。
好了,現在就相當於我們有了一個塊設備,大小為16MB,現在可以做的事就是格式化這個設備為ext4文件系統。
接下來我們就可以將這個文件系統掛載到某個目錄上進行操作了,我們可以在這個文件系統中創建文件,目錄等。