每個塊驅動的核心是它的請求函數(shù). 這個函數(shù)是真正做工作的地方 --或者至少開始的地方; 剩下的都是開銷. 因此, 我們花不少時間來看在塊驅動中的請求處理.
一個磁盤驅動的性能可能是系統(tǒng)整個性能的關鍵部分. 因此, 內(nèi)核的塊子系統(tǒng)編寫時在性能上考慮了很多; 它做所有可能的事情來使你的驅動從它控制的設備上獲得最多. 這是一個好事情, 其中它盲目地使能快速 I/O. 另一方面, 塊子系統(tǒng)沒必要在驅動 API 中曝露大量復雜性. 有可能編寫一個非常簡單的請求函數(shù)( 我們將很快見到 ), 但是如果你的驅動必須在一個高層次上操作復雜的硬件, 它可能是任何樣子.
塊驅動的請求方法有下面的原型:
void request(request_queue_t *queue);
這個函數(shù)被調用, 無論何時內(nèi)核認為你的驅動是時候處理對設備的讀, 寫, 或者其他操作. 請求函數(shù)在返回之前實際不需要完成所有的在隊列中的請求; 實際上, 它可能不完成它們?nèi)魏我粋€, 對大部分真實設備. 它必須, 但是, 驅動這些請求并且確保它們最終被驅動全部處理.
每個設備有一個請求隊列. 這是因為實際的從和到磁盤的傳輸可能在遠離內(nèi)核請求它們時發(fā)生, 并且因為內(nèi)核需要這個靈活性來調度每個傳送, 在最好的時刻(將影響磁盤上鄰近扇區(qū)的請求集合到一起, 例如). 并且這個請求函數(shù), 你可能記得, 和一個請求隊列相關, 當這個隊列被創(chuàng)建時. 讓我們回顧 sbull 如何創(chuàng)建它的隊列:
dev->queue = blk_init_queue(sbull_request, &dev->lock);
這樣, 當這個隊列被創(chuàng)建時, 請求函數(shù)和它關聯(lián)到一起. 我們還提供了一個自旋鎖作為隊列創(chuàng)建過程的一部分. 無論何時我們的請求函數(shù)被調用, 內(nèi)核持有這個鎖. 結果, 請求函數(shù)在原子上下文中運行; 它必須遵循所有的 5 章討論過的原子代碼的通用規(guī)則.
在你的請求函數(shù)持有鎖時, 隊列鎖還阻止內(nèi)核去排隊任何對你的設備的其他請求. 在一些條件下, 你可能考慮在請求函數(shù)運行時丟棄這個鎖. 如果你這樣做, 但是, 你必須保證不存取請求隊列, 或者任何其他的被這個鎖保護的數(shù)據(jù)結構, 在這個鎖不被持有時. 你必須重新請求這個鎖, 在請求函數(shù)返回之前.
最后, 請求函數(shù)的啟動(常常地)與任何用戶空間進程之間是完全異步的. 你不能假設內(nèi)核運行在發(fā)起當前請求的進程上下文. 你不知道由這個請求提供的 I/O 緩沖是否在內(nèi)核或者用戶空間. 因此任何類型的明確存取用戶空間的操作都是錯誤的并且將肯定引起麻煩. 如你將見到的, 你的驅動需要知道的關于請求的所有事情, 都包含在通過請求隊列傳遞給你的結構中.
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);
}
}
這個函數(shù)介紹了 struct request 結構. 我們之后將詳細檢查 struct request; 現(xiàn)在, 只需說它表示一個我們要執(zhí)行的塊 I/O 請求.
內(nèi)核提供函數(shù) elv_next_request 來獲得隊列中第一個未完成的請求; 當沒有請求要被處理時這個函數(shù)返回 NULL. 注意 elf_next 不從隊列里去除請求. 如果你連續(xù)調用它 2 次, 它 2 次都返回同一個請求結構. 在這個簡單的操作模式中, 請求只在它們完成時被剝離隊列.
一個塊請求隊列可包含實際上不從磁盤和自磁盤移動塊的請求. 這些請求可包括供應商特定的, 低層的診斷操作或者和特殊設備模式相關的指令, 例如給可記錄介質的報文寫模式. 大部分塊驅動不知道如何處理這樣的請求, 并且簡單地失敗它們; sbull 也以這種方式工作. 對 block_fs_request 的調用告訴我們是否我們在查看一個文件系統(tǒng)請求--一個一旦數(shù)據(jù)塊的. 如果這個請求不是一個文件系統(tǒng)請求, 我們傳遞它到 end_request:
void end_request(struct request *req, int succeeded);
當我們處理了非文件系統(tǒng)請求, 之后我們傳遞 succeeded 為 0 來指示我們沒有成功完成這個請求. 否則, 我們調用 sbull_transfer 來真正移動數(shù)據(jù), 使用一套在請求結構中提供的成員:
sector_t sector;
我們設備上起始扇區(qū)的索引. 記住這個扇區(qū)號, 象所有這樣的在內(nèi)核和驅動之間傳遞的數(shù)目, 是以 512-字節(jié)扇區(qū)來表示的. 如果你的硬件使用一個不同的扇區(qū)大小, 你需要相應地調整扇區(qū). 例如, 如果硬件是 2048-字節(jié)的扇區(qū), 你需要用 4 來除起始扇區(qū)號, 在安放它到對硬件的請求之前.
unsigned long nr_sectors;
要被傳送的扇區(qū)(512-字節(jié))數(shù)目.
char *buffer;
一個指向緩沖的指針, 數(shù)據(jù)應當被傳送到或者從的緩沖. 這個指針是一個內(nèi)核虛擬地址并且可被驅動直接解引用, 如果需要.
rq_data_dir(struct request *req);
這個宏從請求中抽取傳送的方向; 一個 0 返回值表示從設備中讀, 非 0 返回值表示寫入設備.
有了這個信息, sbull 驅動可實現(xiàn)實際的數(shù)據(jù)傳送, 使用一個簡單的 memcpy 調用 -- 我們數(shù)據(jù)已經(jīng)在內(nèi)存, 畢竟. 進行這個拷貝操作的函數(shù)( sbull_transfer ) 也處理扇區(qū)大小的調整, 并確保我們沒有拷貝超過我們的虛擬設備的尾.
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 實現(xiàn)了一個完整的, 簡單的基于 RAM 的磁盤設備. 但是, 對于很多類型的設備, 它不是一個實際的驅動, 由于幾個理由.
這些原因的第一個是 sbull 同步執(zhí)行請求, 一次一個. 高性能的磁盤設備能夠在同時有很多個請求停留; 磁盤的板上控制器因此可以優(yōu)化的順序(有人希望)執(zhí)行它們. 如果我們只處理隊列中的第一個請求, 我們在給定時間不能有多個請求被滿足. 能夠工作于多個請求要求對請求隊列和請求結構的深入理解; 下面幾節(jié)會幫助來建立這種理解.
但是, 有另外一個問題要考慮. 當系統(tǒng)進行大的傳輸, 包含多個在一起的磁盤扇區(qū), 就獲得最好的性能. 磁盤操作的最高開銷常常是讀寫頭的定位; 一旦這個完成, 實際上需要的讀或者寫數(shù)據(jù)的時間幾乎可忽略. 設計和實現(xiàn)文件系統(tǒng)和虛擬內(nèi)存子系統(tǒng)的開發(fā)者理解這點, 因此他們盡力在磁盤上連續(xù)地查找相關的數(shù)據(jù), 并且在一次請求中傳送盡可能多扇區(qū). 塊子系統(tǒng)也在這個方面起作用; 請求隊列包含大量邏輯,目的是找到鄰近的請求并且接合它們?yōu)楦蟮牟僮?
sbull 驅動, 但是, 采取所有這些工作并且簡單地忽略它. 一次只有一個緩沖被傳送, 意味著最大的單次傳送幾乎從不超過單個頁的大小. 一個塊驅動能做的比那個要好的多, 但是它需要一個對請求結構和bio結構的更深的理解, 請求是從它們建立的.
下面幾節(jié)更深入地研究塊層如何完成它的工作, 已經(jīng)這些工作導致的數(shù)據(jù)結構.
最簡單的說, 一個塊請求隊列就是: 一個塊 I/O 請求的隊列. 如果你往下查看, 一個請求隊列是一令人吃驚得復雜的數(shù)據(jù)結構. 幸運的是, 驅動不必擔心大部分的復雜性.
請求隊列跟蹤等候的塊I/O請求. 但是它們也在這些請求的創(chuàng)建中扮演重要角色. 請求隊列存儲參數(shù), 來描述這個設備能夠支持什么類型的請求: 它們的最大大小, 多少不同的段可進入一個請求, 硬件扇區(qū)大小, 對齊要求, 等等. 如果你的請求隊列被正確配置了, 它應當從不交給你一個你的設備不能處理的請求.
請求隊列還實現(xiàn)一個插入接口, 這個接口允許使用多 I/O 調度器(或者電梯). 一個 I/O 調度器的工作是提交 I/O 請求給你的驅動, 以最大化性能的方式. 為此, 大部分 I/O 調度器累積批量的 I/O 請求, 排列它們?yōu)檫f增(或遞減)的塊索引順序, 并且以那個順序提交請求給驅動. 磁頭, 當給定一列排序的請求時, 從磁盤的一頭到另一頭工作, 非常象一個滿載的電梯, 在一個方向移動直到所有它的"請求"(等待出去的人)已被滿足. 2.6 內(nèi)核包含一個"底線調度器", 它努力確保每個請求在預設的最大時間內(nèi)被滿足, 以及一個"預測調度器", 它實際上短暫停止設備, 在一個預想中的讀請求之后, 這樣另一個鄰近的讀將幾乎是馬上到達. 到本書為止, 缺省的調度器是預測調度器, 它看來有最好的交互的系統(tǒng)性能.
I/O 調度器還負責合并鄰近的請求. 當一個新 I/O 請求被提交給調度器, 它在隊列里搜尋包含鄰近扇區(qū)的請求; 如果找到一個, 并且如果結果的請求不是太大, 這 2 個請求被合并.
請求隊列有一個 struct request_queue 或者 request_queue_t 類型. 這個類型, 和許多操作它的函數(shù), 定義在 <linux/blkdev.h>. 如果你對請求隊列的實現(xiàn)感興趣, 你可找到大部分代碼在 drivers/block/ll_rw_block.c 和 elevator.c.
如同我們在我們的例子代碼中見到的, 一個請求隊列是一個動態(tài)的數(shù)據(jù)結構, 它必須被塊 I/O 子系統(tǒng)創(chuàng)建. 這個創(chuàng)建和初始化一個隊列的函數(shù)是:
request_queue_t *blk_init_queue(request_fn_proc *request, spinlock_t *lock);
當然, 參數(shù)是, 這個隊列的請求函數(shù)和一個控制對隊列存取的自旋鎖. 這個函數(shù)分配內(nèi)存(實際上, 不少內(nèi)存)并且可能失敗因為這個; 你應當一直檢查返回值, 在試圖使用這個隊列之前.
作為初始化一個請求隊列的一部分, 你可設置成員 queuedata(它是一個 void * 指針 )為任何你喜歡的值. 這個成員是請求隊列的對于我們在其他結構中見到的 private_data 的對等體.
為返回一個請求隊列給系統(tǒng)(在模塊卸載時間, 通常), 調用 blk_cleanup_queue:
void blk_cleanup_queue(request_queue_t *);
這個調用后, 你的驅動從給定的隊列中不再看到請求,并且不應當再次引用它.
有非常少的函數(shù)來操作隊列中的請求 -- 至少, 考慮到驅動. 你必須持有隊列鎖, 在你調用這些函數(shù)之前.
返回要處理的下一個請求的函數(shù)是 elv_next_request:
struct request *elv_next_request(request_queue_t *queue);
我們已經(jīng)在簡單的 sbull 例子中見到這個函數(shù). 它返回一個指向下一個要處理的請求的指針(由 I/O 調度器所決定的)或者 NULL 如果沒有請求要處理. elv_next_request 留這個請求在隊列上, 但是標識它為活動的; 這個標識阻止了 I/O 調度器試圖合并其他的請求到這些你開始執(zhí)行的.
為實際上從一個隊列中去除一個請求, 使用 blkdev_dequeue_request:
void blkdev_dequeue_request(struct request *req);
如果你的驅動同時從同一個隊列中操作多個請求, 它必須以這樣的方式將它們解出隊列.
如果你由于同樣的理由需要放置一個出列請求回到隊列中, 你可以調用:
void elv_requeue_request(request_queue_t *queue, struct request *req);
塊層輸出了一套函數(shù), 可被驅動用來控制一個請求隊列如何操作. 這些函數(shù)包括:
void blk_stop_queue(request_queue_t queue);void blk_start_queue(request_queue_t queue);
如果你的設備已到到達一個狀態(tài), 它不能處理等候的命令, 你可調用 blk_stop_queue 來告知塊層. 在這個調用之后, 你的請求函數(shù)將不被調用直到你調用 blk_start_queue. 不用說, 你不應當忘記重啟隊列, 當你的設備可處理更多請求時. 隊列鎖必須被持有當調用任何一個這些函數(shù)時.
void blk_queue_bounce_limit(request_queue_t *queue, u64 dma_addr);
告知內(nèi)核你的設備可進行 DMA 的最高物理地址的函數(shù). 如果一個請求包含一個超出這個限制的內(nèi)存引用, 一個反彈緩沖將被用來給這個操作; 當然, 這是一個進行塊 I/O 的昂貴方式, 并且應當盡量避免. 你可在這個參數(shù)中提供任何可能的值, 或者使用預先定義的符號 BLK_BOUNCE_HIGH(使用反彈緩沖給高內(nèi)存頁), BLK_BOUNCE_ISA (驅動只可 DMA 到 16MB 的 ISA 區(qū)), 或者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);
設置參數(shù)的函數(shù), 這些參數(shù)描述可被設備滿足的請求. blk_queue_max 可用來以扇區(qū)方式設置任一請求的最大的大小; 缺省是 255. blk_queue_max_phys_segments 和 blk_queue_max_hw_segments 都控制多少物理段(系統(tǒng)內(nèi)存中不相鄰的區(qū))可包含在一個請求中. 使用 blk_queue_max_phys_segments 來說你的驅動準備處理多少段; 例如, 這可能是一個靜態(tài)分配的散布表的大小. blk_queue_max_hw_segments, 相反, 是設備可處理的最多的段數(shù). 這 2 個參數(shù)缺省都是 128. 最后, blk_queue_max_segment_size 告知內(nèi)核任一個請求的段可能是多大字節(jié); 缺省是 65,536 字節(jié).
blk_queue_segment_boundary(request_queue_t *queue, unsigned long mask);
一些設備無法處理跨越一個特殊大小內(nèi)存邊界的請求; 如果你的設備是其中之一, 使用這個函數(shù)來告知內(nèi)核這個邊界. 例如, 如果你的設備處理跨 4-MB 邊界的請求有困難, 傳遞一個 0x3fffff 掩碼. 缺省的掩碼是 0xffffffff.
void blk_queue_dma_alignment(request_queue_t *queue, int mask);
告知內(nèi)核關于你的設備施加于 DMA 傳送的內(nèi)存對齊限制的函數(shù). 所有的請求被創(chuàng)建有給定的對齊, 并且請求的長度也匹配這個對齊. 缺省的掩碼是 0x1ff, 它導致所有的請求被對齊到 512-字節(jié)邊界.
void blk_queue_hardsect_size(request_queue_t *queue, unsigned short max);
告知內(nèi)核你的設備的硬件扇區(qū)大小. 所有由內(nèi)核產(chǎn)生的請求是這個大小的倍數(shù)并且被正確對齊. 所有的在塊層和驅動之間的通訊繼續(xù)以 512-字節(jié)扇區(qū)來表達, 但是.
在我們的簡單例子里, 我們遇到了這個請求結構. 但是, 我們未曾接觸這個復雜的數(shù)據(jù)結構. 在本節(jié), 我們看, 詳細地, 塊 I/O 請求在 Linux 內(nèi)核中如何被表示.
每個請求結構代表一個塊 I/O 請求, 盡管它可能是由幾個獨立的請求在更高層次合并而成. 對任何特殊的請求而傳送的扇區(qū)可能分布在整個主內(nèi)存, 盡管它們常常對應塊設備中的多個連續(xù)的扇區(qū). 這個請求被表示為多個段, 每個對應一個內(nèi)存中的緩沖. 內(nèi)核可能合并多個涉及磁盤上鄰近扇區(qū)的請求, 但是它從不合并在單個請求結構中的讀和寫操作. 內(nèi)核還確保不合并請求, 如果結果會破壞任何的在前面章節(jié)中描述的請求隊列限制.
基本上, 一個請求結構被實現(xiàn)為一個 bio 結構的鏈表, 結合一些維護信息來使驅動可以跟蹤它的位置, 當它在完成這個請求中. 這個 bio 結構是一個塊 I/O 請求移植的低級描述; 我們現(xiàn)在看看它.
當內(nèi)核, 以一個文件系統(tǒng)的形式, 虛擬文件子系統(tǒng), 或者一個系統(tǒng)調用, 決定一組塊必須傳送到或從一個塊 I/O 設備; 它裝配一個 bio 結構來描述那個操作. 那個結構接著被遞給這個塊 I/O 代碼, 這個代碼合并它到一個存在的請求結構, 或者, 如果需要, 創(chuàng)建一個新的. 這個 bio 結構包含一個塊驅動需要來進行請求的任何東西, 而不必涉及使這個請求啟動的用戶空間進程.
bio 結構, 在 <linux/bio.h> 中定義, 包含許多成員對驅動作者是有用的:
sector_t bi_sector;
這個 bio 要被傳送的第一個(512字節(jié))扇區(qū).
unsigned int bi_size;
被傳送的數(shù)據(jù)大小, 以字節(jié)計. 相反, 常常更易使用 bio_sectors(bio), 一個給定以扇區(qū)計的大小的宏.
unsigned long bi_flags;
一組描述 bio 的標志; 最低有效位被置位如果這是一個寫請求(盡管宏 bio_data_dir(bio)應當用來代替直接加鎖這個標志).
unsigned short bio_phys_segments;unsigned short bio_hw_segments;
包含在這個 BIO 中的物理段的數(shù)目, 和在 DMA 映射完成后被硬件看到的段數(shù)目, 分別地.
一個 bio 的核心, 但是, 是一個稱為 bi_io_vec 的數(shù)組, 它由下列結構組成:
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};
圖 bio 結構顯示了這些結構如何結合在一起. 如同你所見到的, 在一個塊 I/O 請求被轉換為一個 bio 結構后, 它已被分為單獨的物理內(nèi)存頁. 所有的一個驅動需要做的事情是步進全部這個結構數(shù)組(它們有 bi_vcnt 個), 和在每個頁內(nèi)傳遞數(shù)據(jù)(但是只 len 字節(jié), 從 offset 開始).
圖?16.1.?bio 結構
如果你的驅動器尊敬屏障請求, 第一步是通知塊層這個事實. 屏障處理是另一個請求隊列; 它被設置為:
void blk_queue_ordered(request_queue_t *queue, int flag);
為指示你的驅動實現(xiàn)了屏障請求, 設置 flag 參數(shù)為一個非零值.
實際的屏障請求實現(xiàn)是簡單地測試在請求結構中關聯(lián)的標志. 已經(jīng)提供了一個宏來進行這個測試:
int blk_barrier_rq(struct request *req);
如果這個宏返回一個非零值, 這個請求是一個屏障請求. 根據(jù)你的硬件如何工作, 你可能必須停止從隊列中獲取請求, 直到屏障請求已經(jīng)完成. 另外的驅動器能理解屏障請求; 在這個情況中, 你的驅動所有的必須做的是對這些驅動器發(fā)出正確的操作.
塊驅動常常試圖重試第一次失敗的請求. 這個做法可產(chǎn)生一個更加可靠的系統(tǒng)并且?guī)椭鷣肀苊鈹?shù)據(jù)丟失. 內(nèi)核, 但是, 有時標識請求為不可重入的. 這樣的請求應當完全盡快失敗, 如果它們無法在第一次試的時候執(zhí)行.
如果你的驅動在考慮重試一個失敗的請求, 他應當首先調用:
int blk_noretry_request(struct request *req);
如果這個宏返回非零值, 你的驅動應當放棄這個請求, 使用一個錯誤碼來代替重試它.
如同我們將見到的, 有幾個不同的方式來使用一個請求結構. 它們所有的都使用幾個通用的函數(shù), 但是, 它們處理一個 I/O 請求或者部分請求的完成. 這 2 個函數(shù)都是原子的并且可從一個原子上下文被安全地調用.
當你的設備已經(jīng)完成傳送一些或者全部扇區(qū), 在一個 I/O 請求中, 它必須通知塊子系統(tǒng), 使用:
int end_that_request_first(struct request *req, int success, int count);
這個函數(shù)告知塊代碼, 你的驅動已經(jīng)完成 count 個扇區(qū)地傳送, 從你最后留下的地方開始. 如果 I/O 是成功的, 傳遞 success 為 1; 否則傳遞 0. 注意你必須指出完成, 按照從第一個扇區(qū)到最后一個的順序; 如果你的驅動和設備有些共謀來亂序完成請求, 你必須存儲這個亂序的完成狀態(tài)直到介入的扇區(qū)已經(jīng)被傳遞.
從 end_that_request_first 的返回值是一個指示, 指示是否所有的這個請求中的扇區(qū)已經(jīng)被傳送或者沒有. 一個 0 返回值表示所有的扇區(qū)已經(jīng)被傳送并且這個請求完成. 在這點, 你必須使用 blkdev_dequeue_request 來從隊列中解除請求(如果你還沒有這樣做)并且傳遞它到:
void end_that_request_last(struct request *req);
end_that_request_last 通知任何在等待這個請求的人, 這個請求已經(jīng)完成并且回收這個請求結構; 它必須在持有隊列鎖時被調用.
在我們的簡單的 sbull 例子里, 我們不使用任何上面的函數(shù). 相反, 那個例子, 被稱為 end_request. 為顯示這個調用的效果, 這里有整個的 end_request 函數(shù), 如果在 2.6.10 內(nèi)核中見到的:
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);
}
}
函數(shù) add_disk_randomness 使用塊 I/O 請求的定時來貢獻熵給系統(tǒng)的隨機數(shù)池; 它應當被調用僅當磁盤的定時是真正的隨機的. 對大部分的機械設備這是真的, 但是對一個基于內(nèi)存的虛擬設備它不是真的, 例如 sbull. 因此, 下一節(jié)中更復雜的 sbull 版本不調用 add_disk_randomness.
現(xiàn)在你了解了足夠多的來編寫一個塊驅動, 可直接使用組成一個請求的 bio 結構. 但是, 一個例子可能會有幫助. 如果這個 sbull 驅動被加載為 request_mode 參數(shù)被設為 1, 它注冊一個知道 bio 的請求函數(shù)來代替我們上面見到的簡單函數(shù). 那個函數(shù)看來如此:
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);
}
}
}
這個函數(shù)簡單地獲取每個請求, 傳遞它到 sbull_xfer_request, 接著使用 end_that_request_first 和, 如果需要, end_that_request_last 來完成它. 因此, 這個函數(shù)在處理高級隊列并且請求管理部分問題. 真正執(zhí)行一個請求的工作, 但是, 落入 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 用于傳輸?shù)闹羔? 那個函數(shù)看來如此:
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" */
}
這個函數(shù)簡單地步入每個 bio 結構中的段, 獲得一個內(nèi)核虛擬地址來存取緩沖, 接著調用之前我們見到的同樣的 sbull_transfer 函數(shù)來拷貝數(shù)據(jù).
每個設備有它自己的需要, 但是, 作為一個通用的規(guī)則, 剛剛展示的代碼應當作為一個模型, 給許多的需要深入 bio 結構的情形.
如果你工作在一個高性能塊驅動上, 你有機會使用 DMA 來進行真正的數(shù)據(jù)傳輸. 一個塊驅動當然可步入 bio 結構, 如同上面描述的, 為每一個創(chuàng)建一個 DMA 映射, 并且傳遞結構給設備. 但是, 有一個更容易的方法, 如果你的驅動可進行發(fā)散/匯聚 I/O. 函數(shù):
int blk_rq_map_sg(request_queue_t *queue, struct request *req, struct scatterlist *list);
使用來自給定請求的全部段填充給定的列表. 內(nèi)存中鄰近的段在插入散布表之前被接合, 因此你不需要自己探測它們. 返回值是列表中的項數(shù). 這個函數(shù)還回傳, 在它第 3 個參數(shù), 一個適合傳遞給 dma_map_sg 的散布表.(關于 dma_map_sg 的更多信息見 15 章的"發(fā)散-匯聚映射"一節(jié)).
你的驅動必須在調用 blk_rq_map_sg 之前給散布表分配存儲. 這個列表必須能夠至少持有這個請求有的物理段那么多的項; struct request 成員 nr_phys_segments 持有那個數(shù)量, 它不能超過由 blk_queue_max_phys_segments 指定的物理段的最大數(shù)目.
如果你不想 blk_rq_map_sg 來接合鄰近的段, 你可改變這個缺省的行為, 使用一個調用諸如:
clear_bit(QUEUE_FLAG_CLUSTER, &queue->queue_flags);
一些 SCSI 磁盤驅動用這樣的方式標識它們的請求隊列, 因為它們沒有從接合請求中獲益.
前面, 我們已經(jīng)討論了內(nèi)核所作的在隊列中優(yōu)化請求順序的工作; 這個工作包括排列請求和, 或許, 甚至延遲隊列來允許一個預期的請求到達. 這些技術在處理一個真正的旋轉的磁盤驅動器時有助于系統(tǒng)的性能. 但是, 使用一個象 sbull 的設備它們是完全浪費了. 許多面向塊的設備, 例如閃存陣列, 用于數(shù)字相機的存儲卡的讀取器, 并且 RAM 盤真正地有隨機存取的性能, 包含從高級的請求隊列邏輯中獲益. 其他設備, 例如軟件 RAID 陣列或者被邏輯卷管理者創(chuàng)建的虛擬磁盤, 沒有這個塊層的請求隊列被優(yōu)化的性能特征. 對于這類設備, 它最好直接從塊層接收請求, 并且根本不去煩請求隊列.
對于這些情況, 塊層支持"無隊列"的操作模式. 為使用這個模式, 你的驅動必須提供一個"制作請求"函數(shù), 而不是一個請求函數(shù). make_request 函數(shù)有這個原型:
typedef int (make_request_fn) (request_queue_t *q, struct bio *bio);
注意一個請求隊列仍然存在, 即便它從不會真正有任何請求. make_request 函數(shù)用一個 bio 結構作為它的主要參數(shù), 這個 bio 結構表示一個或多個要傳送的緩沖. make_request 函數(shù)做 2 個事情之一: 它可或者直接進行傳輸, 或者重定向這個請求到另一個設備.
直接進行傳送只是使用我們前面描述的存取者方法來完成這個 bio. 因為沒有使用請求結構, 但是, 你的函數(shù)應當通知這個 bio 結構的創(chuàng)建者直接指出完成, 使用對 bio_endio 的調用:
void bio_endio(struct bio *bio, unsigned int bytes, int error);
這里, bytes 是你至今已經(jīng)傳送的字節(jié)數(shù). 它可小于由這個 bio 整體所代表的字節(jié)數(shù); 在這個方式中, 你可指示部分完成, 并且更新在 bio 中的內(nèi)部的"當前緩沖"指針. 你應當再次調用 bio_endio 在你的設備進行進一步處理時, 或者當你不能完成這個請求指出一個錯誤. 錯誤是通過提供一個非零值給 error 參數(shù)來指示的; 這個值通常是一個錯誤碼, 例如 -EIO. make_request 應當返回 0, 不管這個 I/O 是否成功.
如果 sbull 用 request_mode=2 加載, 它操作一個 make_request 函數(shù). 因為 sbull 已經(jīng)有一個函數(shù)看傳送單個 bio, 這個 make_request 函數(shù)簡單:
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 從一個通常的請求函數(shù); 那個工作由 end_that_request_first 代替來處理.
一些塊驅動, 例如那些實現(xiàn)卷管理者和軟件 RAID 陣列的, 真正需要重定向請求到另一個設備來處理真正的 I/O. 編寫這樣的一個驅動超出了本書的范圍. 我們, 但是, 注意如果 make_request 函數(shù)返回一個非零值, bio 被再次提交. 一個"堆疊"驅動, 可, 因此, 修改 bi_bdev 成員來指向一個不同的設備, 改變起始扇區(qū)值, 接著返回; 塊系統(tǒng)接著傳遞 bio 到新設備. 還有一個 bio_split 調用來劃分一個 bio 到多個塊以提交給多個設備. 盡管如果隊列參數(shù)被之前設置, 劃分一個 bio 幾乎從不需要.
任何一個方式, 你都必須告知塊子系統(tǒng), 你的驅動在使用一個自定義的 make_request 函數(shù). 為此, 你必須分配一個請求隊列, 使用:
request_queue_t *blk_alloc_queue(int flags);
這個函數(shù)不同于 blk_init_queue, 它不真正建立隊列來持有請求. flags 參數(shù)是一組分配標志被用來為隊列分配內(nèi)存; 常常地正確值是 GFP_KERNEL. 一旦你有一個隊列, 傳遞它和你的 make_request 函數(shù)到 blk_queue_make_request:
void blk_queue_make_request(request_queue_t *queue, make_request_fn *func);
sbull 代碼來設置 make_request 函數(shù), 象:
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 會發(fā)現(xiàn), 所有的隊列都有一個 make_request 函數(shù). 缺省的版本, generic_make_request, 處理 bio 和一個請求結構的結合. 通過提供一個它自己的 make_request 函數(shù), 一個驅動真正只覆蓋一個特定的請求隊列方法, 并且排序大部分工作.
更多建議: