塊設備是Linux三大設備之一,其驅動模型主要針對磁盤,Flash等存儲類設備,塊設備(blockdevice)是一種具備必定結構的隨機存取設備,對這種設備的讀寫是按塊(因此叫塊設備)進行的,他使用緩衝區來存放暫時的數據,待條件成熟後,從緩存一次性寫入設備或者從設備一次性讀到緩衝區。做爲存儲設備,塊設備驅動的核心問題就是哪些page->segment->block->sector與哪些sector有數據交互,本文以3.14爲藍本,探討內核中的塊設備驅動模型。node
下圖是Linux中的塊設備模型示意圖,應用層程序有兩種方式訪問一個塊設備:/dev和文件系統掛載點,前者和字符設備同樣,一般用於配置,後者就是咱們mount以後經過文件系統直接訪問一個塊設備了。linux
做爲一種存儲設備,和字符設備相比,塊設備有如下幾種不一樣:算法
字符設備 | 塊設備 |
---|---|
1byte | 塊,硬件塊各有不一樣,可是內核都使用512byte描述 |
順序訪問 | 隨機訪問 |
沒有緩存,實時操做 | 有緩存,不是實時操做 |
通常提供接口給應用層 | 塊設備通常提供接口給文件系統 |
是被用戶程序調用 | 由文件系統程序調用 |
此外,大多數狀況下,磁盤控制器都是直接使用DMA方式進行數據傳送。數據庫
就是電梯算法。咱們知道,磁盤是的讀寫是經過機械性的移動磁頭來實現讀寫的,理論上磁盤設備知足塊設備的隨機讀寫的要求,可是出於節約磁盤,提升效率的考慮,咱們但願當磁頭處於某一個位置的時候,一塊兒將最近須要寫在附近的數據寫入,而不是這寫一下,那寫一下而後再回來,IO調度就是將上層發下來的IO請求的順序進行從新排序以及對多個請求進行合併,這樣就能夠實現上述的提升效率、節約磁盤的目的。這種解決問題的思路使用電梯算法,一個運行中的電梯,一我的20樓->1樓,另一我的15->5樓,電梯不會先將第一我的送到1樓再去15樓接第二我的將其送到5樓,而是從20樓下來,到15樓的時候停下接人,到5樓將第二個放下,最後到達1樓,一句話,電梯算法最終服務的優先順序並不按照按按鈕的前後順序。Linux內核中提供了下面的幾種電梯算法來實現IO調度:api
咱們能夠經過內核傳參的方式指定使用的調度算法數組
kernel elevator=deadline
或者,使用以下命令改變內核調度算法緩存
echo SCHEDULER > /sys/block/DEVICE/queue/scheduler
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框架
block_device_operations描述磁盤的操做方法集,block_device_operations之於gendisk,相似於file_operations之於cdevdom
bvec_iter描述一個bio_vec中的一個sector信息
blk_queue_make_request()綁定處理函數到一個沒有IO調度的request_queue,處理函數函數是void (make_request_fn) (struct request_queue q, struct bio bio);類型
rq_for_each_segment()遍歷一個request中的全部的bio中的全部的segment
最後三個遍歷算法都是用在request_queue綁定的處理函數中,這個函數負責對上層請求的處理。
一樣是面向對象的設計方法,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()來實現對設備的引用計數
和字符設備同樣,若是使用/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
每個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來完成上述的初始化和綁定
//初始化+綁定 struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
//初始化 struct request_queue *blk_alloc_queue(gfp_t gfp_mask) //綁定 void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)
針對請求隊列的操做是塊設備的一個核心任務, 其實質就是對請求隊列操做函數的編寫, 這個函數的主要功能就是從請求隊列中獲取請求並根據請求進行相應的操做 內核中已經提供了大量的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)
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用來描述單一的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中的一"段"內存
描述指定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-->描述的起始地址偏移量
用於記錄當前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 };
遍歷一個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中的每個segment
242 #define bio_for_each_segment(bvl, bio, iter) \ 243 __bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)
遍歷一個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; }