22.Linux-塊設備驅動之框架詳細分析(詳解)

本節目的:html

    經過分析2.6內核下的塊設備驅動框架,知道如何來寫驅動node


 

1.以前咱們學的都是字符設備驅動,先來回憶一下算法

字符設備驅動:數組

當咱們的應用層讀寫(read()/write())字符設備驅動時,是按字節/字符來讀寫數據的,期間沒有任何緩存區,由於數據量小,不能隨機讀取數據,例如:按鍵、LED、鼠標、鍵盤等緩存

 

2.接下來本節開始學習塊設備驅動app

塊設備: 框架

塊設備是i/o設備中的一類, 當咱們的應用層對該設備讀寫時,是按扇區大小來讀寫數據的,若讀寫的數據小於扇區的大小,就會須要緩存區, 能夠隨機讀寫設備的任意位置處的數據,例如 普通文件(*.txt,*.c等),硬盤,U盤,SD卡,函數

 

3.塊設備結構:學習

  • 段(Segments):由若干個塊組成。是Linux內存管理機制中一個內存頁或者內存頁的一部分。
  • 塊  (Blocks):   由Linux制定對內核或文件系統等數據處理的基本單位。一般由1個或多個扇區組成。(對Linux操做系統而言)
  • 扇區(Sectors):塊設備的基本單位。一般在512字節到32768字節之間,默認512字節

 

4.咱們以txt文件爲例,來簡要分析下塊設備流程:優化

好比:當咱們要寫一個很小的數據到txt文件某個位置時, 因爲塊設備寫的數據是按扇區爲單位,但又不能破壞txt文件裏其它位置,那麼就引入了一個「緩存區」,將全部數據讀到緩存區裏,而後修改緩存數據,再將整個數據放入txt文件對應的某個扇區中,當咱們對txt文件屢次寫入很小的數據的話,那麼就會重複不斷地對扇區讀出,寫入,這樣會浪費不少時間在讀/寫硬盤上,因此內核提供了一個隊列的機制,再沒有關閉txt文件以前,會將讀寫請求進行優化,排序,合併等操做,從而提升訪問硬盤的效率

(PS:內核中是經過elv_merge()函數實現將隊列優化,排序,合併,後面會分析到)

5.接下來開始分析塊設備框架

當咱們對一個*.txt寫入數據時,文件系統會轉換爲對塊設備上扇區的訪問,也就是調用ll_rw_block()函數,從這個函數開始就進入了設備層.

5.1先來分析ll_rw_block()函數(/fs/buffer.c):

