天天看點

塊驅動

Linux Device Driver書籍(15)

第 16 章 塊驅動

至今, 我們的讨論一直限于字元驅動. 但是, 在 Linux 系統中有其他類型的驅動, 并且到時候要開闊我們的視野了. 是以, 本章讨論塊驅動.

一個塊驅動提供裝置的存取, 這個裝置可随機地以固定大小的塊傳送資料--主要的是, 磁盤驅動. Linux 核心看待塊裝置根本上不同于字元裝置; 結果, 塊驅動有明顯不同的接口和它們自己的特殊的挑戰.

高效的塊驅動對于性能是重要的 -- 不隻是為在使用者應用程式的明确的讀和寫. 現代的有虛拟記憶體的系統将不需要的資料移向(希望地)二級存儲中, 它常常是一個磁盤驅動器. 塊驅動是核心記憶體和二級存儲之間的導管; 是以, 它們可組成虛拟記憶體子系統的一部分. 雖然可能編寫一個塊驅動不必知道 struct page 和其他重要的記憶體概念, 任何需要編寫一個高性能驅動的人必須使用 15 章所涉及的内容.

許多塊層的設計圍繞性能. 許多字元裝置可在它們的最大速率以下運作, 并且系統的總體性能不被影響. 但是如果它的塊 I/O 子系統沒有調整好, 系統不能很好地運作. Linux 塊驅動接口允許你從一個塊裝置中獲得最多輸出, 但是有必要, 施加一些你必須處理的複雜性. 好的是, 2.6 的塊接口比之前的核心很大提高.

如你會期望的, 本章的讨論集中在一個例子驅動, 它實作了一個面向塊的, 基于記憶體的裝置. 基本上, 它是一個 ramdisk. 核心硬體包含了一個很進階的 ramdisk 實作, 但是我們的驅動(稱為 sbull)讓我們示範建立一個塊驅動, 同時最小化無關的複雜性.

在進入細節之前, 我們精确定義幾個詞語. 一個塊是一個固定大小的資料塊, 大小由核心決定. 塊常常是 4096 位元組, 但是這個值可依賴體系和使用的檔案系統而變化. 一個扇區, 相反, 是一個小塊, 它的大小常常由底層的硬體決定. 核心期望處理實作 512-位元組扇區的裝置. 如果你的裝置使用不同的大小, 核心調整并且避免産生硬體無法處理的 I/O 請求. 但是, 它值得記住, 任何時候核心給你一個扇區号, 它是工作在一個 512-位元組扇區的世界. 如果你使用不同的硬體扇區大小, 你必須相應地調整核心的扇區号. 我們在 sbull 驅動中見如何完成這個.

16.1. 注冊

塊驅動, 象字元驅動, 必須使用一套注冊接口來使核心可使用它們的裝置. 概念是類似的, 但是塊裝置注冊的細節是都不同的. 你有一整套新的資料結構和裝置操作要學習.

16.1.1. 塊驅動注冊

大部分塊驅動采取的第一步是注冊它們自己到核心. 這個任務的函數是 register_blkdev(在  中定義):

int register_blkdev(unsigned int major, const char *name);

參數是你的裝置要使用的主編号和關聯的名子(核心将顯示它在 /proc/devices). 如果 major 傳遞為0, 核心配置設定一個新的主編号并且傳回它給調用者. 如常, 自 register_blkdev 的一個負的傳回值訓示已發生了一個錯誤.

取消注冊的對應函數是:

int unregister_blkdev(unsigned int major, const char *name);

這裡, 參數必須比對傳遞給 register_blkdev 的那些, 否則這個函數傳回 -EINVAL 并且什麼都不登出.

在2.6核心, 對 register_blkdev 的調用完全是可選的. 由 register_blkdev 所進行的功能已随時間正在減少; 這個調用唯一的任務是 (1) 如果需要, 配置設定一個動态主編号, 并且 (2) 在 /proc/devices 建立一個入口. 在将來的核心, register_blkdev 可能被一起去掉. 同時, 但是, 大部分驅動仍然調用它; 它是慣例.

16.1.2. 磁盤注冊

雖然 register_blkdev 可用來獲得一個主編号, 它不使任何磁盤驅動器對系統可用. 有一個分開的注冊接口你必須使用來管理單獨的驅動器. 使用這個接口要求熟悉一對新結構, 這就是我們的起點.

