Linux塊設備IO子系統(一) _驅動模型

塊設備是Linux三大設備之一,其驅動模型主要針對磁盤,Flash等存儲類設備,塊設備(blockdevice)是一種具備必定結構的隨機存取設備,對這種設備的讀寫是按(因此叫塊設備)進行的,他使用緩衝區來存放暫時的數據,待條件成熟後,從緩存一次性寫入設備或者從設備一次性讀到緩衝區。做爲存儲設備,塊設備驅動的核心問題就是哪些page->segment->block->sector與哪些sector有數據交互,本文以3.14爲藍本,探討內核中的塊設備驅動模型。node

框架

下圖是Linux中的塊設備模型示意圖,應用層程序有兩種方式訪問一個塊設備:/dev和文件系統掛載點,前者和字符設備同樣,一般用於配置,後者就是咱們mount以後經過文件系統直接訪問一個塊設備了。linux

  1. read()系統調用最終會調用一個適當的VFS函數(read()-->sys_read()-->vfs_read()),將文件描述符fd和文件內的偏移量offset傳遞給它。
  2. VFS會判斷這個SCI的處理方式,若是訪問的內容已經被緩存在RAM中(磁盤高速緩存機制),就直接訪問,不然從磁盤中讀取
  3. 爲了從物理磁盤中讀取,內核依賴映射層mapping layer,即上圖中的磁盤文件系統
    1. 肯定該文件所在文件系統的塊的大小,並根據文件塊的大小計算所請求數據的長度。本質上,文件被拆成不少塊,所以內核須要肯定請求數據所在的塊
    2. 映射層調用一個具體的文件系統的函數,這個層的函數會訪問文件的磁盤節點,而後根據邏輯塊號肯定所請求數據在磁盤上的位置。
  4. 內核利用通用塊層(generic block layer)啓動IO操做來傳達所請求的數據,一般,一個IO操做只針對磁盤上一組連續的塊。
  5. IO調度程序根據預先定義的內核策略將待處理的IO進行重排和合並
  6. 塊設備驅動程序向磁盤控制器硬件接口發送適當的指令,進行實際的數據操做

塊設備 VS 字符設備

做爲一種存儲設備,和字符設備相比,塊設備有如下幾種不一樣:算法

字符設備 塊設備
1byte 塊,硬件塊各有不一樣,可是內核都使用512byte描述
順序訪問 隨機訪問
沒有緩存,實時操做 有緩存,不是實時操做
通常提供接口給應用層 塊設備通常提供接口給文件系統
是被用戶程序調用 由文件系統程序調用

此外,大多數狀況下,磁盤控制器都是直接使用DMA方式進行數據傳送。數據庫

IO調度

就是電梯算法。咱們知道,磁盤是的讀寫是經過機械性的移動磁頭來實現讀寫的,理論上磁盤設備知足塊設備的隨機讀寫的要求,可是出於節約磁盤,提升效率的考慮,咱們但願當磁頭處於某一個位置的時候,一塊兒將最近須要寫在附近的數據寫入,而不是這寫一下,那寫一下而後再回來,IO調度就是將上層發下來的IO請求的順序進行從新排序以及對多個請求進行合併,這樣就能夠實現上述的提升效率、節約磁盤的目的。這種解決問題的思路使用電梯算法,一個運行中的電梯,一我的20樓->1樓,另一我的15->5樓,電梯不會先將第一我的送到1樓再去15樓接第二我的將其送到5樓,而是從20樓下來,到15樓的時候停下接人,到5樓將第二個放下,最後到達1樓,一句話,電梯算法最終服務的優先順序並不按照按按鈕的前後順序。Linux內核中提供了下面的幾種電梯算法來實現IO調度:api

  • No-op I/O scheduler只實現了簡單的FIFO的,只進行最簡單的合併,比較適合基於Flash的存儲
  • Anticipatory I/O scheduler推遲IO請求(大約幾個微秒),以期能對他們進行排序,得到更高效率
  • Deadline I/O scheduler試圖把每次請求的延遲降到最低,同時也會對BIO從新排序,特別適用於讀取較多的場合,好比數據庫
  • CFQ I/O scheduler爲系統內全部的任務分配均勻的IO帶寬,提供一個公平的工做環境,在多媒體環境中,能保證音視頻及時從磁盤中讀取數據,是當前內核默認的調度器

