Linux設備驅動工程師之路之——塊設備驅動
K-Style
轉載請註明來自於衡陽師範學院08電2 K-Style http://blog.csdn.net/ayangke,QQ:843308498 郵箱:[email protected]
一、重要知識點
1.塊設備和字符設備的區別
a.字符設備可訪問字節大小數據,塊設備只能訪問固定大小的整塊數據(一般爲512字節)。
b.塊設備支持隨機訪問,字符設備只能順序訪問。
2.塊設備子系統體系架構
如圖
從上到下依次爲VFS虛擬文件系統、各種類型的磁盤系統、通用塊設備層、I/O調度層(優化訪問上層的請求(讀寫請求))、塊設備驅動層、塊設備硬件層。
我們編寫驅動程序要完成的是調用I/O調度層提供的相關接口對塊設備硬件層進行讀寫及相關操作。
3.塊設備驅動程序註冊
塊設備驅動程序使用
int register_blk_dev(unsigned int major, const char*name)向內核註冊。如果major爲0,則內核爲止分配一個主設備號。在內核2.6中,對register_blk_dev的調用時完全可選的,該接口只做了兩件事:一是動態分配主設備號,二是在/proc/devices中創建一個入口項。大多數驅動仍會調用,因爲這是一個傳統。
4.註冊磁盤
雖然register_blk能夠獲得主設備號,當它並不能讓系統使用任何磁盤,因此爲了管理獨立的磁盤,必須使用另外一個單獨的註冊接口
void add_disk(struct gendist *gd)
下面我們再來看看參數struct gendisk結構
5.磁盤描述結構struct gendisk
內核使用gendisk結構來表示一個獨立的磁盤設備
struct gendisk
{
int major; //主設備號
intfirst_minor; //第一個次設備號
intminors; //最大次設備數,如果不能分區則爲1
chardisk_name[32]; //設備名稱
structhd_struct **part; //磁盤上的分區信息
structblock_device_operations *fops; //塊設備操作結構體
structrequest_queue *queue; //請求隊列
void*private_data; //私有數據
sector_tcapacity; //扇區數,512字節爲1扇區
…………
}
我們再來看塊設備的操作結構體struct block_device_operations
6.塊設備操作結構體struct block_device_operations
struct struct block_device_operations
{
int (*open)(struct inode *, struct file*);
int(*release)(struct inode*, struct file *);
int (*ioctl)(struct inode*, struct file *, unsigned, unsigned long);
int (*media_changed)(struct gendisk *)
int (*revalidate_disk)(struct gendisk *)
int (*getgeo)(structblock_device *, struct hd_geometry*);
structmodule *owner;
}
int (*open)(structinode *, struct file*);當系統執行mount、創建分區、在分區上創建文件系統,運行文件系統檢查程序等時被調用。
int(*release)(struct inode*, struct file *);當系統執行umount等其他關閉設備操作時被調用。
int (*ioctl)(structinode*, struct file *, unsigned, unsigned long);用來提供一些特殊的操作,比如說查詢磁盤物理信息等。
int (*media_changed)(structgendisk *)
int (*revalidate_disk)(structgendisk *)
這兩個用來支持可移動介質。上層調用media_change以檢查介質是否被改變如改變將返回非0值。
在介質改變後,上層將調用revalidate_disk來重新對新的介質進行一些初始化工作。
int (*getgeo)(struct block_device *, structhd_geometry*);用來填充驅動器信息。
在這裏我們就發現塊設備和字符設備驅動的區別了,該操作結構體中沒有讀寫函數。因爲塊設備的讀寫操作是與I/O調度層的I/O請求綁定在一起的,一旦I/O調度層有I/O請求就會調用塊設備的讀寫操作函數。下面開始介紹塊設備如何響應I/O請求。
7.I/O請求
當內核以文件系統,虛擬子系統或者調用形式從塊設備輸入、輸出塊數據是,它將使用一個bio結構,用來描述這個操作。該結構會被傳遞給I/O調度層,I/O調度層會把它合併到一個已經存在的request結構中,或者根據需要再創建一個request結構中。爲什麼要這樣做呢?因爲內核爲了使提高塊設備的讀寫效率,它會將對相鄰的扇區進行操作的多個請求(bio)合併成一個request。同樣爲了提高塊設備的讀寫效率,I/O調度層又將每個request進行一些排序處理組成一個隊列(request_que_t),使驅動以某種順序去讀取request_que_t的每一個request,然後進行塊設備的實際讀寫操作。綜上,bio是最基本的請求,然後內核會將對相鄰扇區訪問的bio組成一個request,接着再把request按照某種調度算法排序組成一個隊列request_que_t。我們驅動程序要實現的就是提取每一個quest,然後獲取其中的信息進行讀寫操作。
但是有一個問題,並不是所有塊設備都像磁盤設備那樣扇區之類的結構,比如說flash,ram盤之類的,對這一類的設備進行上述的I/O調度反而會使效率降低,所有內核又提供了實現I/O請求的另外一種方式,就是繞過請求隊列,也就是繞過request和request_que_t直接對bio結構進行處理。
下面我們分別來介紹實現I/O請求響應的兩種方式。
8.響應I/O請求實現方式一:request隊列方式
request數據結構
struct request
{
struct list_head queuelist; //形成request鏈表的鏈表結構
sector_t sector; //要操作的首個扇區
unsigned long nr_sectors; //要操作的扇區數
struct bio *bio; //請求的bio鏈表頭
struct bio *biotail; //請求的bio結構體的鏈表尾
……
}
操作請求隊列的函數
初始化請求隊列
struct request_queue *blk_init_queue(request_fn_proc*rfn, spinlock_t *lock)
rfn爲請求隊列的響應函數,這樣就將驅動響應函數和I/O請求綁定到了一起。
lock是訪問隊列權限的自旋鎖。
將該函數的返回值賦給gendisk結構的queue成員,這樣就I/O調度層就會把組織好的request形成的隊列填充到queue裏面,然後調用rfn來響應對該塊設備的I/O請求。rfn的原型爲
typedef void (request_fn_proc) (request_que_t *q),它只有一個參數就是request_que_t隊列。
清除請求隊列
void blk_cleanup_queue(request_queue_t *q)
當塊設備驅動模塊卸載時調用此函數。
返回隊列中下一個要處理的的請求(request):
struct request *elv_next_request(request_queue_t *queue)
並刪除一個請求
void blkdev_dequeue_request(struct request *req)
9.響應I/O請求實現方式二:直接響應bio方式
bio結構的核心是一個名爲bi_io_vec數組,它是由下面的結構組成的:
struct bio_vec {
struct page *bv_page;
unsignedint bv_len;
unsignedint bv_offset;
}
它表示了一個映射的物理頁的信息。內核使用bio_for_each_segment(bvec,bio, segno)來遍歷每個bio_vec結構。bvec是指當前的dio_vec入口, segno是段號。
驅動是程序使用blk_alloc_queue函數分配一個請求隊列來告訴塊設備子系統,I/O請求響應的是使用bio方式。
request_queue_t *blk_alloc_queue(int flags)
該函數與blk_init_queue的不同之處在於它並未真正實現一個保存的請求隊列。flag是一系列標誌用來爲隊列分配內存。通常是GFP_KERNEL。一旦擁有了隊列,將它與make_request將響應函數傳遞給blk_queue_make_request:
void blk_queue_make_request(request_queue_t *queue,mak_request_fn *func);
請求響應函數的原型爲
typedef int (make_request_fn) (request *q, struct bio *bio)
可以看出內核傳遞了一個bio結構給I/O請求響應函數,func可以讀取bio的信息進行塊設備的讀寫操作。
二、驅動代碼分析
該驅動將一段內存模擬成一個塊設備驅動,並使用bio方式實現I/O請求的響應
#include<linux/module.h>
#include<linux/moduleparam.h>
#include <linux/init.h>
#include <linux/sched.h>
#include<linux/kernel.h> /* printk() */
#include <linux/slab.h> /* kmalloc() */
#include <linux/fs.h> /* everything... */
#include <linux/errno.h> /* error codes */
#include <linux/timer.h>
#include <linux/types.h> /* size_t */
#include <linux/fcntl.h> /* O_ACCMODE */
#include <linux/hdreg.h> /* HDIO_GETGEO */
#include<linux/kdev_t.h>
#include<linux/vmalloc.h>
#include <linux/genhd.h>
#include<linux/blkdev.h>
#include<linux/buffer_head.h> /*invalidate_bdev */
#include <linux/bio.h>
#include<linux/version.h>
#defineSIMP_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR
#defineSIMP_BLKDEV_DISKNAME "simp_blkdev"
#define SIMP_BLKDEV_BYTES (16*1024*1024)
static struct request_queue*simp_blkdev_queue;
static struct gendisk*simp_blkdev_disk;
unsigned charsimp_blkdev_data[SIMP_BLKDEV_BYTES];
static intsimp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
struct bio_vec *bvec;
int i;
void *dsk_mem;
//判斷要訪問的數據是否大於塊設備最大容量,如果是則調用bio_endio通知內核完成請求。
if ((bio->bi_sector << 9) +bio->bi_size > SIMP_BLKDEV_BYTES) {
printk(KERN_ERRSIMP_BLKDEV_DISKNAME
": bad request:block=%llu, count=%u\n",
(unsigned longlong)bio->bi_sector, bio->bi_size);
#if LINUX_VERSION_CODE <KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
dsk_mem = simp_blkdev_data +(bio->bi_sector << 9);
//遍歷bio鏈表中的每一個bio_vec元素,然後判斷是讀還是寫操作進行數據傳輸,傳輸完成後調用bio_endio通知內核完成請求。
bio_for_each_segment(bvec, bio, i) {
void *iovec_mem;
switch (bio_rw(bio)) {
case READ:
case READA:
iovec_mem =kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(iovec_mem,dsk_mem, bvec->bv_len);
kunmap(bvec->bv_page);
break;
case WRITE:
iovec_mem =kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(dsk_mem,iovec_mem, bvec->bv_len);
kunmap(bvec->bv_page);
break;
default:
printk(KERN_ERRSIMP_BLKDEV_DISKNAME
": unknownvalue of bio_rw: %lu\n",
bio_rw(bio));
#if LINUX_VERSION_CODE <KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0,-EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
dsk_mem += bvec->bv_len;
}
#if LINUX_VERSION_CODE <KERNEL_VERSION(2, 6, 24)
bio_endio(bio, bio->bi_size, 0);
#else
bio_endio(bio, 0);
#endif
return 0;
}
struct block_device_operationssimp_blkdev_fops = {
.owner = THIS_MODULE,
};
static int __initsimp_blkdev_init(void)
{
int ret;
//分配響應隊列
simp_blkdev_queue =blk_alloc_queue(GFP_KERNEL);
if (!simp_blkdev_queue) {
ret = -ENOMEM;
goto err_alloc_queue;
}
//將內核響應隊列和I/O請求響應函數綁定
blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);
//分配一個gendisk結構
simp_blkdev_disk = alloc_disk(1);
if (!simp_blkdev_disk) {
ret = -ENOMEM;
goto err_alloc_disk;
}
//初始化gendisk結構
strcpy(simp_blkdev_disk->disk_name,SIMP_BLKDEV_DISKNAME);
simp_blkdev_disk->major =SIMP_BLKDEV_DEVICEMAJOR;
simp_blkdev_disk->first_minor = 0;
simp_blkdev_disk->fops =&simp_blkdev_fops;
simp_blkdev_disk->queue =simp_blkdev_queue;//初始化I/O請求隊列
set_capacity(simp_blkdev_disk,SIMP_BLKDEV_BYTES>>9);
add_disk(simp_blkdev_disk);//向內核添加一個gendisk對象
return 0;
err_alloc_disk:
blk_cleanup_queue(simp_blkdev_queue);
err_alloc_queue:
return ret;
}
static void __exitsimp_blkdev_exit(void)
{
del_gendisk(simp_blkdev_disk);//註銷gendisk對象
put_disk(simp_blkdev_disk);//減小gendisk引用計數
blk_cleanup_queue(simp_blkdev_queue);//清楚請求隊列
}
module_init(simp_blkdev_init);
module_exit(simp_blkdev_exit);