Linux 塊設備驅動 (一)


1、塊設備的I/O操作特點

字符設備與塊設備的區別:

  塊設備只能以塊為單位接受輸入和返回輸出,而字符設備則以字符為單位。

  塊設備對於I/O請求有對應的緩沖區,因此它們可以選擇以什么順序進行響應,字符設備無需緩沖區且直接被讀寫。

  字符設備只能被順序讀寫,而塊設備可以隨機讀寫。 但是對於磁盤等機械設備而言,順序的組織塊設備的訪問可以提高性能

  總體而言,塊設備驅動比字符設備驅動要復雜得多,在I/O操作上表現出極大的不同,緩沖、I/O調度、請求隊列等都是與塊設備驅動相關的概念。

對於扇區1、10、3、2的請求被調整為對扇區1、2、3、10的請求。但是對於SD卡、RAMDISK等非機械設備來說則沒必要調整

2.block_device_operations結構體

1、打開和釋放 

int (*open)(struct inode *inode,struct file *filp); int(*release)(struct inode *inode,struct file *filp); //與字符設備驅動類似,當設備打開和關閉的時候調用它們

2、io控制

 int (*ioctl)(struct inode *inode,struct file *filp,unsigned int cmd, unsigned long arg); //上述函數是ioctl()系統調用的實現,塊設備包含大量的標准請求,這些標准請求由Linux塊設備層處理,因此大部分塊設備驅動的ioctl()函數相當短。 

3、介質改變 

int (*media_changed)(struct gendisk *gd); 
/*被內核調用來檢查是否驅動器中的介質已經改變。如果是,則返回一個非0值。否則返回0.這個函數僅適用於支持可移動介質的驅動器,通常需要在驅動器中增加一個表示介質狀態是否改變的標志變量,非可移動設備的驅動不需要實現這個方法。*/

4、使介質有效 

int (*revalidate_disk)(struct gendisk *gd);// revalidate_disk()函數被調用來響應一個介質改變,它給驅動一個機會來進行必要的工作以使新介質准備好。

5、獲得驅動器信息

 int (*getgeo)(struct block_device *,struct hd_geometry *) ;//該函數根據驅動器的幾何信息填充一個hd_geometry的結構體,包含磁頭、扇區、柱面等信息。

6、模塊指針 

struct module * owner;// 一個指向擁有這個結構體的模塊的指針,它通常被初始化為THIS_MODULE.

3. gendisk分析

在Linux內核中,使用gendisk(通用磁盤)結構體來表示1個獨立的磁盤設備(或分區)。

int major;/* 主設備號 */

int first_minor; /*第1個次設備號*/

int minors: //磁盤使用這些成員描述設備號。
        //最大的次設備數,如果不能分區,則為1,一個驅動器至少使用一個次設備號。
       //如果驅動器是可被分區的(大多數情況下),用戶將要為每個可能的分區都分配一個次設備號。minors通常取16,他使得一個“完整的的磁盤“包含15個分區。 某些磁盤驅動程序設置每個設備可使用64個次設備號。

char disk_name[32];
    //設置磁盤設備名字的成員。該名字將顯示在/proc/partitions和sysfs中。

struct block_device_operations *fops;
     //設置前面所述的各種設備操作;

struct request_queue *queue;
    //內核使用該結構為設備管理i/o請求;在”請求過程“中詳細進行論述。

int flags;
    //用來描述驅動器狀態的標志(很少使用)。如果用戶設備包含了可移動介質,其將被設置為GENHD_FL_REMOVABLE。

sector_t capacity;
    //以512字節為一個扇區時,該驅動器可包含的扇區數。

void *preivate_data;
    //塊設備驅動程序可能使用該成員保存指向其內部數據的指針。

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字節為單位。

 

4.塊設備驅動模塊加載和卸載函數模板

在塊設備驅動的模塊加載函數中通常需要完成如下工作:
① 分配、初始化請求隊列,綁定請求隊列和請求函數。
② 分配、初始化gendisk,給gendisk的major、fops、queue等成員賦值,最后添加gendisk。
③ 注冊塊設備驅動。

使用blk_alloc_queue

