1. 程式人生 > >22.Linux-塊設備驅動之框架詳細分析(詳解)

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

磁盤設備 回調 隊列 rst 學習 更新 表示 索引 函數實現

1.之前我們學的都是字符設備驅動,先來回憶一下

字符設備驅動:

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

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

塊設備:

塊設備是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結構體

原文:https://www.cnblogs.com/lifexy/p/7651667.html

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