16.1.2.1. 塊裝置操作

字元裝置通過 file_ 操作結構使它們的操作對系統可用. 一個類似的結構用在塊裝置上; 它是 struct block_device_operations, 定義在 . 下面是一個對這個結構中的成員的簡短的概覽; 當我們進入 sbull 驅動的細節時詳細重新通路它們.

int (*open)(struct inode *inode, struct file *filp);

int (*release)(struct inode *inode, struct file *filp);

就像它們的字元驅動對等體一樣工作的函數; 無論何時裝置被打開和關閉都調用它們. 一個字元驅動可能通過啟動裝置或者鎖住門(為可移出的媒體)來響應一個 open 調用. 如果你将媒體鎖入裝置, 你當然應當在 release 方法中解鎖.

int (*ioctl)(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);

實作 ioctl 系統調用的方法. 但是, 塊層首先解釋大量的标準請求; 是以大部分的塊驅動 ioctl 方法相當短.

int (*media_changed) (struct gendisk *gd);

被核心調用來檢查是否使用者已經改變了驅動器中的媒體的方法, 如果是這樣傳回一個非零值. 顯然, 這個方法僅适用于支援可移出的媒體的驅動器(并且最好給驅動一個"媒體被改變"标志); 在其他情況下可被忽略.

struct gendisk 參數是核心任何表示單個磁盤; 我們将在下一節檢視這個結構.

int (*revalidate_disk) (struct gendisk *gd);

revalidate_disk 方法被調用來響應一個媒體改變; 它給驅動一個機會來進行需要的任何工作使新媒體準備好使用. 這個函數傳回一個 int 值, 但是值被核心忽略.

struct module *owner;

一個指向擁有這個結構的子產品的指針; 它應當常常被初始化為 THIS_MODULE.

專心的讀者可能已注意到這個清單一個有趣的省略: 沒有實際讀或寫資料的函數. 在塊 I/O 子系統, 這些操作由請求函數處理, 它們應當有它們自己的一節并且在本章後面讨論. 在我們談論服務請求之前, 我們必須完成對磁盤注冊的讨論.

16.1.2.2. gendisk 結構

struct gendisk (定義于 ) 是單獨一個磁盤驅動器的核心表示. 事實上, 核心還使用 gendisk 來表示分區, 但是驅動作者不必知道這點. struct gedisk 中有幾個成員, 必須被一個塊驅動初始化:

int major;

int first_minor;

int minors;

描述被磁盤使用的裝置号的成員. 至少, 一個驅動器必須使用最少一個次編号. 如果你的驅動會是可分區的, 但是(并且大部分應當是), 你要配置設定一個次編号給每個可能的分區. 次編号的一個普通的值是 16, 它允許"全磁盤"裝置盒 15 個分區. 一些磁盤驅動使用 64 個次編号給每個裝置.

char disk_name[32];

應當被設定為磁盤驅動器名子的成員. 它出現在 /proc/partitions 和 sysfs.

struct block_device_operations *fops;

來自前一節的裝置操作集合.

struct request_queue *queue;

被核心用來管理這個裝置的 I/O 請求的結構; 我們在"請求處理"一節中檢查它.

int flags;

一套标志(很少使用), 描述驅動器的狀态. 如果你的裝置有可移出的媒體, 你應當設定 GENHD_FL_REMOVABLE. CD-ROM 驅動器可設定 GENHD_FL_CD. 如果, 由于某些原因, 你不需要分區資訊出現在 /proc/partitions, 設定 GENHD_FL_SUPPRESS_PARTITIONS_INFO.

sector_t capacity;

這個驅動器的容量, 以512-位元組扇區來計. sector_t 類型可以是 64 位寬. 驅動不應當直接設定這個成員; 相反, 傳遞扇區數目給 set_capacity.

void *private_data;

塊驅動可使用這個成員作為一個指向它們自己内部資料的指針.

核心提供了一小部分函數來使用 gendisk 結構. 我們在這裡介紹它們, 接着看 sbull 如何使用它們來使系統可使用它的磁盤驅動器.