咱們能夠經過內核傳參的方式指定使用的調度算法數組

kernel elevator=deadline

或者,使用以下命令改變內核調度算法緩存

echo SCHEDULER > /sys/block/DEVICE/queue/scheduler

Page->Segment->Block->Sector VS Sector

VS左面的是數據交互中的內存部分,Page就是內存映射的最小單位; Segment就是一個Page中咱們要操做的一部分,由若干個相鄰的塊組成; Block是邏輯上的進行數據存取的最小單位,是文件系統的抽象,邏輯塊的大小是在格式化的時候肯定的, 一個 Block 最多僅能容納一個文件(即不存在多個文件同一個block的狀況)。若是一個文件比block小,他也會佔用一個block,於是block中空餘的空間會浪費掉。而一個大文件,能夠佔多個甚至數十個成百上千萬的block。Linux內核要求 Block_Size = Sector_Size * (2的n次方),而且Block_Size <= 內存的Page_Size(頁大小), 如ext2 fs的block缺省是4k。若block太大,則存取小文件時,有空間浪費的問題;若block過小,則硬盤的 Block 數目會大增,而形成 inode 在指向 block 的時候的一些搜尋時間的增長,又會形成大文件讀寫方面的效率較差,block是VFS和文件系統傳送數據的基本單位。block對應磁盤上的一個或多個相鄰的扇區,而VFS將其當作是一個單一的數據單元,塊設備的block的大小不是惟一的,建立一個磁盤文件系統時,管理員能夠選擇合適的扇區的大小,同一個磁盤的幾個分區可使用不一樣的塊大小。此外,對塊設備文件的每次讀或寫操做是一種"原始"訪問,由於它繞過了磁盤文件系統,內核經過使用最大的塊(4096)執行該操做。Linux對內存中的block會被進一步劃分爲Sector,Sector是硬件設備傳送數據的基本單位,這個Sector就是512byte,和物理設備上的概念不同,若是實際的設備的sector不是512byte,而是4096byte(eg SSD),那麼只須要將多個內核sector對應一個設備sector便可app

VS右邊是物理上的概念,磁盤中一個Sector是512Byte,SSD中一個Sector是4K框架

核心結構與方法簡述

核心結構

  • gendisk是一個物理磁盤或分區在內核中的描述
  • block_device_operations描述磁盤的操做方法集,block_device_operations之於gendisk,相似於file_operations之於cdevdom

  • request_queue對象表示針對一個gendisk對象的全部請求的隊列,是相應gendisk對象的一個域
  • request表示通過IO調度以後的針對一個gendisk(磁盤)的一個"請求",是request_queue的一個節點。多個request構成了一個request_queue
  • bio表示應用程序對一個gendisk(磁盤)原始的訪問請求,一個bio由多個bio_vec,多個bio通過IO調度和合並以後能夠造成一個request。
  • bio_vec描述的應用層準備讀寫一個gendisk(磁盤)時須要使用的內存頁page的一部分,即上文中的"段",多個bio_vec和bio_iter造成一個bio
  • bvec_iter描述一個bio_vec中的一個sector信息

核心方法

  • set_capacity()設置gendisk對應的磁盤的物理參數
  • blk_init_queue()分配+初始化+綁定一個有IO調度的gendisk的requst_queue,處理函數是**void (request_fn_proc) (struct request_queue *q);**類型
  • blk_alloc_queue() 分配+初始化一個沒有IO調度的gendisk的request_queue,
  • blk_queue_make_request()綁定處理函數到一個沒有IO調度的request_queue,處理函數函數是void (make_request_fn) (struct request_queue q, struct bio bio);類型

  • __rq_for_each_bio()遍歷一個request中的全部的bio
  • bio_for_each_segment()遍歷一個bio中全部的segment
  • rq_for_each_segment()遍歷一個request中的全部的bio中的全部的segment
    最後三個遍歷算法都是用在request_queue綁定的處理函數中,這個函數負責對上層請求的處理。

