Linux設備驅動工程師之路之——塊設備驅動

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


發佈了67 篇原創文章 · 獲贊 9 · 訪問量 32萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章