void ll_rw_block(int rw, int nr, struct buffer_head *bhs[])
//rw:讀寫標誌位,  nr:bhs[]長度,  bhs[]:要讀寫的數據數組
{
      int i; 
      for (i = 0; i < nr; i++) {
      struct buffer_head *bh = bhs[i];                 //獲取nr個buffer_head
       ... ...
       if (rw == WRITE || rw == SWRITE) {
              if (test_clear_buffer_dirty(bh)) {
              ... ...
              submit_bh(WRITE, bh);                //提交WRITE寫標誌的buffer_head   
         continue; }} else { if (!buffer_uptodate(bh)) { ... ... submit_bh(rw, bh); //提交其它標誌的buffer_head continue; }} unlock_buffer(bh); } }

 

其中buffer_head結構體,就是咱們的緩衝區描述符,存放緩存區的各類信息,結構體以下所示:

struct buffer_head {
    unsigned long b_state;          //緩衝區狀態標誌 
    struct buffer_head *b_this_page;    //頁面中的緩衝區 
    struct page *b_page;           //存儲緩衝區位於哪一個頁面
    sector_t b_blocknr;           //邏輯塊號
    size_t b_size;              //塊的大小
    char *b_data;               //頁面中的緩衝區

    struct block_device *b_bdev;     //塊設備,來表示一個獨立的磁盤設備

    bh_end_io_t *b_end_io;         //I/O完成方法
 
    void *b_private;             //完成方法數據
 
    struct list_head b_assoc_buffers;   //相關映射鏈表

    /* mapping this buffer is associated with */
    struct address_space *b_assoc_map;   
    atomic_t b_count;             //緩衝區使用計數 
};

 

 

5.2而後進入submit_bh()中, submit_bh()函數以下:

int submit_bh(int rw, struct buffer_head * bh)
{
       struct bio *bio;                    //定義一個bio(block input output),也就是塊設備i/o
       ... ...
       bio = bio_alloc(GFP_NOIO, 1);      //分配bio
      /*根據buffer_head(bh)構造bio */
       bio->bi_sector = bh->b_blocknr * (bh->b_size >> 9);      //存放邏輯塊號
       bio->bi_bdev = bh->b_bdev;                              //存放對應的塊設備
       bio->bi_io_vec[0].bv_page = bh->b_page;           //存放緩衝區所在的物理頁面
       bio->bi_io_vec[0].bv_len = bh->b_size;              //存放扇區的大小
       bio->bi_io_vec[0].bv_offset = bh_offset(bh);            //存放扇區中以字節爲單位的偏移量

       bio->bi_vcnt = 1;                                    //計數值
       bio->bi_idx = 0;                                     //索引值
       bio->bi_size = bh->b_size;                         //存放扇區的大小

       bio->bi_end_io = end_bio_bh_io_sync;             //設置i/o回調函數
       bio->bi_private = bh;                               //指向哪一個緩衝區
       ... ...
       submit_bio(rw, bio);                           //提交bio
       ... ...
}

 

submit_bh()函數就是經過bh來構造bio,而後調用submit_bio()提交bio

 

5.3 submit_bio()函數以下:

void submit_bio(int rw, struct bio *bio)
{
       ... ...
       generic_make_request(bio);        
}

 

最終調用generic_make_request(),把bio數據提交到相應塊設備的請求隊列中,generic_make_request()函數主要是實現對bio的提交處理

 

5.4 generic_make_request()函數以下所示:

void generic_make_request(struct bio *bio)
{
 if (current->bio_tail) {                   // current->bio_tail不爲空,表示有bio正在提交
              *(current->bio_tail) = bio;     //將當前的bio放到以前的bio->bi_next裏面
              bio->bi_next = NULL;    //更新bio->bi_next=0;
              current->bio_tail = &bio->bi_next; //而後將當前的bio->bi_next放到current->bio_tail裏,使下次的bio就會放到當前bio->bi_next裏面了
return; }
BUG_ON(bio
->bi_next); do { current->bio_list = bio->bi_next; if (bio->bi_next == NULL) current->bio_tail = &current->bio_list; else bio->bi_next = NULL; __generic_make_request(bio); //調用__generic_make_request()提交bio bio = current->bio_list; } while (bio); current->bio_tail = NULL; /* deactivate */ }

 

從上面的註釋和代碼分析到,只有當第一次進入generic_make_request()時, current->bio_tail爲NULL,才能調用__generic_make_request().

__generic_make_request()首先由bio對應的block_device獲取申請隊列q,而後要檢查對應的設備是否是分區,若是是分區的話要將扇區地址進行從新計算,最後調用q的成員函數make_request_fn完成bio的遞交.

5.5 __generic_make_request()函數以下所示:

 static inline void __generic_make_request(struct bio *bio)
{
request_queue_t *q;    
int ret;  
 ... ...
       do {
              q = bdev_get_queue(bio->bi_bdev);  //經過bio->bi_bdev獲取申請隊列q
              ... ...
              ret = q->make_request_fn(q, bio);             //提交申請隊列q和bio
       } while (ret);
}

 

這個q->make_request_fn()又是什麼函數?到底作了什麼,咱們搜索下它在哪裏被初始化的

以下圖,搜索make_request_fn,它在blk_queue_make_request()函數中被初始化mfn這個參數

 

繼續搜索blk_queue_make_request,找到它被誰調用,賦入的mfn參數是什麼

以下圖,找到它在blk_init_queue_node()函數中被調用

 

最終q->make_request_fn()執行的是__make_request()函數 

 

5.6咱們來看看__make_request()函數,對提交的申請隊列q和bio作了什麼

static int __make_request(request_queue_t *q, struct bio *bio)
{

  struct request *req;          //塊設備自己的隊列
  ... ...
//(1)將以前的申請隊列q和傳入的bio,經過排序,合併在自己的req隊列中
  el_ret = elv_merge(q, &req, bio);
  ... ...

  init_request_from_bio(req, bio);        //合併失敗,單獨將bio放入req隊列
  add_request(q, req);                  //單獨將以前的申請隊列q放入req隊列
  ... ...
  __generic_unplug_device(q);      //(2) 執行申請隊列的處理函數     
 }

1)上面的elv_merge()函數,就是內核中的電梯算法(elevator merge),它就相似咱們坐的電梯,經過一個標誌,向上或向下.

好比申請隊列中有如下6個申請:

4(in),2(out),5(in),3(out),6(in),1(out)   //其中in:寫出隊列到扇區,ou:讀入隊列

最後執行下來,就會排序合併,先寫出4,5,6,隊列,再讀入1,2,3隊列

2) 上面的__generic_unplug_device()函數以下:

void __generic_unplug_device(request_queue_t *q)
{      if (unlikely(blk_queue_stopped(q)))
              return;
       if (!blk_remove_plug(q))
              return;
       q->request_fn(q);         
}

最終執行q的成員request_fn()函數, 執行申請隊列的處理函數

     

6.本節框架分析總結,以下圖所示:

 

 

7.其中q->request_fn是一個request_fn_proc結構體,以下圖所示:

 

7.1那這個申請隊列q->request_fn又是怎麼來的?

咱們參考自帶的塊設備驅動程序drivers\block\xd.c

在入口函數中發現有這麼一句:

static struct request_queue *xd_queue;             //定義一個申請隊列xd_queue

xd_queue = blk_init_queue(do_xd_request, &xd_lock);       //分配一個申請隊列

 

其中blk_init_queue()函數原型以下所示:

request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
//  *rfn: request_fn_proc結構體,用來執行申請隊列中的處理函數
//  *lock:隊列訪問權限的自旋鎖(spinlock),該鎖須要經過DEFINE_SPINLOCK()函數來定義

 

顯然就是將do_xd_request()掛到xd_queue->request_fn裏.而後返回這個request_queue隊列

 7.2咱們再看看申請隊列的處理函數 do_xd_request()是如何處理的,函數以下:

static void do_xd_request (request_queue_t * q) { struct request *req; if (xdc_busy) return;  while ((req = elv_next_request(q)) != NULL)    //(1)while獲取申請隊列中的須要處理的申請
 {  int res = 0; ... ...    for (retry = 0; (retry < XD_RETRIES) && !res; retry++) res = xd_readwrite(rw, disk, req->buffer, block, count);
                
//將獲取申請req的buffer成員 讀寫到disk扇區中,當讀寫失敗返回0,成功返回1
   end_request(req, res); //申請隊列中的的申請已處理結束,當res=0,表示讀寫失敗 } }

 

(1)爲何要while一直獲取?

由於這個q是個申請隊列,裏面會有多個申請,以前是使用電梯算法elv_merge()函數合併的,因此獲取也要經過電梯算法elv_next_request()函數獲取.

經過上面代碼和註釋,內核中的申請隊列q最終都是交給驅動處理,由驅動來對扇區讀寫

 

8.接下來咱們就看看drivers\block\xd.c的入口函數大概流程,是如何建立塊設備驅動的

static DEFINE_SPINLOCK(xd_lock);     //定義一個自旋鎖,用到申請隊列中
static
struct request_queue *xd_queue; //定義一個申請隊列xd_queue static int __init xd_init(void) //入口函數 { if (register_blkdev(XT_DISK_MAJOR, "xd")) //1.建立一個塊設備,保存在/proc/devices中 goto out1; xd_queue = blk_init_queue(do_xd_request, &xd_lock); //2.分配一個申請隊列,後面會賦給gendisk結構體的queue成員 ... ... for (i = 0; i < xd_drives; i++) { ... ... struct gendisk *disk = alloc_disk(64); //3.分配一個gendisk結構體, 64:次設備號個數,也稱爲分區個數
/* 4.接下來設置gendisk結構體 */ disk->major = XT_DISK_MAJOR; //設置主設備號 disk->first_minor = i<<6; //設置次設備號 disk->fops = &xd_fops; //設置塊設備驅動的操做函數 disk->queue = xd_queue; //設置queue申請隊列,用於管理該設備IO申請隊列 ... ... xd_gendisk[i] = disk; } ... ... for (i = 0; i < xd_drives; i++) add_disk(xd_gendisk[i]); //5.註冊gendisk結構體 }

 

其中gendisk(通用磁盤)結構體是用來存儲該設備的硬盤信息,包括請求隊列、分區鏈表和塊設備操做函數集等,結構體以下所示:

struct gendisk {
  int major;                        /*設備主設備號*/
  int first_minor;                  /*起始次設備號*/
  int minors;                       /*次設備號的數量,也稱爲分區數量,若是改值爲1,表示沒法分區*/
  char disk_name[32];              /*設備名稱*/
  struct hd_struct **part;          /*分區表的信息*/
  int part_uevent_suppress;
  struct block_device_operations *fops;  /*塊設備操做集合 */
  struct request_queue *queue;           /*申請隊列,用於管理該設備IO申請隊列的指針*/
  void *private_data;                    /*私有數據*/
  sector_t capacity;                     /*扇區數,512字節爲1個扇區,描述設備容量*/
  ....
    };

 

9.因此註冊一個塊設備驅動,須要如下步驟:

  1. 建立一個塊設備
  2. 分配一個申請隊列
  3. 分配一個gendisk結構體
  4. 設置gendisk結構體的成員
  5. 註冊gendisk結構體

 

未完待續~ ~下節便開始寫塊設備驅動程序

相關文章
相關標籤/搜索