static int __init xxx_init(void)
{
     //分配gendisk
     xxx_disks = alloc_disk(1);
     if (!xxx_disks)
    {
         goto out;
    }
  
    /*
    塊設備驅動注冊
    在2.6內核中,對 register_blkdev()的調用完全是可選的,register_blkdev()的功能已隨時間正在減少,這個調用最多只完全2件事:
    ① 如果需要,分配一個動態主設備號。
    ② 在/proc/devices中創建一個入口。
    在將來的內核中,register_blkdev()可能會被去掉。但是目前的大部分驅動仍然調用它。
    */
    if (register_blkdev(XXX_MAJOR, "xxx"))
    {
        err =  - EIO;
        goto out;
    }
 
    //“請求隊列”分配
    xxx_queue = blk_alloc_queue(GFP_KERNEL);
    if (!xxx_queue)
    {
        goto out_queue;
    }
 
    blk_queue_make_request(xxx_queue, &xxx_make_request); //綁定“制造請求”函數
    blk_queue_hardsect_size(xxx_queue, xxx_blocksize); //硬件扇區尺寸設置
 
    //gendisk初始化
    xxx_disks->major = XXX_MAJOR;
    xxx_disks->first_minor = 0;
    xxx_disks->fops = &xxx_op;
    xxx_disks->queue = xxx_queue;
    sprintf(xxx_disks->disk_name, "xxx%d", i);
    set_capacity(xxx_disks, xxx_size); //xxx_size以512bytes為單位
    add_disk(xxx_disks); //添加gendisk
 
return 0;
    out_queue: unregister_blkdev(XXX_MAJOR, "xxx");
    out: put_disk(xxx_disks);
    blk_cleanup_queue(xxx_queue);
 
    return  - ENOMEM;
}

使用blk_init_queue

static int __init xxx_init(void)
{
    //塊設備驅動注冊
    if (register_blkdev(XXX_MAJOR, "xxx"))
    {
        err =  - EIO;
        goto out;
    }
  
    //請求隊列初始化
    xxx_queue = blk_init_queue(xxx_request, xxx_lock);
    if (!xxx_queue)
    {    
        goto out_queue;
    }
   
    blk_queue_hardsect_size(xxx_queue, xxx_blocksize); //硬件扇區尺寸設置
   
    //gendisk初始化
    xxx_disks->major = XXX_MAJOR;
    xxx_disks->first_minor = 0;
    xxx_disks->fops = &xxx_op;
    xxx_disks->queue = xxx_queue;
    sprintf(xxx_disks->disk_name, "xxx%d", i);
    set_capacity(xxx_disks, xxx_size *2);
    add_disk(xxx_disks); //添加gendisk
 
return 0;
out_queue: unregister_blkdev(XXX_MAJOR, "xxx");
out: put_disk(xxx_disks);
    blk_cleanup_queue(xxx_queue);

    return  - ENOMEM;
}

每個塊設備驅動程序的核心是它的請求函數。

實際的工作,如設備的啟動,都是在這個函數中完成。

驅動程序的新能,是這個操作系統性能的重要組成部分,因此內核的塊設備子系統在編寫的時候就非常注意性能方面的問題。

塊設備驅動模塊卸載函數模板

① 清除請求隊列。
② 刪除gendisk和對gendisk的引用。
③ 刪除對塊設備的引用,注銷塊設備驅動。

static void __exit xxx_exit(void)
{
    if (bdev)
    {
        invalidate_bdev(xxx_bdev, 1);
        blkdev_put(xxx_bdev);
    }
    
    del_gendisk(xxx_disks); //刪除gendisk
    put_disk(xxx_disks);
    blk_cleanup_queue(xxx_queue[i]); //清除請求隊列
    unregister_blkdev(XXX_MAJOR, "xxx");
}

5.塊設備驅動I/O

request函數介紹

原型:

void request(request_queue_t *queue);

這個函數不能由驅動自己調用,只有當內核認為是時候讓驅動處理對設備的讀寫等操作時,它才調用這個函數。

請求函數可以在沒有完成請求隊列中的所有請求的情況下返回,甚至它1個請求不完成都可以返回。但是,對大部分設備而言,在請求函數中處理完所有請求后再返回通常是值得推薦的方法。

對request函數的調用是與用戶空間進程中的動作是完全異步的,因此不能直接對用戶空間進行訪問。

塊設備驅動請求函數例程:

static void xxx_request(request_queue_t *q)
{
    struct request *req;
    while ((req = elv_next_request(q)) != NULL)
    {
        struct xxx_dev *dev = req->rq_disk->private_data;
        if (!blk_fs_request(req)) //不是文件系統請求
        {
            printk(KERN_NOTICE "Skip non-fs request/n");
            end_request(req, 0);//通知請求處理失敗
            continue;
        }
        
        xxx_transfer(dev, req->sector, req->current_nr_sectors, req->buffer,
        rq_data_dir(req)); //處理這個請求
        end_request(req, 1); //通知成功完成這個請求
    }
}

//完成具體的塊設備I/O操作
static void xxx_transfer(struct xxx_dev *dev, unsigned long sector, unsigned
long nsect, char *buffer, int write)
{
    unsigned long offset = sector * KERNEL_SECTOR_SIZE;
    unsigned long nbytes = nsect * KERNEL_SECTOR_SIZE;
    
    if ((offset + nbytes) > dev->size)
    {
        printk(KERN_NOTICE "Beyond-end write (%ld %ld)/n", offset, nbytes);
        return ;
    }
    
    if (write)
    {
        write_dev(offset, buffer, nbytes);  //向設備些nbytes個字節的數據
    }
    else
    {
        read_dev(offset, buffer, nbytes); //從設備讀nbytes個字節的數據
    }
}