核心結構與方法詳述

gendisk

一樣是面向對象的設計方法,Linux內核使用gendisk對象描述一個系統的中的塊設備,相似於Windows系統中的磁盤分區和物理磁盤的關係,OS眼中的磁盤都是邏輯磁盤,也就是一個磁盤分區,一個物理磁盤能夠對應多個磁盤分區,在Linux中,這個gendisk就是用來描述一個邏輯磁盤,也就是一個磁盤分區。

165 struct gendisk {
169         int major;                      /* major number of driver */
170         int first_minor;
171         int minors;   
174         char disk_name[DISK_NAME_LEN];  /* name of major driver */
175         char *(*devnode)(struct gendisk *gd, umode_t *mode);
177         unsigned int events;            /* supported events */
178         unsigned int async_events;      /* async events, subset of all */
185         struct disk_part_tbl __rcu *part_tbl;
186         struct hd_struct part0;
188         const struct block_device_operations *fops;
189         struct request_queue *queue;
190         void *private_data;
192         int flags;
193         struct device *driverfs_dev;  // FIXME: remove
194         struct kobject *slave_dir;
196         struct timer_rand_state *random;
197         atomic_t sync_io;               /* RAID */
198         struct disk_events *ev;
200         struct blk_integrity *integrity;
202         int node_id;
203 };

struct gendisk
--169-->驅動的主設備號
--170-->第一個次設備號
--171-->次設備號的數量,即容許的最大分區的數量,1表示不容許分區
--174-->設備名稱
--185-->分區表數組首地址
--186-->第一個分區,至關於part_tbl->part[0]
--188-->操做方法集指針
--189-->請求對象指針
--190-->私有數據指針
--193-->表示這是一個設備

gendisk是一個動態分配的結構體,因此不要本身手動來分配,而是使用內核相應的API來分配,其中會作一些初始化的工做

struct gendisk *alloc_disk(int minors);

//註冊gendisk類型對象到內核
void add_disk(struct gendisk *disk);

//從內核註銷gendisk對象
void del_gendisk(struct gendisk *gp);

上面幾個API是一個塊設備驅動中必不可少的部分,下面的兩個主要是用來內核對於設備管理用的,一般不要驅動來實現

//對gendisk的引用計數+1
struct kobject *get_disk(struct gendisk *disk);

//對gendisk的引用計數-1
void put_disk(struct gendisk *disk);

這兩個API最終回調用kobject *get_disk() 和kobject_put()來實現對設備的引用計數

block_device_operations

和字符設備同樣,若是使用/dev接口訪問塊設備,最終就會回調這個操做方法集的註冊函數

//include/linux/blkdev.h
1558 struct block_device_operations {
1559         int (*open) (struct block_device *, fmode_t);
1560         void (*release) (struct gendisk *, fmode_t);
1561         int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
1562         int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
1563         int (*direct_access) (struct block_device *, sector_t,
1564                                                 void **, unsigned long *);
1565         unsigned int (*check_events) (struct gendisk *disk,
1566                                       unsigned int clearing);
1568         int (*media_changed) (struct gendisk *);
1569         void (*unlock_native_capacity) (struct gendisk *);
1570         int (*revalidate_disk) (struct gendisk *);
1571         int (*getgeo)(struct block_device *, struct hd_geometry *);
1573         void (*swap_slot_free_notify) (struct block_device *, unsigned long);
1574         struct module *owner;
1575 };