struct gendisk 是一個動态配置設定的結構, 它需要特别的核心操作來初始化; 驅動不能自己配置設定這個結構. 相反, 你必須調用:

struct gendisk *alloc_disk(int minors);

minors 參數應當是這個磁盤使用的次編号數目; 注意你不能在之後改變 minors 成員并且期望事情可以正确工作. 當不再需要一個磁盤時, 它應當被釋放, 使用:

void del_gendisk(struct gendisk *gd);

一個 gendisk 是一個被引用計數的結構(它含有一個 kobject). 有 get_disk 和 put_disk 函數用來操作引用計數, 但是驅動應當從不需要做這個. 正常地, 對 del_gendisk 的調用去掉了最一個 gendisk 的最終的引用, 但是不保證這樣. 是以, 這個結構可能繼續存在(并且你的方法可能被調用)在調用 del_gendisk 之後. 但是, 如果你删除這個結構當沒有使用者時(即, 在最後的釋放之後, 或者在你的子產品清理函數), 你可确信你不會再收到它的資訊.

配置設定一個 gendisk 結構不能使系統可使用這個磁盤. 要做到這點, 你必須初始化這個結構并且調用 add_disk:

void add_disk(struct gendisk *gd);

這裡記住一件重要的事情:一旦你調用add_disk, 這個磁盤是"活的"并且它的方法可被在任何時間被調用. 實際上, 這樣的第一個調用将可能發生, 即便在 add_disk 傳回之前; 核心将讀前幾個位元組以試圖找到一個分區表. 是以你不應當調用 add_disk 直到你的驅動被完全初始化并且準備好響應對那個磁盤的請求.

16.1.3. 在 sbull 中的初始化

