## 16.3.?請求處理
每個塊驅動的核心是它的請求函數. 這個函數是真正做工作的地方 --或者至少開始的地方; 剩下的都是開銷. 因此, 我們花不少時間來看在塊驅動中的請求處理.
一個磁盤驅動的性能可能是系統整個性能的關鍵部分. 因此, 內核的塊子系統編寫時在性能上考慮了很多; 它做所有可能的事情來使你的驅動從它控制的設備上獲得最多. 這是一個好事情, 其中它盲目地使能快速 I/O. 另一方面, 塊子系統沒必要在驅動 API 中曝露大量復雜性. 有可能編寫一個非常簡單的請求函數( 我們將很快見到 ), 但是如果你的驅動必須在一個高層次上操作復雜的硬件, 它可能是任何樣子.
### 16.3.1.?對請求方法的介紹
塊驅動的請求方法有下面的原型:
~~~
void request(request_queue_t *queue);
~~~
這個函數被調用, 無論何時內核認為你的驅動是時候處理對設備的讀, 寫, 或者其他操作. 請求函數在返回之前實際不需要完成所有的在隊列中的請求; 實際上, 它可能不完成它們任何一個, 對大部分真實設備. 它必須, 但是, 驅動這些請求并且確保它們最終被驅動全部處理.
每個設備有一個請求隊列. 這是因為實際的從和到磁盤的傳輸可能在遠離內核請求它們時發生, 并且因為內核需要這個靈活性來調度每個傳送, 在最好的時刻(將影響磁盤上鄰近扇區的請求集合到一起, 例如). 并且這個請求函數, 你可能記得, 和一個請求隊列相關, 當這個隊列被創建時. 讓我們回顧 sbull 如何創建它的隊列:
~~~
dev->queue = blk_init_queue(sbull_request, &dev->lock);
~~~
這樣, 當這個隊列被創建時, 請求函數和它關聯到一起. 我們還提供了一個自旋鎖作為隊列創建過程的一部分. 無論何時我們的請求函數被調用, 內核持有這個鎖. 結果, 請求函數在原子上下文中運行; 它必須遵循所有的 5 章討論過的原子代碼的通用規則.
在你的請求函數持有鎖時, 隊列鎖還阻止內核去排隊任何對你的設備的其他請求. 在一些條件下, 你可能考慮在請求函數運行時丟棄這個鎖. 如果你這樣做, 但是, 你必須保證不存取請求隊列, 或者任何其他的被這個鎖保護的數據結構, 在這個鎖不被持有時. 你必須重新請求這個鎖, 在請求函數返回之前.
最后, 請求函數的啟動(常常地)與任何用戶空間進程之間是完全異步的. 你不能假設內核運行在發起當前請求的進程上下文. 你不知道由這個請求提供的 I/O 緩沖是否在內核或者用戶空間. 因此任何類型的明確存取用戶空間的操作都是錯誤的并且將肯定引起麻煩. 如你將見到的, 你的驅動需要知道的關于請求的所有事情, 都包含在通過請求隊列傳遞給你的結構中.
### 16.3.2.?一個簡單的請求方法
sbull 例子驅動提供了幾個不同的方法給請求處理. 缺省地, sbull 使用一個方法, 稱為 sbull_request, 它打算作為一個最簡單地請求方法的例子. 別忙, 它在這里:
~~~
static void sbull_request(request_queue_t *q)
{
struct request *req;
while ((req = elv_next_request(q)) != NULL) {
struct sbull_dev *dev = req->rq_disk->private_data;
if (! blk_fs_request(req)) {
printk (KERN_NOTICE "Skip non-fs request\n");
end_request(req, 0);
continue;
}
sbull_transfer(dev, req->sector, req->current_nr_sectors,
req->buffer, rq_data_dir(req));
end_request(req, 1);
}
}
~~~
這個函數介紹了 struct request 結構. 我們之后將詳細檢查 struct request; 現在, 只需說它表示一個我們要執行的塊 I/O 請求.
內核提供函數 elv_next_request 來獲得隊列中第一個未完成的請求; 當沒有請求要被處理時這個函數返回 NULL. 注意 elf_next 不從隊列里去除請求. 如果你連續調用它 2 次, 它 2 次都返回同一個請求結構. 在這個簡單的操作模式中, 請求只在它們完成時被剝離隊列.
一個塊請求隊列可包含實際上不從磁盤和自磁盤移動塊的請求. 這些請求可包括供應商特定的, 低層的診斷操作或者和特殊設備模式相關的指令, 例如給可記錄介質的報文寫模式. 大部分塊驅動不知道如何處理這樣的請求, 并且簡單地失敗它們; sbull 也以這種方式工作. 對 block_fs_request 的調用告訴我們是否我們在查看一個文件系統請求--一個一旦數據塊的. 如果這個請求不是一個文件系統請求, 我們傳遞它到 end_request:
~~~
void end_request(struct request *req, int succeeded);
~~~
當我們處理了非文件系統請求, 之后我們傳遞 succeeded 為 0 來指示我們沒有成功完成這個請求. 否則, 我們調用 sbull_transfer 來真正移動數據, 使用一套在請求結構中提供的成員:
sector_t sector;
我們設備上起始扇區的索引. 記住這個扇區號, 象所有這樣的在內核和驅動之間傳遞的數目, 是以 512-字節扇區來表示的. 如果你的硬件使用一個不同的扇區大小, 你需要相應地調整扇區. 例如, 如果硬件是 2048-字節的扇區, 你需要用 4 來除起始扇區號, 在安放它到對硬件的請求之前.
unsigned long nr_sectors;
要被傳送的扇區(512-字節)數目.
char *buffer;
一個指向緩沖的指針, 數據應當被傳送到或者從的緩沖. 這個指針是一個內核虛擬地址并且可被驅動直接解引用, 如果需要.
rq_data_dir(struct request *req);
這個宏從請求中抽取傳送的方向; 一個 0 返回值表示從設備中讀, 非 0 返回值表示寫入設備.
有了這個信息, sbull 驅動可實現實際的數據傳送, 使用一個簡單的 memcpy 調用 -- 我們數據已經在內存, 畢竟. 進行這個拷貝操作的函數( sbull_transfer ) 也處理扇區大小的調整, 并確保我們沒有拷貝超過我們的虛擬設備的尾.
~~~
static void sbull_transfer(struct sbull_dev *dev, unsigned long sector, unsigned long nsect, char *buffer, int write)
{
unsigned long offset = sector*KERNEL_SECTOR_SIZE;
unsigned long nbytes = nsect*KERNEL_SECTOR_SIZE;
if ((offset + nbytes) > dev->size)
{
printk (KERN_NOTICE "Beyond-end write (%ld %ld)\n", offset, nbytes);
return;
}
if (write)
memcpy(dev->data + offset, buffer, nbytes);
else
memcpy(buffer, dev->data + offset, nbytes);
}
~~~
用這個代碼, sbull 實現了一個完整的, 簡單的基于 RAM 的磁盤設備. 但是, 對于很多類型的設備, 它不是一個實際的驅動, 由于幾個理由.
這些原因的第一個是 sbull 同步執行請求, 一次一個. 高性能的磁盤設備能夠在同時有很多個請求停留; 磁盤的板上控制器因此可以優化的順序(有人希望)執行它們. 如果我們只處理隊列中的第一個請求, 我們在給定時間不能有多個請求被滿足. 能夠工作于多個請求要求對請求隊列和請求結構的深入理解; 下面幾節會幫助來建立這種理解.
但是, 有另外一個問題要考慮. 當系統進行大的傳輸, 包含多個在一起的磁盤扇區, 就獲得最好的性能. 磁盤操作的最高開銷常常是讀寫頭的定位; 一旦這個完成, 實際上需要的讀或者寫數據的時間幾乎可忽略. 設計和實現文件系統和虛擬內存子系統的開發者理解這點, 因此他們盡力在磁盤上連續地查找相關的數據, 并且在一次請求中傳送盡可能多扇區. 塊子系統也在這個方面起作用; 請求隊列包含大量邏輯,目的是找到鄰近的請求并且接合它們為更大的操作.
sbull 驅動, 但是, 采取所有這些工作并且簡單地忽略它. 一次只有一個緩沖被傳送, 意味著最大的單次傳送幾乎從不超過單個頁的大小. 一個塊驅動能做的比那個要好的多, 但是它需要一個對請求結構和bio結構的更深的理解, 請求是從它們建立的.
下面幾節更深入地研究塊層如何完成它的工作, 已經這些工作導致的數據結構.
### 16.3.3.?請求隊列
最簡單的說, 一個塊請求隊列就是: 一個塊 I/O 請求的隊列. 如果你往下查看, 一個請求隊列是一令人吃驚得復雜的數據結構. 幸運的是, 驅動不必擔心大部分的復雜性.
請求隊列跟蹤等候的塊I/O請求. 但是它們也在這些請求的創建中扮演重要角色. 請求隊列存儲參數, 來描述這個設備能夠支持什么類型的請求: 它們的最大大小, 多少不同的段可進入一個請求, 硬件扇區大小, 對齊要求, 等等. 如果你的請求隊列被正確配置了, 它應當從不交給你一個你的設備不能處理的請求.
請求隊列還實現一個插入接口, 這個接口允許使用多 I/O 調度器(或者電梯). 一個 I/O 調度器的工作是提交 I/O 請求給你的驅動, 以最大化性能的方式. 為此, 大部分 I/O 調度器累積批量的 I/O 請求, 排列它們為遞增(或遞減)的塊索引順序, 并且以那個順序提交請求給驅動. 磁頭, 當給定一列排序的請求時, 從磁盤的一頭到另一頭工作, 非常象一個滿載的電梯, 在一個方向移動直到所有它的"請求"(等待出去的人)已被滿足. 2.6 內核包含一個"底線調度器", 它努力確保每個請求在預設的最大時間內被滿足, 以及一個"預測調度器", 它實際上短暫停止設備, 在一個預想中的讀請求之后, 這樣另一個鄰近的讀將幾乎是馬上到達. 到本書為止, 缺省的調度器是預測調度器, 它看來有最好的交互的系統性能.
I/O 調度器還負責合并鄰近的請求. 當一個新 I/O 請求被提交給調度器, 它在隊列里搜尋包含鄰近扇區的請求; 如果找到一個, 并且如果結果的請求不是太大, 這 2 個請求被合并.
請求隊列有一個 struct request_queue 或者 request_queue_t 類型. 這個類型, 和許多操作它的函數, 定義在 <linux/blkdev.h>. 如果你對請求隊列的實現感興趣, 你可找到大部分代碼在 drivers/block/ll_rw_block.c 和 elevator.c.
#### 16.3.3.1.?隊列的創建和刪除
如同我們在我們的例子代碼中見到的, 一個請求隊列是一個動態的數據結構, 它必須被塊 I/O 子系統創建. 這個創建和初始化一個隊列的函數是:
~~~
request_queue_t *blk_init_queue(request_fn_proc *request, spinlock_t *lock);
~~~
當然, 參數是, 這個隊列的請求函數和一個控制對隊列存取的自旋鎖. 這個函數分配內存(實際上, 不少內存)并且可能失敗因為這個; 你應當一直檢查返回值, 在試圖使用這個隊列之前.
作為初始化一個請求隊列的一部分, 你可設置成員 queuedata(它是一個 void * 指針 )為任何你喜歡的值. 這個成員是請求隊列的對于我們在其他結構中見到的 private_data 的對等體.
為返回一個請求隊列給系統(在模塊卸載時間, 通常), 調用 blk_cleanup_queue:
~~~
void blk_cleanup_queue(request_queue_t *);
~~~
這個調用后, 你的驅動從給定的隊列中不再看到請求,并且不應當再次引用它.
#### 16.3.3.2.?排隊函數
有非常少的函數來操作隊列中的請求 -- 至少, 考慮到驅動. 你必須持有隊列鎖, 在你調用這些函數之前.
返回要處理的下一個請求的函數是 elv_next_request:
~~~
struct request *elv_next_request(request_queue_t *queue);
~~~
我們已經在簡單的 sbull 例子中見到這個函數. 它返回一個指向下一個要處理的請求的指針(由 I/O 調度器所決定的)或者 NULL 如果沒有請求要處理. elv_next_request 留這個請求在隊列上, 但是標識它為活動的; 這個標識阻止了 I/O 調度器試圖合并其他的請求到這些你開始執行的.
為實際上從一個隊列中去除一個請求, 使用 blkdev_dequeue_request:
~~~
void blkdev_dequeue_request(struct request *req);
~~~
如果你的驅動同時從同一個隊列中操作多個請求, 它必須以這樣的方式將它們解出隊列.
如果你由于同樣的理由需要放置一個出列請求回到隊列中, 你可以調用:
~~~
void elv_requeue_request(request_queue_t *queue, struct request *req);
~~~
#### 16.3.3.3.?隊列控制函數
塊層輸出了一套函數, 可被驅動用來控制一個請求隊列如何操作. 這些函數包括:
void blk_stop_queue(request_queue_t *queue);void blk_start_queue(request_queue_t *queue);
如果你的設備已到到達一個狀態, 它不能處理等候的命令, 你可調用 blk_stop_queue 來告知塊層. 在這個調用之后, 你的請求函數將不被調用直到你調用 blk_start_queue. 不用說, 你不應當忘記重啟隊列, 當你的設備可處理更多請求時. 隊列鎖必須被持有當調用任何一個這些函數時.
void blk_queue_bounce_limit(request_queue_t *queue, u64 dma_addr);
告知內核你的設備可進行 DMA 的最高物理地址的函數. 如果一個請求包含一個超出這個限制的內存引用, 一個反彈緩沖將被用來給這個操作; 當然, 這是一個進行塊 I/O 的昂貴方式, 并且應當盡量避免. 你可在這個參數中提供任何可能的值, 或者使用預先定義的符號 BLK_BOUNCE_HIGH(使用反彈緩沖給高內存頁), BLK_BOUNCE_ISA (驅動只可 DMA 到 16MB 的 ISA 區), 或者BLK_BOUCE_ANY(驅動可進行 DMA 到任何地址). 缺省值是 BLK_BOUNCE_HIGH.
void blk_queue_max_sectors(request_queue_t *queue, unsigned short max);void blk_queue_max_phys_segments(request_queue_t *queue, unsigned short max);void blk_queue_max_hw_segments(request_queue_t *queue, unsigned short max);void blk_queue_max_segment_size(request_queue_t *queue, unsigned int max);
設置參數的函數, 這些參數描述可被設備滿足的請求. blk_queue_max 可用來以扇區方式設置任一請求的最大的大小; 缺省是 255. blk_queue_max_phys_segments 和 blk_queue_max_hw_segments 都控制多少物理段(系統內存中不相鄰的區)可包含在一個請求中. 使用 blk_queue_max_phys_segments 來說你的驅動準備處理多少段; 例如, 這可能是一個靜態分配的散布表的大小. blk_queue_max_hw_segments, 相反, 是設備可處理的最多的段數. 這 2 個參數缺省都是 128. 最后, blk_queue_max_segment_size 告知內核任一個請求的段可能是多大字節; 缺省是 65,536 字節.
blk_queue_segment_boundary(request_queue_t *queue, unsigned long mask);
一些設備無法處理跨越一個特殊大小內存邊界的請求; 如果你的設備是其中之一, 使用這個函數來告知內核這個邊界. 例如, 如果你的設備處理跨 4-MB 邊界的請求有困難, 傳遞一個 0x3fffff 掩碼. 缺省的掩碼是 0xffffffff.
void blk_queue_dma_alignment(request_queue_t *queue, int mask);
告知內核關于你的設備施加于 DMA 傳送的內存對齊限制的函數. 所有的請求被創建有給定的對齊, 并且請求的長度也匹配這個對齊. 缺省的掩碼是 0x1ff, 它導致所有的請求被對齊到 512-字節邊界.
void blk_queue_hardsect_size(request_queue_t *queue, unsigned short max);
告知內核你的設備的硬件扇區大小. 所有由內核產生的請求是這個大小的倍數并且被正確對齊. 所有的在塊層和驅動之間的通訊繼續以 512-字節扇區來表達, 但是.
### 16.3.4.?請求的分析
在我們的簡單例子里, 我們遇到了這個請求結構. 但是, 我們未曾接觸這個復雜的數據結構. 在本節, 我們看, 詳細地, 塊 I/O 請求在 Linux 內核中如何被表示.
每個請求結構代表一個塊 I/O 請求, 盡管它可能是由幾個獨立的請求在更高層次合并而成. 對任何特殊的請求而傳送的扇區可能分布在整個主內存, 盡管它們常常對應塊設備中的多個連續的扇區. 這個請求被表示為多個段, 每個對應一個內存中的緩沖. 內核可能合并多個涉及磁盤上鄰近扇區的請求, 但是它從不合并在單個請求結構中的讀和寫操作. 內核還確保不合并請求, 如果結果會破壞任何的在前面章節中描述的請求隊列限制.
基本上, 一個請求結構被實現為一個 bio 結構的鏈表, 結合一些維護信息來使驅動可以跟蹤它的位置, 當它在完成這個請求中. 這個 bio 結構是一個塊 I/O 請求移植的低級描述; 我們現在看看它.
#### 16.3.4.1.?bio 結構
當內核, 以一個文件系統的形式, 虛擬文件子系統, 或者一個系統調用, 決定一組塊必須傳送到或從一個塊 I/O 設備; 它裝配一個 bio 結構來描述那個操作. 那個結構接著被遞給這個塊 I/O 代碼, 這個代碼合并它到一個存在的請求結構, 或者, 如果需要, 創建一個新的. 這個 bio 結構包含一個塊驅動需要來進行請求的任何東西, 而不必涉及使這個請求啟動的用戶空間進程.
bio 結構, 在 <linux/bio.h> 中定義, 包含許多成員對驅動作者是有用的:
sector_t bi_sector;
這個 bio 要被傳送的第一個(512字節)扇區.
unsigned int bi_size;
被傳送的數據大小, 以字節計. 相反, 常常更易使用 bio_sectors(bio), 一個給定以扇區計的大小的宏.
unsigned long bi_flags;
一組描述 bio 的標志; 最低有效位被置位如果這是一個寫請求(盡管宏 bio_data_dir(bio)應當用來代替直接加鎖這個標志).
unsigned short bio_phys_segments;unsigned short bio_hw_segments;
包含在這個 BIO 中的物理段的數目, 和在 DMA 映射完成后被硬件看到的段數目, 分別地.
一個 bio 的核心, 但是, 是一個稱為 bi_io_vec 的數組, 它由下列結構組成:
~~~
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};
~~~
圖 [bio 結構](# "圖?16.1.?bio 結構")顯示了這些結構如何結合在一起. 如同你所見到的, 在一個塊 I/O 請求被轉換為一個 bio 結構后, 它已被分為單獨的物理內存頁. 所有的一個驅動需要做的事情是步進全部這個結構數組(它們有 bi_vcnt 個), 和在每個頁內傳遞數據(但是只 len 字節, 從 offset 開始).
**圖?16.1.?bio 結構**

直接使用 bi_io_vec 數組不被推薦, 為了內核開發者可以在以后改變 bio 結構而不會引起破壞. 為此, 一組宏被提供來簡化使用 bio 結構. 開始的地方是 bio_for_each_segment, 它簡單地循環 bi_io_vec 數組中每個未被處理的項. 這個宏應當如下用:
~~~
int segno;
struct bio_vec *bvec;
bio_for_each_segment(bvec, bio, segno) {
/* Do something with this segment
}
~~~
在這個循環中, bvec 指向當前的 bio_vec 項, 并且 segno 是當前的段號. 這些值可被用來設置 DMA 發送器(一個使用 blk_rq_map_sg 的替代方法在"塊請求和 DMA"一節中描述). 如果你需要直接存取頁, 你應當首先確保一個正確的內核虛擬地址存在; 為此, 你可使用:
~~~
char *__bio_kmap_atomic(struct bio *bio, int i, enum km_type type);
void __bio_kunmap_atomic(char *buffer, enum km_type type);
~~~
這個底層的函數允許你直接映射在一個給定的 bio_vec 中找到的緩沖, 由索引 i 所指定的. 一個原子的 kmap 被創建; 調用者必須提供合適的來使用的槽位(如同在 15 章的"內存映射和 struct page"一節中描述的).
塊層還維護一組位于 bio 結構的指針來跟蹤請求處理的當前狀態. 幾個宏來提供對這個狀態的存取:
struct page *bio_page(struct bio *bio);
返回一個指向頁結構的指針, 表示下一個被傳送的頁.
int bio_offset(struct bio *bio);
返回頁內的被傳送的數據的偏移.
int bio_cur_sectors(struct bio *bio);
返回要被傳送出當前頁的扇區數.
char *bio_data(struct bio *bio);
返回一個內核邏輯地址, 指向被傳送的數據. 注意這個地址可用僅當請求的頁不在高內存中; 在其他情況下調用它是一個錯誤. 缺省地, 塊子系統不傳遞高內存緩沖到你的驅動, 但是如果你已使用 blk_queue_bounce_limit 改變設置, 你可能不該使用 bio_data.
char *bio_kmap_irq(struct bio *bio, unsigned long *flags);void bio_kunmap_irq(char *buffer, unsigned long *flags);
bio_kmap_irq 給任何緩沖返回一個內核虛擬地址, 不管它是否在高或低內存. 一個原子 kmap 被使用, 因此你的驅動在這個映射被激活時不能睡眠. 使用 bio_kunmap_irq 來去映射緩沖. 注意因為使用一個原子 kmap, 你不能一次映射多于一個段.
剛剛描述的所有函數都存取當前緩沖 -- 還未被傳送的第一個緩沖, 只要內核知道. 驅動常常想使用 bio 中的幾個緩沖, 在它們任何一個指出完成之前(使用 end_that_request_first, 馬上就講到), 因此這些函數常常沒有用. 幾個其他的宏存在來使用 bio 結構的內部接口(詳情見 <linux/bio.h>).
#### 16.3.4.2.?請求結構成員
現在我們有了 bio 結構如何工作的概念, 我們可以深入 struct request 并且看請求處理如何工作. 這個結構的成員包括:
sector_t hard_sector;unsigned long hard_nr_sectors;unsigned int hard_cur_sectors;
追蹤請求硬件完成的扇區的成員. 第一個尚未被傳送的扇區被存儲到 hard_sector, 已經傳送的扇區總數在 hard_nr_sectors, 并且在當前 bio 中剩余的扇區數是 hard_cur_sectors. 這些成員打算只用在塊子系統; 驅動不應當使用它們.
struct bio *bio;
bio 是給這個請求的 bio 結構的鏈表. 你不應當直接存取這個成員; 使用 rq_for_each_bio(后面描述) 代替.
char *buffer;
在本章前面的簡單驅動例子使用這個成員來找到傳送的緩沖. 隨著我們的深入理解, 我們現在可見到這個成員僅僅是在當前 bio 上調用 bio_data 的結果.
unsigned short nr_phys_segments;
被這個請求在物理內存中占用的獨特段的數目, 在鄰近頁已被合并后.
struct list_head queuelist;
鏈表結構(如同在 11 章中"鏈表"一節中描述的), 連接這個請求到請求隊列. 如果(并且只是)你從隊列中去除 blkdev_dequeue_request, 你可能使用這個列表頭來跟蹤這個請求, 在一個被你的驅動維護的內部列表中.
圖 [一個帶有一個部分被處理的請求的請求隊列](# "圖?16.2.?一個帶有一個部分被處理的請求的請求隊列") 展示了請求隊列和它的組件 bio 結構如何對應到一起. 在圖中, 這個請求已被部分滿足. cbio 和 buffer 處于指向尚未傳送的第一個 bio.
**圖?16.2.?一個帶有一個部分被處理的請求的請求隊列**

有許多不同的字段在請求結構中, 但是本節中的列表應當對大部分驅動編寫者是足夠的.
#### 16.3.4.3.?屏障請求
塊層在你的驅動見到它們之前重新排序來提高 I/O 性能. 你的驅動, 也可以重新排序請求, 如果有理由這樣做. 常常地, 這種重新排序通過傳遞多個請求到驅動并且使硬件考慮優化的順序來實現. 但是, 對于不嚴格的請求順序有一個問題: 有些應用程序要求保證某些操作在其他的啟動前完成. 例如, 關系數據庫管理者, 必須絕對確保它們的日志信息刷新到驅動器, 在執行在數據庫內容上的一次交易之前. 日志式文件系統, 現在在大部分 Linux 系統中使用, 有非常類似的排序限制. 如果錯誤的操作被重新排序, 結果可能是嚴重的, 無法探測的數據破壞.
2.6 塊層解決這個問題通過一個屏障請求的概念. 如果一個請求被標識為 REQ_HARDBARRER 標志, 它必須被寫入驅動器在任何后續的請求被初始化之前. "被寫入設備", 我們意思是數據必須實際位于并且是持久的在物理介質中. 許多的驅動器進行寫請求的緩存; 這個緩存提高了性能, 但是它可能使屏障請求的目的失敗. 如果一個電力失效在關鍵數據仍然在驅動器的緩存中時發生, 數據仍然被丟失即便驅動器報告完成. 因此一個實現屏障請求的驅動器必須采取步驟來強制驅動器真正寫這些數據到介質中.
如果你的驅動器尊敬屏障請求, 第一步是通知塊層這個事實. 屏障處理是另一個請求隊列; 它被設置為:
~~~
void blk_queue_ordered(request_queue_t *queue, int flag);
~~~
為指示你的驅動實現了屏障請求, 設置 flag 參數為一個非零值.
實際的屏障請求實現是簡單地測試在請求結構中關聯的標志. 已經提供了一個宏來進行這個測試:
~~~
int blk_barrier_rq(struct request *req);
~~~
如果這個宏返回一個非零值, 這個請求是一個屏障請求. 根據你的硬件如何工作, 你可能必須停止從隊列中獲取請求, 直到屏障請求已經完成. 另外的驅動器能理解屏障請求; 在這個情況中, 你的驅動所有的必須做的是對這些驅動器發出正確的操作.
#### 16.3.4.4.?不可重入請求
塊驅動常常試圖重試第一次失敗的請求. 這個做法可產生一個更加可靠的系統并且幫助來避免數據丟失. 內核, 但是, 有時標識請求為不可重入的. 這樣的請求應當完全盡快失敗, 如果它們無法在第一次試的時候執行.
如果你的驅動在考慮重試一個失敗的請求, 他應當首先調用:
~~~
int blk_noretry_request(struct request *req);
~~~
如果這個宏返回非零值, 你的驅動應當放棄這個請求, 使用一個錯誤碼來代替重試它.
### 16.3.5.?請求完成函數
如同我們將見到的, 有幾個不同的方式來使用一個請求結構. 它們所有的都使用幾個通用的函數, 但是, 它們處理一個 I/O 請求或者部分請求的完成. 這 2 個函數都是原子的并且可從一個原子上下文被安全地調用.
當你的設備已經完成傳送一些或者全部扇區, 在一個 I/O 請求中, 它必須通知塊子系統, 使用:
~~~
int end_that_request_first(struct request *req, int success, int count);
~~~
這個函數告知塊代碼, 你的驅動已經完成 count 個扇區地傳送, 從你最后留下的地方開始. 如果 I/O 是成功的, 傳遞 success 為 1; 否則傳遞 0. 注意你必須指出完成, 按照從第一個扇區到最后一個的順序; 如果你的驅動和設備有些共謀來亂序完成請求, 你必須存儲這個亂序的完成狀態直到介入的扇區已經被傳遞.
從 end_that_request_first 的返回值是一個指示, 指示是否所有的這個請求中的扇區已經被傳送或者沒有. 一個 0 返回值表示所有的扇區已經被傳送并且這個請求完成. 在這點, 你必須使用 blkdev_dequeue_request 來從隊列中解除請求(如果你還沒有這樣做)并且傳遞它到:
~~~
void end_that_request_last(struct request *req);
~~~
end_that_request_last 通知任何在等待這個請求的人, 這個請求已經完成并且回收這個請求結構; 它必須在持有隊列鎖時被調用.
在我們的簡單的 sbull 例子里, 我們不使用任何上面的函數. 相反, 那個例子, 被稱為 end_request. 為顯示這個調用的效果, 這里有整個的 end_request 函數, 如果在 2.6.10 內核中見到的:
~~~
void end_request(struct request *req, int uptodate)
{
if (!end_that_request_first(req, uptodate, req->hard_cur_sectors)) {
add_disk_randomness(req->rq_disk);
blkdev_dequeue_request(req);
end_that_request_last(req);
}
}
~~~
函數 add_disk_randomness 使用塊 I/O 請求的定時來貢獻熵給系統的隨機數池; 它應當被調用僅當磁盤的定時是真正的隨機的. 對大部分的機械設備這是真的, 但是對一個基于內存的虛擬設備它不是真的, 例如 sbull. 因此, 下一節中更復雜的 sbull 版本不調用 add_disk_randomness.
#### 16.3.5.1.?使用 bio
現在你了解了足夠多的來編寫一個塊驅動, 可直接使用組成一個請求的 bio 結構. 但是, 一個例子可能會有幫助. 如果這個 sbull 驅動被加載為 request_mode 參數被設為 1, 它注冊一個知道 bio 的請求函數來代替我們上面見到的簡單函數. 那個函數看來如此:
~~~
static void sbull_full_request(request_queue_t *q)
{
struct request *req;
int sectors_xferred;
struct sbull_dev *dev = q->queuedata;
while ((req = elv_next_request(q)) != NULL) {
if (! blk_fs_request(req)) {
printk (KERN_NOTICE "Skip non-fs request\n");
end_request(req, 0);
continue;
}
sectors_xferred = sbull_xfer_request(dev, req);
if (! end_that_request_first(req, 1, sectors_xferred)) {
blkdev_dequeue_request(req);
end_that_request_last(req);
}
}
}
~~~
這個函數簡單地獲取每個請求, 傳遞它到 sbull_xfer_request, 接著使用 end_that_request_first 和, 如果需要, end_that_request_last 來完成它. 因此, 這個函數在處理高級隊列并且請求管理部分問題. 真正執行一個請求的工作, 但是, 落入 sbull_xfer_request:
~~~
static int sbull_xfer_request(struct sbull_dev *dev, struct request *req)
{
struct bio *bio;
int nsect = 0;
rq_for_each_bio(bio, req)
{
sbull_xfer_bio(dev, bio);
nsect += bio->bi_size/KERNEL_SECTOR_SIZE;
}
return nsect;
}
~~~
這里我們介紹另一個宏: rq_for_each_bio. 如同你可能期望的, 這個宏簡單地步入請求中的每個 bio 結構, 給我們一個可傳遞給 sbull_xfer_bio 用于傳輸的指針. 那個函數看來如此:
~~~
static int sbull_xfer_bio(struct sbull_dev *dev, struct bio *bio)
{
int i;
struct bio_vec *bvec;
sector_t sector = bio->bi_sector;
/* Do each segment independently. */
bio_for_each_segment(bvec, bio, i)
{
char *buffer = __bio_kmap_atomic(bio, i, KM_USER0);
sbull_transfer(dev, sector, bio_cur_sectors(bio),
buffer, bio_data_dir(bio) == WRITE);
sector += bio_cur_sectors(bio);
__bio_kunmap_atomic(bio, KM_USER0);
}
return 0; /* Always "succeed" */
}
~~~
這個函數簡單地步入每個 bio 結構中的段, 獲得一個內核虛擬地址來存取緩沖, 接著調用之前我們見到的同樣的 sbull_transfer 函數來拷貝數據.
每個設備有它自己的需要, 但是, 作為一個通用的規則, 剛剛展示的代碼應當作為一個模型, 給許多的需要深入 bio 結構的情形.
#### 16.3.5.2.?塊請求和 DMA
如果你工作在一個高性能塊驅動上, 你有機會使用 DMA 來進行真正的數據傳輸. 一個塊驅動當然可步入 bio 結構, 如同上面描述的, 為每一個創建一個 DMA 映射, 并且傳遞結構給設備. 但是, 有一個更容易的方法, 如果你的驅動可進行發散/匯聚 I/O. 函數:
~~~
int blk_rq_map_sg(request_queue_t *queue, struct request *req, struct scatterlist *list);
~~~
使用來自給定請求的全部段填充給定的列表. 內存中鄰近的段在插入散布表之前被接合, 因此你不需要自己探測它們. 返回值是列表中的項數. 這個函數還回傳, 在它第 3 個參數, 一個適合傳遞給 dma_map_sg 的散布表.(關于 dma_map_sg 的更多信息見 15 章的"發散-匯聚映射"一節).
你的驅動必須在調用 blk_rq_map_sg 之前給散布表分配存儲. 這個列表必須能夠至少持有這個請求有的物理段那么多的項; struct request 成員 nr_phys_segments 持有那個數量, 它不能超過由 blk_queue_max_phys_segments 指定的物理段的最大數目.
如果你不想 blk_rq_map_sg 來接合鄰近的段, 你可改變這個缺省的行為, 使用一個調用諸如:
~~~
clear_bit(QUEUE_FLAG_CLUSTER, &queue->queue_flags);
~~~
一些 SCSI 磁盤驅動用這樣的方式標識它們的請求隊列, 因為它們沒有從接合請求中獲益.
#### 16.3.5.3.?不用一個請求隊列
前面, 我們已經討論了內核所作的在隊列中優化請求順序的工作; 這個工作包括排列請求和, 或許, 甚至延遲隊列來允許一個預期的請求到達. 這些技術在處理一個真正的旋轉的磁盤驅動器時有助于系統的性能. 但是, 使用一個象 sbull 的設備它們是完全浪費了. 許多面向塊的設備, 例如閃存陣列, 用于數字相機的存儲卡的讀取器, 并且 RAM 盤真正地有隨機存取的性能, 包含從高級的請求隊列邏輯中獲益. 其他設備, 例如軟件 RAID 陣列或者被邏輯卷管理者創建的虛擬磁盤, 沒有這個塊層的請求隊列被優化的性能特征. 對于這類設備, 它最好直接從塊層接收請求, 并且根本不去煩請求隊列.
對于這些情況, 塊層支持"無隊列"的操作模式. 為使用這個模式, 你的驅動必須提供一個"制作請求"函數, 而不是一個請求函數. make_request 函數有這個原型:
~~~
typedef int (make_request_fn) (request_queue_t *q, struct bio *bio);
~~~
注意一個請求隊列仍然存在, 即便它從不會真正有任何請求. make_request 函數用一個 bio 結構作為它的主要參數, 這個 bio 結構表示一個或多個要傳送的緩沖. make_request 函數做 2 個事情之一: 它可或者直接進行傳輸, 或者重定向這個請求到另一個設備.
直接進行傳送只是使用我們前面描述的存取者方法來完成這個 bio. 因為沒有使用請求結構, 但是, 你的函數應當通知這個 bio 結構的創建者直接指出完成, 使用對 bio_endio 的調用:
~~~
void bio_endio(struct bio *bio, unsigned int bytes, int error);
~~~
這里, bytes 是你至今已經傳送的字節數. 它可小于由這個 bio 整體所代表的字節數; 在這個方式中, 你可指示部分完成, 并且更新在 bio 中的內部的"當前緩沖"指針. 你應當再次調用 bio_endio 在你的設備進行進一步處理時, 或者當你不能完成這個請求指出一個錯誤. 錯誤是通過提供一個非零值給 error 參數來指示的; 這個值通常是一個錯誤碼, 例如 -EIO. make_request 應當返回 0, 不管這個 I/O 是否成功.
如果 sbull 用 request_mode=2 加載, 它操作一個 make_request 函數. 因為 sbull 已經有一個函數看傳送單個 bio, 這個 make_request 函數簡單:
~~~
static int sbull_make_request(request_queue_t *q, struct bio *bio)
{
struct sbull_dev *dev = q->queuedata;
int status;
status = sbull_xfer_bio(dev, bio);
bio_endio(bio, bio->bi_size, status);
return 0;
}
~~~
請注意你應當從不調用 bio_endio 從一個通常的請求函數; 那個工作由 end_that_request_first 代替來處理.
一些塊驅動, 例如那些實現卷管理者和軟件 RAID 陣列的, 真正需要重定向請求到另一個設備來處理真正的 I/O. 編寫這樣的一個驅動超出了本書的范圍. 我們, 但是, 注意如果 make_request 函數返回一個非零值, bio 被再次提交. 一個"堆疊"驅動, 可, 因此, 修改 bi_bdev 成員來指向一個不同的設備, 改變起始扇區值, 接著返回; 塊系統接著傳遞 bio 到新設備. 還有一個 bio_split 調用來劃分一個 bio 到多個塊以提交給多個設備. 盡管如果隊列參數被之前設置, 劃分一個 bio 幾乎從不需要.
任何一個方式, 你都必須告知塊子系統, 你的驅動在使用一個自定義的 make_request 函數. 為此, 你必須分配一個請求隊列, 使用:
~~~
request_queue_t *blk_alloc_queue(int flags);
~~~
這個函數不同于 blk_init_queue, 它不真正建立隊列來持有請求. flags 參數是一組分配標志被用來為隊列分配內存; 常常地正確值是 GFP_KERNEL. 一旦你有一個隊列, 傳遞它和你的 make_request 函數到 blk_queue_make_request:
~~~
void blk_queue_make_request(request_queue_t *queue, make_request_fn *func);
~~~
sbull 代碼來設置 make_request 函數, 象:
~~~
dev->queue = blk_alloc_queue(GFP_KERNEL);
if (dev->queue == NULL)
goto out_vfree;
blk_queue_make_request(dev->queue, sbull_make_request);
~~~
對于好奇的人, 花些時間深入 drivers/block/ll_rw_block.c 會發現, 所有的隊列都有一個 make_request 函數. 缺省的版本, generic_make_request, 處理 bio 和一個請求結構的結合. 通過提供一個它自己的 make_request 函數, 一個驅動真正只覆蓋一個特定的請求隊列方法, 并且排序大部分工作.
- Linux設備驅動第三版
- 第 1 章 設備驅動簡介
- 1.1. 驅動程序的角色
- 1.2. 劃分內核
- 1.3. 設備和模塊的分類
- 1.4. 安全問題
- 1.5. 版本編號
- 1.6. 版權條款
- 1.7. 加入內核開發社團
- 1.8. 本書的內容
- 第 2 章 建立和運行模塊
- 2.1. 設置你的測試系統
- 2.2. Hello World 模塊
- 2.3. 內核模塊相比于應用程序
- 2.4. 編譯和加載
- 2.5. 內核符號表
- 2.6. 預備知識
- 2.7. 初始化和關停
- 2.8. 模塊參數
- 2.9. 在用戶空間做
- 2.10. 快速參考
- 第 3 章 字符驅動
- 3.1. scull 的設計
- 3.2. 主次編號
- 3.3. 一些重要數據結構
- 3.4. 字符設備注冊
- 3.5. open 和 release
- 3.6. scull 的內存使用
- 3.7. 讀和寫
- 3.8. 使用新設備
- 3.9. 快速參考
- 第 4 章 調試技術
- 4.1. 內核中的調試支持
- 4.2. 用打印調試
- 4.3. 用查詢來調試
- 4.4. 使用觀察來調試
- 4.5. 調試系統故障
- 4.6. 調試器和相關工具
- 第 5 章 并發和競爭情況
- 5.1. scull 中的缺陷
- 5.2. 并發和它的管理
- 5.3. 旗標和互斥體
- 5.4. Completions 機制
- 5.5. 自旋鎖
- 5.6. 鎖陷阱
- 5.7. 加鎖的各種選擇
- 5.8. 快速參考
- 第 6 章 高級字符驅動操作
- 6.1. ioctl 接口
- 6.2. 阻塞 I/O
- 6.3. poll 和 select
- 6.4. 異步通知
- 6.5. 移位一個設備
- 6.6. 在一個設備文件上的存取控制
- 6.7. 快速參考
- 第 7 章 時間, 延時, 和延后工作
- 7.1. 測量時間流失
- 7.2. 獲知當前時間
- 7.3. 延后執行
- 7.4. 內核定時器
- 7.5. Tasklets 機制
- 7.6. 工作隊列
- 7.7. 快速參考
- 第 8 章 分配內存
- 8.1. kmalloc 的真實故事
- 8.2. 后備緩存
- 8.3. get_free_page 和其友
- 8.4. 每-CPU 的變量
- 8.5. 獲得大量緩沖
- 8.6. 快速參考
- 第 9 章 與硬件通訊
- 9.1. I/O 端口和 I/O 內存
- 9.2. 使用 I/O 端口
- 9.3. 一個 I/O 端口例子
- 9.4. 使用 I/O 內存
- 9.5. 快速參考
- 第 10 章 中斷處理
- 10.1. 準備并口
- 10.2. 安裝一個中斷處理
- 10.3. 前和后半部
- 10.4. 中斷共享
- 10.5. 中斷驅動 I/O
- 10.6. 快速參考
- 第 11 章 內核中的數據類型
- 11.1. 標準 C 類型的使用
- 11.2. 安排一個明確大小給數據項
- 11.3. 接口特定的類型
- 11.4. 其他移植性問題
- 11.5. 鏈表
- 11.6. 快速參考
- 第 12 章 PCI 驅動
- 12.1. PCI 接口
- 12.2. 回顧: ISA
- 12.3. PC/104 和 PC/104+
- 12.4. 其他的 PC 總線
- 12.5. SBus
- 12.6. NuBus 總線
- 12.7. 外部總線
- 12.8. 快速參考
- 第 13 章 USB 驅動
- 13.1. USB 設備基礎知識
- 13.2. USB 和 sysfs
- 13.3. USB 的 Urbs
- 13.4. 編寫一個 USB 驅動
- 13.5. 無 urb 的 USB 傳送
- 13.6. 快速參考
- 第 14 章 Linux 設備模型
- 14.1. Kobjects, Ksets 和 Subsystems
- 14.2. 低級 sysfs 操作
- 14.3. 熱插拔事件產生
- 14.4. 總線, 設備, 和驅動
- 14.5. 類
- 14.6. 集成起來
- 14.7. 熱插拔
- 14.8. 處理固件
- 14.9. 快速參考
- 第 15 章 內存映射和 DMA
- 15.1. Linux 中的內存管理
- 15.2. mmap 設備操作
- 15.3. 進行直接 I/O
- 15.4. 直接內存存取
- 15.5. 快速參考
- 第 16 章 塊驅動
- 16.1. 注冊
- 16.2. 塊設備操作
- 16.3. 請求處理
- 16.4. 一些其他的細節
- 16.5. 快速參考
- 第 17 章 網絡驅動
- 17.1. snull 是如何設計的
- 17.2. 連接到內核
- 17.3. net_device 結構的詳情
- 17.4. 打開與關閉
- 17.5. 報文傳送
- 17.6. 報文接收
- 17.7. 中斷處理
- 17.8. 接收中斷緩解
- 17.9. 連接狀態的改變
- 17.10. Socket 緩存
- 17.11. MAC 地址解析
- 17.12. 定制 ioctl 命令
- 17.13. 統計信息
- 17.14. 多播
- 17.15. 幾個其他細節
- 17.16. 快速參考
- 第 18 章 TTY 驅動
- 18.1. 一個小 TTY 驅動
- 18.2. tty_driver 函數指針
- 18.3. TTY 線路設置
- 18.4. ioctls 函數
- 18.5. TTY 設備的 proc 和 sysfs 處理
- 18.6. tty_driver 結構的細節
- 18.7. tty_operaions 結構的細節
- 18.8. tty_struct 結構的細節
- 18.9. 快速參考