void end_request(struct request *req, int uptodate)
{    
    /*
    當設備已經完成1個I/O請求的部分或者全部扇區傳輸后,
    end_that_request_first這個函數告知塊設備層,塊設備驅動已經完成count個扇區的傳送
    返回值為0表示所有的扇區已經被傳送並且這個請求完成
    */
    if (!end_that_request_first(req, uptodate, req->hard_cur_sectors))
    {
        /*
        使用塊 I/O 請求的定時來給系統的隨機數池貢獻熵,它不影響塊設備驅動。
        但是,僅當磁盤的操作時間是真正隨機的時候(大部分機械設備如此),才應該調用它。
        */
        add_disk_randomness (req->rq_disk);
        blkdev_dequeue_request (req);//從隊列中清除這個請求
        end_that_request_last(req);//通知所有正在等待這個請求完成的對象請求已經完成並回收這個請求結構體。
    }
 }

請求隊列

一個塊設備請求隊列是:包含塊設備I/O請求的序列。

請求隊列跟蹤未完成的塊設備的I/O請求,保存了描述設備所能處理的請求的參數:最大尺寸、在同一個請求中所包含的獨立段的數目、硬件扇區的大小、對齊需求等。

請求隊列實現了插件接口,以便可以使用多個I/O調度器。

I/O調度器積累了大量的請求,根據塊索引號升序(或者降序)排列他們,並按照這個順序向驅動程序發送請求。

磁頭從一個磁盤的末尾移向另一個磁盤,如同單向電梯一樣,直到每個請求都得到滿足。

隊列的創建與刪除

一個請求隊列就是一個動態的數據結構,該結構必須由塊設備的I/O子系統創建。

request_queue_t *blk_init_queue(request_fn_proc *request,spinlock_t *lock) ;//該函數參數是處理這個隊列的request指針和控制訪問隊列權限的自旋鎖。

void blk_cleanup_queue(request_queue_t *);//刪除隊列

隊列中的函數

返回隊列中下一個要處理的請求

struct request *elv_next_request(request_queue_t *queue);

將請求從隊列中刪除

void blkdev_dequeue_request(struct request *req);

隊列控制函數

驅動程序使用塊設備層到處的一組函數去控制請求隊列的操作。

void blk_stop_queue(request_queue_t *queue)

void blk_start_queue(request_queue_t *queue) //如果驅動程序進入不能處理更多命令的狀態,就會調用blk_stop_queue以通知塊設備層,以暫停調用request函數。當有能力處理更多請求時,需要調用blk_start_queue重新開始調用。

void blk_queue_bounce_limit(request_queue_t *queue,u64 dma_addr); //該函數告訴內核驅動程序執行DMA所使用的最高物理內存。如果一個請求包含了超越界限的內存引用,將使用回彈緩沖區(bounce buffer)進行處理。 

請求過程剖析

每個request結構都代表了一個塊設備的I/O請求。

一個特定的請求可以分布在整個內存中,但多數是對相鄰的扇區進行操作。

如果多個請求都是對磁盤中的相鄰扇區進行操作,則內核將對他們進行合並。

從本質上講,一個request結構是作為一個bio結構的鏈表實現的。

bio

bio結構 bio結構在<linux/bio.h>中定義,包含了驅動程序作者所要使用的諸多成員。

sector_t bi_sector; //該bio結構所要傳輸的第一個扇區(512字節)

unsigned int bi_size; //以字節為單位所需傳輸的數據大小。

unsigned long bi_flags; //bio中一系列的標志位;如果是寫請求,最低有效位將被設置。

unsigned short bio_phys_segments;

unsigned short bio_hw_segments; //當DMA 映射完成時,它們分別表示bio中包含的物理段的數目和硬件所能操作的數目。

request結構成員

sector_t hard_sector;

unsigned long hard_nr_sectors;

unsigned int hard_cur_sectors;// 用於跟蹤那些驅動程序還未完成的扇區。還未傳輸的第一個扇區保存在hard_sector中,等待傳輸扇區的總數量保存在hard_nr_sectors中,當前bio中剩余的扇區數目包含在hard_cur_sectors中。

struct bio *bio;// 該請求的bio結構鏈表。

struct list_head queuelist;// 內核鏈表結構,用來把請求連接到請求隊列中。

屏障請求

在驅動程序收到請求前,塊設備層重新組合了請求以提高I/O性能。出於同樣的目的,驅動程序也可以重新組合請求。

但是一些應用程序的某些操作,要寫在另外一些操作之前,比如關系數據庫在執行一個關系數據庫內容的會話前,日志信息要寫到驅動器上。

2.6內核采用屏障(barrier)請求解決問題:如果一個請求被設置了REQ_HARDBARRER標志,那么其后請求被初始化前,它必須被寫進驅動器。

不可重試請求

當第一次請求失敗后,塊設備驅動程序經常要重試請求。這樣的性能使得系統更可靠,不會丟失數據。

但是,內核在某些情況下標記請求是不可重試的。這些請求如果在第一次執行失敗后,要盡快拋棄。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM