Linux塊裝置驅動
第十三章 Linux塊裝置驅動
本章導讀
塊裝置是與字元裝置並列的概念,這兩類裝置在Linux中驅動的結構有較大差異,總體而言,塊裝置驅動比字元裝置驅動要複雜得多,在I/O操作上表現出極大的不同,緩衝、I/O排程、請求佇列等都是與塊裝置驅動相關的概念。本章將向您展示Linux塊裝置驅動的程式設計方法。
13.1節分析塊裝置I/O操作的特點,對比字元裝置與塊裝置在I/O操作上的差異。
13.2節從整體上描述Linux塊裝置驅動的結構,分析主要的資料結構、函式及其關係。
13.3~13.5節分別闡述塊裝置驅動模組載入與解除安裝、開啟與釋放和ioctl()函式。
13.6節非常重要,它講述了塊裝置I/O操作所依賴的請求佇列的概念及用法。
13.2節與13.3~13.6節是整體與部分的關係,13.2~13.6節與13.7節是迭代遞進關係。
13.7節在13.1~13.6節講解內容的基礎上,總結Linux下塊裝置的讀寫流程。而13.7節則給出了塊裝置驅動的具體例項,即RAMDISK的驅動。
13.1塊裝置的I/O操作特點
字元裝置與塊裝置I/O操作的不同在於:
① 塊裝置只能以塊為單位接受輸入和返回輸出,而字元裝置則以位元組為單位。大多數裝置是字元裝置,因為它們不需要緩衝而且不以固定塊大小進行操作。
② 塊裝置對於I/O請求有對應的緩衝區,因此它們可以選擇以什麼順序進行響應,字元裝置無需緩衝且被直接讀寫。對於儲存裝置而言調整讀寫的順序作用巨大,因為在讀寫連續的扇區比分離的扇區更快。
③ 字元裝置只能被順序讀寫,而塊裝置可以隨機訪問。雖然塊裝置可隨機訪問,但是對於磁碟這類機械裝置而言,順序地組織塊裝置的訪問可以提高效能。如圖13.1,對磁碟1、10、3、2的請求被調整為對1、2、3、10的請求可以提高讀寫效能。注意,對SD卡、RAMDISK等塊裝置而言,不存在機械上的原因,進行這樣的調整沒有必要。
圖13.1 調整塊裝置I/O操作的順序
13.2 Linux塊裝置驅動結構
13.2.1 block_device_operations結構體
在塊裝置驅動中,有1個類似於字元裝置驅動中file_operations結構體的block_device_operations結構體,它是對塊裝置操作的集合,定義如程式碼清單13.1。
程式碼清單13.1 block_device_operations結構體
1 struct block_device_operations
2 {
3 int(*open)(struct inode *, struct file*); //開啟
4 int(*release)(struct inode *, struct file*); //釋放
5 int(*ioctl)(struct inode *, struct file *, unsigned, unsigned long); //ioctl
6 long(*unlocked_ioctl)(struct file *, unsigned, unsigned long);
7 long(*compat_ioctl)(struct file *, unsigned, unsigned long);
8 int(*direct_access)(struct block_device *, sector_t, unsigned long*);
9 int(*media_changed)(struct gendisk*); //介質被改變?
10 int(*revalidate_disk)(struct gendisk*); //使介質有效
11 int(*getgeo)(struct block_device *, struct hd_geometry*);//填充驅動器資訊
12 struct module *owner; //模組擁有者
13 };
下面對其主要的成員函式進行分析:
• 開啟和釋放
int (*open)(struct inode *inode, struct file *filp);
int (*release)(struct inode *inode, struct file *filp);
與字元裝置驅動類似,當裝置被開啟和關閉時將呼叫它們。
• IO控制
int (*ioctl)(struct inode *inode, struct file *filp, unsigned int cmd,
unsigned long arg);
上述函式是ioctl() 系統呼叫的實現,塊裝置包含大量的標準請求,這些標準請求由Linux塊裝置層處理,因此大部分塊裝置驅動的ioctl()函式相當短。
• 介質改變
int (*media_changed) (struct gendisk *gd);
被核心呼叫來檢查是否驅動器中的介質已經改變,如果是,則返回一個非零值,否則返回0。這個函式僅適用於支援可移動介質的驅動器(非可移動裝置的驅動不需要實現這個方法),通常需要在驅動中增加1個表示介質狀態是否改變的標誌變數。
• 使介質有效
int (*revalidate_disk) (struct gendisk *gd);
revalidate_disk()函式被呼叫來響應一個介質改變,它給驅動一個機會來進行必要的工作以使新介質準備好。
• 獲得驅動器資訊
int (*getgeo)(struct block_device *, struct hd_geometry *);
該函式根據驅動器的幾何資訊填充一個hd_geometry結構體,hd_geometry結構體包含磁頭、扇區、柱面等資訊。
• 模組指標
struct module *owner;
一個指向擁有這個結構體的模組的指標,它通常被初始化為THIS_MODULE。
13.2.2 gendisk結構體
在Linux核心中,使用gendisk(通用磁碟)結構體來表示1個獨立的磁碟裝置(或分割槽),這個結構體的定義如程式碼清單13.2。
程式碼清單13.2 gendisk結構體
1 struct gendisk
2 {
3 int major; /* 主裝置號 */
4 int first_minor; /*第1個次裝置號*/
5 int minors; /* 最大的次裝置數,如果不能分割槽,則為1*/
6 char disk_name[32]; /* 裝置名稱 */
7 struct hd_struct **part; /* 磁碟上的分割槽資訊 */
8 struct block_device_operations *fops; /*塊裝置操作結構體*/
9 struct request_queue *queue; /*請求佇列*/
10 void *private_data; /*私有資料*/
11 sector_t capacity; /*扇區數,512位元組為1個扇區*/
12
13 int flags;
14 char devfs_name[64];
15 int number;
16 struct device *driverfs_dev;
17 struct kobject kobj;
18
19 struct timer_rand_state *random;
20 int policy;
21
22 atomic_t sync_io; /* RAID */
23 unsigned long stamp;
24 int in_flight;
25 #ifdef CONFIG_SMP
26 struct disk_stats *dkstats;
27 #else
28 struct disk_stats dkstats;
29 #endif
30 };
major、first_minor和minors共同表徵了磁碟的主、次裝置號,同一個磁碟的各個分割槽共享1個主裝置號,而次裝置號則不同。fops為block_device_operations,即上節描述的塊裝置操作集合。queue是核心用來管理這個裝置的 I/O請求佇列的指標。capacity表明裝置的容量,以512個位元組為單位。private_data可用於指向磁碟的任何私有資料,用法與字元裝置驅動file結構體的private_data類似。
Linux核心提供了一組函式來操作gendisk,主要包括:
• 分配gendisk
gendisk結構體是一個動態分配的結構體,它需要特別的核心操作來初始化,驅動不能自己分配這個結構體,而應該使用下列函式來分配gendisk:
struct gendisk *alloc_disk(int minors);
minors 引數是這個磁碟使用的次裝置號的數量,一般也就是磁碟分割槽的數量,此後minors不能被修改。
• 增加gendisk
gendisk結構體被分配之後,系統還不能使用這個磁碟,需要呼叫如下函式來註冊這個磁碟裝置:
void add_disk(struct gendisk *gd);
特別要注意的是對add_disk()的呼叫必須發生在驅動程式的初始化工作完成並能響應磁碟的請求之後。
• 釋放gendisk
當不再需要一個磁碟時,應當使用如下函式釋放gendisk:
void del_gendisk(struct gendisk *gd);
• gendisk引用計數
gendisk中包含1個kobject成員,因此,它是一個可被引用計數的結構體。透過get_disk()和put_disk()函式可用來操作引用計數,這個工作一般不需要驅動親自做。通常對 del_gendisk()的呼叫會去掉gendisk的最終引用計數,但是這一點並不是一定的。因此,在del_gendisk()被呼叫後,這個結構體可能繼續存在。
• 設定gendisk容量
void set_capacity(struct gendisk *disk, sector_t size);
塊裝置中最小的可定址單元是扇區,扇區大小一般是2的整數倍,最常見的大小是512位元組。扇區的大小是裝置的物理屬性,扇區是所有塊裝置的基本單元,塊裝置無法對比它還小的單元進行定址和操作,不過許多塊裝置能夠一次就傳輸多個扇區。雖然大多數塊裝置的扇區大小都是512位元組,不過其它大小的扇區也很常見,比如,很多CD-ROM盤的扇區都是2K大小。
不管物理裝置的真實扇區大小是多少,核心與塊裝置驅動互動的扇區都以512位元組為單位。因此,set_capacity()函式也以512位元組為單位。
13.2.3 request與bio結構體
1、請求
在Linux塊裝置驅動中,使用request結構體來表徵等待進行的I/O請求,這個結構體的定義如程式碼清單13.3。
程式碼清單13.3 request結構體
1 struct request
2 {
3 struct list_head queuelist; /*連結串列結構*/
4 unsigned long flags; /* REQ_ */
5
6 sector_t sector; /* 要傳送的下1個扇區 */
7 unsigned long nr_sectors; /*要傳送的扇區數目*/
8 /*當前要傳送的扇區數目*/
9 unsigned int current_nr_sectors;
10
11 sector_t hard_sector; /*要完成的下1個扇區*/
12 unsigned long hard_nr_sectors; /*要被完成的扇區數目*/
13 /*當前要被完成的扇區數目*/
14 unsigned int hard_cur_sectors;
15
16 struct bio *bio; /*請求的 bio 結構體的連結串列*/
17 struct bio *biotail; /*請求的 bio 結構體的連結串列尾*/
18
19 void *elevator_private;
20
21 unsigned short ioprio;
22
23 int rq_status;
24 struct gendisk *rq_disk;
25 int errors;
26 unsigned long start_time;
27
28 /*請求在實體記憶體中佔據的不連續的段的數目,scatter/gather列表的尺寸*/
29 unsigned short nr_phys_segments;
30
31 /*與nr_phys_segments相同,但考慮了系統I/O MMU的remap */
32 unsigned short nr_hw_segments;
33
34 int tag;
35 char *buffer; /*傳送的緩衝,核心虛擬地址*/
36
37 int ref_count; /* 引用計數 */
38 ...
39 };
request結構體的主要成員包括:
sector_t hard_sector;
unsigned long hard_nr_sectors;
unsigned int hard_cur_sectors;
上述3個成員標識還未完成的扇區,hard_sector是第1個尚未傳輸的扇區,hard_nr_sectors是尚待完成的扇區數,hard_cur_sectors是並且當前I/O操作中待完成的扇區數。這些成員只用於核心塊裝置層,驅動不應當使用它們。
sector_t sector;
unsigned long nr_sectors;
unsigned int current_nr_sectors;
驅動中會經常與這3個成員打交道,這3個成員在核心和驅動互動中發揮著重大作用。它們以512位元組大小為1個扇區,如果硬體的扇區大小不是512位元組,則需要進行相應的調整。例如,如果硬體的扇區大小是2048位元組,則在進行硬體操作之前,需要用4來除起始扇區號。
hard_sector、hard_nr_sectors、hard_cur_sectors與sector、nr_sectors、current_nr_sectors之間可認為是“副本”關係。
struct bio *bio;
bio是這個請求中包含的bio結構體的連結串列,驅動中不宜直接存取這個成員,而應該使用後文將介紹的rq_for_each_bio()。
char *buffer;
指向緩衝區的指標,資料應當被傳送到或者來自這個緩衝區,這個指標是一個核心虛擬地址,可被驅動直接引用。
unsigned short nr_phys_segments;
該值表示相鄰的頁被合併後,這個請求在實體記憶體中佔據的段的數目。如果裝置支援分散/聚集(SG,scatter/gather)操作,可依據此欄位申請sizeof(scatterlist)* nr_phys_segments的記憶體,並使用下列函式進行DMA對映:
int blk_rq_map_sg(request_queue_t *q, struct request *req,
struct scatterlist *sglist);
該函式與dma_map_sg()類似,它返回scatterlist列表入口的數量。
struct list_head queuelist;
用於連結這個請求到請求佇列的連結串列結構,呼叫blkdev_dequeue_request()可從佇列中移除請求。
使用如下宏可以從request獲得資料傳送的方向:
rq_data_dir(struct request *req);
0返回值表示從裝置中讀,非 0返回值表示向裝置寫。
2、請求佇列
一個塊請求佇列是一個塊 I/O 請求的佇列,其定義如程式碼清單13.4。
程式碼清單13.4 request佇列結構體
1 struct request_queue
2 {
3 ...
4 /* 保護佇列結構體的自旋鎖 */
5 spinlock_t __queue_lock;
6 spinlock_t *queue_lock;
7
8 /* 佇列kobject */
9 struct kobject kobj;
10
11 /* 佇列設定 */
12 unsigned long nr_requests; /* 最大請求數量 */
13 unsigned int nr_congestion_on;
14 unsigned int nr_congestion_off;
15 unsigned int nr_batching;
16
17 unsigned short max_sectors; /* 最大的扇區數 */
18 unsigned short max_hw_sectors;
19 unsigned short max_phys_segments; /* 最大的段數 */
20 unsigned short max_hw_segments;
21 unsigned short hardsect_size; /* 硬體扇區尺寸 */
22 unsigned int max_segment_size; /* 最大的段尺寸 */
23
24 unsigned long seg_boundary_mask; /* 段邊界掩碼 */
25 unsigned int dma_alignment; /* DMA 傳送的記憶體對齊限制 */
26
27 struct blk_queue_tag *queue_tags;
28
29 atomic_t refcnt; /* 引用計數 */
30
31 unsigned int in_flight;
32
33 unsigned int sg_timeout;
34 unsigned int sg_reserved_size;
35 int node;
36
37 struct list_head drain_list;
38
39 struct request *flush_rq;
40 unsigned char ordered;
41 };
請求佇列跟蹤等候的塊I/O請求,它儲存用於描述這個裝置能夠支援的請求的型別資訊、它們的最大大小、多少不同的段可進入一個請求、硬體扇區大小、對齊要求等引數,其結果是:如果請求佇列被配置正確了,它不會交給該裝置一個不能處理的請求。
請求佇列還實現一個插入介面,這個介面允許使用多個I/O排程器,I/O排程器(也稱電梯)的工作是以最優效能的方式向驅動提交I/O請求。大部分I/O 排程器累積批次的 I/O 請求,並將它們排列為遞增(或遞減)的塊索引順序後提交給驅動。進行這些工作的原因在於,對於磁頭而言,當給定順序排列的請求時,可以使得磁碟順序地從一頭到另一頭工作,非常像一個滿載的電梯,在一個方向移動直到所有它的“請求”已被滿足。
另外,I/O排程器還負責合併鄰近的請求,當一個新 I/O 請求被提交給排程器後,它會在佇列裡搜尋包含鄰近扇區的請求;如果找到一個,並且如果結果的請求不是太大,排程器將合併這2個請求。
對磁碟等塊裝置進行I/O操作順序的排程類似於電梯的原理,先服務完上樓的乘客,再服務下樓的乘客效率會更高,而“上躥下跳”,順序響應使用者的請求則會導致電梯無序地忙亂。
Linux 2.6包含4個I/O排程器,它們分別是No-op I/O scheduler、Anticipatory I/O scheduler、Deadline I/O scheduler與CFQ I/O scheduler。
Noop I/O scheduler是一個簡化的排程程式,它只作最基本的合併與排序。
Anticipatory I/O scheduler是當前核心中預設的I/O排程器,它擁有非常好的效能,在2.5中它就相當引人注意。在與2.4核心進行的對比測試中,在2.4中多項以分鐘為單位完成的任務,它則是以秒為單位來完成的,正因為如此它成為目前2.6中預設的I/O排程器。Anticipatory I/O scheduler的缺點是比較龐大與複雜,在一些特殊的情況下,特別是在資料吞吐量非常大的資料庫系統中它會變得比較緩慢。
Deadline I/O scheduler是針對Anticipatory I/O scheduler的缺點進行改善而來的,表現出的效能幾乎與Anticipatory I/O scheduler一樣好,但是比Anticipatory小巧。
CFQ I/O scheduler為系統內的所有任務分配相同的頻寬,提供一個公平的工作環境,它比較適合桌面環境。事實上在測試中它也有不錯的表現,mplayer、xmms等多媒體播放器與它配合的相當好,回放平滑,幾乎沒有因訪問磁碟而出現的跳幀現象。
核心block目錄中的noop-iosched.c、as-iosched.c、deadline-iosched.c和cfq-iosched.c檔案分別實現了上述排程演算法。
可以透過給kernel新增啟動引數,選擇使用的IO排程演算法,如:
kernel elevator=deadline
• 初始化請求佇列
request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
該函式的第1個引數是請求處理函式的指標,第2個引數是控制訪問佇列許可權的自旋鎖,這個函式會發生記憶體分配的行為,故它可能會失敗,因此一定要檢查它的返回值。這個函式一般在塊裝置驅動的模組載入函式中呼叫。
• 清除請求佇列
void blk_cleanup_queue(request_queue_t * q);
這個函式完成將請求佇列返回給系統的任務,一般在塊裝置驅動模組解除安裝函式中呼叫。
而blk_put_queue()宏則定義為:
#define blk_put_queue(q) blk_cleanup_queue((q))
• 分配“請求佇列”
request_queue_t *blk_alloc_queue(int gfp_mask);
對於FLASH、RAM盤等完全隨機訪問的非機械裝置,並不需要進行復雜的I/O排程,這個時候,應該使用上述函式分配1個“請求佇列”,並使用如下函式來繫結“請求佇列”和“製造請求”函式。
void blk_queue_make_request(request_queue_t * q, make_request_fn * mfn);
在13.6.2節我們會看到,這種方式分配的“請求佇列”實際上不包含任何請求,所以給其加上引號。
• 提取請求
struct request *elv_next_request(request_queue_t *queue);
上述函式用於返回下一個要處理的請求(由 I/O 排程器決定),如果沒有請求則返回NULL。elv_next_request()不會清除請求,它仍然將這個請求保留在佇列上,但是標識它為活動的,這個標識將阻止I/O 排程器合併其它的請求到已開始執行的請求。因為elv_next_request()不從佇列裡清除請求,因此連續呼叫它2次,2次會返回同一個請求結構體。
• 去除請求
void blkdev_dequeue_request(struct request *req);
上述函式從佇列中去除1個請求。如果驅動中同時從同一個佇列中操作了多個請求,它必須以這樣的方式將它們從佇列中去除。
如果需要將1個已經出列的請求歸還到佇列中,可以呼叫:
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_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_sectors()描述任一請求可包含的最大扇區數,預設值為255;blk_queue_max_phys_segments()和 blk_queue_max_hw_segments()都控制1個請求中可包含的最大物理段(系統記憶體中不相鄰的區),blk_queue_max_hw_segments()考慮了系統I/O記憶體管理單元的重對映,這2個引數預設都是 128。blk_queue_max_segment_size告知核心請求段的最大位元組數,預設值為65,536。
• 通告核心
void blk_queue_bounce_limit(request_queue_t *queue, u64 dma_addr);
該函式用於告知核心塊裝置執行DMA時可使用的最高實體地址dma_addr,如果一個請求包含超出這個限制的記憶體引用,一個“反彈”緩衝區將被用來給這個操作。這種方式的代價昂貴,因此應儘量避免使用。
可以給dma_addr引數提供任何可能的值或使用預先定義的宏,如BLK_BOUNCE_HIGH(對高階記憶體頁使用反彈緩衝區)、BLK_BOUNCE_ISA(驅動只可在16M的ISA區執行DMA)或者BLK_BOUCE_ANY(驅動可在任何地址執行DMA),預設值是BLK_BOUNCE_HIGH。
blk_queue_segment_boundary(request_queue_t *queue, unsigned long mask);
如果我們正在驅動編寫的裝置無法處理跨越一個特殊大小記憶體邊界的請求,應該使用這個函式來告知核心這個邊界。例如,如果裝置處理跨4MB 邊界的請求有困難,應該傳遞一個0x3fffff 掩碼。預設的掩碼是0xffffffff(對應4GB邊界)。
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位元組扇區為單位進行。
3、塊I/O
通常1個bio對應1個I/O請求,程式碼清單13.5給出了bio結構體的定義。IO排程演算法可將連續的bio合併成1個請求。所以,1個請求可以包含多個bio。
程式碼清單13.5 bio結構體
1 struct bio
2 {
3 sector_t bi_sector; /* 要傳輸的第1個扇區 */
4 struct bio *bi_next; /* 下一個bio */
5 struct block_device *bi_bdev;
6 unsigned long bi_flags; /* 狀態,命令等 */
7 unsigned long bi_rw; /* 低位表示READ/WRITE,高位表示優先順序*/
8
9 unsigned short bi_vcnt; /* bio_vec數量 */
10 unsigned short bi_idx; /* 當前bvl_vec索引 */
11
12 /*不相鄰的物理段的數目*/
13 unsigned short bi_phys_segments;
14
15 /*物理合併和DMA remap合併後不相鄰的物理段的數目*/
16 unsigned short bi_hw_segments;
17
18 unsigned int bi_size; /* 以位元組為單位所需傳輸的資料大小 */
19
20 /* 為了明瞭最大的hw尺寸,我們考慮這個bio中第1個和最後1個
21 虛擬的可合併的段的尺寸 */
22 unsigned int bi_hw_front_size;
23 unsigned int bi_hw_back_size;
24
25 unsigned int bi_max_vecs; /* 我們能持有的最大bvl_vecs數 */
26
27 struct bio_vec *bi_io_vec; /* 實際的vec列表 */
28
29 bio_end_io_t *bi_end_io;
30 atomic_t bi_cnt;
31
32 void *bi_private;
33
34 bio_destructor_t *bi_destructor; /* destructor */
35 };
下面我們對其中的核心成員進行分析:
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的陣列,它由bio_vec結構體組成,bio_vec結構體的定義如程式碼清單13.6。
程式碼清單13.6 bio_vec結構體
1 struct bio_vec
2 {
3 struct page *bv_page; /* 頁指標 */
4 unsigned int bv_len; /* 傳輸的位元組數 */
5 unsigned int bv_offset; /* 偏移位置 */
6 };
我們不應該直接訪問bio的bio_vec成員,而應該使用bio_for_each_segment()宏來進行這項工作,可以用這個宏迴圈遍歷整個bio中的每個段,這個宏的定義如程式碼清單13.7。
程式碼清單13.7 bio_for_each_segment宏
1 #define __bio_for_each_segment(bvl, bio, i, start_idx) /
2 for (bvl = bio_iovec_idx((bio), (start_idx)), i = (start_idx); /
3 i < (bio)->bi_vcnt; /
4 bvl++, i++)
5
6 #define bio_for_each_segment(bvl, bio, i) /
7 __bio_for_each_segment(bvl, bio, i, (bio)->bi_idx
圖13.2(a)描述了request佇列、request與bio資料結構之間的關係,13.2(b)表示了request、bio和bio_vec資料結構之間的關係,13.2(c)表示了bio與bio_vec資料結構之間的關係,因此整個圖13.2遞迴地呈現了request佇列、request、bio和bio_vec這4個結構體之間的關係。
(a) request與bio
(b) request、bio和bio_vec
(c)bio與bio_vec
圖13.2 request佇列、request、bio和bio_vec結構體之間的關係
核心還提供了一組函式(宏)用於操作bio:
int bio_data_dir(struct bio *bio);
這個函式可用於獲得資料傳輸的方向是READ還是WRITE。
struct page *bio_page(struct bio *bio) ;
這個函式可用於獲得目前的頁指標。
int bio_offset(struct bio *bio) ;
這個函式返回操作對應的當前頁內的偏移,通常塊I/O操作本身就是頁對齊的。
int bio_cur_sectors(struct bio *bio) ;
這個函式返回當前bio_vec要傳輸的扇區數。
char *bio_data(struct bio *bio) ;
這個函式返回資料緩衝區的核心虛擬地址。
char *bvec_kmap_irq(struct bio_vec *bvec, unsigned long *flags) ;
這個函式返回一個核心虛擬地址,這個地址可用於存取被給定的bio_vec入口指向的資料緩衝區。它也會遮蔽中斷並返回1個原子kmap,因此,在bvec_kunmap_irq()被呼叫以前,驅動不應該睡眠。
void bvec_kunmap_irq(char *buffer, unsigned long *flags);
這個函式是bvec_kmap_irq()函式的“反函式”,它撤銷bvec_kmap_irq()建立的對映。
char *bio_kmap_irq(struct bio *bio, unsigned long *flags);
這個函式是對bvec_kmap_irq()的包裝,它返回給定的bio的當前bio_vec入口的對映。
char *__bio_kmap_atomic(struct bio *bio, int i, enum km_type type);
這個函式透過kmap_atomic()獲得返回給定bio的第i個緩衝區的虛擬地址。
void __bio_kunmap_atomic(char *addr, enum km_type type);
這個函式返還由__bio_kmap_atomic()獲得的核心虛擬地址。
另外,對bio的引用計數透過如下函式完成:
void bio_get(struct bio *bio); //引用bio
void bio_put(struct bio *bio); //釋放對bio的引用
13.2.4塊裝置驅動註冊與登出
塊裝置驅動中的第1個工作通常是註冊它們自己到核心,完成這個任務的函式是 register_blkdev(),其原型為:
int register_blkdev(unsigned int major, const char *name);
major引數是塊裝置要使用的主裝置號,name為裝置名,它會在/proc/devices中被顯示。 如果major為0,核心會自動分配一個新的主裝置號,register_blkdev()函式的返回值就是這個主裝置號。如果register_blkdev()返回1個負值,表明發生了一個錯誤。
與register_blkdev()對應的登出函式是unregister_blkdev(),其原型為:
int unregister_blkdev(unsigned int major, const char *name);
這裡,傳遞給register_blkdev()的引數必須與傳遞給register_blkdev()的引數匹配,否則這個函式返回-EINVAL。
值得一提的是,在2.6核心中,對 register_blkdev()的呼叫完全是可選的,register_blkdev()的功能已隨時間正在減少,這個呼叫最多隻完全2件事:
① 如果需要,分配一個動態主裝置號。
② 在/proc/devices中建立一個入口。
在將來的核心中,register_blkdev()可能會被去掉。但是目前的大部分驅動仍然呼叫它。程式碼清單13.8給出了1個塊裝置驅動註冊的模板。
程式碼清單13.8 塊裝置驅動註冊模板
1 xxx_major = register_blkdev(xxx_major, "xxx");
2 if (xxx_major <= 0) //註冊失敗
3 {
4 printk(KERN_WARNING "xxx: unable to get major number/n");
5 return -EBUSY;
6 }
13.3 Linux塊裝置驅動模組載入與解除安裝
在塊裝置驅動的模組載入函式中通常需要完成如下工作:
① 分配、初始化請求佇列,繫結請求佇列和請求函式。
② 分配、初始化gendisk,給gendisk的major、fops、queue等成員賦值,最後新增gendisk。
③ 註冊塊裝置驅動。
程式碼清單13.9和13.10分別給出了使用blk_alloc_queue()分配請求佇列並使用blk_queue_make_request()繫結“請求佇列”和“製造請求”函式,以及使用blk_init_queue()初始化請求佇列並繫結請求佇列與請求處理函式2種不同情況下的塊裝置驅動模組載入函式模板。
程式碼清單13.9 塊裝置驅動模組載入函式模板(使用blk_alloc_queue)
1 static int __init xxx_init(void)
2 {
3 //分配gendisk
4 xxx_disks = alloc_disk(1);
5 if (!xxx_disks)
6 {
7 goto out;
8 }
9
10 //塊裝置驅動註冊
11 if (register_blkdev(XXX_MAJOR, "xxx"))
12 {
13 err = - EIO;
14 goto out;
15 }
16
17 //“請求佇列”分配
18 xxx_queue = blk_alloc_queue(GFP_KERNEL);
19 if (!xxx_queue)
20 {
21 goto out_queue;
22 }
23
24 blk_queue_make_request(xxx_queue, &xxx_make_request); //繫結“製造請求”函式
25 blk_queue_hardsect_size(xxx_queue, xxx_blocksize); //硬體扇區尺寸設定
26
27 //gendisk初始化
28 xxx_disks->major = XXX_MAJOR;
29 xxx_disks->first_minor = 0;
30 xxx_disks->fops = &xxx_op;
31 xxx_disks->queue = xxx_queue;
32 sprintf(xxx_disks->disk_name, "xxx%d", i);
33 set_capacity(xxx_disks, xxx_size); //xxx_size以512bytes為單位
34 add_disk(xxx_disks); //新增gendisk
35
36 return 0;
37 out_queue: unregister_blkdev(XXX_MAJOR, "xxx");
38 out: put_disk(xxx_disks);
39 blk_cleanup_queue(xxx_queue);
40
41 return - ENOMEM;
42 }
程式碼清單13.10 塊裝置驅動模組載入函式模板(使用blk_init_queue)
1 static int __init xxx_init(void)
2 {
3 //塊裝置驅動註冊
4 if (register_blkdev(XXX_MAJOR, "xxx"))
5 {
6 err = - EIO;
7 goto out;
8 }
9
10 //請求佇列初始化
11 xxx_queue = blk_init_queue(xxx_request, xxx_lock);
12 if (!xxx_queue)
13 {
14 goto out_queue;
15 }
16
17 blk_queue_hardsect_size(xxx_queue, xxx_blocksize); //硬體扇區尺寸設定
18
19 //gendisk初始化
20 xxx_disks->major = XXX_MAJOR;
21 xxx_disks->first_minor = 0;
22 xxx_disks->fops = &xxx_op;
23 xxx_disks->queue = xxx_queue;
24 sprintf(xxx_disks->disk_name, "xxx%d", i);
25 set_capacity(xxx_disks, xxx_size *2);
26 add_disk(xxx_disks); //新增gendisk
27
28 return 0;
29 out_queue: unregister_blkdev(XXX_MAJOR, "xxx");
30 out: put_disk(xxx_disks);
31 blk_cleanup_queue(xxx_queue);
32
33 return - ENOMEM;
34 }
在塊裝置驅動的模組解除安裝函式中通常需要與模組載入函式相反的工作:
① 清除請求佇列。
② 刪除gendisk和對gendisk的引用。
③ 刪除對塊裝置的引用,登出塊裝置驅動。
程式碼清單13.11給出了塊裝置驅動模組解除安裝函式的模板。
程式碼清單13.11 塊裝置驅動模組解除安裝函式模板
1 static void __exit xxx_exit(void)
2 {
3 if (bdev)
4 {
5 invalidate_bdev(xxx_bdev, 1);
6 blkdev_put(xxx_bdev);
7 }
8 del_gendisk(xxx_disks); //刪除gendisk
9 put_disk(xxx_disks);
10 blk_cleanup_queue(xxx_queue[i]); //清除請求佇列
11 unregister_blkdev(XXX_MAJOR, "xxx");
12 }
13.4塊裝置的開啟與釋放
塊裝置驅動的open()和release()函式並非是必須的,1個簡單的塊裝置驅動可以不提供open()和release()函式。
塊裝置驅動的open()函式和其字元裝置驅動中的對等體非常類似,都以相關的inode和file結構體指標作為引數。當一個節點引用一個塊裝置時,inode->i_bdev->bd_disk 包含一個指向關聯 gendisk 結構體的指標。因此,類似於字元裝置驅動,我們也可以將gendisk的private_data賦給file的private_data,private_data同樣最好是指向描述該裝置的裝置結構體xxx_dev的指標,如程式碼清單13.12。
程式碼清單13.12 塊裝置的open()函式中賦值private_data
1 static int xxx_open(struct inode *inode, struct file *filp)
2 {
3 struct xxx_dev *dev = inode->i_bdev->bd_disk->private_data;
4 filp->private_data = dev; //賦值file的private_data
5 ...
6 return 0;
7 }
在一個處理真實的硬體裝置的驅動中,open()和release()方法還應當設定驅動和硬體的狀態,這些工作可能包括啟停磁碟、加鎖一個可移出裝置和分配DMA緩衝等。
13.5塊裝置驅動的ioctl函式
與字元裝置驅動一樣,塊裝置可以包含一個 ioctl()函式以提供對裝置的I/O控制能力。實際上,高層的塊裝置層程式碼處理了絕大多數ioctl(),因此,具體的塊裝置驅動中通常不再需要實現很多ioctl命令。
程式碼清單13.13給出的ioctl()函式只實現1個命令HDIO_GETGEO,用於獲得磁碟的幾何資訊(geometry,指CHS,即Cylinder、Head、Sector/Track)。
程式碼清單13.13 塊裝置驅動的I/O控制函式模板
1 int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd,
2 unsigned long arg)
3 {
4 long size;
5 struct hd_geometry geo;
6 struct xxx_dev *dev = filp->private_data; //透過file->private獲得裝置結構體
7
8 switch (cmd)
9 {
10 case HDIO_GETGEO:
11 size = dev->size *(hardsect_size / KERNEL_SECTOR_SIZE);
12 geo.cylinders = (size &~0x3f) >> 6;
13 geo.heads = 4;
14 geo.sectors = 16;
15 geo.start = 4;
16 if (copy_to_user((void __user*)arg, &geo, sizeof(geo)))
17 {
18 return - EFAULT;
19 }
20 return 0;
21 }
22
23 return - ENOTTY; //不知道的命令
24 }
13.6塊裝置驅動I/O請求處理
13.6.1使用請求佇列
塊裝置驅動請求函式的原型為:
void request(request_queue_t *queue);
這個函式不能由驅動自己呼叫,只有當核心認為是時候讓驅動處理對裝置的讀寫等操作時,它才呼叫這個函式。
請求函式可以在沒有完成請求佇列中的所有請求的情況下返回,甚至它1個請求不完成都可以返回。但是,對大部分裝置而言,在請求函式中處理完所有請求後再返回通常是值得推薦的方法。程式碼清單13.14給出了1個簡單的request()函式的例子。
程式碼清單13.14 塊裝置驅動請求函式例程
1 static void xxx_request(request_queue_t *q)
2 {
3 struct request *req;
4 while ((req = elv_next_request(q)) != NULL)
5 {
6 struct xxx_dev *dev = req->rq_disk->private_data;
7 if (!blk_fs_request(req)) //不是檔案系統請求
8 {
9 printk(KERN_NOTICE "Skip non-fs request/n");
10 end_request(req, 0);//通知請求處理失敗
11 continue;
12 }
13 xxx_transfer(dev, req->sector, req->current_nr_sectors, req->buffer,
14 rq_data_dir(req)); //處理這個請求
15 end_request(req, 1); //通知成功完成這個請求
16 }
17 }
18
19 //完成具體的塊裝置I/O操作
20 static void xxx_transfer(struct xxx_dev *dev, unsigned long sector, unsigned
21 long nsect, char *buffer, int write)
22 {
23 unsigned long ffset = sector * KERNEL_SECTOR_SIZE;
24 unsigned long nbytes = nsect * KERNEL_SECTOR_SIZE;
25 if ((offset + nbytes) > dev->size)
26 {
27 printk(KERN_NOTICE "Beyond-end write (%ld %ld)/n", offset, nbytes);
28 return ;
29 }
30 if (write)
31 {
32 write_dev(offset, buffer, nbytes); //向裝置些nbytes個位元組的資料
33 }
34 else
35 {
36 read_dev(offset, buffer, nbytes); //從裝置讀nbytes個位元組的資料
37 }
38 }
上述程式碼第4行使用elv_next_request()獲得佇列中第一個未完成的請求,end_request()會將請求從請求佇列中剝離。第7行判斷請求是否為檔案系統請求,如果不是,則直接清除,呼叫end_request(),傳遞給end_request()的第2個引數為0意味著處理該請求失敗。而第15行傳遞給end_request()的第2個引數為1意味著該請求處理成功。
end_request()函式非常重要,其原始碼如程式碼清單13.15。
程式碼清單13.15 end_request()函式原始碼
1 void end_request(struct request *req, int uptodate)
2 {
3
4 if (!end_that_request_first(req, uptodate, req->hard_cur_sectors))
5 {
6 add_disk_randomness (req->rq_disk);
7 blkdev_dequeue_request (req);
8 end_that_request_last(req);
9 }
10 }
當裝置已經完成1個I/O請求的部分或者全部扇區傳輸後,它必須通告塊裝置層,上述程式碼中的第4行完成這個工作。end_that_request_first()函式的原型為:
int end_that_request_first(struct request *req, int success, int count);
這個函式告知塊裝置層,塊裝置驅動已經完成count個扇區的傳送。end_that_request_first()的返回值是一個標誌,指示是否這個請求中的所有扇區已經被傳送。返回值為0表示所有的扇區已經被傳送並且這個請求完成,之後,我們必須使用 blkdev_dequeue_request()來從佇列中清除這個請求。最後,將這個請求傳遞給end_that_request_last()函式:
void end_that_request_last(struct request *req);
end_that_request_last()通知所有正在等待這個請求完成的物件請求已經完成並回收這個請求結構體。
第6行的add_disk_randomness()函式的作用是使用塊 I/O 請求的定時來給系統的隨機數池貢獻熵,它不影響塊裝置驅動。但是,僅當磁碟的操作時間是真正隨機的時候(大部分機械裝置如此),才應該呼叫它。
程式碼清單13.16給出了1個更復雜的請求函式,它進行了3層遍歷:遍歷請求佇列中的每個請求;遍歷請求中的每個bio;遍歷bio中的每個段。
程式碼清單13.16 請求函式遍歷請求、bio和段
1 static void xxx_full_request(request_queue_t *q)
2 {
3 struct request *req;
4 int sectors_xferred;
5 struct xxx_dev *dev = q->queuedata;
6 /* 遍歷每個請求 */
7 while ((req = elv_next_request(q)) != NULL)
8 {
9 if (!blk_fs_request(req))
10 {
11 printk(KERN_NOTICE "Skip non-fs request/n");
12
13 end_request(req, 0);
14 continue;
15 }
16 sectors_xferred = xxx_xfer_request(dev, req);
17 if (!end_that_request_first(req, 1, sectors_xferred))
18 {
19 blkdev_dequeue_request(req);
20 end_that_request_last(req);
21 }
22 }
23 }
24 /* 請求處理 */
25 static int xxx_xfer_request(struct xxx_dev *dev, struct request *req)
26 {
27 struct bio *bio;
28 int nsect = 0;
29 /* 遍歷請求中的每個bio */
30 rq_for_each_bio(bio, req)
31 {
32 xxx_xfer_bio(dev, bio);
33 nsect += bio->bi_size / KERNEL_SECTOR_SIZE;
34 }
35 return nsect;
36 }
37 /* bio處理 */
38 static int xxx_xfer_bio(struct xxx_dev *dev, struct bio *bio)
39 {
40 int i;
41 struct bio_vec *bvec;
42 sector_t sector = bio->bi_sector;
43
44 /* 遍歷每1段 */
45 bio_for_each_segment(bvec, bio, i)
46 {
47 char *buffer = __bio_kmap_atomic(bio, i, KM_USER0);
48 xxx_transfer(dev, sector, bio_cur_sectors(bio), buffer, bio_data_dir(bio)
49 == WRITE);
50 sector += bio_cur_sectors(bio);
51 __bio_kunmap_atomic(bio, KM_USER0);
52 }
53 return 0;
54 }
圖13.3呈現了1個請求佇列內request、bio以及bio中segment的層層遍歷關係。
圖13.3 遍歷1個請求佇列
13.6.2不使用請求佇列
使用請求佇列對於一個機械的磁碟裝置而言的確有助於提高系統的效能,但是對於許多塊裝置,如數位相機的儲存卡、RAM盤等完全可真正隨機訪問的裝置而言,無法從高階的請求佇列邏輯中獲益。對於這些裝置,塊層支援“無佇列”的操作模式,為使用這個模式,驅動必須提供一個“製造請求”函式,而不是一個請求函式,“製造請求”函式的原型為:
typedef int (make_request_fn) (request_queue_t *q, struct bio *bio);
上述函式的第1個引數仍然是“請求佇列”,但是這個“請求佇列”實際不包含任何請求。因此,“製造請求”函式的主要引數是bio結構體,這個bio結構體表示1個或多個要傳送的緩衝區。“製造請求”函式或者直接進行傳輸,或者把請求重定向給其它裝置。
在“製造請求”函式中處理bio的方式與13.6.1節中描述的完全一致,但是在處理完成後應該使用bio_endio()函式通知處理結束:
void bio_endio(struct bio *bio, unsigned int bytes, int error);
引數bytes 是已經傳送的位元組數,它可以比這個bio所代表的位元組數少,這意味著“部分完成”,同時bio結構體中的當前緩衝區指標需要更新。當裝置進一步處理這個bio後,驅動應該再次呼叫 bio_endio(),如果不能完成這個請求,應指出一個錯誤,錯誤碼賦值給error引數。
不管對應的I/O處理成功與否,“製造請求”函式都應該返回0。如果“製造請求”函式返回一個非零值,bio 將被再次提交。
程式碼清單13.17給出了1個“製造請求”函式的例子。
程式碼清單13.17 “製造請求”函式例程
1 static int xxx_make_request(request_queue_t *q, struct bio *bio)
2 {
3 struct xxx_dev *dev = q->queuedata;
4 int status;
5 status = xxx_xfer_bio(dev, bio); //處理bio
6 bio_endio(bio, bio->bi_size, status); //通告結束
7 return 0;
8 }
為了使用無佇列的I/O請求處理,驅動模組載入函式應遵循程式碼清單13.9的模板而非13.10的模板,而使用請求佇列時,驅動模組載入函式應遵循程式碼清單13.10的模板。
13.7例項1:RAMDISK驅動
13.7.1 RAMDISK的硬體原理
RAMDISK(RAM盤)是一種模擬磁碟,其資料實際上儲存在RAM中,它使用一部分記憶體空間來模擬出一個磁碟,以塊裝置的方式來訪問這片記憶體,RAMDISK對應的裝置檔案一般為/dev/ram%d。
使用如下一組命令就可以建立並掛載RAMDISK:
mkdir /tmp/ramdisk0 建立裝載點
mke2fs /dev/ram0 建立一個檔案系統
mount /dev/ram0 /tmp/ramdisk0 裝載ramdisk
其中,mke2f /dev/ram0命令的執行會回饋類似於如下的資訊:
mke2fs 1.14, 9-Jan-1999 for EXT2 FS 0.5b, 95/08/09
Linux ext2 filesystem format
Filesystem label=
1024 inodes, 4096 blocks
204 blocks (4.98%) reserved for the super user
First data block=1
Block size=1024 (log=0)
Fragment size=1024 (log=0)
1 block group
8192 blocks per group, 8192 fragments per group
1024 inodes per group
表明建立了1個4MB的塊裝置,共包含4096塊,每塊1024位元組。
13.7.2 RAMDISK驅動模組載入與解除安裝
RAMDISK的驅動模組載入函式完成的工作與13.3節給出的模板完全一致,由於RAM盤屬於完全的隨機裝置,宜使用無佇列的I/O處理方式,其驅動中實現瞭如圖13.4所示的與塊裝置驅動模板對應的函式。
圖13.4 塊裝置驅動模板與RAMDISK裝置驅動的對映
程式碼清單13.18給出了RAMDISK裝置驅動的模組載入與解除安裝函式,實現的功能與模組是一致的。
程式碼清單13.18 RAMDISK裝置驅動的模組載入與解除安裝函式
1 static int __init rd_init(void)
2 {
3 int i;
4 int err = - ENOMEM;
5 //調整塊尺寸
6 if (rd_blocksize > PAGE_SIZE || rd_blocksize < 512 || (rd_blocksize &
7 (rd_blocksize - 1)))
8 {
9 printk("RAMDISK: wrong blocksize %d, reverting to defaults/n", rd_blocksize) ;
10
11 rd_blocksize = BLOCK_SIZE;
12 }
13 //分配gendisk
14 for (i = 0; i < CONFIG_BLK_DEV_RAM_COUNT; i++)
15 {
16 rd_disks[i] = alloc_disk(1); //分配gendisk
17 if (!rd_disks[i])
18 goto out;
19 }
20 //塊裝置註冊
21 if (register_blkdev(RAMDISK_MAJOR, "ramdisk"))
22 //註冊塊裝置
23 {
24 err = - EIO;
25 goto out;
26 }
27
28 devfs_mk_dir("rd"); //建立devfs目錄
29
30 for (i = 0; i < CONFIG_BLK_DEV_RAM_COUNT; i++)
31 {
32 struct gendisk *disk = rd_disks[i];
33 //分配並繫結請求佇列與“製造請求”函式
34 rd_queue[i] = blk_alloc_queue(GFP_KERNEL);
35 if (!rd_queue[i])
36 goto out_queue;
37
38 blk_queue_make_request(rd_queue[i], &rd_make_request); //繫結“製造請求”函式
39 blk_queue_hardsect_size(rd_queue[i], rd_blocksize); //硬體扇區尺寸設定
40
41 //初始化gendisk
42 disk->major = RAMDISK_MAJOR;
43 disk->first_minor = i;
44 disk->fops = &rd_bd_op;
45 disk->queue = rd_queue[i];
46 disk->flags |= GENHD_FL_SUPPRESS_PARTITION_INFO;
47 sprintf(disk->disk_name, "ram%d", i);
48 sprintf(disk->devfs_name, "rd/%d", i);
49 set_capacity(disk, rd_size *2);
50 add_disk(rd_disks[i]); //新增gendisk
51 }
52
53 // rd_size以kB為單位
54 printk("RAMDISK driver initialized: "
55 "%d RAMDISKs of %dK size %d blocksize/n",
56 CONFIG_BLK_DEV_RAM_COUNT,rd_size, rd_blocksize);
57
58 return 0;
59 out_queue: unregister_blkdev(RAMDISK_MAJOR, "ramdisk");
60 out:
61 while (i--)
62 {
63 put_disk(rd_disks[i]);
64 blk_cleanup_queue(rd_queue[i]);
65 }
66 return err;
67 }
68
69 static void __exit rd_cleanup(void)
70 {
71 int i;
72
73 for (i = 0; i < CONFIG_BLK_DEV_RAM_COUNT; i++)
74 {
75 struct block_device *bdev = rd_bdev[i];
76 rd_bdev[i] = NULL;
77 if (bdev)
78 {
79 invalidate_bdev(bdev, 1);
80 blkdev_put(bdev);
81 }
82 del_gendisk(rd_disks[i]); //刪除gendisk
83 put_disk(rd_disks[i]); //釋放對gendisk的引用
84 blk_cleanup_queue(rd_queue[i]); //清除請求佇列
85 }
86 devfs_remove("rd");
87 unregister_blkdev(RAMDISK_MAJOR, "ramdisk"); //塊裝置登出
88 }
13.7.3 RAMDISK裝置驅動block_device_operations及成員函式
RAMDISK提供block_device_operations結構體中2個成員函式open()和ioctl()的實現,程式碼清單13.19給出了RAMDISK裝置驅動的block_device_operations結構體定義及open()和ioctl()函式。
程式碼清單13.19 RAMDISK裝置驅動block_device_operations結構體及成員函式
1 static struct block_device_operations rd_bd_op =
2 {
3 .owner = THIS_MODULE,
4 .open = rd_open,
5 .ioctl = rd_ioctl,
6 };
7
8 static int rd_open(struct inode *inode, struct file *filp)
9 {
10 unsigned unit = iminor(inode);//獲得次裝置號
11
12 if (rd_bdev[unit] == NULL) {
13 struct block_device *bdev = inode->i_bdev;//獲得block_device結構體指標
14 struct address_space *mapping; //地址空間
15 unsigned bsize;
16 gfp_t gfp_mask;
17 /* 設定inode成員 */
18 inode = igrab(bdev->bd_inode);
19 rd_bdev[unit] = bdev;
20 bdev->bd_openers++;
21 bsize = bdev_hardsect_size(bdev);
22 bdev->bd_block_size = bsize;
23 inode->i_blkbits = blksize_bits(bsize);
24 inode->i_size = get_capacity(bdev->bd_disk)<<9;
25
26 mapping = inode->i_mapping;
27 mapping->a_ops = &ramdisk_aops;
28 mapping->backing_dev_info = &rd_backing_dev_info;
29 bdev->bd_inode_backing_dev_info = &rd_file_backing_dev_info;
30
31 gfp_mask = mapping_gfp_mask(mapping);
32 gfp_mask &= ~(__GFP_FS|__GFP_IO);
33 gfp_mask |= __GFP_HIGH;
34 mapping_set_gfp_mask(mapping, gfp_mask);
35 }
36
37 return 0;
38 }
39
40 static int rd_ioctl(struct inode *inode, struct file *file,
41 unsigned int cmd, unsigned long arg)
42 {
43 int error;
44 struct block_device *bdev = inode->i_bdev;
45
46 if (cmd != BLKFLSBUF) /* 不是flush buffer cache 命令 */
47 return -ENOTTY;
48 /* 重新整理buffer cache */
49 error = -EBUSY;
50 down(&bdev->bd_sem);
51 if (bdev->bd_openers <= 2) {
52 truncate_inode_pages(bdev->bd_inode->i_mapping, 0);
53 error = 0;
54 }
55 up(&bdev->bd_sem);
56 return error;
57 }
13.7.4 RAMDISK I/O請求處理
鑑於RAMDISK是一種完全隨機裝置,其驅動中宜使用“製造請求”函式而非請求函式,這個函式的實現如程式碼清單13.20。
程式碼清單13.20 RAMDISK裝置驅動“製造請求”函式
1 static int rd_make_request(request_queue_t *q, struct bio *bio)
2 {
3 struct block_device *bdev = bio->bi_bdev;
4 struct address_space *mapping = bdev->bd_inode->i_mapping;
5 sector_t sector = bio->bi_sector;
6 unsigned long len = bio->bi_size >> 9;
7 int rw = bio_data_dir(bio);//資料傳輸方向:讀/寫?
8 struct bio_vec *bvec;
9 int ret = 0, i;
10
11 if (sector + len > get_capacity(bdev->bd_disk))
12 //超過容量
13 goto fail;
14
15 if (rw == READA)
16 rw = READ;
17 //遍歷每個段
18 bio_for_each_segment(bvec, bio, i)
19 {
20 ret |= rd_blkdev_pagecache_IO(rw, bvec, sector, mapping);
21 sector += bvec->bv_len >> 9;
22 }
23 if (ret)
24 goto fail;
25
26 bio_endio(bio, bio->bi_size, 0); //處理結束
27 return 0;
28 fail: bio_io_error(bio, bio->bi_size);
29 return 0;
30 }
13.8例項2:IDE硬碟裝置驅動
13.8.1 IDE硬碟裝置原理
IDE(Integrated Drive Electronics)介面,也就是整合驅動器電路介面,原名為ATA(AT Attachment,AT嵌入式)介面,其本意為將硬碟控制器與盤體整合在一起的硬碟驅動器,經歷了ATA-1到ATA-7以及SATA-1和SATA-2的發展歷史。ATA-1至ATA-4採用40芯排線纜,ATA-5至ATA-7則採用40針80芯線纜,雖然線纜數量增加了,但是邏輯原理沒有變,只是透過物理上的改變來達到改善PCB訊號完整性的目的,它提供更多的地線並使訊號線臨近地線,從而減少電流回流的面積。SATA-1和SATA-2與ATA-1至ATA-7相比,資料傳輸方式由並行轉變為序列。
IDE介面的硬體原理實際上非常簡單,對CPU的外圍匯流排進行簡單的擴充套件後就可外接IDE控制器,表13.2給出了40針IDE介面的引腳定義。
表13.2 IDE介面的引腳定義
引腳 訊號 訊號描述 訊號方向 引腳 訊號 訊號描述 訊號方向
1 RSET 復位 I 2 GND 地 I/O
3 DD7 資料位7 I/O 4 DD8 資料位8 I/O
5 DD6 資料位6 I/O 6 DD9 資料位9 I/O
7 DD5 資料位5 I/O 8 DD10 資料位10 I/O
9 DD4 資料位4 I/O 10 DD11 資料位11 I/O
11 DD3 資料位3 I/O 12 DD12 資料位12 I/O
13 DD2 資料位2 I/O 14 DD13 資料位13 I/O
15 DD1 資料位1 I/O 16 DD14 資料位14 I/O
17 DD0 資料位0 I/O 18 DD15 資料位15 I/O
19 GND 地 20 N.C 未用
21 DMARQ DMA請求 O 22 GND 地
23 DIOW/ 寫選通 I 24 GND 地
25 DIOR/ 讀選通 I 26 GND 地
27 IORDY 通道就緒 O 28 DPSYNC:CXEL 同步電纜選擇
29 DMACK/ DMA應答 O 30 GND 地
31 INTRQ/ 中斷請求 O 32 IOCS13/ 16為IO O
33 DA1 地址1 I 34 PDIAG/ 診斷完成 O
35 DA0 地址0 I 36 DA2 地址2 I
37 CS1FX/ 片選0 I 38 CS3FX/ 片選1 I
39 DASP/ 驅動器啟用 O 40 GND 地
IDE控制器提供了一組暫存器,透過這些暫存器,主機能控制IDE驅動器的行為和查詢其狀態,表13.3給出了IDE介面暫存器的定義。
表13.3 IDE介面暫存器定義
片選1 片選0 地址2 地址1 地址0 讀 寫 位數
1 0 0 0 0 資料暫存器 資料暫存器 16
1 0 0 0 1 錯誤暫存器 特徵暫存器 8
1 0 0 1 0 扇區數暫存器 扇區數暫存器 8
1 0 0 1 1 扇區號暫存器 扇區號暫存器 8
1 0 1 0 0 柱面號暫存器(低8位) 柱面號暫存器(低8位) 8
1 0 1 0 1 柱面號暫存器(高8位) 柱面號暫存器(高8位) 8
1 0 1 1 0 驅動器選擇/磁頭暫存器 驅動器選擇/磁頭暫存器 8
1 0 1 1 1 狀態暫存器 命令暫存器 8
0 1 1 1 0 狀態暫存器 裝置控制器暫存器 8
IDE硬碟的傳輸模式有以下3種:
• PIO(Programmed I/O)模式:PIO模式是一種透過CPU執行I/O埠指令來進行資料讀寫的資料交換模式,是最早先的硬碟資料傳輸模式,資料傳輸速率低下,CPU佔有率也很高。
• DMA(Driect Memory Access)模式:DMA模式是一種不經過CPU而直接從記憶體了存取資料的資料交換模式。PIO模式下硬碟和記憶體之間的資料傳輸是由CPU來控制的;而在DMA模式下,CPU只須向DMA控制器下達指令,讓DMA控制器來處理資料的傳送,資料傳送完畢再把資訊反饋給CPU,這樣就很大程度上減輕了CPU的資源佔有率。
• Ultra DMA(簡稱UDMA)模式:它在包含了DMA模式的優點的基礎上,又增加了CRC校驗技術,提高資料傳輸過程中的準確性,安全性得到保障。另外,在以往的硬碟資料傳輸模式下,一個時鐘週期只傳輸一次資料,而在UDMA 模式中逐漸應用了Double Data Rate(雙倍資料傳輸)技術,它在時鐘的上升沿和下降沿各自進行一次資料傳輸,使資料傳輸速度成倍增長。
除了可以以CHS(Cylinder、Head 和 Sector)的方式定位硬碟的扇區外,還可以用LBA(邏輯塊線性地址)的方式來定位,CHS可以換算為LBA。CHS 設計最多隻允許 65536 個柱面、16 個磁頭,以及 255 扇區/磁軌。這就將容量限制為 267386880 個扇區,即大約 137 GB。
假設用c表示當前柱面號,h表示當前磁頭號,cs表示起始柱面號,hs表示起始磁頭號,ss表示起始扇區號,ps表示每磁軌有多少個扇區,ph表示每柱面有多少個磁軌(一般情況下,cs=0、hs=0、ss=1、ps=63、ph=255),LBA與CHS有如下對應關係:
lba=(c-cs)*ph*ps+(h-hs)*ps+(s-ss)
LBA使得系統忽略硬碟的幾何結構,交由驅動器來完成。系統不需要去查詢 CHS 值,而只需要查詢邏輯塊地址(Logical Block Address,LBA),驅動器電子裝置會找出要讀或寫的實際扇區。而LBA48(48位邏輯塊地址)則可以使系統支援超過137GB的硬碟。
Linux核心中,與IDE驅動相關的檔案被放置在/drivers/ide目錄下,這個目錄包含ide.c、ide-cd.c、ide-cd.h、ide-disk.c、ide-dma.c、ide-floppy.c、ide-generic.c、ide-io.c、ide-iops.c、ide-lib.c、ide-pnp.c、ide-probe.c、ide-proc.c、ide-tape.c、ide-taskfile.c、ide-timing.h檔案以及針對ARM、PPC、MIPS等外圍IDE裝置驅動的目錄。整個IDE裝置驅動的體系結構及其複雜,但大多數都不需要關心,驅動工程師要使Linux支援某嵌入式系統中的IDE硬碟,所需編寫的程式碼量是非常少的。13.8.2~13.8.3純粹出於學習目的,對IDE硬碟驅動的block_device_operations及IO請求處理過程進行分析以進一步加深讀者對Linux塊裝置I/O操作方法的印象,13.8.4則從工程角度出發,講解如何使Linux支援新系統中的IDE硬碟。
13.8.2 IDE硬碟裝置驅動block_device_operations及成員函式
IDE硬碟驅動的block_device_operations中包含了開啟、釋放、IO控制、獲得幾何資訊、媒介改變和使介質有效的成員函式,這些函式的實現較簡單,如程式碼清單13.21。
程式碼清單13.21 IDE硬碟驅動block_device_operations結構體及其成員函式
1 static struct block_device_operations idedisk_ops =
2 {
3 .owner = THIS_MODULE,
4 .open = idedisk_open,
5 .release = idedisk_release,
6 .ioctl = idedisk_ioctl,
7 .getgeo = idedisk_getgeo, //得到幾何資訊
8 .media_changed = idedisk_media_changed, //媒介改變
9 .revalidate_disk= idedisk_revalidate_disk //使介質有效
10 };
11
12 static int idedisk_ioctl(struct inode *inode, struct file *file,
13 unsigned int cmd, unsigned long arg)
14 {
15 struct block_device *bdev = inode->i_bdev;
16 struct ide_disk_obj *idkp = ide_disk_g(bdev->bd_disk);
17 return generic_ide_ioctl(idkp->drive, file, bdev, cmd, arg);//通用IDE的IO控制
18 }
19
20
21 static int idedisk_getgeo(struct block_device *bdev, struct hd_geometry *geo)
22 {
23 struct ide_disk_obj *idkp = ide_disk_g(bdev->bd_disk);
24 ide_drive_t *drive = idkp->drive;
25 /* 得到幾何資訊,CHS */
26 geo->heads = drive->bios_head;
27 geo->sectors = drive->bios_sect;
28 geo->cylinders = (u16)drive->bios_cyl; /* truncate */
29 return 0;
30 }
31
32 static int idedisk_open(struct inode *inode, struct file *filp)
33 {
34 struct gendisk *disk = inode->i_bdev->bd_disk;
35 struct ide_disk_obj *idkp;
36 ide_drive_t *drive;
37
38 if (!(idkp = ide_disk_get(disk)))
39 return -ENXIO;
40
41 drive = idkp->drive;
42
43 drive->usage++; //使用計數加1
44 if (drive->removable && drive->usage == 1) {
45 ide_task_t args;
46 memset(&args, 0, sizeof(ide_task_t));
47 args.tfRegister[IDE_COMMAND_OFFSET] = WIN_DOORLOCK;
48 args.command_type = IDE_DRIVE_TASK_NO_DATA;
49 args.handler = &task_no_data_intr;
50 check_disk_change(inode->i_bdev);
51
52 if (drive->doorlocking && ide_raw_taskfile(drive, &args, NULL))
53 drive->doorlocking = 0;
54 }
55 return 0;
56 }
57
58 static int idedisk_release(struct inode *inode, struct file *filp)
59 {
60 struct gendisk *disk = inode->i_bdev->bd_disk;
61 struct ide_disk_obj *idkp = ide_disk_g(disk);
62 ide_drive_t *drive = idkp->drive;
63
64 if (drive->usage == 1)
65 ide_cacheflush_p(drive);
66 if (drive->removable && drive->usage == 1) {
67 ide_task_t args;
68 memset(&args, 0, sizeof(ide_task_t));
69 args.tfRegister[IDE_COMMAND_OFFSET] = WIN_DOORUNLOCK;
70 args.command_type = IDE_DRIVE_TASK_NO_DATA;
71 args.handler = &task_no_data_intr;
72 if (drive->doorlocking && ide_raw_taskfile(drive, &args, NULL))
73 drive->doorlocking = 0;
74 }
75 drive->usage--; //使用計數減1
76
77 ide_disk_put(idkp);
78 return 0;
79 }
13.8.3 IDE硬碟裝置驅動I/O請求處理
Linux對IDE驅動進行了再封裝,定義了ide_driver_t結構體,這個結構體容納了IDE硬碟的探測、移除、請求處理和結束請求處理等函式指標。結束請求處理函式ide_end_request()是對end_request()函式針對IDE的修改。程式碼清單13.21給出了ide_driver_t結構體的定義。
程式碼清單13.22 ide_driver_t結構體
1 static ide_driver_t idedisk_driver = {
2 .gen_driver = {
3 .owner = THIS_MODULE,
4 .name = "ide-disk",
5 .bus = &ide_bus_type,
6 },
7 .probe = ide_disk_probe, //探測
8 .remove = ide_disk_remove, //移除
9 .shutdown = ide_device_shutdown, //關閉
10 .version = IDEDISK_VERSION,
11 .media = ide_disk, //媒介型別
12 .supports_dsc_overlap = 0,
13 .do_request = ide_do_rw_disk, //請求處理函式
14 .end_request = ide_end_request, //請求處理結束
15 .error = __ide_error,
16 .abort = __ide_abort,
17 .proc = idedisk_proc,
18 };
程式碼清單13.22第13行的ide_do_rw_disk()函式完成硬碟I/O操作請求的處理,如程式碼清單13.23所示。
程式碼清單13.23 IDE硬碟驅動I/O請求處理
1 static ide_startstop_t ide_do_rw_disk(ide_drive_t *drive, struct request *rq,
2 sector_t block)
3 {
4 ide_hwif_t *hwif = HWIF(drive);
5
6 BUG_ON(drive->blocked);
7
8 if (!blk_fs_request(rq)) //不是檔案系統請求
9 {
10 blk_dump_rq_flags(rq, "ide_do_rw_disk - bad command");
11 ide_end_request(drive, 0, 0); //以失敗結束該請求
12 return ide_stopped;
13 }
14
15 pr_debug("%s: %sing: block=%llu, sectors=%lu, buffer=0x%08lx/n",
16 drive->name,rq_data_dir(rq) == READ ? "read" : "writ", (unsigned long
17 long)block, rq->nr_sectors, (unsigned long)rq->buffer);
18
19 if (hwif->rw_disk)
20 hwif->rw_disk(drive, rq);
21
22 return __ide_do_rw_disk(drive, rq, block); //具體的請求處理
23 }
24
25 static ide_startstop_t __ide_do_rw_disk(ide_drive_t *drive, struct request *rq,
26 sector_t block)
27 {
28 ide_hwif_t *hwif = HWIF(drive);
29 unsigned int dma = drive->using_dma;
30 u8 lba48 = (drive->addressing == 1) ? 1 : 0;
31 task_ioreg_t command = WIN_NOP;
32 ata_nsector_t nsectors;
33
34 nsectors.all = (u16)rq->nr_sectors; //要傳送的扇區數
35
36 if (hwif->no_lba48_dma && lba48 && dma)
37 {
38 if (block + rq->nr_sectors > 1ULL << 28)
39 dma = 0;
40 else
41 lba48 = 0;
42 }
43
44 if (!dma)
45 {
46 ide_init_sg_cmd(drive, rq);
47 ide_map_sg(drive, rq);
48 }
49
50 if (IDE_CONTROL_REG)
51 hwif->OUTB(drive->ctl, IDE_CONTROL_REG);
52
53 if (drive->select.b.lba)
54 {
55 if (lba48) //48位LBA
56 {
57 ...
58 }
59 else
60 {
61 //LBA方式,寫入要讀寫的位置資訊到IDE暫存器
62 hwif->OUTB(0x00, IDE_FEATURE_REG);
63 hwif->OUTB(nsectors.b.low, IDE_NSECTOR_REG);
64 hwif->OUTB(block, IDE_SECTOR_REG);
65 hwif->OUTB(block >>= 8, IDE_LCYL_REG);
66 hwif->OUTB(block >>= 8, IDE_HCYL_REG);
67 hwif->OUTB(((block >> 8) &0x0f) | drive->select.all,IDE_SELECT_REG);
68 }
69 }
70 else
71 {
72 unsigned int sect, head, cyl, track;
73 track = (int)block / drive->sect;
74 sect = (int)block % drive->sect + 1;
75 hwif->OUTB(sect, IDE_SECTOR_REG);
76 head = track % drive->head;
77 cyl = track / drive->head;
78
79 pr_debug("%s: CHS=%u/%u/%u/n", drive->name, cyl, head, sect);
80 //CHS方式,寫入要讀寫的位置資訊到IDE暫存器
81 hwif->OUTB(0x00, IDE_FEATURE_REG);
82 hwif->OUTB(nsectors.b.low, IDE_NSECTOR_REG);
83 hwif->OUTB(cyl, IDE_LCYL_REG);
84 hwif->OUTB(cyl >> 8, IDE_HCYL_REG);
85 hwif->OUTB(head | drive->select.all, IDE_SELECT_REG);
86 }
87
88 if (dma) //DMA方式
89 {
90 if (!hwif->dma_setup(drive)) //設定DMA成功
91 {
92 if (rq_data_dir(rq))
93 {
94 command = lba48 ? WIN_WRITEDMA_EXT : WIN_WRITEDMA;
95 if (drive->vdma)
96 command = lba48 ? WIN_WRITE_EXT : WIN_WRITE;
97 }
98 else
99 {
100 command = lba48 ? WIN_READDMA_EXT : WIN_READDMA;
101 if (drive->vdma)
102 command = lba48 ? WIN_READ_EXT : WIN_READ;
103 }
104 hwif->dma_exec_cmd(drive, command);
105 hwif->dma_start(drive);
106 return ide_started;
107 }
108 /* 回到PIO模式 */
109 ide_init_sg_cmd(drive, rq);
110 }
111
112 if (rq_data_dir(rq) == READ) //資料傳輸方向是讀
113 {
114 if (drive->mult_count)
115 {
116 hwif->data_phase = TASKFILE_MULTI_IN;
117 command = lba48 ? WIN_MULTREAD_EXT : WIN_MULTREAD;
118 }
119 else
120 {
121 hwif->data_phase = TASKFILE_IN;
122 command = lba48 ? WIN_READ_EXT : WIN_READ;
123 }
124 //執行讀命令
125 ide_execute_command(drive, command, &task_in_intr, WAIT_CMD, NULL);
126 return ide_started;
127 }
128 else //資料傳輸方向是寫
129 {
130 if (drive->mult_count)
131 {
132 hwif->data_phase = TASKFILE_MULTI_OUT;
133 command = lba48 ? WIN_MULTWRITE_EXT : WIN_MULTWRITE;
134 }
135 else
136 {
137 hwif->data_phase = TASKFILE_OUT;
138 command = lba48 ? WIN_WRITE_EXT : WIN_WRITE;
139 }
140
141 //寫IDE命令暫存器寫入寫命令
142 hwif->OUTB(command, IDE_COMMAND_REG);
143
144 return pre_task_out_intr(drive, rq);
145 }
146 }
從程式碼清單13.23可知,真正開始執行I/O操作的是其22行引用的__ide_do_rw_disk()函式。這個函式會根據不同的操作模式,將要讀寫的LBA或CHS資訊寫入IDE暫存器內,並給其命令暫存器寫入讀、寫命令。
為了進行硬碟讀寫操作,第61~67和80~85行將引數寫入地址暫存器和特性暫存器,如果是讀,第125行呼叫的ide_execute_command()會將讀命令寫入命令暫存器;如果是寫,第142行將寫命令寫入IDE命令暫存器IDE_COMMAND_REG。
真正呼叫ide_driver_t結構體中do_request()成員函式即ide_do_rw_disk()的是ide-io.c檔案中的start_request()函式,這個函式會過濾掉一些請求,最終將讀寫I/O操作請求傳遞給ide_do_rw_disk()函式,如程式碼清單13.24。
程式碼清單13.24 開始執行1個IDE請求的start_request()函式
1 static ide_startstop_t start_request(ide_drive_t *drive, struct request *rq)
2 {
3 ide_startstop_t startstop;
4 sector_t block;
5
6 BUG_ON(!(rq->flags &REQ_STARTED));
7
8 /* 超過了最大失敗次數 */
9 if (drive->max_failures && (drive->failures > drive->max_failures))
10 {
11 goto kill_rq;
12 }
13
14 block = rq->sector; //要傳輸的下1個扇區
15 if (blk_fs_request(rq) && (drive->media == ide_disk || drive->media ==
16 ide_floppy)) //是檔案系統請求,是IDE盤
17 {
18 block += drive->sect0;
19 }
20 /* 如果將0扇區重對映到1扇區 */
21 if (block == 0 && drive->remap_0_to_1 == 1)
22 block = 1;
23
24 if (blk_pm_suspend_request(rq) && rq->pm->pm_step ==
25 ide_pm_state_start_suspend)
26 drive->blocked = 1;
27 else if (blk_pm_resume_request(rq) && rq->pm->pm_step ==
28 ide_pm_state_start_resume)
29 {
30 /* 醒來後的第1件事就是等待BSY位不忙 */
31 int rc;
32
33 rc = ide_wait_not_busy(HWIF(drive), 35000);
34 if (rc)
35 printk(KERN_WARNING "%s: bus not ready on wakeup/n", drive->name);
36 SELECT_DRIVE(drive);
37 HWIF(drive)->OUTB(8, HWIF(drive)->io_ports[IDE_CONTROL_OFFSET]);
38 rc = ide_wait_not_busy(HWIF(drive), 10000);
39 if (rc)
40 printk(KERN_WARNING "%s: drive not ready on wakeup/n", drive->name);
41 }
42
43 SELECT_DRIVE(drive);
44 if (ide_wait_stat(&startstop, drive, drive->ready_stat, BUSY_STAT | DRQ_STAT,
45 WAIT_READY)) //等待驅動器READY
46 {
47 printk(KERN_ERR "%s: drive not ready for command/n", drive->name);
48 return startstop;
49 }
50 if (!drive->special.all)
51 {
52 ide_driver_t *drv;
53
54 //其它非讀寫請求
55 if (rq->flags &(REQ_DRIVE_CMD | REQ_DRIVE_TASK))
56 return execute_drive_cmd(drive, rq);
57 else if (rq->flags &REQ_DRIVE_TASKFILE)
58 return execute_drive_cmd(drive, rq);
59 else if (blk_pm_request(rq))
60 {
61 startstop = ide_start_power_step(drive, rq);
62 if (startstop == ide_stopped && rq->pm->pm_step == ide_pm_state_completed)
63 ide_complete_pm_request(drive, rq);
64 return startstop;
65 }
66
67 drv = *(ide_driver_t **)rq->rq_disk->private_data;
68 return drv->do_request(drive, rq, block); //處理IO操作請求
69 }
70 return do_special(drive);
71 kill_rq: ide_kill_rq(drive, rq);
72 return ide_stopped;
73 }
IDE硬碟驅動對I/O請求結束處理進行了針對IDE的整理,並填充在ide_driver_t結構體的end_request成員中,對應的函式為ide_end_request(),如程式碼清單13.25。
程式碼清單13.25 IDE I/O請求結束處理
1 /* ide_end_request:完成了一個IDE請求
2 引數: nr_sectors 被完成的扇區數量
3 */
4 int ide_end_request (ide_drive_t *drive, int uptodate, int nr_sectors)
5 {
6 struct request *rq;
7 unsigned long flags;
8 int ret = 1;
9
10 spin_lock_irqsave(&ide_lock, flags); //獲得自旋鎖
11 rq = HWGROUP(drive)->rq;
12
13 if (!nr_sectors)
14 nr_sectors = rq->hard_cur_sectors;
15
16 ret = __ide_end_request(drive, rq, uptodate, nr_sectors); //具體的結束請求處理
17
18 spin_unlock_irqrestore(&ide_lock, flags); //釋放獲得自旋鎖
19 return ret;
20 }
21
22 static int __ide_end_request(ide_drive_t *drive, struct request *rq, int
23 uptodate, int nr_sectors)
24 {
25 int ret = 1;
26
27 BUG_ON(!(rq->flags &REQ_STARTED));
28
29 /* 如果對請求設定了failfast,立即完成整個請求 */
30 if (blk_noretry_request(rq) && end_io_error(uptodate))
31 nr_sectors = rq->hard_nr_sectors;
32
33 if (!blk_fs_request(rq) && end_io_error(uptodate) && !rq->errors)
34 rq->errors = - EIO;
35
36 /* 決定是否再使能DMA,如果DMA超過3次,採用PIO模式 */
37 if (drive->state == DMA_PIO_RETRY && drive->retry_pio <= 3)
38 {
39 drive->state = 0;
40 HWGROUP(drive)->hwif->ide_dma_on(drive);
41 }
42
43 //結束請求
44 if (!end_that_request_first(rq, uptodate, nr_sectors))
45 {
46 add_disk_randomness(rq->rq_disk); //給系統的隨機數池貢獻熵
47 blkdev_dequeue_request(rq);
48 HWGROUP(drive)->rq = NULL;
49 end_that_request_last(rq, uptodate);
50 ret = 0;
51 }
52
53 return ret;
54 }
13.8.4在核心中增加對新系統IDE裝置的支援
儘管IDE的驅動非常複雜,但是由於其訪問方式符合ATA標準,因而核心提供的I/O操作方面的程式碼是通用的,為了使核心能找到新系統中的IDE硬碟,工程師只需編寫少量的針對特定硬體平臺的底層程式碼。
使用ide_register_hw()函式可註冊IDE硬體介面,其原型為:
int ide_register_hw(hw_regs_t *hw, ide_hwif_t **hwifp);
這個函式接收的2個引數對應的資料結構為hw_regs_s和ide_hwif_t,其定義如程式碼清單13.26。
程式碼清單13.26 hw_regs_s和ide_hwif_t結構體的定義
1 typedef struct hw_regs_s
2 {
3 unsigned long io_ports[IDE_NR_PORTS]; /* task file暫存器 */
4 int irq; /* 中斷號 */
5 int dma; /* DMA入口 */
6 ide_ack_intr_t *ack_intr; /* 確認中斷 */
7 hwif_chipset_t chipset;
8 struct device *dev;
9 } hw_regs_t;
10
11 typedef struct hwif_s
12 {
13 ...
14
15 char name[6]; /* 介面名,如"ide0" */
16
17 hw_regs_t hw; /* 硬體資訊 */
18 ide_drive_t drives[MAX_DRIVES]; /* 驅動器資訊 */
19
20 u8 major; /* 主裝置號 */
21 u8 index; /* 索引,如0對應ide0,1對應ide1 */
22 ...
23 //DMA操作
24 int(*dma_setup)(ide_drive_t*);
25 void(*dma_exec_cmd)(ide_drive_t *, u8);
26 void(*dma_start)(ide_drive_t*);
27 int(*ide_dma_end)(ide_drive_t *drive);
28 int(*ide_dma_check)(ide_drive_t *drive);
29 int(*ide_dma_on)(ide_drive_t *drive);
30 int(*ide_dma_off_quietly)(ide_drive_t *drive);
31 int(*ide_dma_test_irq)(ide_drive_t *drive);
32 int(*ide_dma_host_on)(ide_drive_t *drive);
33 int(*ide_dma_host_off)(ide_drive_t *drive);
34 int(*ide_dma_lostirq)(ide_drive_t *drive);
35 int(*ide_dma_timeout)(ide_drive_t *drive);
36 //暫存器訪問
37 void(*OUTB)(u8 addr, unsigned long port);
38 void(*OUTBSYNC)(ide_drive_t *drive, u8 addr, unsigned long port);
39 void(*OUTW)(u16 addr, unsigned long port);
40 void(*OUTL)(u32 addr, unsigned long port);
41 void(*OUTSW)(unsigned long port, void *addr, u32 count);
42 void(*OUTSL)(unsigned long port, void *addr, u32 count);
43
44 u8(*INB)(unsigned long port);
45 u16(*INW)(unsigned long port);
46 u32(*INL)(unsigned long port);
47 void(*INSW)(unsigned long port, void *addr, u32 count);
48 void(*INSL)(unsigned long port, void *addr, u32 count);
49
50 ...
51 } ____cacheline_internodealigned_in_smp ide_hwif_t;
hw_regs_s結構體描述了IDE介面的暫存器、用到的中斷號和DMA入口,是對暫存器和硬體資源的描述,而ide_hwif_t是對IDE介面硬體訪問方法的描述。
因此,為使得新系統支援IDE並被核心偵測到,工程師只需要初始化hw_regs_s和ide_hwif_t這2個結構體並使用ide_register_hw()註冊IDE介面即可。程式碼清單13.27給出了H8/300系列微控制器IDE驅動的介面卡註冊程式碼。
程式碼清單13.27 H8/300系列微控制器IDE介面註冊
1 /* 暫存器操作函式 */
2 static void mm_outw(u16 d, unsigned long a)
3 {
4 __asm__("mov.b %w0,r2h/n/t"
5 "mov.b %x0,r2l/n/t"
6 "mov.w r2,@%1"
7 :
8 :"r"(d),"r"(a)
9 :"er2");
10 }
11
12 static u16 mm_inw(unsigned long a)
13 {
14 register u16 r __asm__("er0");
15 __asm__("mov.w @%1,r2/n/t"
16 "mov.b r2l,%x0/n/t"
17 "mov.b r2h,%w0"
18 :"=r"(r)
19 :"r"(a)
20 :"er2");
21 return r;
22 }
23
24 static void mm_outsw(unsigned long addr, void *buf, u32 len)
25 {
26 unsigned short *bp = (unsigned short *)buf;
27 for (; len > 0; len--, bp++)
28 *(volatile u16 *)addr = bswap(*bp);
29 }
30
31 static void mm_insw(unsigned long addr, void *buf, u32 len)
32 {
33 unsigned short *bp = (unsigned short *)buf;
34 for (; len > 0; len--, bp++)
35 *bp = bswap(*(volatile u16 *)addr);
36 }
37
38 #define H8300_IDE_GAP (2)
39
40 /* hw_regs_t結構體初始化 */
41 static inline void hw_setup(hw_regs_t *hw)
42 {
43 int i;
44
45 memset(hw, 0, sizeof(hw_regs_t));
46 for (i = 0; i <= IDE_STATUS_OFFSET; i++)
47 hw->io_ports[i] = CONFIG_H8300_IDE_BASE + H8300_IDE_GAP*i;
48 hw->io_ports[IDE_CONTROL_OFFSET] = CONFIG_H8300_IDE_ALT;
49 hw->irq = EXT_IRQ0 + CONFIG_H8300_IDE_IRQ;
50 hw->dma = NO_DMA;
51 hw->chipset = ide_generic;
52 }
53
54 /* ide_hwif_t結構體初始化 */
55 static inline void hwif_setup(ide_hwif_t *hwif)
56 {
57 default_hwif_iops(hwif);
58
59 hwif->mmio = 2;
60 hwif->OUTW = mm_outw;
61 hwif->OUTSW = mm_outsw;
62 hwif->INW = mm_inw;
63 hwif->INSW = mm_insw;
64 hwif->OUTL = NULL;
65 hwif->INL = NULL;
66 hwif->OUTSL = NULL;
67 hwif->INSL = NULL;
68 }
69
70 /* 註冊IDE介面卡 */
71 void __init h8300_ide_init(void)
72 {
73 hw_regs_t hw;
74 ide_hwif_t *hwif;
75 int idx;
76
77 /* 申請記憶體區域 */
78 if (!request_region(CONFIG_H8300_IDE_BASE, H8300_IDE_GAP*8, "ide-h8300"))
79 goto out_busy;
80 if (!request_region(CONFIG_H8300_IDE_ALT, H8300_IDE_GAP, "ide-h8300")) {
81 release_region(CONFIG_H8300_IDE_BASE, H8300_IDE_GAP*8);
82 goto out_busy;
83 }
84
85 hw_setup(&hw); //初始化hw_regs_t
86
87 /* 註冊IDE介面 */
88 idx = ide_register_hw(&hw, &hwif);
89 if (idx == -1) {
90 printk(KERN_ERR "ide-h8300: IDE I/F register failed/n");
91 return;
92 }
93
94 hwif_setup(hwif); //設定ide_hwif_t
95 printk(KERN_INFO "ide%d: H8/300 generic IDE interface/n", idx);
96 return;
97
98 out_busy:
99 printk(KERN_ERR "ide-h8300: IDE I/F resource already used./n");
100 }
第1~36行定義了暫存器讀寫函式,41行的hw_setup()函式用於初始化hw_regs_t結構體,55行的hwif_setup()函式用於初始化ide_hwif_t結構體,71行的h8300_ide_init()函式中使用ide_register_hw()註冊了這個介面。
13.9總結
塊裝置的I/O操作方式與字元裝置存在較大的不同,因而引入了request_queue、request、bio等一系列資料結構。在整個塊裝置的I/O操作中,貫穿於始終的就是“請求”,字元裝置的I/O操作則是直接進行不繞彎,塊裝置的I/O操作會排隊和整合。
驅動的任務是處理請求,對請求的排隊和整合由I/O排程演算法解決,因此,塊裝置驅動的核心就是請求處理函式或“製造請求”函式。
儘管在塊裝置驅動中仍然存在block_device_operations結構體及其成員函式,但其不再包含讀寫一類的成員函式,而只是包含開啟、釋放及I/O控制等與具體讀寫無關的函式。
塊裝置驅動的結構相當複雜的,但幸運的是,塊裝置不像字元裝置那麼包羅永珍,它通常就是儲存裝置,而且驅動的主體已經由Linux核心提供,針對一個特定的硬體系統,驅動工程師所涉及到的工作往往只是編寫少量的與硬體直接互動的程式碼。
本章導讀
塊裝置是與字元裝置並列的概念,這兩類裝置在Linux中驅動的結構有較大差異,總體而言,塊裝置驅動比字元裝置驅動要複雜得多,在I/O操作上表現出極大的不同,緩衝、I/O排程、請求佇列等都是與塊裝置驅動相關的概念。本章將向您展示Linux塊裝置驅動的程式設計方法。
13.1節分析塊裝置I/O操作的特點,對比字元裝置與塊裝置在I/O操作上的差異。
13.2節從整體上描述Linux塊裝置驅動的結構,分析主要的資料結構、函式及其關係。
13.3~13.5節分別闡述塊裝置驅動模組載入與解除安裝、開啟與釋放和ioctl()函式。
13.6節非常重要,它講述了塊裝置I/O操作所依賴的請求佇列的概念及用法。
13.2節與13.3~13.6節是整體與部分的關係,13.2~13.6節與13.7節是迭代遞進關係。
13.7節在13.1~13.6節講解內容的基礎上,總結Linux下塊裝置的讀寫流程。而13.7節則給出了塊裝置驅動的具體例項,即RAMDISK的驅動。
13.1塊裝置的I/O操作特點
字元裝置與塊裝置I/O操作的不同在於:
① 塊裝置只能以塊為單位接受輸入和返回輸出,而字元裝置則以位元組為單位。大多數裝置是字元裝置,因為它們不需要緩衝而且不以固定塊大小進行操作。
② 塊裝置對於I/O請求有對應的緩衝區,因此它們可以選擇以什麼順序進行響應,字元裝置無需緩衝且被直接讀寫。對於儲存裝置而言調整讀寫的順序作用巨大,因為在讀寫連續的扇區比分離的扇區更快。
③ 字元裝置只能被順序讀寫,而塊裝置可以隨機訪問。雖然塊裝置可隨機訪問,但是對於磁碟這類機械裝置而言,順序地組織塊裝置的訪問可以提高效能。如圖13.1,對磁碟1、10、3、2的請求被調整為對1、2、3、10的請求可以提高讀寫效能。注意,對SD卡、RAMDISK等塊裝置而言,不存在機械上的原因,進行這樣的調整沒有必要。
圖13.1 調整塊裝置I/O操作的順序
13.2 Linux塊裝置驅動結構
13.2.1 block_device_operations結構體
在塊裝置驅動中,有1個類似於字元裝置驅動中file_operations結構體的block_device_operations結構體,它是對塊裝置操作的集合,定義如程式碼清單13.1。
程式碼清單13.1 block_device_operations結構體
1 struct block_device_operations
2 {
3 int(*open)(struct inode *, struct file*); //開啟
4 int(*release)(struct inode *, struct file*); //釋放
5 int(*ioctl)(struct inode *, struct file *, unsigned, unsigned long); //ioctl
6 long(*unlocked_ioctl)(struct file *, unsigned, unsigned long);
7 long(*compat_ioctl)(struct file *, unsigned, unsigned long);
8 int(*direct_access)(struct block_device *, sector_t, unsigned long*);
9 int(*media_changed)(struct gendisk*); //介質被改變?
10 int(*revalidate_disk)(struct gendisk*); //使介質有效
11 int(*getgeo)(struct block_device *, struct hd_geometry*);//填充驅動器資訊
12 struct module *owner; //模組擁有者
13 };
下面對其主要的成員函式進行分析:
• 開啟和釋放
int (*open)(struct inode *inode, struct file *filp);
int (*release)(struct inode *inode, struct file *filp);
與字元裝置驅動類似,當裝置被開啟和關閉時將呼叫它們。
• IO控制
int (*ioctl)(struct inode *inode, struct file *filp, unsigned int cmd,
unsigned long arg);
上述函式是ioctl() 系統呼叫的實現,塊裝置包含大量的標準請求,這些標準請求由Linux塊裝置層處理,因此大部分塊裝置驅動的ioctl()函式相當短。
• 介質改變
int (*media_changed) (struct gendisk *gd);
被核心呼叫來檢查是否驅動器中的介質已經改變,如果是,則返回一個非零值,否則返回0。這個函式僅適用於支援可移動介質的驅動器(非可移動裝置的驅動不需要實現這個方法),通常需要在驅動中增加1個表示介質狀態是否改變的標誌變數。
• 使介質有效
int (*revalidate_disk) (struct gendisk *gd);
revalidate_disk()函式被呼叫來響應一個介質改變,它給驅動一個機會來進行必要的工作以使新介質準備好。
• 獲得驅動器資訊
int (*getgeo)(struct block_device *, struct hd_geometry *);
該函式根據驅動器的幾何資訊填充一個hd_geometry結構體,hd_geometry結構體包含磁頭、扇區、柱面等資訊。
• 模組指標
struct module *owner;
一個指向擁有這個結構體的模組的指標,它通常被初始化為THIS_MODULE。
13.2.2 gendisk結構體
在Linux核心中,使用gendisk(通用磁碟)結構體來表示1個獨立的磁碟裝置(或分割槽),這個結構體的定義如程式碼清單13.2。
程式碼清單13.2 gendisk結構體
1 struct gendisk
2 {
3 int major; /* 主裝置號 */
4 int first_minor; /*第1個次裝置號*/
5 int minors; /* 最大的次裝置數,如果不能分割槽,則為1*/
6 char disk_name[32]; /* 裝置名稱 */
7 struct hd_struct **part; /* 磁碟上的分割槽資訊 */
8 struct block_device_operations *fops; /*塊裝置操作結構體*/
9 struct request_queue *queue; /*請求佇列*/
10 void *private_data; /*私有資料*/
11 sector_t capacity; /*扇區數,512位元組為1個扇區*/
12
13 int flags;
14 char devfs_name[64];
15 int number;
16 struct device *driverfs_dev;
17 struct kobject kobj;
18
19 struct timer_rand_state *random;
20 int policy;
21
22 atomic_t sync_io; /* RAID */
23 unsigned long stamp;
24 int in_flight;
25 #ifdef CONFIG_SMP
26 struct disk_stats *dkstats;
27 #else
28 struct disk_stats dkstats;
29 #endif
30 };
major、first_minor和minors共同表徵了磁碟的主、次裝置號,同一個磁碟的各個分割槽共享1個主裝置號,而次裝置號則不同。fops為block_device_operations,即上節描述的塊裝置操作集合。queue是核心用來管理這個裝置的 I/O請求佇列的指標。capacity表明裝置的容量,以512個位元組為單位。private_data可用於指向磁碟的任何私有資料,用法與字元裝置驅動file結構體的private_data類似。
Linux核心提供了一組函式來操作gendisk,主要包括:
• 分配gendisk
gendisk結構體是一個動態分配的結構體,它需要特別的核心操作來初始化,驅動不能自己分配這個結構體,而應該使用下列函式來分配gendisk:
struct gendisk *alloc_disk(int minors);
minors 引數是這個磁碟使用的次裝置號的數量,一般也就是磁碟分割槽的數量,此後minors不能被修改。
• 增加gendisk
gendisk結構體被分配之後,系統還不能使用這個磁碟,需要呼叫如下函式來註冊這個磁碟裝置:
void add_disk(struct gendisk *gd);
特別要注意的是對add_disk()的呼叫必須發生在驅動程式的初始化工作完成並能響應磁碟的請求之後。
• 釋放gendisk
當不再需要一個磁碟時,應當使用如下函式釋放gendisk:
void del_gendisk(struct gendisk *gd);
• gendisk引用計數
gendisk中包含1個kobject成員,因此,它是一個可被引用計數的結構體。透過get_disk()和put_disk()函式可用來操作引用計數,這個工作一般不需要驅動親自做。通常對 del_gendisk()的呼叫會去掉gendisk的最終引用計數,但是這一點並不是一定的。因此,在del_gendisk()被呼叫後,這個結構體可能繼續存在。
• 設定gendisk容量
void set_capacity(struct gendisk *disk, sector_t size);
塊裝置中最小的可定址單元是扇區,扇區大小一般是2的整數倍,最常見的大小是512位元組。扇區的大小是裝置的物理屬性,扇區是所有塊裝置的基本單元,塊裝置無法對比它還小的單元進行定址和操作,不過許多塊裝置能夠一次就傳輸多個扇區。雖然大多數塊裝置的扇區大小都是512位元組,不過其它大小的扇區也很常見,比如,很多CD-ROM盤的扇區都是2K大小。
不管物理裝置的真實扇區大小是多少,核心與塊裝置驅動互動的扇區都以512位元組為單位。因此,set_capacity()函式也以512位元組為單位。
13.2.3 request與bio結構體
1、請求
在Linux塊裝置驅動中,使用request結構體來表徵等待進行的I/O請求,這個結構體的定義如程式碼清單13.3。
程式碼清單13.3 request結構體
1 struct request
2 {
3 struct list_head queuelist; /*連結串列結構*/
4 unsigned long flags; /* REQ_ */
5
6 sector_t sector; /* 要傳送的下1個扇區 */
7 unsigned long nr_sectors; /*要傳送的扇區數目*/
8 /*當前要傳送的扇區數目*/
9 unsigned int current_nr_sectors;
10
11 sector_t hard_sector; /*要完成的下1個扇區*/
12 unsigned long hard_nr_sectors; /*要被完成的扇區數目*/
13 /*當前要被完成的扇區數目*/
14 unsigned int hard_cur_sectors;
15
16 struct bio *bio; /*請求的 bio 結構體的連結串列*/
17 struct bio *biotail; /*請求的 bio 結構體的連結串列尾*/
18
19 void *elevator_private;
20
21 unsigned short ioprio;
22
23 int rq_status;
24 struct gendisk *rq_disk;
25 int errors;
26 unsigned long start_time;
27
28 /*請求在實體記憶體中佔據的不連續的段的數目,scatter/gather列表的尺寸*/
29 unsigned short nr_phys_segments;
30
31 /*與nr_phys_segments相同,但考慮了系統I/O MMU的remap */
32 unsigned short nr_hw_segments;
33
34 int tag;
35 char *buffer; /*傳送的緩衝,核心虛擬地址*/
36
37 int ref_count; /* 引用計數 */
38 ...
39 };
request結構體的主要成員包括:
sector_t hard_sector;
unsigned long hard_nr_sectors;
unsigned int hard_cur_sectors;
上述3個成員標識還未完成的扇區,hard_sector是第1個尚未傳輸的扇區,hard_nr_sectors是尚待完成的扇區數,hard_cur_sectors是並且當前I/O操作中待完成的扇區數。這些成員只用於核心塊裝置層,驅動不應當使用它們。
sector_t sector;
unsigned long nr_sectors;
unsigned int current_nr_sectors;
驅動中會經常與這3個成員打交道,這3個成員在核心和驅動互動中發揮著重大作用。它們以512位元組大小為1個扇區,如果硬體的扇區大小不是512位元組,則需要進行相應的調整。例如,如果硬體的扇區大小是2048位元組,則在進行硬體操作之前,需要用4來除起始扇區號。
hard_sector、hard_nr_sectors、hard_cur_sectors與sector、nr_sectors、current_nr_sectors之間可認為是“副本”關係。
struct bio *bio;
bio是這個請求中包含的bio結構體的連結串列,驅動中不宜直接存取這個成員,而應該使用後文將介紹的rq_for_each_bio()。
char *buffer;
指向緩衝區的指標,資料應當被傳送到或者來自這個緩衝區,這個指標是一個核心虛擬地址,可被驅動直接引用。
unsigned short nr_phys_segments;
該值表示相鄰的頁被合併後,這個請求在實體記憶體中佔據的段的數目。如果裝置支援分散/聚集(SG,scatter/gather)操作,可依據此欄位申請sizeof(scatterlist)* nr_phys_segments的記憶體,並使用下列函式進行DMA對映:
int blk_rq_map_sg(request_queue_t *q, struct request *req,
struct scatterlist *sglist);
該函式與dma_map_sg()類似,它返回scatterlist列表入口的數量。
struct list_head queuelist;
用於連結這個請求到請求佇列的連結串列結構,呼叫blkdev_dequeue_request()可從佇列中移除請求。
使用如下宏可以從request獲得資料傳送的方向:
rq_data_dir(struct request *req);
0返回值表示從裝置中讀,非 0返回值表示向裝置寫。
2、請求佇列
一個塊請求佇列是一個塊 I/O 請求的佇列,其定義如程式碼清單13.4。
程式碼清單13.4 request佇列結構體
1 struct request_queue
2 {
3 ...
4 /* 保護佇列結構體的自旋鎖 */
5 spinlock_t __queue_lock;
6 spinlock_t *queue_lock;
7
8 /* 佇列kobject */
9 struct kobject kobj;
10
11 /* 佇列設定 */
12 unsigned long nr_requests; /* 最大請求數量 */
13 unsigned int nr_congestion_on;
14 unsigned int nr_congestion_off;
15 unsigned int nr_batching;
16
17 unsigned short max_sectors; /* 最大的扇區數 */
18 unsigned short max_hw_sectors;
19 unsigned short max_phys_segments; /* 最大的段數 */
20 unsigned short max_hw_segments;
21 unsigned short hardsect_size; /* 硬體扇區尺寸 */
22 unsigned int max_segment_size; /* 最大的段尺寸 */
23
24 unsigned long seg_boundary_mask; /* 段邊界掩碼 */
25 unsigned int dma_alignment; /* DMA 傳送的記憶體對齊限制 */
26
27 struct blk_queue_tag *queue_tags;
28
29 atomic_t refcnt; /* 引用計數 */
30
31 unsigned int in_flight;
32
33 unsigned int sg_timeout;
34 unsigned int sg_reserved_size;
35 int node;
36
37 struct list_head drain_list;
38
39 struct request *flush_rq;
40 unsigned char ordered;
41 };
請求佇列跟蹤等候的塊I/O請求,它儲存用於描述這個裝置能夠支援的請求的型別資訊、它們的最大大小、多少不同的段可進入一個請求、硬體扇區大小、對齊要求等引數,其結果是:如果請求佇列被配置正確了,它不會交給該裝置一個不能處理的請求。
請求佇列還實現一個插入介面,這個介面允許使用多個I/O排程器,I/O排程器(也稱電梯)的工作是以最優效能的方式向驅動提交I/O請求。大部分I/O 排程器累積批次的 I/O 請求,並將它們排列為遞增(或遞減)的塊索引順序後提交給驅動。進行這些工作的原因在於,對於磁頭而言,當給定順序排列的請求時,可以使得磁碟順序地從一頭到另一頭工作,非常像一個滿載的電梯,在一個方向移動直到所有它的“請求”已被滿足。
另外,I/O排程器還負責合併鄰近的請求,當一個新 I/O 請求被提交給排程器後,它會在佇列裡搜尋包含鄰近扇區的請求;如果找到一個,並且如果結果的請求不是太大,排程器將合併這2個請求。
對磁碟等塊裝置進行I/O操作順序的排程類似於電梯的原理,先服務完上樓的乘客,再服務下樓的乘客效率會更高,而“上躥下跳”,順序響應使用者的請求則會導致電梯無序地忙亂。
Linux 2.6包含4個I/O排程器,它們分別是No-op I/O scheduler、Anticipatory I/O scheduler、Deadline I/O scheduler與CFQ I/O scheduler。
Noop I/O scheduler是一個簡化的排程程式,它只作最基本的合併與排序。
Anticipatory I/O scheduler是當前核心中預設的I/O排程器,它擁有非常好的效能,在2.5中它就相當引人注意。在與2.4核心進行的對比測試中,在2.4中多項以分鐘為單位完成的任務,它則是以秒為單位來完成的,正因為如此它成為目前2.6中預設的I/O排程器。Anticipatory I/O scheduler的缺點是比較龐大與複雜,在一些特殊的情況下,特別是在資料吞吐量非常大的資料庫系統中它會變得比較緩慢。
Deadline I/O scheduler是針對Anticipatory I/O scheduler的缺點進行改善而來的,表現出的效能幾乎與Anticipatory I/O scheduler一樣好,但是比Anticipatory小巧。
CFQ I/O scheduler為系統內的所有任務分配相同的頻寬,提供一個公平的工作環境,它比較適合桌面環境。事實上在測試中它也有不錯的表現,mplayer、xmms等多媒體播放器與它配合的相當好,回放平滑,幾乎沒有因訪問磁碟而出現的跳幀現象。
核心block目錄中的noop-iosched.c、as-iosched.c、deadline-iosched.c和cfq-iosched.c檔案分別實現了上述排程演算法。
可以透過給kernel新增啟動引數,選擇使用的IO排程演算法,如:
kernel elevator=deadline
• 初始化請求佇列
request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
該函式的第1個引數是請求處理函式的指標,第2個引數是控制訪問佇列許可權的自旋鎖,這個函式會發生記憶體分配的行為,故它可能會失敗,因此一定要檢查它的返回值。這個函式一般在塊裝置驅動的模組載入函式中呼叫。
• 清除請求佇列
void blk_cleanup_queue(request_queue_t * q);
這個函式完成將請求佇列返回給系統的任務,一般在塊裝置驅動模組解除安裝函式中呼叫。
而blk_put_queue()宏則定義為:
#define blk_put_queue(q) blk_cleanup_queue((q))
• 分配“請求佇列”
request_queue_t *blk_alloc_queue(int gfp_mask);
對於FLASH、RAM盤等完全隨機訪問的非機械裝置,並不需要進行復雜的I/O排程,這個時候,應該使用上述函式分配1個“請求佇列”,並使用如下函式來繫結“請求佇列”和“製造請求”函式。
void blk_queue_make_request(request_queue_t * q, make_request_fn * mfn);
在13.6.2節我們會看到,這種方式分配的“請求佇列”實際上不包含任何請求,所以給其加上引號。
• 提取請求
struct request *elv_next_request(request_queue_t *queue);
上述函式用於返回下一個要處理的請求(由 I/O 排程器決定),如果沒有請求則返回NULL。elv_next_request()不會清除請求,它仍然將這個請求保留在佇列上,但是標識它為活動的,這個標識將阻止I/O 排程器合併其它的請求到已開始執行的請求。因為elv_next_request()不從佇列裡清除請求,因此連續呼叫它2次,2次會返回同一個請求結構體。
• 去除請求
void blkdev_dequeue_request(struct request *req);
上述函式從佇列中去除1個請求。如果驅動中同時從同一個佇列中操作了多個請求,它必須以這樣的方式將它們從佇列中去除。
如果需要將1個已經出列的請求歸還到佇列中,可以呼叫:
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_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_sectors()描述任一請求可包含的最大扇區數,預設值為255;blk_queue_max_phys_segments()和 blk_queue_max_hw_segments()都控制1個請求中可包含的最大物理段(系統記憶體中不相鄰的區),blk_queue_max_hw_segments()考慮了系統I/O記憶體管理單元的重對映,這2個引數預設都是 128。blk_queue_max_segment_size告知核心請求段的最大位元組數,預設值為65,536。
• 通告核心
void blk_queue_bounce_limit(request_queue_t *queue, u64 dma_addr);
該函式用於告知核心塊裝置執行DMA時可使用的最高實體地址dma_addr,如果一個請求包含超出這個限制的記憶體引用,一個“反彈”緩衝區將被用來給這個操作。這種方式的代價昂貴,因此應儘量避免使用。
可以給dma_addr引數提供任何可能的值或使用預先定義的宏,如BLK_BOUNCE_HIGH(對高階記憶體頁使用反彈緩衝區)、BLK_BOUNCE_ISA(驅動只可在16M的ISA區執行DMA)或者BLK_BOUCE_ANY(驅動可在任何地址執行DMA),預設值是BLK_BOUNCE_HIGH。
blk_queue_segment_boundary(request_queue_t *queue, unsigned long mask);
如果我們正在驅動編寫的裝置無法處理跨越一個特殊大小記憶體邊界的請求,應該使用這個函式來告知核心這個邊界。例如,如果裝置處理跨4MB 邊界的請求有困難,應該傳遞一個0x3fffff 掩碼。預設的掩碼是0xffffffff(對應4GB邊界)。
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位元組扇區為單位進行。
3、塊I/O
通常1個bio對應1個I/O請求,程式碼清單13.5給出了bio結構體的定義。IO排程演算法可將連續的bio合併成1個請求。所以,1個請求可以包含多個bio。
程式碼清單13.5 bio結構體
1 struct bio
2 {
3 sector_t bi_sector; /* 要傳輸的第1個扇區 */
4 struct bio *bi_next; /* 下一個bio */
5 struct block_device *bi_bdev;
6 unsigned long bi_flags; /* 狀態,命令等 */
7 unsigned long bi_rw; /* 低位表示READ/WRITE,高位表示優先順序*/
8
9 unsigned short bi_vcnt; /* bio_vec數量 */
10 unsigned short bi_idx; /* 當前bvl_vec索引 */
11
12 /*不相鄰的物理段的數目*/
13 unsigned short bi_phys_segments;
14
15 /*物理合併和DMA remap合併後不相鄰的物理段的數目*/
16 unsigned short bi_hw_segments;
17
18 unsigned int bi_size; /* 以位元組為單位所需傳輸的資料大小 */
19
20 /* 為了明瞭最大的hw尺寸,我們考慮這個bio中第1個和最後1個
21 虛擬的可合併的段的尺寸 */
22 unsigned int bi_hw_front_size;
23 unsigned int bi_hw_back_size;
24
25 unsigned int bi_max_vecs; /* 我們能持有的最大bvl_vecs數 */
26
27 struct bio_vec *bi_io_vec; /* 實際的vec列表 */
28
29 bio_end_io_t *bi_end_io;
30 atomic_t bi_cnt;
31
32 void *bi_private;
33
34 bio_destructor_t *bi_destructor; /* destructor */
35 };
下面我們對其中的核心成員進行分析:
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的陣列,它由bio_vec結構體組成,bio_vec結構體的定義如程式碼清單13.6。
程式碼清單13.6 bio_vec結構體
1 struct bio_vec
2 {
3 struct page *bv_page; /* 頁指標 */
4 unsigned int bv_len; /* 傳輸的位元組數 */
5 unsigned int bv_offset; /* 偏移位置 */
6 };
我們不應該直接訪問bio的bio_vec成員,而應該使用bio_for_each_segment()宏來進行這項工作,可以用這個宏迴圈遍歷整個bio中的每個段,這個宏的定義如程式碼清單13.7。
程式碼清單13.7 bio_for_each_segment宏
1 #define __bio_for_each_segment(bvl, bio, i, start_idx) /
2 for (bvl = bio_iovec_idx((bio), (start_idx)), i = (start_idx); /
3 i < (bio)->bi_vcnt; /
4 bvl++, i++)
5
6 #define bio_for_each_segment(bvl, bio, i) /
7 __bio_for_each_segment(bvl, bio, i, (bio)->bi_idx
圖13.2(a)描述了request佇列、request與bio資料結構之間的關係,13.2(b)表示了request、bio和bio_vec資料結構之間的關係,13.2(c)表示了bio與bio_vec資料結構之間的關係,因此整個圖13.2遞迴地呈現了request佇列、request、bio和bio_vec這4個結構體之間的關係。
(a) request與bio
(b) request、bio和bio_vec
(c)bio與bio_vec
圖13.2 request佇列、request、bio和bio_vec結構體之間的關係
核心還提供了一組函式(宏)用於操作bio:
int bio_data_dir(struct bio *bio);
這個函式可用於獲得資料傳輸的方向是READ還是WRITE。
struct page *bio_page(struct bio *bio) ;
這個函式可用於獲得目前的頁指標。
int bio_offset(struct bio *bio) ;
這個函式返回操作對應的當前頁內的偏移,通常塊I/O操作本身就是頁對齊的。
int bio_cur_sectors(struct bio *bio) ;
這個函式返回當前bio_vec要傳輸的扇區數。
char *bio_data(struct bio *bio) ;
這個函式返回資料緩衝區的核心虛擬地址。
char *bvec_kmap_irq(struct bio_vec *bvec, unsigned long *flags) ;
這個函式返回一個核心虛擬地址,這個地址可用於存取被給定的bio_vec入口指向的資料緩衝區。它也會遮蔽中斷並返回1個原子kmap,因此,在bvec_kunmap_irq()被呼叫以前,驅動不應該睡眠。
void bvec_kunmap_irq(char *buffer, unsigned long *flags);
這個函式是bvec_kmap_irq()函式的“反函式”,它撤銷bvec_kmap_irq()建立的對映。
char *bio_kmap_irq(struct bio *bio, unsigned long *flags);
這個函式是對bvec_kmap_irq()的包裝,它返回給定的bio的當前bio_vec入口的對映。
char *__bio_kmap_atomic(struct bio *bio, int i, enum km_type type);
這個函式透過kmap_atomic()獲得返回給定bio的第i個緩衝區的虛擬地址。
void __bio_kunmap_atomic(char *addr, enum km_type type);
這個函式返還由__bio_kmap_atomic()獲得的核心虛擬地址。
另外,對bio的引用計數透過如下函式完成:
void bio_get(struct bio *bio); //引用bio
void bio_put(struct bio *bio); //釋放對bio的引用
13.2.4塊裝置驅動註冊與登出
塊裝置驅動中的第1個工作通常是註冊它們自己到核心,完成這個任務的函式是 register_blkdev(),其原型為:
int register_blkdev(unsigned int major, const char *name);
major引數是塊裝置要使用的主裝置號,name為裝置名,它會在/proc/devices中被顯示。 如果major為0,核心會自動分配一個新的主裝置號,register_blkdev()函式的返回值就是這個主裝置號。如果register_blkdev()返回1個負值,表明發生了一個錯誤。
與register_blkdev()對應的登出函式是unregister_blkdev(),其原型為:
int unregister_blkdev(unsigned int major, const char *name);
這裡,傳遞給register_blkdev()的引數必須與傳遞給register_blkdev()的引數匹配,否則這個函式返回-EINVAL。
值得一提的是,在2.6核心中,對 register_blkdev()的呼叫完全是可選的,register_blkdev()的功能已隨時間正在減少,這個呼叫最多隻完全2件事:
① 如果需要,分配一個動態主裝置號。
② 在/proc/devices中建立一個入口。
在將來的核心中,register_blkdev()可能會被去掉。但是目前的大部分驅動仍然呼叫它。程式碼清單13.8給出了1個塊裝置驅動註冊的模板。
程式碼清單13.8 塊裝置驅動註冊模板
1 xxx_major = register_blkdev(xxx_major, "xxx");
2 if (xxx_major <= 0) //註冊失敗
3 {
4 printk(KERN_WARNING "xxx: unable to get major number/n");
5 return -EBUSY;
6 }
13.3 Linux塊裝置驅動模組載入與解除安裝
在塊裝置驅動的模組載入函式中通常需要完成如下工作:
① 分配、初始化請求佇列,繫結請求佇列和請求函式。
② 分配、初始化gendisk,給gendisk的major、fops、queue等成員賦值,最後新增gendisk。
③ 註冊塊裝置驅動。
程式碼清單13.9和13.10分別給出了使用blk_alloc_queue()分配請求佇列並使用blk_queue_make_request()繫結“請求佇列”和“製造請求”函式,以及使用blk_init_queue()初始化請求佇列並繫結請求佇列與請求處理函式2種不同情況下的塊裝置驅動模組載入函式模板。
程式碼清單13.9 塊裝置驅動模組載入函式模板(使用blk_alloc_queue)
1 static int __init xxx_init(void)
2 {
3 //分配gendisk
4 xxx_disks = alloc_disk(1);
5 if (!xxx_disks)
6 {
7 goto out;
8 }
9
10 //塊裝置驅動註冊
11 if (register_blkdev(XXX_MAJOR, "xxx"))
12 {
13 err = - EIO;
14 goto out;
15 }
16
17 //“請求佇列”分配
18 xxx_queue = blk_alloc_queue(GFP_KERNEL);
19 if (!xxx_queue)
20 {
21 goto out_queue;
22 }
23
24 blk_queue_make_request(xxx_queue, &xxx_make_request); //繫結“製造請求”函式
25 blk_queue_hardsect_size(xxx_queue, xxx_blocksize); //硬體扇區尺寸設定
26
27 //gendisk初始化
28 xxx_disks->major = XXX_MAJOR;
29 xxx_disks->first_minor = 0;
30 xxx_disks->fops = &xxx_op;
31 xxx_disks->queue = xxx_queue;
32 sprintf(xxx_disks->disk_name, "xxx%d", i);
33 set_capacity(xxx_disks, xxx_size); //xxx_size以512bytes為單位
34 add_disk(xxx_disks); //新增gendisk
35
36 return 0;
37 out_queue: unregister_blkdev(XXX_MAJOR, "xxx");
38 out: put_disk(xxx_disks);
39 blk_cleanup_queue(xxx_queue);
40
41 return - ENOMEM;
42 }
程式碼清單13.10 塊裝置驅動模組載入函式模板(使用blk_init_queue)
1 static int __init xxx_init(void)
2 {
3 //塊裝置驅動註冊
4 if (register_blkdev(XXX_MAJOR, "xxx"))
5 {
6 err = - EIO;
7 goto out;
8 }
9
10 //請求佇列初始化
11 xxx_queue = blk_init_queue(xxx_request, xxx_lock);
12 if (!xxx_queue)
13 {
14 goto out_queue;
15 }
16
17 blk_queue_hardsect_size(xxx_queue, xxx_blocksize); //硬體扇區尺寸設定
18
19 //gendisk初始化
20 xxx_disks->major = XXX_MAJOR;
21 xxx_disks->first_minor = 0;
22 xxx_disks->fops = &xxx_op;
23 xxx_disks->queue = xxx_queue;
24 sprintf(xxx_disks->disk_name, "xxx%d", i);
25 set_capacity(xxx_disks, xxx_size *2);
26 add_disk(xxx_disks); //新增gendisk
27
28 return 0;
29 out_queue: unregister_blkdev(XXX_MAJOR, "xxx");
30 out: put_disk(xxx_disks);
31 blk_cleanup_queue(xxx_queue);
32
33 return - ENOMEM;
34 }
在塊裝置驅動的模組解除安裝函式中通常需要與模組載入函式相反的工作:
① 清除請求佇列。
② 刪除gendisk和對gendisk的引用。
③ 刪除對塊裝置的引用,登出塊裝置驅動。
程式碼清單13.11給出了塊裝置驅動模組解除安裝函式的模板。
程式碼清單13.11 塊裝置驅動模組解除安裝函式模板
1 static void __exit xxx_exit(void)
2 {
3 if (bdev)
4 {
5 invalidate_bdev(xxx_bdev, 1);
6 blkdev_put(xxx_bdev);
7 }
8 del_gendisk(xxx_disks); //刪除gendisk
9 put_disk(xxx_disks);
10 blk_cleanup_queue(xxx_queue[i]); //清除請求佇列
11 unregister_blkdev(XXX_MAJOR, "xxx");
12 }
13.4塊裝置的開啟與釋放
塊裝置驅動的open()和release()函式並非是必須的,1個簡單的塊裝置驅動可以不提供open()和release()函式。
塊裝置驅動的open()函式和其字元裝置驅動中的對等體非常類似,都以相關的inode和file結構體指標作為引數。當一個節點引用一個塊裝置時,inode->i_bdev->bd_disk 包含一個指向關聯 gendisk 結構體的指標。因此,類似於字元裝置驅動,我們也可以將gendisk的private_data賦給file的private_data,private_data同樣最好是指向描述該裝置的裝置結構體xxx_dev的指標,如程式碼清單13.12。
程式碼清單13.12 塊裝置的open()函式中賦值private_data
1 static int xxx_open(struct inode *inode, struct file *filp)
2 {
3 struct xxx_dev *dev = inode->i_bdev->bd_disk->private_data;
4 filp->private_data = dev; //賦值file的private_data
5 ...
6 return 0;
7 }
在一個處理真實的硬體裝置的驅動中,open()和release()方法還應當設定驅動和硬體的狀態,這些工作可能包括啟停磁碟、加鎖一個可移出裝置和分配DMA緩衝等。
13.5塊裝置驅動的ioctl函式
與字元裝置驅動一樣,塊裝置可以包含一個 ioctl()函式以提供對裝置的I/O控制能力。實際上,高層的塊裝置層程式碼處理了絕大多數ioctl(),因此,具體的塊裝置驅動中通常不再需要實現很多ioctl命令。
程式碼清單13.13給出的ioctl()函式只實現1個命令HDIO_GETGEO,用於獲得磁碟的幾何資訊(geometry,指CHS,即Cylinder、Head、Sector/Track)。
程式碼清單13.13 塊裝置驅動的I/O控制函式模板
1 int xxx_ioctl(struct inode *inode, struct file *filp, unsigned int cmd,
2 unsigned long arg)
3 {
4 long size;
5 struct hd_geometry geo;
6 struct xxx_dev *dev = filp->private_data; //透過file->private獲得裝置結構體
7
8 switch (cmd)
9 {
10 case HDIO_GETGEO:
11 size = dev->size *(hardsect_size / KERNEL_SECTOR_SIZE);
12 geo.cylinders = (size &~0x3f) >> 6;
13 geo.heads = 4;
14 geo.sectors = 16;
15 geo.start = 4;
16 if (copy_to_user((void __user*)arg, &geo, sizeof(geo)))
17 {
18 return - EFAULT;
19 }
20 return 0;
21 }
22
23 return - ENOTTY; //不知道的命令
24 }
13.6塊裝置驅動I/O請求處理
13.6.1使用請求佇列
塊裝置驅動請求函式的原型為:
void request(request_queue_t *queue);
這個函式不能由驅動自己呼叫,只有當核心認為是時候讓驅動處理對裝置的讀寫等操作時,它才呼叫這個函式。
請求函式可以在沒有完成請求佇列中的所有請求的情況下返回,甚至它1個請求不完成都可以返回。但是,對大部分裝置而言,在請求函式中處理完所有請求後再返回通常是值得推薦的方法。程式碼清單13.14給出了1個簡單的request()函式的例子。
程式碼清單13.14 塊裝置驅動請求函式例程
1 static void xxx_request(request_queue_t *q)
2 {
3 struct request *req;
4 while ((req = elv_next_request(q)) != NULL)
5 {
6 struct xxx_dev *dev = req->rq_disk->private_data;
7 if (!blk_fs_request(req)) //不是檔案系統請求
8 {
9 printk(KERN_NOTICE "Skip non-fs request/n");
10 end_request(req, 0);//通知請求處理失敗
11 continue;
12 }
13 xxx_transfer(dev, req->sector, req->current_nr_sectors, req->buffer,
14 rq_data_dir(req)); //處理這個請求
15 end_request(req, 1); //通知成功完成這個請求
16 }
17 }
18
19 //完成具體的塊裝置I/O操作
20 static void xxx_transfer(struct xxx_dev *dev, unsigned long sector, unsigned
21 long nsect, char *buffer, int write)
22 {
23 unsigned long ffset = sector * KERNEL_SECTOR_SIZE;
24 unsigned long nbytes = nsect * KERNEL_SECTOR_SIZE;
25 if ((offset + nbytes) > dev->size)
26 {
27 printk(KERN_NOTICE "Beyond-end write (%ld %ld)/n", offset, nbytes);
28 return ;
29 }
30 if (write)
31 {
32 write_dev(offset, buffer, nbytes); //向裝置些nbytes個位元組的資料
33 }
34 else
35 {
36 read_dev(offset, buffer, nbytes); //從裝置讀nbytes個位元組的資料
37 }
38 }
上述程式碼第4行使用elv_next_request()獲得佇列中第一個未完成的請求,end_request()會將請求從請求佇列中剝離。第7行判斷請求是否為檔案系統請求,如果不是,則直接清除,呼叫end_request(),傳遞給end_request()的第2個引數為0意味著處理該請求失敗。而第15行傳遞給end_request()的第2個引數為1意味著該請求處理成功。
end_request()函式非常重要,其原始碼如程式碼清單13.15。
程式碼清單13.15 end_request()函式原始碼
1 void end_request(struct request *req, int uptodate)
2 {
3
4 if (!end_that_request_first(req, uptodate, req->hard_cur_sectors))
5 {
6 add_disk_randomness (req->rq_disk);
7 blkdev_dequeue_request (req);
8 end_that_request_last(req);
9 }
10 }
當裝置已經完成1個I/O請求的部分或者全部扇區傳輸後,它必須通告塊裝置層,上述程式碼中的第4行完成這個工作。end_that_request_first()函式的原型為:
int end_that_request_first(struct request *req, int success, int count);
這個函式告知塊裝置層,塊裝置驅動已經完成count個扇區的傳送。end_that_request_first()的返回值是一個標誌,指示是否這個請求中的所有扇區已經被傳送。返回值為0表示所有的扇區已經被傳送並且這個請求完成,之後,我們必須使用 blkdev_dequeue_request()來從佇列中清除這個請求。最後,將這個請求傳遞給end_that_request_last()函式:
void end_that_request_last(struct request *req);
end_that_request_last()通知所有正在等待這個請求完成的物件請求已經完成並回收這個請求結構體。
第6行的add_disk_randomness()函式的作用是使用塊 I/O 請求的定時來給系統的隨機數池貢獻熵,它不影響塊裝置驅動。但是,僅當磁碟的操作時間是真正隨機的時候(大部分機械裝置如此),才應該呼叫它。
程式碼清單13.16給出了1個更復雜的請求函式,它進行了3層遍歷:遍歷請求佇列中的每個請求;遍歷請求中的每個bio;遍歷bio中的每個段。
程式碼清單13.16 請求函式遍歷請求、bio和段
1 static void xxx_full_request(request_queue_t *q)
2 {
3 struct request *req;
4 int sectors_xferred;
5 struct xxx_dev *dev = q->queuedata;
6 /* 遍歷每個請求 */
7 while ((req = elv_next_request(q)) != NULL)
8 {
9 if (!blk_fs_request(req))
10 {
11 printk(KERN_NOTICE "Skip non-fs request/n");
12
13 end_request(req, 0);
14 continue;
15 }
16 sectors_xferred = xxx_xfer_request(dev, req);
17 if (!end_that_request_first(req, 1, sectors_xferred))
18 {
19 blkdev_dequeue_request(req);
20 end_that_request_last(req);
21 }
22 }
23 }
24 /* 請求處理 */
25 static int xxx_xfer_request(struct xxx_dev *dev, struct request *req)
26 {
27 struct bio *bio;
28 int nsect = 0;
29 /* 遍歷請求中的每個bio */
30 rq_for_each_bio(bio, req)
31 {
32 xxx_xfer_bio(dev, bio);
33 nsect += bio->bi_size / KERNEL_SECTOR_SIZE;
34 }
35 return nsect;
36 }
37 /* bio處理 */
38 static int xxx_xfer_bio(struct xxx_dev *dev, struct bio *bio)
39 {
40 int i;
41 struct bio_vec *bvec;
42 sector_t sector = bio->bi_sector;
43
44 /* 遍歷每1段 */
45 bio_for_each_segment(bvec, bio, i)
46 {
47 char *buffer = __bio_kmap_atomic(bio, i, KM_USER0);
48 xxx_transfer(dev, sector, bio_cur_sectors(bio), buffer, bio_data_dir(bio)
49 == WRITE);
50 sector += bio_cur_sectors(bio);
51 __bio_kunmap_atomic(bio, KM_USER0);
52 }
53 return 0;
54 }
圖13.3呈現了1個請求佇列內request、bio以及bio中segment的層層遍歷關係。
圖13.3 遍歷1個請求佇列
13.6.2不使用請求佇列
使用請求佇列對於一個機械的磁碟裝置而言的確有助於提高系統的效能,但是對於許多塊裝置,如數位相機的儲存卡、RAM盤等完全可真正隨機訪問的裝置而言,無法從高階的請求佇列邏輯中獲益。對於這些裝置,塊層支援“無佇列”的操作模式,為使用這個模式,驅動必須提供一個“製造請求”函式,而不是一個請求函式,“製造請求”函式的原型為:
typedef int (make_request_fn) (request_queue_t *q, struct bio *bio);
上述函式的第1個引數仍然是“請求佇列”,但是這個“請求佇列”實際不包含任何請求。因此,“製造請求”函式的主要引數是bio結構體,這個bio結構體表示1個或多個要傳送的緩衝區。“製造請求”函式或者直接進行傳輸,或者把請求重定向給其它裝置。
在“製造請求”函式中處理bio的方式與13.6.1節中描述的完全一致,但是在處理完成後應該使用bio_endio()函式通知處理結束:
void bio_endio(struct bio *bio, unsigned int bytes, int error);
引數bytes 是已經傳送的位元組數,它可以比這個bio所代表的位元組數少,這意味著“部分完成”,同時bio結構體中的當前緩衝區指標需要更新。當裝置進一步處理這個bio後,驅動應該再次呼叫 bio_endio(),如果不能完成這個請求,應指出一個錯誤,錯誤碼賦值給error引數。
不管對應的I/O處理成功與否,“製造請求”函式都應該返回0。如果“製造請求”函式返回一個非零值,bio 將被再次提交。
程式碼清單13.17給出了1個“製造請求”函式的例子。
程式碼清單13.17 “製造請求”函式例程
1 static int xxx_make_request(request_queue_t *q, struct bio *bio)
2 {
3 struct xxx_dev *dev = q->queuedata;
4 int status;
5 status = xxx_xfer_bio(dev, bio); //處理bio
6 bio_endio(bio, bio->bi_size, status); //通告結束
7 return 0;
8 }
為了使用無佇列的I/O請求處理,驅動模組載入函式應遵循程式碼清單13.9的模板而非13.10的模板,而使用請求佇列時,驅動模組載入函式應遵循程式碼清單13.10的模板。
13.7例項1:RAMDISK驅動
13.7.1 RAMDISK的硬體原理
RAMDISK(RAM盤)是一種模擬磁碟,其資料實際上儲存在RAM中,它使用一部分記憶體空間來模擬出一個磁碟,以塊裝置的方式來訪問這片記憶體,RAMDISK對應的裝置檔案一般為/dev/ram%d。
使用如下一組命令就可以建立並掛載RAMDISK:
mkdir /tmp/ramdisk0 建立裝載點
mke2fs /dev/ram0 建立一個檔案系統
mount /dev/ram0 /tmp/ramdisk0 裝載ramdisk
其中,mke2f /dev/ram0命令的執行會回饋類似於如下的資訊:
mke2fs 1.14, 9-Jan-1999 for EXT2 FS 0.5b, 95/08/09
Linux ext2 filesystem format
Filesystem label=
1024 inodes, 4096 blocks
204 blocks (4.98%) reserved for the super user
First data block=1
Block size=1024 (log=0)
Fragment size=1024 (log=0)
1 block group
8192 blocks per group, 8192 fragments per group
1024 inodes per group
表明建立了1個4MB的塊裝置,共包含4096塊,每塊1024位元組。
13.7.2 RAMDISK驅動模組載入與解除安裝
RAMDISK的驅動模組載入函式完成的工作與13.3節給出的模板完全一致,由於RAM盤屬於完全的隨機裝置,宜使用無佇列的I/O處理方式,其驅動中實現瞭如圖13.4所示的與塊裝置驅動模板對應的函式。
圖13.4 塊裝置驅動模板與RAMDISK裝置驅動的對映
程式碼清單13.18給出了RAMDISK裝置驅動的模組載入與解除安裝函式,實現的功能與模組是一致的。
程式碼清單13.18 RAMDISK裝置驅動的模組載入與解除安裝函式
1 static int __init rd_init(void)
2 {
3 int i;
4 int err = - ENOMEM;
5 //調整塊尺寸
6 if (rd_blocksize > PAGE_SIZE || rd_blocksize < 512 || (rd_blocksize &
7 (rd_blocksize - 1)))
8 {
9 printk("RAMDISK: wrong blocksize %d, reverting to defaults/n", rd_blocksize) ;
10
11 rd_blocksize = BLOCK_SIZE;
12 }
13 //分配gendisk
14 for (i = 0; i < CONFIG_BLK_DEV_RAM_COUNT; i++)
15 {
16 rd_disks[i] = alloc_disk(1); //分配gendisk
17 if (!rd_disks[i])
18 goto out;
19 }
20 //塊裝置註冊
21 if (register_blkdev(RAMDISK_MAJOR, "ramdisk"))
22 //註冊塊裝置
23 {
24 err = - EIO;
25 goto out;
26 }
27
28 devfs_mk_dir("rd"); //建立devfs目錄
29
30 for (i = 0; i < CONFIG_BLK_DEV_RAM_COUNT; i++)
31 {
32 struct gendisk *disk = rd_disks[i];
33 //分配並繫結請求佇列與“製造請求”函式
34 rd_queue[i] = blk_alloc_queue(GFP_KERNEL);
35 if (!rd_queue[i])
36 goto out_queue;
37
38 blk_queue_make_request(rd_queue[i], &rd_make_request); //繫結“製造請求”函式
39 blk_queue_hardsect_size(rd_queue[i], rd_blocksize); //硬體扇區尺寸設定
40
41 //初始化gendisk
42 disk->major = RAMDISK_MAJOR;
43 disk->first_minor = i;
44 disk->fops = &rd_bd_op;
45 disk->queue = rd_queue[i];
46 disk->flags |= GENHD_FL_SUPPRESS_PARTITION_INFO;
47 sprintf(disk->disk_name, "ram%d", i);
48 sprintf(disk->devfs_name, "rd/%d", i);
49 set_capacity(disk, rd_size *2);
50 add_disk(rd_disks[i]); //新增gendisk
51 }
52
53 // rd_size以kB為單位
54 printk("RAMDISK driver initialized: "
55 "%d RAMDISKs of %dK size %d blocksize/n",
56 CONFIG_BLK_DEV_RAM_COUNT,rd_size, rd_blocksize);
57
58 return 0;
59 out_queue: unregister_blkdev(RAMDISK_MAJOR, "ramdisk");
60 out:
61 while (i--)
62 {
63 put_disk(rd_disks[i]);
64 blk_cleanup_queue(rd_queue[i]);
65 }
66 return err;
67 }
68
69 static void __exit rd_cleanup(void)
70 {
71 int i;
72
73 for (i = 0; i < CONFIG_BLK_DEV_RAM_COUNT; i++)
74 {
75 struct block_device *bdev = rd_bdev[i];
76 rd_bdev[i] = NULL;
77 if (bdev)
78 {
79 invalidate_bdev(bdev, 1);
80 blkdev_put(bdev);
81 }
82 del_gendisk(rd_disks[i]); //刪除gendisk
83 put_disk(rd_disks[i]); //釋放對gendisk的引用
84 blk_cleanup_queue(rd_queue[i]); //清除請求佇列
85 }
86 devfs_remove("rd");
87 unregister_blkdev(RAMDISK_MAJOR, "ramdisk"); //塊裝置登出
88 }
13.7.3 RAMDISK裝置驅動block_device_operations及成員函式
RAMDISK提供block_device_operations結構體中2個成員函式open()和ioctl()的實現,程式碼清單13.19給出了RAMDISK裝置驅動的block_device_operations結構體定義及open()和ioctl()函式。
程式碼清單13.19 RAMDISK裝置驅動block_device_operations結構體及成員函式
1 static struct block_device_operations rd_bd_op =
2 {
3 .owner = THIS_MODULE,
4 .open = rd_open,
5 .ioctl = rd_ioctl,
6 };
7
8 static int rd_open(struct inode *inode, struct file *filp)
9 {
10 unsigned unit = iminor(inode);//獲得次裝置號
11
12 if (rd_bdev[unit] == NULL) {
13 struct block_device *bdev = inode->i_bdev;//獲得block_device結構體指標
14 struct address_space *mapping; //地址空間
15 unsigned bsize;
16 gfp_t gfp_mask;
17 /* 設定inode成員 */
18 inode = igrab(bdev->bd_inode);
19 rd_bdev[unit] = bdev;
20 bdev->bd_openers++;
21 bsize = bdev_hardsect_size(bdev);
22 bdev->bd_block_size = bsize;
23 inode->i_blkbits = blksize_bits(bsize);
24 inode->i_size = get_capacity(bdev->bd_disk)<<9;
25
26 mapping = inode->i_mapping;
27 mapping->a_ops = &ramdisk_aops;
28 mapping->backing_dev_info = &rd_backing_dev_info;
29 bdev->bd_inode_backing_dev_info = &rd_file_backing_dev_info;
30
31 gfp_mask = mapping_gfp_mask(mapping);
32 gfp_mask &= ~(__GFP_FS|__GFP_IO);
33 gfp_mask |= __GFP_HIGH;
34 mapping_set_gfp_mask(mapping, gfp_mask);
35 }
36
37 return 0;
38 }
39
40 static int rd_ioctl(struct inode *inode, struct file *file,
41 unsigned int cmd, unsigned long arg)
42 {
43 int error;
44 struct block_device *bdev = inode->i_bdev;
45
46 if (cmd != BLKFLSBUF) /* 不是flush buffer cache 命令 */
47 return -ENOTTY;
48 /* 重新整理buffer cache */
49 error = -EBUSY;
50 down(&bdev->bd_sem);
51 if (bdev->bd_openers <= 2) {
52 truncate_inode_pages(bdev->bd_inode->i_mapping, 0);
53 error = 0;
54 }
55 up(&bdev->bd_sem);
56 return error;
57 }
13.7.4 RAMDISK I/O請求處理
鑑於RAMDISK是一種完全隨機裝置,其驅動中宜使用“製造請求”函式而非請求函式,這個函式的實現如程式碼清單13.20。
程式碼清單13.20 RAMDISK裝置驅動“製造請求”函式
1 static int rd_make_request(request_queue_t *q, struct bio *bio)
2 {
3 struct block_device *bdev = bio->bi_bdev;
4 struct address_space *mapping = bdev->bd_inode->i_mapping;
5 sector_t sector = bio->bi_sector;
6 unsigned long len = bio->bi_size >> 9;
7 int rw = bio_data_dir(bio);//資料傳輸方向:讀/寫?
8 struct bio_vec *bvec;
9 int ret = 0, i;
10
11 if (sector + len > get_capacity(bdev->bd_disk))
12 //超過容量
13 goto fail;
14
15 if (rw == READA)
16 rw = READ;
17 //遍歷每個段
18 bio_for_each_segment(bvec, bio, i)
19 {
20 ret |= rd_blkdev_pagecache_IO(rw, bvec, sector, mapping);
21 sector += bvec->bv_len >> 9;
22 }
23 if (ret)
24 goto fail;
25
26 bio_endio(bio, bio->bi_size, 0); //處理結束
27 return 0;
28 fail: bio_io_error(bio, bio->bi_size);
29 return 0;
30 }
13.8例項2:IDE硬碟裝置驅動
13.8.1 IDE硬碟裝置原理
IDE(Integrated Drive Electronics)介面,也就是整合驅動器電路介面,原名為ATA(AT Attachment,AT嵌入式)介面,其本意為將硬碟控制器與盤體整合在一起的硬碟驅動器,經歷了ATA-1到ATA-7以及SATA-1和SATA-2的發展歷史。ATA-1至ATA-4採用40芯排線纜,ATA-5至ATA-7則採用40針80芯線纜,雖然線纜數量增加了,但是邏輯原理沒有變,只是透過物理上的改變來達到改善PCB訊號完整性的目的,它提供更多的地線並使訊號線臨近地線,從而減少電流回流的面積。SATA-1和SATA-2與ATA-1至ATA-7相比,資料傳輸方式由並行轉變為序列。
IDE介面的硬體原理實際上非常簡單,對CPU的外圍匯流排進行簡單的擴充套件後就可外接IDE控制器,表13.2給出了40針IDE介面的引腳定義。
表13.2 IDE介面的引腳定義
引腳 訊號 訊號描述 訊號方向 引腳 訊號 訊號描述 訊號方向
1 RSET 復位 I 2 GND 地 I/O
3 DD7 資料位7 I/O 4 DD8 資料位8 I/O
5 DD6 資料位6 I/O 6 DD9 資料位9 I/O
7 DD5 資料位5 I/O 8 DD10 資料位10 I/O
9 DD4 資料位4 I/O 10 DD11 資料位11 I/O
11 DD3 資料位3 I/O 12 DD12 資料位12 I/O
13 DD2 資料位2 I/O 14 DD13 資料位13 I/O
15 DD1 資料位1 I/O 16 DD14 資料位14 I/O
17 DD0 資料位0 I/O 18 DD15 資料位15 I/O
19 GND 地 20 N.C 未用
21 DMARQ DMA請求 O 22 GND 地
23 DIOW/ 寫選通 I 24 GND 地
25 DIOR/ 讀選通 I 26 GND 地
27 IORDY 通道就緒 O 28 DPSYNC:CXEL 同步電纜選擇
29 DMACK/ DMA應答 O 30 GND 地
31 INTRQ/ 中斷請求 O 32 IOCS13/ 16為IO O
33 DA1 地址1 I 34 PDIAG/ 診斷完成 O
35 DA0 地址0 I 36 DA2 地址2 I
37 CS1FX/ 片選0 I 38 CS3FX/ 片選1 I
39 DASP/ 驅動器啟用 O 40 GND 地
IDE控制器提供了一組暫存器,透過這些暫存器,主機能控制IDE驅動器的行為和查詢其狀態,表13.3給出了IDE介面暫存器的定義。
表13.3 IDE介面暫存器定義
片選1 片選0 地址2 地址1 地址0 讀 寫 位數
1 0 0 0 0 資料暫存器 資料暫存器 16
1 0 0 0 1 錯誤暫存器 特徵暫存器 8
1 0 0 1 0 扇區數暫存器 扇區數暫存器 8
1 0 0 1 1 扇區號暫存器 扇區號暫存器 8
1 0 1 0 0 柱面號暫存器(低8位) 柱面號暫存器(低8位) 8
1 0 1 0 1 柱面號暫存器(高8位) 柱面號暫存器(高8位) 8
1 0 1 1 0 驅動器選擇/磁頭暫存器 驅動器選擇/磁頭暫存器 8
1 0 1 1 1 狀態暫存器 命令暫存器 8
0 1 1 1 0 狀態暫存器 裝置控制器暫存器 8
IDE硬碟的傳輸模式有以下3種:
• PIO(Programmed I/O)模式:PIO模式是一種透過CPU執行I/O埠指令來進行資料讀寫的資料交換模式,是最早先的硬碟資料傳輸模式,資料傳輸速率低下,CPU佔有率也很高。
• DMA(Driect Memory Access)模式:DMA模式是一種不經過CPU而直接從記憶體了存取資料的資料交換模式。PIO模式下硬碟和記憶體之間的資料傳輸是由CPU來控制的;而在DMA模式下,CPU只須向DMA控制器下達指令,讓DMA控制器來處理資料的傳送,資料傳送完畢再把資訊反饋給CPU,這樣就很大程度上減輕了CPU的資源佔有率。
• Ultra DMA(簡稱UDMA)模式:它在包含了DMA模式的優點的基礎上,又增加了CRC校驗技術,提高資料傳輸過程中的準確性,安全性得到保障。另外,在以往的硬碟資料傳輸模式下,一個時鐘週期只傳輸一次資料,而在UDMA 模式中逐漸應用了Double Data Rate(雙倍資料傳輸)技術,它在時鐘的上升沿和下降沿各自進行一次資料傳輸,使資料傳輸速度成倍增長。
除了可以以CHS(Cylinder、Head 和 Sector)的方式定位硬碟的扇區外,還可以用LBA(邏輯塊線性地址)的方式來定位,CHS可以換算為LBA。CHS 設計最多隻允許 65536 個柱面、16 個磁頭,以及 255 扇區/磁軌。這就將容量限制為 267386880 個扇區,即大約 137 GB。
假設用c表示當前柱面號,h表示當前磁頭號,cs表示起始柱面號,hs表示起始磁頭號,ss表示起始扇區號,ps表示每磁軌有多少個扇區,ph表示每柱面有多少個磁軌(一般情況下,cs=0、hs=0、ss=1、ps=63、ph=255),LBA與CHS有如下對應關係:
lba=(c-cs)*ph*ps+(h-hs)*ps+(s-ss)
LBA使得系統忽略硬碟的幾何結構,交由驅動器來完成。系統不需要去查詢 CHS 值,而只需要查詢邏輯塊地址(Logical Block Address,LBA),驅動器電子裝置會找出要讀或寫的實際扇區。而LBA48(48位邏輯塊地址)則可以使系統支援超過137GB的硬碟。
Linux核心中,與IDE驅動相關的檔案被放置在/drivers/ide目錄下,這個目錄包含ide.c、ide-cd.c、ide-cd.h、ide-disk.c、ide-dma.c、ide-floppy.c、ide-generic.c、ide-io.c、ide-iops.c、ide-lib.c、ide-pnp.c、ide-probe.c、ide-proc.c、ide-tape.c、ide-taskfile.c、ide-timing.h檔案以及針對ARM、PPC、MIPS等外圍IDE裝置驅動的目錄。整個IDE裝置驅動的體系結構及其複雜,但大多數都不需要關心,驅動工程師要使Linux支援某嵌入式系統中的IDE硬碟,所需編寫的程式碼量是非常少的。13.8.2~13.8.3純粹出於學習目的,對IDE硬碟驅動的block_device_operations及IO請求處理過程進行分析以進一步加深讀者對Linux塊裝置I/O操作方法的印象,13.8.4則從工程角度出發,講解如何使Linux支援新系統中的IDE硬碟。
13.8.2 IDE硬碟裝置驅動block_device_operations及成員函式
IDE硬碟驅動的block_device_operations中包含了開啟、釋放、IO控制、獲得幾何資訊、媒介改變和使介質有效的成員函式,這些函式的實現較簡單,如程式碼清單13.21。
程式碼清單13.21 IDE硬碟驅動block_device_operations結構體及其成員函式
1 static struct block_device_operations idedisk_ops =
2 {
3 .owner = THIS_MODULE,
4 .open = idedisk_open,
5 .release = idedisk_release,
6 .ioctl = idedisk_ioctl,
7 .getgeo = idedisk_getgeo, //得到幾何資訊
8 .media_changed = idedisk_media_changed, //媒介改變
9 .revalidate_disk= idedisk_revalidate_disk //使介質有效
10 };
11
12 static int idedisk_ioctl(struct inode *inode, struct file *file,
13 unsigned int cmd, unsigned long arg)
14 {
15 struct block_device *bdev = inode->i_bdev;
16 struct ide_disk_obj *idkp = ide_disk_g(bdev->bd_disk);
17 return generic_ide_ioctl(idkp->drive, file, bdev, cmd, arg);//通用IDE的IO控制
18 }
19
20
21 static int idedisk_getgeo(struct block_device *bdev, struct hd_geometry *geo)
22 {
23 struct ide_disk_obj *idkp = ide_disk_g(bdev->bd_disk);
24 ide_drive_t *drive = idkp->drive;
25 /* 得到幾何資訊,CHS */
26 geo->heads = drive->bios_head;
27 geo->sectors = drive->bios_sect;
28 geo->cylinders = (u16)drive->bios_cyl; /* truncate */
29 return 0;
30 }
31
32 static int idedisk_open(struct inode *inode, struct file *filp)
33 {
34 struct gendisk *disk = inode->i_bdev->bd_disk;
35 struct ide_disk_obj *idkp;
36 ide_drive_t *drive;
37
38 if (!(idkp = ide_disk_get(disk)))
39 return -ENXIO;
40
41 drive = idkp->drive;
42
43 drive->usage++; //使用計數加1
44 if (drive->removable && drive->usage == 1) {
45 ide_task_t args;
46 memset(&args, 0, sizeof(ide_task_t));
47 args.tfRegister[IDE_COMMAND_OFFSET] = WIN_DOORLOCK;
48 args.command_type = IDE_DRIVE_TASK_NO_DATA;
49 args.handler = &task_no_data_intr;
50 check_disk_change(inode->i_bdev);
51
52 if (drive->doorlocking && ide_raw_taskfile(drive, &args, NULL))
53 drive->doorlocking = 0;
54 }
55 return 0;
56 }
57
58 static int idedisk_release(struct inode *inode, struct file *filp)
59 {
60 struct gendisk *disk = inode->i_bdev->bd_disk;
61 struct ide_disk_obj *idkp = ide_disk_g(disk);
62 ide_drive_t *drive = idkp->drive;
63
64 if (drive->usage == 1)
65 ide_cacheflush_p(drive);
66 if (drive->removable && drive->usage == 1) {
67 ide_task_t args;
68 memset(&args, 0, sizeof(ide_task_t));
69 args.tfRegister[IDE_COMMAND_OFFSET] = WIN_DOORUNLOCK;
70 args.command_type = IDE_DRIVE_TASK_NO_DATA;
71 args.handler = &task_no_data_intr;
72 if (drive->doorlocking && ide_raw_taskfile(drive, &args, NULL))
73 drive->doorlocking = 0;
74 }
75 drive->usage--; //使用計數減1
76
77 ide_disk_put(idkp);
78 return 0;
79 }
13.8.3 IDE硬碟裝置驅動I/O請求處理
Linux對IDE驅動進行了再封裝,定義了ide_driver_t結構體,這個結構體容納了IDE硬碟的探測、移除、請求處理和結束請求處理等函式指標。結束請求處理函式ide_end_request()是對end_request()函式針對IDE的修改。程式碼清單13.21給出了ide_driver_t結構體的定義。
程式碼清單13.22 ide_driver_t結構體
1 static ide_driver_t idedisk_driver = {
2 .gen_driver = {
3 .owner = THIS_MODULE,
4 .name = "ide-disk",
5 .bus = &ide_bus_type,
6 },
7 .probe = ide_disk_probe, //探測
8 .remove = ide_disk_remove, //移除
9 .shutdown = ide_device_shutdown, //關閉
10 .version = IDEDISK_VERSION,
11 .media = ide_disk, //媒介型別
12 .supports_dsc_overlap = 0,
13 .do_request = ide_do_rw_disk, //請求處理函式
14 .end_request = ide_end_request, //請求處理結束
15 .error = __ide_error,
16 .abort = __ide_abort,
17 .proc = idedisk_proc,
18 };
程式碼清單13.22第13行的ide_do_rw_disk()函式完成硬碟I/O操作請求的處理,如程式碼清單13.23所示。
程式碼清單13.23 IDE硬碟驅動I/O請求處理
1 static ide_startstop_t ide_do_rw_disk(ide_drive_t *drive, struct request *rq,
2 sector_t block)
3 {
4 ide_hwif_t *hwif = HWIF(drive);
5
6 BUG_ON(drive->blocked);
7
8 if (!blk_fs_request(rq)) //不是檔案系統請求
9 {
10 blk_dump_rq_flags(rq, "ide_do_rw_disk - bad command");
11 ide_end_request(drive, 0, 0); //以失敗結束該請求
12 return ide_stopped;
13 }
14
15 pr_debug("%s: %sing: block=%llu, sectors=%lu, buffer=0x%08lx/n",
16 drive->name,rq_data_dir(rq) == READ ? "read" : "writ", (unsigned long
17 long)block, rq->nr_sectors, (unsigned long)rq->buffer);
18
19 if (hwif->rw_disk)
20 hwif->rw_disk(drive, rq);
21
22 return __ide_do_rw_disk(drive, rq, block); //具體的請求處理
23 }
24
25 static ide_startstop_t __ide_do_rw_disk(ide_drive_t *drive, struct request *rq,
26 sector_t block)
27 {
28 ide_hwif_t *hwif = HWIF(drive);
29 unsigned int dma = drive->using_dma;
30 u8 lba48 = (drive->addressing == 1) ? 1 : 0;
31 task_ioreg_t command = WIN_NOP;
32 ata_nsector_t nsectors;
33
34 nsectors.all = (u16)rq->nr_sectors; //要傳送的扇區數
35
36 if (hwif->no_lba48_dma && lba48 && dma)
37 {
38 if (block + rq->nr_sectors > 1ULL << 28)
39 dma = 0;
40 else
41 lba48 = 0;
42 }
43
44 if (!dma)
45 {
46 ide_init_sg_cmd(drive, rq);
47 ide_map_sg(drive, rq);
48 }
49
50 if (IDE_CONTROL_REG)
51 hwif->OUTB(drive->ctl, IDE_CONTROL_REG);
52
53 if (drive->select.b.lba)
54 {
55 if (lba48) //48位LBA
56 {
57 ...
58 }
59 else
60 {
61 //LBA方式,寫入要讀寫的位置資訊到IDE暫存器
62 hwif->OUTB(0x00, IDE_FEATURE_REG);
63 hwif->OUTB(nsectors.b.low, IDE_NSECTOR_REG);
64 hwif->OUTB(block, IDE_SECTOR_REG);
65 hwif->OUTB(block >>= 8, IDE_LCYL_REG);
66 hwif->OUTB(block >>= 8, IDE_HCYL_REG);
67 hwif->OUTB(((block >> 8) &0x0f) | drive->select.all,IDE_SELECT_REG);
68 }
69 }
70 else
71 {
72 unsigned int sect, head, cyl, track;
73 track = (int)block / drive->sect;
74 sect = (int)block % drive->sect + 1;
75 hwif->OUTB(sect, IDE_SECTOR_REG);
76 head = track % drive->head;
77 cyl = track / drive->head;
78
79 pr_debug("%s: CHS=%u/%u/%u/n", drive->name, cyl, head, sect);
80 //CHS方式,寫入要讀寫的位置資訊到IDE暫存器
81 hwif->OUTB(0x00, IDE_FEATURE_REG);
82 hwif->OUTB(nsectors.b.low, IDE_NSECTOR_REG);
83 hwif->OUTB(cyl, IDE_LCYL_REG);
84 hwif->OUTB(cyl >> 8, IDE_HCYL_REG);
85 hwif->OUTB(head | drive->select.all, IDE_SELECT_REG);
86 }
87
88 if (dma) //DMA方式
89 {
90 if (!hwif->dma_setup(drive)) //設定DMA成功
91 {
92 if (rq_data_dir(rq))
93 {
94 command = lba48 ? WIN_WRITEDMA_EXT : WIN_WRITEDMA;
95 if (drive->vdma)
96 command = lba48 ? WIN_WRITE_EXT : WIN_WRITE;
97 }
98 else
99 {
100 command = lba48 ? WIN_READDMA_EXT : WIN_READDMA;
101 if (drive->vdma)
102 command = lba48 ? WIN_READ_EXT : WIN_READ;
103 }
104 hwif->dma_exec_cmd(drive, command);
105 hwif->dma_start(drive);
106 return ide_started;
107 }
108 /* 回到PIO模式 */
109 ide_init_sg_cmd(drive, rq);
110 }
111
112 if (rq_data_dir(rq) == READ) //資料傳輸方向是讀
113 {
114 if (drive->mult_count)
115 {
116 hwif->data_phase = TASKFILE_MULTI_IN;
117 command = lba48 ? WIN_MULTREAD_EXT : WIN_MULTREAD;
118 }
119 else
120 {
121 hwif->data_phase = TASKFILE_IN;
122 command = lba48 ? WIN_READ_EXT : WIN_READ;
123 }
124 //執行讀命令
125 ide_execute_command(drive, command, &task_in_intr, WAIT_CMD, NULL);
126 return ide_started;
127 }
128 else //資料傳輸方向是寫
129 {
130 if (drive->mult_count)
131 {
132 hwif->data_phase = TASKFILE_MULTI_OUT;
133 command = lba48 ? WIN_MULTWRITE_EXT : WIN_MULTWRITE;
134 }
135 else
136 {
137 hwif->data_phase = TASKFILE_OUT;
138 command = lba48 ? WIN_WRITE_EXT : WIN_WRITE;
139 }
140
141 //寫IDE命令暫存器寫入寫命令
142 hwif->OUTB(command, IDE_COMMAND_REG);
143
144 return pre_task_out_intr(drive, rq);
145 }
146 }
從程式碼清單13.23可知,真正開始執行I/O操作的是其22行引用的__ide_do_rw_disk()函式。這個函式會根據不同的操作模式,將要讀寫的LBA或CHS資訊寫入IDE暫存器內,並給其命令暫存器寫入讀、寫命令。
為了進行硬碟讀寫操作,第61~67和80~85行將引數寫入地址暫存器和特性暫存器,如果是讀,第125行呼叫的ide_execute_command()會將讀命令寫入命令暫存器;如果是寫,第142行將寫命令寫入IDE命令暫存器IDE_COMMAND_REG。
真正呼叫ide_driver_t結構體中do_request()成員函式即ide_do_rw_disk()的是ide-io.c檔案中的start_request()函式,這個函式會過濾掉一些請求,最終將讀寫I/O操作請求傳遞給ide_do_rw_disk()函式,如程式碼清單13.24。
程式碼清單13.24 開始執行1個IDE請求的start_request()函式
1 static ide_startstop_t start_request(ide_drive_t *drive, struct request *rq)
2 {
3 ide_startstop_t startstop;
4 sector_t block;
5
6 BUG_ON(!(rq->flags &REQ_STARTED));
7
8 /* 超過了最大失敗次數 */
9 if (drive->max_failures && (drive->failures > drive->max_failures))
10 {
11 goto kill_rq;
12 }
13
14 block = rq->sector; //要傳輸的下1個扇區
15 if (blk_fs_request(rq) && (drive->media == ide_disk || drive->media ==
16 ide_floppy)) //是檔案系統請求,是IDE盤
17 {
18 block += drive->sect0;
19 }
20 /* 如果將0扇區重對映到1扇區 */
21 if (block == 0 && drive->remap_0_to_1 == 1)
22 block = 1;
23
24 if (blk_pm_suspend_request(rq) && rq->pm->pm_step ==
25 ide_pm_state_start_suspend)
26 drive->blocked = 1;
27 else if (blk_pm_resume_request(rq) && rq->pm->pm_step ==
28 ide_pm_state_start_resume)
29 {
30 /* 醒來後的第1件事就是等待BSY位不忙 */
31 int rc;
32
33 rc = ide_wait_not_busy(HWIF(drive), 35000);
34 if (rc)
35 printk(KERN_WARNING "%s: bus not ready on wakeup/n", drive->name);
36 SELECT_DRIVE(drive);
37 HWIF(drive)->OUTB(8, HWIF(drive)->io_ports[IDE_CONTROL_OFFSET]);
38 rc = ide_wait_not_busy(HWIF(drive), 10000);
39 if (rc)
40 printk(KERN_WARNING "%s: drive not ready on wakeup/n", drive->name);
41 }
42
43 SELECT_DRIVE(drive);
44 if (ide_wait_stat(&startstop, drive, drive->ready_stat, BUSY_STAT | DRQ_STAT,
45 WAIT_READY)) //等待驅動器READY
46 {
47 printk(KERN_ERR "%s: drive not ready for command/n", drive->name);
48 return startstop;
49 }
50 if (!drive->special.all)
51 {
52 ide_driver_t *drv;
53
54 //其它非讀寫請求
55 if (rq->flags &(REQ_DRIVE_CMD | REQ_DRIVE_TASK))
56 return execute_drive_cmd(drive, rq);
57 else if (rq->flags &REQ_DRIVE_TASKFILE)
58 return execute_drive_cmd(drive, rq);
59 else if (blk_pm_request(rq))
60 {
61 startstop = ide_start_power_step(drive, rq);
62 if (startstop == ide_stopped && rq->pm->pm_step == ide_pm_state_completed)
63 ide_complete_pm_request(drive, rq);
64 return startstop;
65 }
66
67 drv = *(ide_driver_t **)rq->rq_disk->private_data;
68 return drv->do_request(drive, rq, block); //處理IO操作請求
69 }
70 return do_special(drive);
71 kill_rq: ide_kill_rq(drive, rq);
72 return ide_stopped;
73 }
IDE硬碟驅動對I/O請求結束處理進行了針對IDE的整理,並填充在ide_driver_t結構體的end_request成員中,對應的函式為ide_end_request(),如程式碼清單13.25。
程式碼清單13.25 IDE I/O請求結束處理
1 /* ide_end_request:完成了一個IDE請求
2 引數: nr_sectors 被完成的扇區數量
3 */
4 int ide_end_request (ide_drive_t *drive, int uptodate, int nr_sectors)
5 {
6 struct request *rq;
7 unsigned long flags;
8 int ret = 1;
9
10 spin_lock_irqsave(&ide_lock, flags); //獲得自旋鎖
11 rq = HWGROUP(drive)->rq;
12
13 if (!nr_sectors)
14 nr_sectors = rq->hard_cur_sectors;
15
16 ret = __ide_end_request(drive, rq, uptodate, nr_sectors); //具體的結束請求處理
17
18 spin_unlock_irqrestore(&ide_lock, flags); //釋放獲得自旋鎖
19 return ret;
20 }
21
22 static int __ide_end_request(ide_drive_t *drive, struct request *rq, int
23 uptodate, int nr_sectors)
24 {
25 int ret = 1;
26
27 BUG_ON(!(rq->flags &REQ_STARTED));
28
29 /* 如果對請求設定了failfast,立即完成整個請求 */
30 if (blk_noretry_request(rq) && end_io_error(uptodate))
31 nr_sectors = rq->hard_nr_sectors;
32
33 if (!blk_fs_request(rq) && end_io_error(uptodate) && !rq->errors)
34 rq->errors = - EIO;
35
36 /* 決定是否再使能DMA,如果DMA超過3次,採用PIO模式 */
37 if (drive->state == DMA_PIO_RETRY && drive->retry_pio <= 3)
38 {
39 drive->state = 0;
40 HWGROUP(drive)->hwif->ide_dma_on(drive);
41 }
42
43 //結束請求
44 if (!end_that_request_first(rq, uptodate, nr_sectors))
45 {
46 add_disk_randomness(rq->rq_disk); //給系統的隨機數池貢獻熵
47 blkdev_dequeue_request(rq);
48 HWGROUP(drive)->rq = NULL;
49 end_that_request_last(rq, uptodate);
50 ret = 0;
51 }
52
53 return ret;
54 }
13.8.4在核心中增加對新系統IDE裝置的支援
儘管IDE的驅動非常複雜,但是由於其訪問方式符合ATA標準,因而核心提供的I/O操作方面的程式碼是通用的,為了使核心能找到新系統中的IDE硬碟,工程師只需編寫少量的針對特定硬體平臺的底層程式碼。
使用ide_register_hw()函式可註冊IDE硬體介面,其原型為:
int ide_register_hw(hw_regs_t *hw, ide_hwif_t **hwifp);
這個函式接收的2個引數對應的資料結構為hw_regs_s和ide_hwif_t,其定義如程式碼清單13.26。
程式碼清單13.26 hw_regs_s和ide_hwif_t結構體的定義
1 typedef struct hw_regs_s
2 {
3 unsigned long io_ports[IDE_NR_PORTS]; /* task file暫存器 */
4 int irq; /* 中斷號 */
5 int dma; /* DMA入口 */
6 ide_ack_intr_t *ack_intr; /* 確認中斷 */
7 hwif_chipset_t chipset;
8 struct device *dev;
9 } hw_regs_t;
10
11 typedef struct hwif_s
12 {
13 ...
14
15 char name[6]; /* 介面名,如"ide0" */
16
17 hw_regs_t hw; /* 硬體資訊 */
18 ide_drive_t drives[MAX_DRIVES]; /* 驅動器資訊 */
19
20 u8 major; /* 主裝置號 */
21 u8 index; /* 索引,如0對應ide0,1對應ide1 */
22 ...
23 //DMA操作
24 int(*dma_setup)(ide_drive_t*);
25 void(*dma_exec_cmd)(ide_drive_t *, u8);
26 void(*dma_start)(ide_drive_t*);
27 int(*ide_dma_end)(ide_drive_t *drive);
28 int(*ide_dma_check)(ide_drive_t *drive);
29 int(*ide_dma_on)(ide_drive_t *drive);
30 int(*ide_dma_off_quietly)(ide_drive_t *drive);
31 int(*ide_dma_test_irq)(ide_drive_t *drive);
32 int(*ide_dma_host_on)(ide_drive_t *drive);
33 int(*ide_dma_host_off)(ide_drive_t *drive);
34 int(*ide_dma_lostirq)(ide_drive_t *drive);
35 int(*ide_dma_timeout)(ide_drive_t *drive);
36 //暫存器訪問
37 void(*OUTB)(u8 addr, unsigned long port);
38 void(*OUTBSYNC)(ide_drive_t *drive, u8 addr, unsigned long port);
39 void(*OUTW)(u16 addr, unsigned long port);
40 void(*OUTL)(u32 addr, unsigned long port);
41 void(*OUTSW)(unsigned long port, void *addr, u32 count);
42 void(*OUTSL)(unsigned long port, void *addr, u32 count);
43
44 u8(*INB)(unsigned long port);
45 u16(*INW)(unsigned long port);
46 u32(*INL)(unsigned long port);
47 void(*INSW)(unsigned long port, void *addr, u32 count);
48 void(*INSL)(unsigned long port, void *addr, u32 count);
49
50 ...
51 } ____cacheline_internodealigned_in_smp ide_hwif_t;
hw_regs_s結構體描述了IDE介面的暫存器、用到的中斷號和DMA入口,是對暫存器和硬體資源的描述,而ide_hwif_t是對IDE介面硬體訪問方法的描述。
因此,為使得新系統支援IDE並被核心偵測到,工程師只需要初始化hw_regs_s和ide_hwif_t這2個結構體並使用ide_register_hw()註冊IDE介面即可。程式碼清單13.27給出了H8/300系列微控制器IDE驅動的介面卡註冊程式碼。
程式碼清單13.27 H8/300系列微控制器IDE介面註冊
1 /* 暫存器操作函式 */
2 static void mm_outw(u16 d, unsigned long a)
3 {
4 __asm__("mov.b %w0,r2h/n/t"
5 "mov.b %x0,r2l/n/t"
6 "mov.w r2,@%1"
7 :
8 :"r"(d),"r"(a)
9 :"er2");
10 }
11
12 static u16 mm_inw(unsigned long a)
13 {
14 register u16 r __asm__("er0");
15 __asm__("mov.w @%1,r2/n/t"
16 "mov.b r2l,%x0/n/t"
17 "mov.b r2h,%w0"
18 :"=r"(r)
19 :"r"(a)
20 :"er2");
21 return r;
22 }
23
24 static void mm_outsw(unsigned long addr, void *buf, u32 len)
25 {
26 unsigned short *bp = (unsigned short *)buf;
27 for (; len > 0; len--, bp++)
28 *(volatile u16 *)addr = bswap(*bp);
29 }
30
31 static void mm_insw(unsigned long addr, void *buf, u32 len)
32 {
33 unsigned short *bp = (unsigned short *)buf;
34 for (; len > 0; len--, bp++)
35 *bp = bswap(*(volatile u16 *)addr);
36 }
37
38 #define H8300_IDE_GAP (2)
39
40 /* hw_regs_t結構體初始化 */
41 static inline void hw_setup(hw_regs_t *hw)
42 {
43 int i;
44
45 memset(hw, 0, sizeof(hw_regs_t));
46 for (i = 0; i <= IDE_STATUS_OFFSET; i++)
47 hw->io_ports[i] = CONFIG_H8300_IDE_BASE + H8300_IDE_GAP*i;
48 hw->io_ports[IDE_CONTROL_OFFSET] = CONFIG_H8300_IDE_ALT;
49 hw->irq = EXT_IRQ0 + CONFIG_H8300_IDE_IRQ;
50 hw->dma = NO_DMA;
51 hw->chipset = ide_generic;
52 }
53
54 /* ide_hwif_t結構體初始化 */
55 static inline void hwif_setup(ide_hwif_t *hwif)
56 {
57 default_hwif_iops(hwif);
58
59 hwif->mmio = 2;
60 hwif->OUTW = mm_outw;
61 hwif->OUTSW = mm_outsw;
62 hwif->INW = mm_inw;
63 hwif->INSW = mm_insw;
64 hwif->OUTL = NULL;
65 hwif->INL = NULL;
66 hwif->OUTSL = NULL;
67 hwif->INSL = NULL;
68 }
69
70 /* 註冊IDE介面卡 */
71 void __init h8300_ide_init(void)
72 {
73 hw_regs_t hw;
74 ide_hwif_t *hwif;
75 int idx;
76
77 /* 申請記憶體區域 */
78 if (!request_region(CONFIG_H8300_IDE_BASE, H8300_IDE_GAP*8, "ide-h8300"))
79 goto out_busy;
80 if (!request_region(CONFIG_H8300_IDE_ALT, H8300_IDE_GAP, "ide-h8300")) {
81 release_region(CONFIG_H8300_IDE_BASE, H8300_IDE_GAP*8);
82 goto out_busy;
83 }
84
85 hw_setup(&hw); //初始化hw_regs_t
86
87 /* 註冊IDE介面 */
88 idx = ide_register_hw(&hw, &hwif);
89 if (idx == -1) {
90 printk(KERN_ERR "ide-h8300: IDE I/F register failed/n");
91 return;
92 }
93
94 hwif_setup(hwif); //設定ide_hwif_t
95 printk(KERN_INFO "ide%d: H8/300 generic IDE interface/n", idx);
96 return;
97
98 out_busy:
99 printk(KERN_ERR "ide-h8300: IDE I/F resource already used./n");
100 }
第1~36行定義了暫存器讀寫函式,41行的hw_setup()函式用於初始化hw_regs_t結構體,55行的hwif_setup()函式用於初始化ide_hwif_t結構體,71行的h8300_ide_init()函式中使用ide_register_hw()註冊了這個介面。
13.9總結
塊裝置的I/O操作方式與字元裝置存在較大的不同,因而引入了request_queue、request、bio等一系列資料結構。在整個塊裝置的I/O操作中,貫穿於始終的就是“請求”,字元裝置的I/O操作則是直接進行不繞彎,塊裝置的I/O操作會排隊和整合。
驅動的任務是處理請求,對請求的排隊和整合由I/O排程演算法解決,因此,塊裝置驅動的核心就是請求處理函式或“製造請求”函式。
儘管在塊裝置驅動中仍然存在block_device_operations結構體及其成員函式,但其不再包含讀寫一類的成員函式,而只是包含開啟、釋放及I/O控制等與具體讀寫無關的函式。
塊裝置驅動的結構相當複雜的,但幸運的是,塊裝置不像字元裝置那麼包羅永珍,它通常就是儲存裝置,而且驅動的主體已經由Linux核心提供,針對一個特定的硬體系統,驅動工程師所涉及到的工作往往只是編寫少量的與硬體直接互動的程式碼。
| ||||
(b) request、bio和bio_vec
|
(c)bio與bio_vec |
圖13.3 遍歷1個請求佇列 |
圖13.4 塊裝置驅動模板與RAMDISK裝置驅動的對映 |
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/28371090/viewspace-759783/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 乾坤合一:Linux裝置驅動之塊裝置驅動Linux
- Linux裝置驅動之字元裝置驅動Linux字元
- linux核心原始碼閱讀-塊裝置驅動Linux原始碼
- 深入淺出:Linux裝置驅動之字元裝置驅動Linux字元
- 蛻變成蝶:Linux裝置驅動之字元裝置驅動Linux字元
- 蛻變成蝶~Linux裝置驅動之字元裝置驅動Linux字元
- 字元裝置驅動 —— 字元裝置驅動框架字元框架
- 【linux】驅動-7-平臺裝置驅動Linux
- Linux裝置驅動程式 (轉)Linux
- Linux裝置驅動程式學習----1.裝置驅動程式簡介Linux
- 乾坤合一:Linux裝置驅動之USB主機和裝置驅動Linux
- 【linux】驅動-6-匯流排-裝置-驅動Linux
- linux 裝置驅動基本概念Linux
- Linux裝置驅動探究第1天----spi驅動(1)Linux
- Linux驅動開發筆記(四):裝置驅動介紹、熟悉雜項裝置驅動和ubuntu開發雜項裝置DemoLinux筆記Ubuntu
- 在Linux中,什麼是裝置驅動程式?如何安裝和解除安裝裝置驅動程式?Linux
- linux裝置驅動編寫入門Linux
- linux裝置驅動編寫基礎Linux
- LINUX下的裝置驅動程式 (轉)Linux
- 《Linux裝置驅動開發詳解(第2版)》——第1章Linux裝置驅動概述及開發環境構建1.1裝置驅動的作用Linux開發環境
- Linux驅動實踐:如何編寫【 GPIO 】裝置的驅動程式?Linux
- 【linux】驅動-9-裝置樹外掛Linux
- linux驅動之獲取裝置樹資訊Linux
- linux裝置驅動中的併發控制Linux
- LED字元裝置驅動字元
- Linux驅動之I2C匯流排裝置以及驅動Linux
- Linux下的硬體驅動——USB裝置(上)(驅動配置部分)(轉)Linux
- 驅動Driver-MISC雜項驅動裝置
- 蛻變成蝶:Linux裝置驅動之DMALinux
- Linux裝置驅動之中斷與定時器Linux定時器
- 字元驅動裝置踩坑字元
- platform 裝置驅動實驗Platform
- 如何編寫一個簡單的Linux驅動(三)——完善裝置驅動Linux
- Linux的input輸入子系統:裝置驅動之按鍵驅動Linux
- Linux下的硬體驅動——USB裝置(下)(驅動開發部分)(轉)Linux
- Windows裝置和驅動的安裝Windows
- Linux驅動之裝置樹的基礎知識Linux
- 【Linux 中斷】紅外接收器裝置驅動Linux