struct block_device_operations
--1559-->當應用層打開一個塊設備的時候被回調
--1560-->當應用層關閉一個塊設備的時候被回調
--1562-->至關於file_operations裏的compat_ioctl,不過塊設備的ioctl包含大量的標準操做,因此在這個接口實現的操做不多
--1567-->在移動塊設備中測試介質是否改變的方法,已通過時,一樣的功能被check_event()實現
--1571-->即get geometry,獲取驅動器的幾何信息,獲取到的信息會被填充在一個hd_geometry結構中
--1574-->模塊所屬,一般填THIS_MODULE

request_queue

每個gendisk對象都有一個request_queue對象,前文說過,塊設備有兩種訪問接口,一種是/dev下,一種是經過文件系統,後者通過IO調度在這個gendisk->request_queue上增長請求,最終回調與request_queue綁定的處理函數,將這些請求向下變成具體的硬件操做

294 struct request_queue {
 298         struct list_head        queue_head; 
 300         struct elevator_queue   *elevator;
 472 };

struct request_queue
--298-->請求隊列的鏈表頭
--300-->請求隊列使用的IO調度算法, 經過內核啓動參數來選擇: kernel elevator=deadline
request_queue_t和gendisk同樣須要使用內核API來分配並初始化,裏面大量的成員不要直接操做, 此外, 請求隊列若是要正常工做還須要綁定到一個處理函數中, 當請求隊列不爲空時, 處理函數會被回調, 這就是塊設備驅動中處理請求的核心部分!

從驅動模型的角度來講, 塊設備主要分爲兩類須要IO調度的和不須要IO調度的, 前者包括磁盤, 光盤等, 後者包括Flash, SD卡等, 爲了保證模型的統一性 , Linux中對這兩種使用一樣的模型可是經過不一樣的API來完成上述的初始化綁定

有IO調度類設備API

//初始化+綁定
struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)

無IO調度類設備API

//初始化
struct request_queue *blk_alloc_queue(gfp_t gfp_mask) 

//綁定
 void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)

共用API

針對請求隊列的操做是塊設備的一個核心任務, 其實質就是對請求隊列操做函數的編寫, 這個函數的主要功能就是從請求隊列中獲取請求並根據請求進行相應的操做 內核中已經提供了大量的API供該函數使用

//清除請求隊列, 一般在卸載函數中使用
void blk_cleanup_queue(struct request_queue *q)  

//從隊列中去除請求
blkdev_dequeue_request()

//提取請求
struct request *blk_fetch_request(struct request_queue *q)

//從隊列中去除請求
struct request *blk_peek_request(struct request_queue *q)

//啓停請求隊列, 當設備進入到不能處理請求隊列的狀態時,應通知通用塊層
void blk_stop_queue(struct request_queue *q) 
void blk_start_queue(struct request_queue *q)

request

97 struct request {
  98         struct list_head queuelist;
 104         struct request_queue *q;
 117         struct bio *bio;
 118         struct bio *biotail;
 119 
 120         struct hlist_node hash; /* merge hash */
 126         union {
 127                 struct rb_node rb_node; /* sort/lookup */
 128                 void *completion_data;
 129         };
 137         union {
 138                 struct {
 139                         struct io_cq            *icq;
 140                         void                    *priv[2];
 141                 } elv;
 142 
 143                 struct {
 144                         unsigned int            seq;
 145                         struct list_head        list;            
 146                         rq_end_io_fn            *saved_end_io;
 147                 } flush;
 148         };
 149 
 150         struct gendisk *rq_disk;
 151         struct hd_struct *part;           
 199 };

struct request
--98-->將這個request掛接到鏈表的節點
--104-->這個request從屬的request_queue
--117-->組成這個request的bio鏈表的頭指針
--118-->組成這個request的bio鏈表的尾指針
--120-->內核hash表頭指針

bio

bio用來描述單一的I/O請求,它記錄了一次I/O操做所必需的相關信息,如用於I/O操做的數據緩存位置,,I/O操做的塊設備起始扇區,是讀操做仍是寫操做等等

