Linux塊裝置驅動
推薦書:《Linux核心原始碼情景分析》
1.字元裝置驅動和使用中等待某一事件的方法
①查詢方式
②休眠喚醒,但是這種沒有超時時間
③poll機制,在休眠喚醒基礎上加一個超時時間
④非同步通知,非同步通知實際上就是發訊號
⑤輸入子系統,這樣比較通用
2.塊裝置相對於字元裝置驅動邏輯的變化
①對於硬碟對讀寫的優化
假如要讀磁頭0的扇區0,然後寫磁頭1的扇區0,然後讀磁頭0的扇區1,若像字元裝置那樣,就會機械山跳轉2次,效率低。
優化:
先不執行,放入佇列,優化後再執行,這裡的優化是指調整順序。
②對於flash
假如要寫同一個塊的扇區0,然後再寫扇區1,若是字元裝置的做法,寫扇區0時需要先把整塊讀取出來,然後修改此塊中
扇區0的資料,然後燒寫整個這個塊。寫扇區1時也是需要先把整塊讀取出來,然後修改此塊中扇區1的資料,然後燒寫整個這個塊。
優化:
先不執行,放入佇列,優化後再執行,這裡的優化是指合併相同塊的寫請求。
所以塊裝置不能向字元裝置一樣直接提供讀寫函式,而是需要先放入佇列之中,優化後再執行。
3.塊裝置驅動程式框架
App:open, read, write "1.txt"
----------------------------------------------- 檔案的讀寫
檔案系統:vfat,tfat, ext2, ext3, yaffs2, jffs2 作用:把檔案的讀寫轉換為扇區的讀寫
------------統一的入口:ll_rw_block()---------- 扇區的讀寫
1.把“讀寫”放入佇列中(可能這一步就有優化)
2.呼叫佇列的處理函式(優化/調整順序/合併)
塊裝置驅動程式: 主要是讀寫塊裝置函式的實現和屬性的提供
-----------------------------------------------
硬體:硬碟,flash,eMMC
4.分析ll_rw_block()
ll_rw_block //fs/buffer.c, 位於fs下,說明是所有檔案系統的一個通用的.c檔案 for (i = 0; i < nr; i++) submit_bh(op, op_flags, bh); //fs/buffer.c struct bio *bio; 通用的構造請求,使用bio來構造請求 submit_bio(bio); generic_make_request(bio); struct request_queue *q = bio->bi_disk->queue; //block/blk-core.c 找對佇列 ret = q->make_request_fn(q, bio); //呼叫佇列的構造請求函式,預設的設定函式是:make_request_fn q->make_request_fn在blk_queue_make_request block/Blk-settings.c中被賦值, blk_queue_make_request在blk_init_allocated_queue block/blk-core.c中被賦值為blk_queue_bio,即make_request_fn=blk_queue_bio blk_queue_bio //預設是這個 elv_merge(q, &req, bio) //block/blk-core.c 以電梯呼叫演算法嘗試合併這個請求 如果合併不成功,呼叫get_request使用bio構造請求,將請求放入佇列中 get_request(q, bio->bi_opf, bio, GFP_NOIO); blk_init_request_from_bio(req, bio); blk_flush_plug_list(plug, false); if (q) queue_unplugged(q, depth, from_schedule); __blk_run_queue q->request_fn(q); //呼叫佇列的處理函式,就是塊裝置驅動實際的讀寫函式
5.寫塊裝置驅動
1.分配構造struct request_queue,用於提供讀寫能力
2.裝置描述,提供屬性
......
====>核心指定了一個結構體:gendisk
驅動框架:
1.分配gendisk: alloc_disk
2.設定
2.1 分配/設定struct request_queue結構,blk_init_queue
2.2 設定gendisk其它資訊
3.註冊gendisk
6.塊裝置的操作是以扇區為單位的,核心機制決定的,就算是使用記憶體模擬的塊裝置也不例外。
7.對於一塊全0的記憶體模擬的磁碟,未格式化會報"unknow partation table",因為其分割槽表為空
8.測試
cat /proc/devices 檢視裝置號
格式化磁碟:# mkfs /dev/ramblock eg: mkdosfs /dev/ramdisk
掛載:# mount /dev/ramblock /tmp
之後就可以通過訪問/tmp目錄操作磁碟了
9.建立磁碟映像
# cat /dev/ramblock > /ramblock.bin 然後再格式化磁碟再echo進去應該也是可以的。
在Ubuntu裡面:# sudo mount -o loop ramblock.bin /mnt loop選項可以把一個普通檔案當作塊裝置進行掛載。loop把它當作一個迴環裝置。
10.把對記憶體的操作打印出來可以對比App的讀寫和實際的IO操作發生的時機的區別。
11.分割槽
# ls /dev/ramblock* -l 次裝置號是0表示整個磁碟,不是分割槽。起始是0應該是first_minor=0決定的。
# fdisk /dev/ramblock m n p 1(此時顯示1-32 sylinder就是驅動中在ramblock_getgeo()中配置的) 1 5,再建立一個分割槽n p 2 6 32 w
(w表示將配置寫到分割槽表中,分割槽表就是第一個扇區)
# ls /dev/ramblock* -l 次裝置號是0表示整個磁碟,次裝置號是1表示第一個主分割槽,次裝置號是2表示第二個主分割槽。
此時也可以分別格式化每一個主分割槽:
eg: # mkdosfs /dev/ramblock1
eg: # mkdosfs /dev/ramblock2
分別掛載:
eg: # mount /dev/ramblock1 /mnt
eg: # mount /dev/ramblock2 /tmp
使用# fdisk /dev/ramdisk 報錯: Unknow value(s) for: Cylinder 不知道柱面數,fdisk是個老工具了,使用它需要驅動告訴它柱面數信
息(目前很多塊裝置都不使用這個儲存方式了)
12.記憶體模擬塊裝置驅動程式碼
/* 參考: drivers\block\z2ram.c */ #include <linux/module.h> #include <linux/errno.h> #include <linux/interrupt.h> #include <linux/mm.h> #include <linux/fs.h> #include <linux/kernel.h> #include <linux/timer.h> #include <linux/genhd.h> #include <linux/hdreg.h> #include <linux/ioport.h> #include <linux/init.h> #include <linux/wait.h> #include <linux/blkdev.h> #include <linux/blkpg.h> #include <linux/delay.h> #include <linux/io.h> #include <asm/system.h> #include <asm/uaccess.h> #include <asm/dma.h> static struct gendisk *ramblock_disk; static request_queue_t *ramblock_queue; static int major; static DEFINE_SPINLOCK(ramblock_lock); #define RAMBLOCK_SIZE (1024*1024) /*使用1M記憶體來模擬磁碟*/ static unsigned char *ramblock_buf; /*這些資訊在使用fdisk工具分割槽的時候會用到*/ static int ramblock_getgeo(struct block_device *bdev, struct hd_geometry *geo) { /* 容量 = heads * cylinders * sectors * 512 */ geo->heads = 2; geo->cylinders = 32; geo->sectors = RAMBLOCK_SIZE/2/32/512; return 0; } static struct block_device_operations ramblock_fops = { .owner = THIS_MODULE, .getgeo = ramblock_getgeo, }; /*目前核心中elv_next_request已經不存在了*/ static void do_ramblock_request(request_queue_t * q) { static int r_cnt = 0; static int w_cnt = 0; struct request *req; //printk("do_ramblock_request %d\n", ++cnt); while ((req = elv_next_request(q)) != NULL) { /* 資料傳輸三要素: 源,目的,長度 */ /* 源/目的: */ unsigned long offset = req->sector * 512; /* 目的/源: */ // req->buffer /* 長度: */ unsigned long len = req->current_nr_sectors * 512; if (rq_data_dir(req) == READ) /*也就是: req->cmd_flags & 1*/ { printk("do_ramblock_request read %d\n", ++r_cnt); memcpy(req->buffer, ramblock_buf+offset, len); /*這裡是使用memcpy來模擬複雜的IO操作*/ } else { /*加了這個列印,可以看出來當向裝置進行寫的時候沒有立即呼叫,而是過來一小會才呼叫的, 與演算法有關,先放到佇列中,然後才執行。 # cp /etc/fstab /tmp(掛載目錄),發現過來一會也沒有呼叫這個函式 # sync 立即就列印了 # cp /etc/fstab /tmp 發現沒有立即寫 # umount /tmp/ 發現立即列印了 */ printk("do_ramblock_request write %d\n", ++w_cnt); memcpy(ramblock_buf+offset, req->buffer, len); } end_request(req, 1); } } static int ramblock_init(void) { /* 1. 分配一個gendisk結構體 */ ramblock_disk = alloc_disk(16); /* 次裝置號個數: 分割槽個數+1 */ /* 2. 設定 */ /* 2.1 分配/設定佇列: 提供讀寫能力 */ ramblock_queue = blk_init_queue(do_ramblock_request, &ramblock_lock); /*arg1: 執行實際讀寫io操作的函式*/ ramblock_disk->queue = ramblock_queue; /* 2.2 設定其他屬性: 比如容量 */ major = register_blkdev(0, "ramblock"); /* cat /proc/devices 看到的是這個檔名嗎?*/ ramblock_disk->major = major; ramblock_disk->first_minor = 0; /*修改它試試*/ sprintf(ramblock_disk->disk_name, "ramblock"); /*區別設定這兩個名字進行測試*/ /*仿照scsi/sd.c sd_format_disk_name測試一下,對比sd卡的,它是什麼/dev/下的裝置節點名就是什麼*/ ramblock_disk->fops = &ramblock_fops; set_capacity(ramblock_disk, RAMBLOCK_SIZE / 512); /*設定磁碟容量,是以扇區為單位的*/ /* 3. 硬體相關操作 */ ramblock_buf = kzalloc(RAMBLOCK_SIZE, GFP_KERNEL); /*這個裝置全為0,分割槽表為空,所以在insmod驅動的時候會報"unknow partation table",需要格式化*/ /* 4. 註冊 */ add_disk(ramblock_disk); return 0; } static void ramblock_exit(void) { unregister_blkdev(major, "ramblock"); del_gendisk(ramblock_disk); put_disk(ramblock_disk); blk_cleanup_queue(ramblock_queue); kfree(ramblock_buf); } module_init(ramblock_init); module_exit(ramblock_exit); MODULE_LICENSE("GPL");