本節目的:
通過分析2.6內核下的塊設備驅動框架,知道如何來寫驅動
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; //相關映射鏈表
struct address_space *b_assoc_map;
atomic_t b_count; //緩沖區使用計數
};
5.2 然后進入submit_bd()中,submit_bh()函數如下:
int submit_bh(int rw, struct buffer_head * bh)
{
struct bio *bio; //定義一個bit(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_hd()函數就是通過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); //提交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_dev獲取申請隊列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:寫出隊列到扇區,out:讀入隊列
最后執行下來,就會排序合并,先寫出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又是怎么來的?
我們參考自帶的塊設備驅動程序driversblockxd.c
在入口函數中發現有這么一句:
static struct request_queue *xd_queue; //定義一個申請隊列xd_queue
xd_queue = blk_init_queue(do_xd_request, &xd_lock); //分配一個申請隊列
其中blk_init_queue()函數原型如下所示:
request_queue_t *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隊列
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、接下來我們就看看driversblockxd.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;
... ...
//2.分配一個申請隊列,后面會賦給gendisk結構體的queue成員
xd_queue = blk_init_queue(do_xd_request, &xd_lock);
... ...
for (i = 0; i < xd_drives; i++) {
... ...
//3.分配一個gendisk結構體,64:次設備號個數,也成為分區個數
struct gendisk *disk = alloc_disk(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、所以注冊一個塊設備驅動,需要以下步驟:
1 創建一個塊設備
2 分配一個申請隊列
3 分配一個gendisk結構體
4 設置gendisk結構體的成員
5 注冊gendisk結構體
上一篇:S3C2440的Linux啟動過程分析(一)——SC2440處理器結構
下一篇:01-S3C2440學習入門概念+環境搭建
推薦閱讀
史海拾趣
Control Sciences Inc公司在電子行業的初期,就以其技術創新而聞名。公司團隊不斷研發新的控制技術,成功打破了當時行業的局限。他們推出的首款智能控制系統,不僅提高了生產效率,還大大降低了能源消耗,為電子行業帶來了巨大的經濟效益。這一創新成果使得Control Sciences Inc在業界嶄露頭角,贏得了眾多客戶的青睞。
在電子行業中,產品質量是企業生存和發展的關鍵。Armel Electronics Inc公司深知此道,始終堅持品質至上的原則。公司從原材料采購到生產流程,再到產品出廠,每一個環節都嚴格把控,確保產品質量的穩定性和可靠性。這種對品質的執著追求,使得Armel的產品在市場上贏得了良好的口碑,并逐漸樹立了公司的品牌形象。
隨著集成電路技術的快速發展,對先進IC供電的需求日益增長。為了應對這一挑戰,CUI Inc.推出了90A數字負載點模塊系列。這些模塊采用先進的封裝技術和設計,具有高電流輸出和優異的性能表現。它們的推出不僅滿足了客戶對高效、可靠供電的需求,還進一步提升了CUI在電源領域的競爭力。
隨著產品質量的不斷提升,遠陽開始將目光投向更廣闊的市場。公司制定了詳細的市場拓展計劃,并加大了品牌營銷力度。2010年前后,遠陽成功進軍國際市場,與多家國際知名企業建立了長期合作關系。同時,公司也注重在國內市場的布局,通過參加各類行業展會、舉辦技術研討會等方式,不斷提升品牌知名度和影響力。這一時期,遠陽的產品線不斷豐富,涵蓋了HDMI、光纖線、DP線等多個系列,成為了數據工程電纜領域的佼佼者。
背景:FRONTIER Electronics作為一家專注于無源元件和集成無源元件設計與制造的公司,自成立以來便致力于技術創新。
發展故事:在21世紀初,FRONTIER Electronics憑借其在電感器、變壓器、二極管等領域的深厚技術積累,成功推出了一系列高性能、高可靠性的電子產品。隨著市場需求的不斷增長,公司逐步擴大生產規模,并在全球范圍內建立了完善的銷售網絡。通過持續的技術研發和市場拓展,FRONTIER Electronics逐漸在電子行業中嶄露頭角,成為該領域的領軍企業之一。
隨著半導體技術的興起,富士通敏銳地捕捉到了這一領域的巨大潛力。公司開始加大在半導體研發方面的投入,致力于開發出具有自主知識產權的半導體產品。經過不懈的努力,富士通在半導體領域取得了多項重大突破,不僅提升了自身的技術實力,也為全球半導體產業的發展做出了積極貢獻。如今,富士通已成為全球領先的半導體制造商之一,其產品在各個領域都得到了廣泛應用。
設計資源 培訓 開發板 精華推薦
- Hercules :安全MCU 初體驗
- 學知識,贏好禮|泰克半導體材料與器件測試技術電源特性篇!
- 深入東芝參考設計庫:夯實您的設計庫,讓您的設計更出彩
- 1月22日下午14:00Mouser攜手Maxim邀您觀看有獎直播:深入淺出可穿戴健康監測
- 有獎直播|羅徹斯特電子半導體全周期解決方案 助您應對供應鏈中斷和元器件停產的挑戰
- 答題抽獎:Mentor Tessent Automotive相關測試解決方案(獎品池還剩不少獎品喲)
- 國產芯4月:開啟ADC和DAC之旅,曬一曬你用過哪個段位的ADC/DAC~
- 2021 Digi‑Key KOL 視頻系列:新型智能曝光算法在人臉識別中的應用
- TI單芯片毫米波傳感器產品組合新品發布會,誠邀參與,預注冊贏好禮
- EEWORLD2018開年送禮,答題贏紅包