22.Linux-塊設備驅動之框架詳細分析(詳解)
1.之前我們學的都是字符設備驅動,先來回憶一下
字符設備驅動:
當我們的應用層讀寫(read()/write())字符設備驅動時,是按字節/字符來讀寫數據的,期間沒有任何緩存區,因為數據量小,不能隨機讀取數據,例如:按鍵、LED、鼠標、鍵盤等
2.接下來本節開始學習塊設備驅動
塊設備:
塊設備是i/o設備中的一類, 當我們的應用層對該設備讀寫時,是按扇區大小來讀寫數據的,若讀寫的數據小於扇區的大小,就會需要緩存區, 可以隨機讀寫設備的任意位置處的數據,例如 普通文件(*.txt,*.c等),硬盤,U盤,SD卡,
3.塊設備結構:
- 段(Segments):由若幹個塊組成。是Linux內存管理機制中一個內存頁或者內存頁的一部分。
- 塊 (Blocks): 由Linux制定對內核或文件系統等數據處理的基本單位。通常由1個或多個扇區組成。(對Linux操作系統而言)
- 扇區(Sectors):塊設備的基本單位。通常在512字節到32768字節之間,默認512字節
4.我們以txt文件為例,來簡要分析下塊設備流程:
比如:當我們要寫一個很小的數據到txt文件某個位置時, 由於塊設備寫的數據是按扇區為單位,但又不能破壞txt文件裏其它位置,那麽就引入了一個“緩存區”,將所有數據讀到緩存區裏,然後修改緩存數據,再將整個數據放入txt文件對應的某個扇區中,當我們對txt文件多次寫入很小的數據的話,那麽就會重復不斷地對扇區讀出,寫入,這樣會浪費很多時間在讀/寫硬盤上,所以內核提供了一個隊列的機制,再沒有關閉txt文件之前,會將讀寫請求進行優化,排序,合並等操作,從而提高訪問硬盤的效率
(PS:內核中是通過elv_merge()函數實現將隊列優化,排序,合並,後面會分析到)
5.接下來開始分析塊設備框架
當我們對一個*.txt寫入數據時,文件系統會轉換為對塊設備上扇區的訪問,也就是調用ll_rw_block()函數,從這個函數開始就進入了設備層.
5.1先來分析ll_rw_block()函數(/fs/buffer.c):
void ll_rw_block(int rw, int nr, struct buffer_head *bhs[]) //rw:讀寫標誌位, nr:bhs[]長度, bhs[]:要讀寫的數據數組 { int i; for (i = 0; i < nr; i++) { struct buffer_head *bh = bhs[i]; //獲取nr個buffer_head ... ... if (rw == WRITE || rw == SWRITE) { if (test_clear_buffer_dirty(bh)) { ... ... submit_bh(WRITE, bh); //提交WRITE寫標誌的buffer_head
continue; }} else { if (!buffer_uptodate(bh)) { ... ... submit_bh(rw, bh); //提交其它標誌的buffer_head continue; }} unlock_buffer(bh); } }
其中buffer_head結構體,就是我們的緩沖區描述符,存放緩存區的各種信息,結構體如下所示:
struct buffer_head { unsigned long b_state; //緩沖區狀態標誌 struct buffer_head *b_this_page; //頁面中的緩沖區 struct page *b_page; //存儲緩沖區位於哪個頁面 sector_t b_blocknr; //邏輯塊號 size_t b_size; //塊的大小 char *b_data; //頁面中的緩沖區 struct block_device *b_bdev; //塊設備,來表示一個獨立的磁盤設備 bh_end_io_t *b_end_io; //I/O完成方法 void *b_private; //完成方法數據 struct list_head b_assoc_buffers; //相關映射鏈表 /* mapping this buffer is associated with */ struct address_space *b_assoc_map; atomic_t b_count; //緩沖區使用計數 };
5.2然後進入submit_bh()中, submit_bh()函數如下:
int submit_bh(int rw, struct buffer_head * bh) { struct bio *bio; //定義一個bio(block input output),也就是塊設備i/o ... ... bio = bio_alloc(GFP_NOIO, 1); //分配bio /*根據buffer_head(bh)構造bio */ bio->bi_sector = bh->b_blocknr * (bh->b_size >> 9); //存放邏輯塊號 bio->bi_bdev = bh->b_bdev; //存放對應的塊設備 bio->bi_io_vec[0].bv_page = bh->b_page; //存放緩沖區所在的物理頁面 bio->bi_io_vec[0].bv_len = bh->b_size; //存放扇區的大小 bio->bi_io_vec[0].bv_offset = bh_offset(bh); //存放扇區中以字節為單位的偏移量 bio->bi_vcnt = 1; //計數值 bio->bi_idx = 0; //索引值 bio->bi_size = bh->b_size; //存放扇區的大小 bio->bi_end_io = end_bio_bh_io_sync; //設置i/o回調函數 bio->bi_private = bh; //指向哪個緩沖區 ... ... submit_bio(rw, bio); //提交bio ... ... }
submit_bh()函數就是通過bh來構造bio,然後調用submit_bio()提交bio
5.3 submit_bio()函數如下:
void submit_bio(int rw, struct bio *bio) { ... ... generic_make_request(bio); }
最終調用generic_make_request(),把bio數據提交到相應塊設備的請求隊列中,generic_make_request()函數主要是實現對bio的提交處理
5.4 generic_make_request()函數如下所示:
void generic_make_request(struct bio *bio) { if (current->bio_tail) { // current->bio_tail不為空,表示有bio正在提交 *(current->bio_tail) = bio; //將當前的bio放到之前的bio->bi_next裏面 bio->bi_next = NULL; //更新bio->bi_next=0; current->bio_tail = &bio->bi_next; //然後將當前的bio->bi_next放到current->bio_tail裏,使下次的bio就會放到當前bio->bi_next裏面了
return; }
BUG_ON(bio->bi_next); do { current->bio_list = bio->bi_next; if (bio->bi_next == NULL) current->bio_tail = ¤t->bio_list; else bio->bi_next = NULL; __generic_make_request(bio); //調用__generic_make_request()提交bio bio = current->bio_list; } while (bio); current->bio_tail = NULL; /* deactivate */ }
從上面的註釋和代碼分析到,只有當第一次進入generic_make_request()時, current->bio_tail為NULL,才能調用__generic_make_request().
__generic_make_request()首先由bio對應的block_device獲取申請隊列q,然後要檢查對應的設備是不是分區,如果是分區的話要將扇區地址進行重新計算,最後調用q的成員函數make_request_fn完成bio的遞交.
5.5 __generic_make_request()函數如下所示:
static inline void __generic_make_request(struct bio *bio) { request_queue_t *q; int ret; ... ... do { q = bdev_get_queue(bio->bi_bdev); //通過bio->bi_bdev獲取申請隊列q ... ... ret = q->make_request_fn(q, bio); //提交申請隊列q和bio } while (ret);
}
這個q->make_request_fn()又是什麽函數?到底做了什麽,我們搜索下它在哪裏被初始化的
如下圖,搜索make_request_fn,它在blk_queue_make_request()函數中被初始化mfn這個參數
繼續搜索blk_queue_make_request,找到它被誰調用,賦入的mfn參數是什麽
如下圖,找到它在blk_init_queue_node()函數中被調用
最終q->make_request_fn()執行的是__make_request()函數
5.6我們來看看__make_request()函數,對提交的申請隊列q和bio做了什麽
static int __make_request(request_queue_t *q, struct bio *bio) { struct request *req; //塊設備本身的隊列 ... ... //(1)將之前的申請隊列q和傳入的bio,通過排序,合並在本身的req隊列中 el_ret = elv_merge(q, &req, bio); ... ... init_request_from_bio(req, bio); //合並失敗,單獨將bio放入req隊列 add_request(q, req); //單獨將之前的申請隊列q放入req隊列 ... ... __generic_unplug_device(q); //(2) 執行申請隊列的處理函數 }
1)上面的elv_merge()函數,就是內核中的電梯算法(elevator merge),它就類似我們坐的電梯,通過一個標誌,向上或向下.
比如申請隊列中有以下6個申請:
4(in),2(out),5(in),3(out),6(in),1(out) //其中in:寫出隊列到扇區,ou:讀入隊列
最後執行下來,就會排序合並,先寫出4,5,6,隊列,再讀入1,2,3隊列
2) 上面的__generic_unplug_device()函數如下:
void __generic_unplug_device(request_queue_t *q) { if (unlikely(blk_queue_stopped(q))) return; if (!blk_remove_plug(q)) return; q->request_fn(q); }
最終執行q的成員request_fn()函數, 執行申請隊列的處理函數
6.本節框架分析總結,如下圖所示:
7.其中q->request_fn是一個request_fn_proc結構體,如下圖所示:
7.1那這個申請隊列q->request_fn又是怎麽來的?
我們參考自帶的塊設備驅動程序drivers\block\xd.c
在入口函數中發現有這麽一句:
static struct request_queue *xd_queue; //定義一個申請隊列xd_queue xd_queue = blk_init_queue(do_xd_request, &xd_lock); //分配一個申請隊列
其中blk_init_queue()函數原型如下所示:
request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock); // *rfn: request_fn_proc結構體,用來執行申請隊列中的處理函數 // *lock:隊列訪問權限的自旋鎖(spinlock),該鎖需要通過DEFINE_SPINLOCK()函數來定義
顯然就是將do_xd_request()掛到xd_queue->request_fn裏.然後返回這個request_queue隊列
7.2我們再看看申請隊列的處理函數 do_xd_request()是如何處理的,函數如下:
static void do_xd_request (request_queue_t * q) { struct request *req; if (xdc_busy) return; while ((req = elv_next_request(q)) != NULL) //(1)while獲取申請隊列中的需要處理的申請 { int res = 0; ... ... for (retry = 0; (retry < XD_RETRIES) && !res; retry++) res = xd_readwrite(rw, disk, req->buffer, block, count);
//將獲取申請req的buffer成員 讀寫到disk扇區中,當讀寫失敗返回0,成功返回1
end_request(req, res); //申請隊列中的的申請已處理結束,當res=0,表示讀寫失敗 } }
(1)為什麽要while一直獲取?
因為這個q是個申請隊列,裏面會有多個申請,之前是使用電梯算法elv_merge()函數合並的,所以獲取也要通過電梯算法elv_next_request()函數獲取.
通過上面代碼和註釋,內核中的申請隊列q最終都是交給驅動處理,由驅動來對扇區讀寫
8.接下來我們就看看drivers\block\xd.c的入口函數大概流程,是如何創建塊設備驅動的
static DEFINE_SPINLOCK(xd_lock); //定義一個自旋鎖,用到申請隊列中
static struct request_queue *xd_queue; //定義一個申請隊列xd_queue static int __init xd_init(void) //入口函數 { if (register_blkdev(XT_DISK_MAJOR, "xd")) //1.創建一個塊設備,保存在/proc/devices中 goto out1; xd_queue = blk_init_queue(do_xd_request, &xd_lock); //2.分配一個申請隊列,後面會賦給gendisk結構體的queue成員 ... ... for (i = 0; i < xd_drives; i++) { ... ... struct gendisk *disk = alloc_disk(64); //3.分配一個gendisk結構體, 64:次設備號個數,也稱為分區個數
/* 4.接下來設置gendisk結構體 */ disk->major = XT_DISK_MAJOR; //設置主設備號 disk->first_minor = i<<6; //設置次設備號 disk->fops = &xd_fops; //設置塊設備驅動的操作函數 disk->queue = xd_queue; //設置queue申請隊列,用於管理該設備IO申請隊列 ... ... xd_gendisk[i] = disk; } ... ... for (i = 0; i < xd_drives; i++) add_disk(xd_gendisk[i]); //5.註冊gendisk結構體 }
其中gendisk(通用磁盤)結構體是用來存儲該設備的硬盤信息,包括請求隊列、分區鏈表和塊設備操作函數集等,結構體如下所示:
struct gendisk { int major; /*設備主設備號*/ int first_minor; /*起始次設備號*/ int minors; /*次設備號的數量,也稱為分區數量,如果改值為1,表示無法分區*/ char disk_name[32]; /*設備名稱*/ struct hd_struct **part; /*分區表的信息*/ int part_uevent_suppress; struct block_device_operations *fops; /*塊設備操作集合 */ struct request_queue *queue; /*申請隊列,用於管理該設備IO申請隊列的指針*/ void *private_data; /*私有數據*/ sector_t capacity; /*扇區數,512字節為1個扇區,描述設備容量*/ .... };
9.所以註冊一個塊設備驅動,需要以下步驟:
- 創建一個塊設備
- 分配一個申請隊列
- 分配一個gendisk結構體
- 設置gendisk結構體的成員
- 註冊gendisk結構體
原文:https://www.cnblogs.com/lifexy/p/7651667.html
22.Linux-塊設備驅動之框架詳細分析(詳解)