是時間進入一些例子了. sbull 驅動(從 O'Reilly 的 FTP 網站, 以及其他例子源碼)實作一套記憶體中的虛拟磁盤驅動器. 對每個驅動器, sbull 配置設定(使用 vmalloc, 為了簡單)一個記憶體數組; 它接着使這個數組可通過塊操作來使用. 這個 sbull 驅動可通過分區這個驅動器, 在上面建立檔案系統, 以及加載到系統層級中來測試.

象我們其他的例子驅動一樣, sbull 允許一個主編号在編譯或者子產品加載時被指定. 如果沒有指定, 動态配置設定一個. 因為對 register_blkdev 的調用被用來動态配置設定, sbull 應當這樣做:

sbull_major = register_blkdev(sbull_major, "sbull");

if (sbull_major

同樣, 象我們在本書已展現的其他虛拟裝置, sbull 裝置由一個内部結構描述:

struct sbull_dev {

int size;  

u8 *data;  

short users;  

short media_change;  

spinlock_t lock;  

struct request_queue *queue;  

struct gendisk *gd;  

struct timer_list timer;    

};  

需要幾個步驟來初始化這個結構, 并且使系統可用關聯的裝置. 我們從基本的初始化開始, 并且配置設定底層的記憶體:

memset (dev, 0, sizeof (struct sbull_dev));

dev->size = nsectors*hardsect_size;

dev->data = vmalloc(dev->size);

if (dev->data == NULL)

{

        printk (KERN_NOTICE "vmalloc failure.\n");

        return;

}

spin_lock_init(&dev->lock);

重要的是在下一步之前配置設定和初始化一個自旋鎖, 下一步是配置設定請求隊列. 我們在進入請求處理時詳細看這個過程; 現在, 隻需說必要的調用是:

dev->queue = blk_init_queue(sbull_request, &dev->lock);

這裡, sbull_request 是我們的請求函數 -- 實際進行塊讀和寫請求的函數. 當我們配置設定一個請求隊列時, 我們必須提供一個自旋鎖來控制對那個隊列的存取. 這個鎖由驅動提供而不是核心通常的部分, 因為, 常常, 請求隊列和其他的驅動資料結構在相同的臨界區; 它們可能被同時存取. 如同任何配置設定記憶體的函數, blk_init_queue 可能失敗, 是以你必須在繼續之前檢查傳回值.

一旦我們有我們的裝置記憶體和請求隊列, 我們可配置設定, 初始化, 并且安裝對應的 gendisk 結構. 做這個工作的代碼是:

dev->gd = alloc_disk(SBULL_MINORS);

if (! dev->gd)

{

        printk (KERN_NOTICE "alloc_disk failure\n");

        goto out_vfree;

}

dev->gd->major = sbull_major;

dev->gd->first_minor = which*SBULL_MINORS;

dev->gd->fops = &sbull_ops;

dev->gd->queue = dev->queue;

dev->gd->private_data = dev;

snprintf (dev->gd->disk_name, 32, "sbull%c", which + 'a');

set_capacity(dev->gd, nsectors*(hardsect_size/KERNEL_SECTOR_SIZE));

add_disk(dev->gd);

這裡, SBULL_MINORS 是每個 sbull 裝置所支援的次編号的數目. 當我們設定第一個次編号給每個裝置, 我們必須考慮被之前的裝置所用的全部編号. 磁盤的名子被設定, 這樣第一個是 sbulla, 第二個是 sbullb, 等等. 使用者空間可接着添加分區号以便它們在第 2 個裝置上的分區可能是 /dev/sbull3.

一旦所有的都被設定, 我們以對 add_disk 的調用來結束. 我們的幾個方法将在 add_disk 傳回時被調用, 是以我們負責做這個調用, 這是初始化我們的裝置的最後一步.

16.1.4. 注意扇區大小

如同我們之前提到的, 核心對待每個磁盤如同一個 512-位元組扇區的數組. 不是所有的硬體都使用那個扇區大小, 但是. 使一個有不同扇區大小的裝置工作不是一件很難的事; 隻要小心處理幾個細節. sbull 裝置輸出一個 hardsect_size 參數, 可被用來改變裝置的"硬體"扇區大小. 通過看它的實作, 你可見到如何添加這個支援到你自己的驅動.

這些細節中的第一個是通知核心你的裝置支援的扇區大小. 硬體扇區大小是一個在請求隊列的參數, 而不是在 gendisk 結構. 這個大小通過調用 blk_queue_hardsect_size 設定的, 在配置設定隊列後馬上進行:

blk_queue_hardsect_size(dev->queue, hardsect_size);

一旦完成那個, 核心堅持你的裝置的硬體扇區大小. 所有的 I/O 請求被正确對齊到一個硬體扇區的起始, 并且每個請求的長度是一個整數的扇區數. 你必須記住, 但是, 核心一直以 512-位元組扇區表述自己; 是以, 有必要相應地轉換所有的扇區号. 是以, 例如, 當 sbull 在它的 gendisk 結構中設定裝置的容量時, 這個調用看來象:

set_capacity(dev->gd, nsectors*(hardsect_size/KERNEL_SECTOR_SIZE));

KERNEL_SECTOR_SIZE 是一個本地定義的常量, 我們用來調整核心的 512-位元組和任何我們已被告知要使用的大小. 在我們檢視 sbull 請求處理邏輯中會不時看到這類計算出來.

16.2. 塊裝置操作

在前面一節中我們對 block_device_operations 有了簡短的介紹. 現在我們詳細些看看這些操作, 在進入請求處理之前. 為此, 是時間提到 sbull 驅動的另一個特性: 它假裝是一個可移出的裝置. 無論何時最後一個使用者關閉裝置, 一個 30 秒的定時器被設定; 如果裝置在這個時間内不被打開, 裝置的内容被清除, 并且核心被告知媒體已被改變. 30 秒延遲給了使用者時間, 例如, 來解除安裝一個 sbull 裝置在建立一個檔案系統之後.

16.2.1. open 和 release 方法

為實作模拟的媒體移出, 當最後一個使用者已關閉裝置時 sbull 必須知道. 一個使用者計數被驅動維護. 它是 open 和 close 方法的工作來保持這個計數最新.

open 方法看起來非常類似于它的字元驅動對等體; 它用相關的節點和檔案結構指針作為參數. 當一個節點引用一個塊裝置, i_bdev->bd_disk 包含一個指向關聯 gendisk 結構的指針; 這個指針可用來獲得一個驅動的給裝置的内部資料結構. 即, 實際上, sbull open 方法做的第一件事:

static int sbull_open(struct inode *inode, struct file *filp)

{

        struct sbull_dev *dev = inode->i_bdev->bd_disk->private_data;

        del_timer_sync(&dev->timer);

        filp->private_data = dev;

        spin_lock(&dev->lock)

        ;

        if (! dev->users)

                check_disk_change(inode->i_bdev);

        dev->users++;

        spin_unlock(&dev->lock)

        ;

        return 0;

}

一旦 sbull_open 有它的裝置結構指針, 它調用 del_timer_sync 來去掉"媒體移出"定時器, 如果有一個是活的. 注意我們不加鎖裝置自旋鎖, 直到定時器被删除後; 如果定時器函數在我們可删除它之前運作, 反過來做會有死鎖. 在裝置加鎖下, 我們調用一個核心函數, 稱為 check_disk_change, 來檢查是否已發生一個媒體改變. 可能有人争論說核心應當做這個調用, 但是标準模式是為驅動來在打開時處理它.

最後一步是遞增使用者計數并且傳回.

釋放方法的任務是, 相反, 來遞減使用者計數, 以及, 如果被訓示了, 啟動媒體移出定時器:

static int sbull_release(struct inode *inode, struct file *filp)

{

        struct sbull_dev *dev = inode->i_bdev->bd_disk->private_data;

        spin_lock(&dev->lock)

        ;

        dev->users--;

        if (!dev->users)

        {

                dev->timer.expires = jiffies + INVALIDATE_DELAY;

                add_timer(&dev->timer);

        }

        spin_unlock(&dev->lock)

        ;

        return 0;

}

在一個處理真實的硬體裝置的驅動中, open 和 release 方法應當相應地設定驅動和硬體的狀态. 這個工作可能包括起停磁盤, 加鎖一個可移出裝置的門, 配置設定 DMA 緩沖, 等等.

你可能奇怪誰實際上打開了一個塊裝置. 有一些操作可導緻一個塊裝置從使用者空間直接打開; 這包括分區一個磁盤, 在一個分區上建立一個檔案系統, 或者運作一個檔案系統檢查器. 當加載一個分區時, 塊驅動也可看到一個 open 調用. 在這個情況下, 沒有使用者空間程序持有一個這個裝置的打開的檔案描述符; 相反, 打開的檔案被核心自身持有. 塊驅動無法知道一個加載操作(它從核心打開裝置)和調用如 mkfs 工具(從使用者空間打開它)之間的差别.

16.2.2. 支援可移出的媒體

block_device_operations 結構包含 2 個方法來支援可移出媒體. 如果你為一個非可移出裝置編寫一個驅動, 你可安全地忽略這些方法. 它們的實作是相對直接的.

media_changed 方法被調用( 從 check_disk_change ) 來看是否媒體已經被改變; 它應當傳回一個非零值, 如果已經發生. sbull 實作是簡單的; 它查詢一個已被設定的标志, 如果媒體移出定時器已逾時:

int sbull_media_changed(struct gendisk *gd)

{

        struct sbull_dev *dev = gd->private_data;

        return dev->media_change;

}

revalidate 方法在媒體改變後被調用; 它的工作是做任何需要的事情來準備驅動對新媒體的操作, 如果有. 在調用 revalidate 之後, 核心試圖重新讀分區表并且啟動這個裝置. sbull 的實作僅僅複位 media_change 标志并且清零裝置記憶體來模拟一個空盤插入.

int sbull_revalidate(struct gendisk *gd)

{

        struct sbull_dev *dev = gd->private_data;

        if (dev->media_change)

        {

                dev->media_change = 0;

                memset (dev->data, 0, dev->size);

        }

        return 0;

}

16.2.3. ioctl 方法

塊裝置可提供一個 ioctl 方法來進行裝置控制函數. 高層的塊子系統代碼在你的驅動能見到它們之前解釋許多的 ioctl 指令, 但是( 全部内容見 drivers/block/ioctl.c , 在核心源碼中). 實際上, 一個現代的塊驅動根本不必實作許多的 ioctl 指令.

sbull ioctl 方法隻處理一個指令 -- 一個對裝置的結構的請求:

int sbull_ioctl (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)

{

        long size;

        struct hd_geometry geo;

        struct sbull_dev *dev = filp->private_data;

        switch(cmd)

        {

        case HDIO_GETGEO:

                size = dev->size*(hardsect_size/KERNEL_SECTOR_SIZE);

                geo.cylinders = (size & ~0x3f) >> 6;

                geo.heads = 4;

                geo.sectors = 16;

                geo.start = 4;

                if (copy_to_user((void __user *) arg, &geo, sizeof(geo)))

                        return -EFAULT;

                return 0;

        }

        return -ENOTTY;

}

提供排列資訊可能看來象一個奇怪的任務, 因為我們的裝置是純粹虛拟的并且和磁道和柱面沒任何關系. 甚至大部分真正的塊硬體都已很多年不再有很多更複雜的結構. 核心不關心一個塊裝置的排列; 隻把它看作一個扇區的線性數組. 但是, 有某些使用者工具仍然想能夠查詢一個磁盤的排列. 特别的, fdisk 工具, 它編輯分區表, 依靠柱面資訊并且如果這個資訊沒有則不能正确工作.

我們希望 sbull 裝置是可分區的, 即便使用老的, 簡單的工具. 是以, 我們已提供了一個 ioctl 方法, 這個方法提供了一個可靠的能夠比對我們裝置容量的排列的假象. 大部分磁盤驅動做類似的事情. 注意, 象通常, 扇區計數被轉換, 如果需要, 來比對核心使用的 512-位元組 的慣例.

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 類型. 這個類型, 和許多操作它的函數, 定義在 . 如果你對請求隊列的實作感興趣, 你可找到大部分代碼在 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 結構, 在  中定義, 包含許多成員對驅動作者是有用的:

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 結構

顯示了這些結構如何結合在一起. 如同你所見到的, 在一個塊 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) {

        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;

}

這個函數簡單地步入每個 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 函數, 一個驅動真正隻覆寫一個特定的請求隊列方法, 并且排序大部分工作.

16.4. 一些其他的細節

本節涵蓋塊層的幾個其他的方面, 對于進階讀者可能有興趣. 對于編寫一個正确的驅動下面的内容都不需要, 但是它們在某些情況下可能是有用的.

16.4.1. 指令預準備

塊層為驅動提供一個進制來檢查和預處理請求, 在它們被從 elv_next_request 傳回前. 這個機制允許驅動提前設立真正的驅動器指令, 決定是否這個請求可被完全處理, 或者進行其他的維護工作.

如果你想使用這個特性, 建立一個指令準備函數, 它要适應這個原型:

typedef int (prep_rq_fn) (request_queue_t *queue, struct request *req);

請求結構包含一個成員 cmd, 它是一個 BLK_MAX_CDB 位元組的數組; 這個數組可被這個準備函數用來存儲實際的硬體指令(或者任何其他的有用資訊). 這個函數應當傳回一個下列的值:

BLKPREP_OK

指令準備正常進行, 并且這個請求可被傳遞給你的驅動的請求函數.

BLKPREP_KILL

這個請求不能完成; 它帶有一個錯誤碼而失敗.

BLKPREP_DEFER

這個請求這次無法完成. 它位于隊列的前面, 但是不能傳遞給請求函數.

準備函數被 elv_next_request 在請求傳回到你的驅動之前立刻調用. 如果這個函數傳回 BLKPREP_DEFER, 從 elv_next_request 傳回給你的驅動的傳回值是 NULL. 這個操作描述可能是有用的, 如果, 例如你的裝置已達到它能夠等候的請求的最大數目.

為使塊層調用你的準備函數, 傳遞它到:

void blk_queue_prep_rq(request_queue_t *queue, prep_rq_fn *func);

預設地, 請求隊列沒有準備函數.

16.4.2. 被辨別的指令排隊

可同時有多個請求被激活的硬體, 常常支援某種被辨別的指令排隊(TCQ). TCQ 簡單地說是關聯一個整數 "tag" 到每個請求的技術, 注意當驅動器完成每個請求時, 他可告知驅動是哪一個. 在以前的核心版本, 實作 TCQ 的塊驅動不得不自己做所有的工作; 在2.6, 一個 TCQ 支援架構已經被添加到塊層, 以給所有的驅動來使用.

如果你的驅動器進行标記指令排隊, 你應當在初始化時通知核心這個事實, 使用:

int blk_queue_init_tags(request_queue_t *queue, int depth, struct blk_queue_tag *tags);

這裡, queue 是你的請求隊列, 而 depth 是你的裝置能夠在任何時間擁有的等待的标記請求的數目. tags 是一個可選的指針指向一個 struct blk_queue_tag 結構數組; 必須有 depth 個. 正常地, tags 可用 NULL, 并且 blk_queue_init_tags 配置設定這個 數組. 如果, 但是, 你需要和多個裝置分享通用的 tags, 你可傳遞這個标記數組指針(存儲在 queue_tags 成員)從另一個請求隊列. 你應當從不真正自己配置設定這個标記數組; 塊層需要初始化這個數組并且不輸出這個初始化函數給子產品.

因為 blk_queue_init_tags 配置設定記憶體, 它可能失敗. 在那個情況下它傳回一個負的錯誤碼給調用者.

如果你的裝置可處理的标記的數目改變了, 你可通知核心, 使用:

int blk_queue_resize_tags(request_queue_t *queue, int new_depth);

這個隊列鎖必須在這個調用期間被持有. 這個調用可能失敗, 傳回一個負錯誤碼.

一個标記和一個請求結構的關聯被 blk_queue_start_tag 來完成, 它必須在成員隊列鎖被持有時調用:

int blk_queue_start_tag(request_queue_t *queue, struct request *req);

如果一個 tag 可用, 這個函數配置設定它給這個請求, 存儲這個辨別号在 req->tag, 并且傳回 0. 它還從隊列中解除這個請求, 并且連接配接它到它自己的辨別跟蹤結構, 是以你的驅動應當小心不從隊列中解除這個請求, 如果在使用辨別. 如果沒有辨別可用, blk_queue_start_tag 将這個請求留在隊列并且傳回一個非零值.

當一個給定的請求的所有的傳送都已完成, 你的驅動應當傳回辨別, 使用:

void blk_queue_end_tag(request_queue_t *queue, struct request *req);

再一次, 你必須持有隊列鎖, 在調用這個函數之前. 這個調用應當在 end_that_request_first 傳回 0 之後進行(意味着這個請求完成), 但要在調用 end_that_request_last 之前. 記住這個請求已經從隊列中解除, 是以它對于你的驅動在此點這樣做可能是一個錯誤.

如果你需要找到關聯到一個給定辨別上的請求(當驅動器報告完成, 例如), 使用 blk_queue_find_tag:

struct request *blk_queue_find_tag(request_queue_t *qeue, int tag);

傳回值是關聯的請求結構, 除非有些事情已經真的出錯了.

如果事情真地出錯了, 你的請求可能發現它自己不得不複位或者對其中一個它的裝置進行一些其他的大動作. 在這種情況下, 任何等待中的辨別指令将不會完成. 塊層提供一個函數可用幫助在這種情況下恢複:

void blk_queue_invalidate_tags(request_queue_t *queue);

這個函數傳回所有的等待的辨別給這個池, 并且将關聯的請求放回請求隊列. 你調用這個函數時必須持有隊列鎖.

16.5. 快速參考

#include  

int register_blkdev(unsigned int major, const char *name);

int unregister_blkdev(unsigned int major, const char *name);

register_blkdev 注冊一個塊驅動到核心, 并且, 可選地, 獲得一個主編号. 一個驅動可被登出, 使用 unregister_blkdev.

struct block_device_operations

持有大部分塊驅動的方法的結構.

#include  

struct gendisk;

描述核心中單個塊裝置的結構.

struct gendisk *alloc_disk(int minors);

void add_disk(struct gendisk *gd);

配置設定 gendisk 結構的函數, 并且傳回它們到系統.

void set_capacity(struct gendisk *gd, sector_t sectors);

儲存設備能力(以 512-位元組)在 gendisk 結構中.

void add_disk(struct gendisk *gd);

添加一個磁盤到核心. 一旦調用這個函數, 你的磁盤的方法可被核心調用.

int check_disk_change(struct block_device *bdev);

一個核心函數, 檢查在給定磁盤驅動器中的媒體改變, 并且采取要求的清理動作當檢測到這樣一個改變.

#include  

request_queue_t blk_init_queue(request_fn_proc *request, spinlock_t *lock);

void blk_cleanup_queue(request_queue_t *);

處理塊請求隊列的建立和删除的函數.

struct request *elv_next_request(request_queue_t *queue);

void end_request(struct request *req, int success);

elv_next_request 從一個請求隊列中獲得下一個請求; end_request 可用在每個簡單驅動器中來辨別一個(或部分)請求完成.

void blkdev_dequeue_request(struct request *req);

void elv_requeue_request(request_queue_t *queue, struct request *req);

從隊列中除去一個請求, 并且放回它的函數如果需要.

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);

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_segment_boundary(request_queue_t *queue, unsigned long mask);