46 struct bio {
 47         struct bio              *bi_next;       /* request queue link */
 48         struct block_device     *bi_bdev;
 49         unsigned long           bi_flags;       /* status, command, etc */
 50         unsigned long           bi_rw;          /* bottom bits READ/WRITE,
 51                                                  * top bits priority
 52                                                  */
 54         struct bvec_iter        bi_iter;
 59         unsigned int            bi_phys_segments;
 65         unsigned int            bi_seg_front_size;
 66         unsigned int            bi_seg_back_size;
 68         atomic_t                bi_remaining;
 70         bio_end_io_t            *bi_end_io;
 72         void                    *bi_private;
 85         unsigned short          bi_vcnt;        /* how many bio_vec's */
 91         unsigned short          bi_max_vecs;    /* max bvl_vecs we can hold */
104         struct bio_vec          bi_inline_vecs[0];
105 };

struct bio
--47-->指向鏈表中下一個bio的指針bi_next
--50-->bi_rw低位表示讀寫READ/WRITE, 高位表示優先級
--90-->bio對象包含bio_vec對象的數目
--91-->這個bio能承載的最大的io_vec的數目
--95-->該bio描述的第一個io_vec
--104-->表示這個bio包含的bio_vec變量的數組,即這個bio對應的某一個page中的一"段"內存

bio_vec

描述指定page中的一塊連續的區域,在bio中描述的就是一個page中的一個"段"(segment)

25 struct bio_vec {   
 26         struct page     *bv_page;
 27         unsigned int    bv_len; 
 28         unsigned int    bv_offset;
 29 };

struct bio_vec
--26-->描述的page
--27-->描述的長度
--28-->描述的起始地址偏移量

bio_iter

用於記錄當前bvec被處理的狀況,用於遍歷bio

31 struct bvec_iter {                                                          
 32         sector_t                bi_sector;      /* device address in 512 byt
 33                                                    sectors */
 34         unsigned int            bi_size;        /* residual I/O count */
 35 
 36         unsigned int            bi_idx;         /* current index into bvl_ve
 37 
 38         unsigned int            bi_bvec_done;   /* number of bytes completed
 39                                                    current bvec */
 40 };

__rq_for_each_bio()

遍歷一個request中的每個bio

738 #define __rq_for_each_bio(_bio, rq)     \                                  
 739         if ((rq->bio))                  \      
 740                 for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)

bio_for_each_segment()

遍歷一個bio中的每個segment

242 #define bio_for_each_segment(bvl, bio, iter)                            \   
243         __bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)

rq_for_each_segment()

遍歷一個request中的每個segment

742 #define rq_for_each_segment(bvl, _rq, _iter)                    \
 743         __rq_for_each_bio(_iter.bio, _rq)                       \
 744                 bio_for_each_segment(bvl, _iter.bio, _iter.iter)

小結

遍歷request_queue,綁定函數的一個必要的工做就是將request_queue中的數據取出, 因此遍歷是必不可少的, 針對有IO調度的設備, 咱們須要從中提取請求再繼續操做, 對於沒有IO調度的設備, 咱們能夠直接從request_queue中提取bio進行操做, 這兩種處理函數的接口就不同,下面的例子是對LDD3中的代碼進行了修剪而來的,相應的API使用的是3.14版本,能夠看出這兩種模式的使用方法的不一樣。

sbull_init
           └── setup_device
                        ├──sbull_make_request
                        │            ├──sbull_xfer_bio
                        │            └──sbull_transfer
                        └──sbull_full_request
                                    ├──blk_fetch_request
                                    └──sbull_xfer_request
                                               ├── __rq_for_each_bio
                                               └── sbull_xfer_bio
                                                            └──sbull_transfer

/*
 * Handle an I/O request.
 * 實現扇區的讀寫
 */
static void sbull_transfer(struct sbull_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 (write)
        memcpy(dev->data + offset, buffer, nbytes);
    else
        memcpy(buffer, dev->data + offset, nbytes);
}
/*
 * Transfer a single BIO.
 */
