《Linux裝置驅動程式》——塊裝置驅動程式
1、一個塊裝置驅動程式主要同通過傳輸固定大小的隨機資料來訪問裝置。Linux核心視塊裝置為與字元裝置相異的基本裝置型別,因此塊裝置驅動程式有自己完成特定任務的
接 口。
2、高效的塊裝置驅動程式在功能上是嚴格要求的,並不僅僅體現在使用者應用程式的都、寫操作中。
3、塊驅動程式是在核心記憶體與其他儲存介質之間的管道,因此它可以認為是虛擬記憶體子系統的組成部分。
4、對塊裝置分層的設計,其著眼點是效能。Linux塊裝置驅動程式介面使得塊裝置可以發揮其最大的功效,但是其複雜程度有時程式設計者必須面對的一個問題。
5、術語
1)、一個數據塊指的是固定大小的資料,而大小的值由核心確定。資料塊的大小通常是4096個位元組,但是可以根據體系結構和所使用的檔案系統進行改變。
2)、與資料塊對應的是扇區,它是由底層硬體決定大小的一個塊。核心所處理的扇區大小是512位元組。
3)、無論何時核心為使用者提供了一個扇區編號,該扇區的大小就是512位元組。如果要是用不同的扇區硬體大小,使用者必須對核心的扇區數進行相應的修改。
二、註冊
一)、註冊塊裝置驅動程式
1、對大多數塊裝置驅動程式來說,第一步是向核心註冊自己。
int register_blkdev(unsigned int majir, const char *name);
2、與其對應的登出塊裝置驅動程式的函式是:
int unregister_blkdev(unsigned int majir, const char *name);
二)、註冊磁碟
1、塊裝置操作
1)、塊裝置使用block_device_operations結構告訴系統對它們的操作介面。(P460)
2)、在此結構中,沒有負責讀和寫函式。在塊裝置的I/O系統中,這些操作由request函式處理。
2、gendisk結構
1)、核心使用gendisk結構來表示一個獨立的磁碟裝置。實際上,核心還使用gendisk結構表示分割槽。
2)、gendisk結構中的許多成員必須由驅動程式初始化。(P461)
3)、gendisk結構是一個動態分配的結構,它需要一些核心的特殊處理來進行初始化;驅動程式不能自己動態分配該結構:
struct gendisk *alloc_disk(int minors);
4)、當不需要一個磁碟時,呼叫下面的函式解除安裝磁碟:
void del_gendisk(struct fendisk *gd);
5)、gendisk是一個引用計數結構。get_disk和put_disk函式負責處理引用計數,但是驅動程式不能直接使用這兩個函式。通常對del_gendisk的呼叫會刪除gendisk中的最
終計數,但是沒有機制能保證其肯定發生。
6)、分配一個gendisk結構並不能使磁碟對系統可用。為達到這個目的,必須初始化結構並呼叫add_disk:
void add_disk(struct gendisk *gd);
三)、sbull中的初始化(書P462-P464 sbull 模組的程式碼)
四)、對扇區帶小奧的說明
1、核心認為每個磁碟都是512位元組大小的扇區所組成的線形陣列。但並不是所有的硬體都使用這個扇區大小。讓一個具有不同的扇區大小的裝置執行起來並不是特別困難,只
是仔細關係許多細節。
2、所有操作的第一步就是通知核心裝置所支援的扇區大小。硬體扇區大小作為一個引數放在請求佇列中,而不是放在fendisk結構中。當分配佇列之後,立即呼叫
blk_queue_hardsect_size設定扇區大小:
blk_queue_hardsect_size(dev->queue, hardsect_size);
3、所有的I/O請求都將定位在硬體扇區的開始位置,並且每個請求的大小都將是扇區大小的整數倍。必須記住,核心總是認為扇區大小是512位元組,因此必須將所有的扇區數進
行轉換。
三、塊裝置操作一)、open和release函式
1、sbull模組中的open函式。
2、sbull模組中的release函式。
二)、對可移動介質的支援
1、block_disk_change結構中包含了兩個函式用以支援移動介質。如果是為非移動裝置編寫驅動程式,則可以忽略這兩個函式。
2、(從check_disk_change函式中)呼叫media_changed函式以檢查介質是否被改變;如果被改變該函式將返回非零值。
1)、sbull中的change實現。
3、在介質改變後將呼叫revalideate函式;為了讓驅動程式能操作新的介質,該函式要完成所有必須的工作。呼叫revalideate函式後,核心將試著重新讀取裝置的分割槽表。
1)、sbull中的revalideate函式實現。
三)、ioctl函式(書P466程式碼)
四、請求處理
一)、request函式介紹
1、塊裝置驅動程式的request函式有以下原型:
void request(request_queue_t *queue);
1)、當核心需要驅動程式處理讀取、寫入以及其他對裝置的操作時,就會呼叫該函式。
2)、在其返回前,request函式不必完成所有在佇列中的請求;實際上,對大多數真實裝置而言,它可能沒有完成任何請求。然而它必須啟動對請求的響應,並且保證所有的
請求最終被驅動程式所處理。
2、每個裝置都有一個請求佇列。當請求佇列生成的時候,request函式就與該佇列繫結在一起。
二)、一個簡單的request函式
1、sbull中sbull_request函式。
1)、核心提供了函式elv_next_request用來獲得佇列中第一個未完成的請求;當沒有請求需要處理時,該函式就返回NULL。注意elv_next_request並不從佇列中刪除請求。
如果不加以干涉而兩次呼叫該函式,則兩次都返回相同的request結構。
2)、block_fs_request呼叫告訴使用者該請求是否是一個檔案系統請求——移動塊資料的請求。如果不是檔案系統請求,則將其傳遞給end_request:
void end_request(struct request *req, int succeeded);
3)、當處理非檔案系統請求時,傳遞0表示不能成功地完成該請求,否則呼叫sbull_transfer對資料進行實際上的移動。該函式使用了request結構中的諸多成員。
2、request結構中的成員:
1)、sector_t sector:在裝置上開始扇區的索引號。
2)、unsigned long nr_sectors:需要傳遞扇區(512位元組)數。
3)、char *buffer:要傳遞或者接收資料的緩衝區指標。
4)、rq_data_dir(struct request *req):此巨集從request中得到傳輸方向。
3、sbull_transfe函式程式碼及其解析。
三)、請求佇列
1、從簡單的直覺上講,一個塊裝置請求佇列可以這樣描述:包含塊裝置I/O請求的序列。
1)、請求佇列跟蹤未完成的塊裝置的I/O請求。
2)、請求佇列還實現了外掛介面,使得多個I/O排程器的使用稱為可能。
2、請求佇列擁有request_queue和request_queue_t結構型別。早<linux/blkdev.h>中定義了該結構及操作該結構的函式。
3、佇列的建立與刪除
1)、建立和初始化請求佇列的函式是:
request_queue_t *blk_init_queue(request_fn_proc, spinlock_t *lock);
2)、為了把請求佇列返回給系統,需要呼叫以下函式
void blk_cleanup_queue(reque_queue_t *);
4、佇列函式
1)、返回佇列中下一個要處理的請求的函式是:
struct requeuest *elv_next_request(request_queue_t *queue);
2)、將請求從佇列中實際刪除,使用以下函式:
void blkdev_dequeue_request(struct request *req);
3)、如果出於某些原因要將拿出佇列的請求在返回給佇列,使用下面的函式:
void elv_requeue_request(request_queue_t *queue, struct request *req);
5、佇列控制函式(P473-P474)
四)、請求過程剖析
1、每個request結構都表示了一個塊裝置的I/O請求。從本質上講,一個request結構是作為一個bio結構的連結串列而實現的。當然是依靠一些管理資訊來組合的,這樣保證在執行
請求的時候,驅動程式能知道執行的位置。
1、bio結構
1)、當核心以檔案系統、虛擬記憶體子系統或者系統呼叫的形式決定從塊I/O裝置輸入、輸出塊資料時它將再結合一個bio結構,用來描述這個操作。
2)、bio結構被傳遞給I/O程式碼,程式碼會把它合併到一個已經存在的request結構中,或者根據需要,在建立一個新的request結構。
3)、bio結構包含了驅動程式執行請求的全部資訊,而不必於初始化這個請求的使用者空間的程序相關聯。
4)、bio結構成員。(P475)
3、為了讓核心開發者能在未來修改bio結構,而又不用重新編寫驅動程式程式碼,並不推薦直接使用bi_io_vec。因此在使用bio結構的時候,提供了一套巨集來簡化工作過程。在
使用bio來操作每個段的開始階段,他只是簡單地在bi_io_vec陣列中遍歷每個沒有被處理的入口,使用下面的巨集:
int segno;
struct bio_vec *bvec;
bio_for_each_segment(bvec, bio, segno) {
/*使用該段進行一定的操作*/
};
4、如果需要直接訪問頁,需要首先保證正確的核心虛擬地址是存在的。為達此目的,可以使用:
char *__bio_kmap_atomic(struct bio *bio, int i, enum km_type type);
void __bio_kunmap_atomic(char *buffer, enum km_type type);
5、為了追蹤正在處理的請求的當前狀態,塊裝置層也在bio結構中維護了一系列指標。提供了一些巨集可訪問當前狀態:
1)、struct page *bio_page(struct bio *bio);
返回下指向一個傳輸頁的page結構指標。
2)、int bio_offset(struct bio *bio);
返回在頁中被傳輸資料的偏移量。
3)、int bio_cur_sectors(struct bio *bio);
返回要在當前頁中傳輸的扇區數量。
4)、char *bio_data(struct bio *bio);
返回指向被傳輸資料的核心邏輯地址。
5)、char *bio_kmap_irq(struct bio *bio, unsigned long *flags);
void bio_kunmap_irq(char *buffer, unsigned long *flags);
6、request結構成員(P477-P478)
7、屏障請求
1)、2.6核心提供了屏障來解決無限制重新組合面臨的問題。
2)、如果驅動程式要實現屏障請求,所要做的第一步是將這一特性通知塊裝置層。屏障操作是另外一個請求佇列;用下面的函式設定:
void blk_queue_ordered(request_queue_t *queue, int flags);
3)、屏障的請求實現只是檢查request結構中的相關標誌,為此,核心提供了一個巨集完成這個工作:
int blk_barrier_rq(struct request *req);
8、不可重試請求
1)、如果驅動程式要重試一個失敗的請求,首先它要呼叫:
int blk_noretry_request(struct request *req);
五)、請求完成函式
1、當裝置完成一個I/O請求的部分或者全部的扇區時,它必須呼叫下面的函式通知塊裝置子系統:
int end_that_request_first(struct request *req, int sucess, int count);
2、end_that_request_first的返回值表明該請求中的所有扇區是否被傳輸。如果返回值是0表示所以得扇區都被傳輸了,該請求執行完畢。此時必須呼叫
blkdev_dequeue_request函式刪除請求(如果還沒有做這步),並把其傳遞給:
end_that_request_last(struct request *req);
3、使用bio
1)、sbull_full_request函式。
2)、sbull_xfer_request函式。
3)、sbull_xfer_bio函式。
4、塊裝置請求和DMA
1)、如果讀者的裝置可以完成“分散/聚集”I/O,有一個更簡單的實現方式。函式:
int blk_rq_map_sg(request_queue_t *queue, struct request *req, struct scatterlist *list);
2)、如果不想讓blk_rq_map_sg合併相鄰的段,可以使用下面的函式改變這個預設的行為:
clear_bit(QUEUE_FLAGS_CLUSTER, &queue->queue_flags);
5、不使用請求佇列
1)、為了能使用“無序列”模式,驅動程式必須提供一個“構造請求”的函式,而不是一個請求處理函式。下面式構造請求處理的原型。
typedef int (make_request_fn) (request_queue_t *q, struct bio *bio);
2)、直接進行傳輸是利用前面介紹過的方法,通過bio傳輸。由於沒有request結構進行操作,因此函式應該夠呼叫bio_endio,告訴bio結構的建立者請求的完成情況:
void bio_endio(struct bio *bio, unsigned int byte, int error);
3)、另外還必須告訴裝置子系統,驅動程式使用定製的make_request函式。為做到這點,必須使用下面的函式分配一個請求佇列:
request_queue_t *blk_alloc_queue(int flags);
4)、一旦擁有了佇列,將blk_alloc_queue與make_request函式傳遞給下列函式:
void blk_queue_make_request(request_queue_t *queue, make_requeue_fn *func);
五、其他一些細節
一)、命令預處理
1、塊裝置層為驅動程式返回elv_next_request前,提供了檢查和預處理請求的機制,該機制允許驅動程式預先建立驅動器命令,決定是否處理該請求,還是以其他方式的處
理。
2、如果想使用這個功能,要按照下面原型撿來命令處理函式:
type int (prep_rq_fn) (request_queue_t *queue, struct request *req);
3、為了讓塊裝置層呼叫預處理函式,將其傳遞給:
void blk_queue_prep_rq(request_queue_t *queue, prep_rq_fn *func);
二)、標記命令列
1、同時擁有多個活動請求的硬體通常支援某種形式的標記命令佇列(TCQ)。TCQ只是為每個請求新增一個整數(標記)技術,這樣當驅動器完成它們中的一個請求後,它
就可以告訴驅動程式完成的是那個。
2、為了能讓驅動器支援標記命令佇列,必須在初始化的時候呼叫下列的函式告訴核心:
int blk_queue_init_tags(request_queue_t *queue, int depth, struct blk_queue_tags *tags);
3、如果裝置能處理的標記數量發生了變化,可以用下面的函式通知核心:
int blk_queue_resize_tags(request_queue_t *queue, int new_depth);
4、使用一下函式將一個標記與一個請求相關聯,呼叫該函式時必須鎖住佇列鎖:
int blk_queue_start_tag(request_queue_t *queue, struct request *req);
5、當一個指定請求的全部資料傳輸完畢後,驅動程式使用下面的函式返回標記:
void blk_queue_end_tag(request_queue_t *queue, struct request *req);
6、如果要為一個指定的標記找到相應的請求,使用blk_queue_find_tag函式:
struct requst *blk_queue_find_tag(request_queue_t *queue, int tag);
7、如果發生了錯誤,驅動程式可能不得不重新置位,或者執行其他一些操作對其進行控制的裝置強制糾錯。在這種情況下,任何未完成標記的命令都不能被執行。塊裝置層提
供了一個函式,可以在這種情況下幫助重複:
void blk_queue_invalidata_tags(request_queue_t *queue);