void blk_queue_dma_alignment(request_queue_t *queue, int mask);

void blk_queue_hardsect_size(request_queue_t *queue, unsigned short max);

設定各種隊列參數的函數, 來控制請求如何被建立給一個特殊裝置; 這些參數在"隊列控制函數"一節中描述.

#include  

struct bio;

低級函數, 表示一個塊 I/O 請求的一部分.

bio_sectors(struct bio *bio);

bio_data_dir(struct bio *bio);

2 個宏定義, 表示一個由 bio 結構描述的傳送的大小和方向.

bio_for_each_segment(bvec, bio, segno);

一個僞控制結構, 用來循環組成一個 bio 結構的各個段.

char *__bio_kmap_atomic(struct bio *bio, int i, enum km_type type);

void __bio_kunmap_atomic(char *buffer, enum km_type type);

__bio_kmap_atomic 可用來建立一個核心虛拟位址給一個在 bio 結構中的給定的段. 映射必須使用 __bio_kunmap_atomic 來恢複.

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);

char *bio_kmap_irq(struct bio *bio, unsigned long *flags);

void bio_kunmap_irq(char *buffer, unsigned long *flags);

一組存取者宏定義, 提供對一個 bio 結構中的"目前"段的存取.

void blk_queue_ordered(request_queue_t *queue, int flag);