static int sbull_xfer_bio(struct sbull_dev *dev, struct bio *bio)
{
    struct bvec_iter i; //用來遍歷bio_vec對象
    struct bio_vec bvec;
    sector_t sector = bio->bi_iter.bi_sector;
    /* Do each segment independently. */
    bio_for_each_segment(bvec, bio, i) { //bvec會遍歷bio中每個bio_vec對象 
        char *buffer = __bio_kmap_atomic(bio, i, KM_USER0);
        sbull_transfer(dev, sector, bio_cur_bytes(bio)>>9 ,buffer, bio_data_dir(bio) == WRITE);
        sector += bio_cur_bytes(bio)>>9;
        __bio_kunmap_atomic(bio, KM_USER0);
    }
    return 0; /* Always "succeed" */
}

/*
 * Transfer a full request.
 */
static int sbull_xfer_request(struct sbull_dev *dev, struct request *req)
{
    struct bio *bio;
    int nsect = 0;
    
    __rq_for_each_bio(bio, req) { 
        sbull_xfer_bio(dev, bio);
        nsect += bio->bi_size/KERNEL_SECTOR_SIZE;
    }
    return nsect;
}

/*
 * Smarter request function that "handles clustering".*/
static void sbull_full_request(struct request_queue *q)
{
    struct request *req;
    int nsect;
    struct sbull_dev *dev ;
    int i = 0;
    while ((req = blk_fetch_request(q)) != NULL) {
        dev = req->rq_disk->private_data;
        nsect = sbull_xfer_request(dev, req);
        __blk_end_request(req, 0, (nsect<<9)); 
        printk ("i = %d\n", ++i);
    }
}

//The direct make request version
static void sbull_make_request(struct request_queue *q, struct bio *bio)
{
    struct sbull_dev *dev = q->queuedata;
    int status;

    status = sbull_xfer_bio(dev, bio);
    bio_endio(bio, status);  

    return;
}
/*
 * The device operations structure.
 */
static struct block_device_operations sbull_ops = {
    .owner  = THIS_MODULE,
    .open   = sbull_open,
    .release= sbull_release,
    .getgeo = sbull_getgeo,
};

/*
 * Set up our internal device.
 */
static void setup_device(struct sbull_dev *dev, int which)
{
    /*
     * Get some memory.
     */
    memset (dev, 0, sizeof (struct sbull_dev));
    dev->size = nsectors * hardsect_size;
    dev->data = vmalloc(dev->size);
    
        
    /*
     * The I/O queue, depending on whether we are using our own
     * make_request function or not.
     */
    switch (request_mode) {
    case RM_NOQUEUE:
        dev->queue = blk_alloc_queue(GFP_KERNEL);
        blk_queue_make_request(dev->queue, sbull_make_request);
        break;

    case RM_FULL:
        dev->queue = blk_init_queue(sbull_full_request, &dev->lock);
        break;
    }
    dev->queue->queuedata = dev;
    /*
     * And the gendisk structure.
     */
    dev->gd = alloc_disk(SBULL_MINORS);
    dev->gd->major = sbull_major;
    dev->gd->first_minor = which*SBULL_MINORS;
    dev->gd->fops = &sbull_ops;
    dev->gd->queue = dev->queue;
    dev->gd->private_data = dev;
    snprintf (dev->gd->disk_name, 32, "sbull%c", which + 'a');
    set_capacity(dev->gd, nsectors*(hardsect_size/KERNEL_SECTOR_SIZE));
    add_disk(dev->gd);
    return;
}

static int __init sbull_init(void)
{
    int i;
    /*
     * Get registered.
     */
    sbull_major = register_blkdev(sbull_major, "sbull");
    /*
     * Allocate the device array, and initialize each one.
     */
    Devices = (struct sbull_dev *)kmalloc(ndevices*sizeof (struct sbull_dev), GFP_KERNEL);
    for (i = 0; i < ndevices; i++) 
        setup_device(Devices + i, i);
    return 0;
}
相關文章
相關標籤/搜索