int blk_barrier_rq(struct request *req);

如果你的驅動實作屏障請求, 調用 blk_queue_ordered -- 如同它應當做的. 宏 blk_barrier_rq 傳回一個非零值如果目前請求是一個屏障請求.

int blk_noretry_request(struct request *req);

這個宏傳回一個非零值, 如果給定的請求不應當在出錯時重新嘗試.

int end_that_request_first(struct request *req, int success, int count);

void end_that_request_last(struct request *req);

使用 end_that_request_firest 來訓示一個塊 I/O 請求的一部分完成. 當那個函數傳回 0, 請求完成并且應當被傳遞給 end_that_request_last.

rq_for_each_bio(bio, request)

另一個用宏定義來實作的控制結構; 它步入構成一個請求的每個 bio.

int blk_rq_map_sg(request_queue_t *queue, struct request *req, struct scatterlist *list);

為一次 DMA 傳送填充給定的散布表, 用需要來映射給定請求中的緩沖的資訊

typedef int (make_request_fn) (request_queue_t *q, struct bio *bio);

make_request 函數的原型.

void bio_endio(struct bio *bio, unsigned int bytes, int error);

訓示一個給定 bio 的完成. 這個函數應當隻用在你的驅動直接擷取 bio , 通過 make_request 函數從塊層.

request_queue_t *blk_alloc_queue(int flags);

void blk_queue_make_request(request_queue_t *queue, make_request_fn *func);

使用 blk_alloc_queue 來配置設定由定制的 make_request 函數使用的請求隊列, . 那個函數應當使用 blk_queue_make_request 來設定.

typedef int (prep_rq_fn) (request_queue_t *queue, struct request *req);

void blk_queue_prep_rq(request_queue_t *queue, prep_rq_fn *func);

一個指令準備函數的原型和設定函數, 它可用來準備必要的硬體指令, 在請求被傳遞給你的請求函數之前.

int blk_queue_init_tags(request_queue_t *queue, int depth, struct blk_queue_tag *tags);

int blk_queue_resize_tags(request_queue_t *queue, int new_depth);

int blk_queue_start_tag(request_queue_t *queue, struct request *req);

void blk_queue_end_tag(request_queue_t *queue, struct request *req);

struct request *blk_queue_find_tag(request_queue_t *qeue, int tag);

void blk_queue_invalidate_tags(request_queue_t *queue);

驅動使用被标記的指令隊列的支援函數.

http://blog.chinaunix.net/u2/78225/showart_1270153.html