Linux块设备IO子系统
? ?
塊設備是Linux三大設備之一,其驅動模型主要針對磁盤,Flash等存儲類設備,塊設備(blockdevice)是一種具有一定結構的隨機存取設備,對這種設備的讀寫是按塊(所以叫塊設備)進行的,他使用緩沖區來存放暫時的數據,待條件成熟后,從緩存一次性寫入設備或者從設備一次性讀到緩沖區。作為存儲設備,塊設備驅動的核心問題就是哪些page->segment->block->sector與哪些sector有數據交互,本文以3.14為藍本,探討內核中的塊設備驅動模型。
#框架
下圖是Linux中的塊設備模型示意圖,應用層程序有兩種方式訪問一個塊設備:
/dev和文件系統掛載點,前者和字符設備一樣,通常用于配置,后者就是我們mount之后通過文件系統直接訪問一個塊設備了。
read()系統調用最終會調用一個適當的VFS函數(read()-->sys_read()-->vfs_read()),將文件描述符fd和文件內的偏移量offset傳遞給它。
VFS會判斷這個SCI的處理方式,如果訪問的內容已經被緩存在RAM中(磁盤高速緩存機制),就直接訪問,否則從磁盤中讀取
為了從物理磁盤中讀取,內核依賴映射層mapping layer,即上圖中的磁盤文件系統
確定該文件所在文件系統的塊的大小,并根據文件塊的大小計算所請求數據的長度。本質上,文件被拆成很多塊,因此內核需要確定請求數據所在的塊
映射層調用一個具體的文件系統的函數,這個層的函數會訪問文件的磁盤節點,然后根據邏輯塊號確定所請求數據在磁盤上的位置。
內核利用通用塊層(generic block layer)啟動IO操作來傳達所請求的數據,通常,一個IO操作只針對磁盤上一組連續的塊。
IO調度程序根據預先定義的內核策略將待處理的IO進行重排和合并
塊設備驅動程序向磁盤控制器硬件接口發送適當的指令,進行實際的數據操作
#塊設備 VS 字符設備
作為一種存儲設備,和字符設備相比,塊設備有以下幾種不同:
| 1byte | 塊,硬件塊各有不同,但是內核都使用512byte描述 |
| 順序訪問 | 隨機訪問 |
| 沒有緩存,實時操作 | 有緩存,不是實時操作 |
| 一般提供接口給應用層 | 塊設備一般提供接口給文件系統 |
| 是被用戶程序調用 | 由文件系統程序調用 |
此外,大多數情況下,磁盤控制器都是直接使用DMA方式進行數據傳送。
#IO調度
就是電梯算法。我們知道,磁盤是的讀寫是通過機械性的移動磁頭來實現讀寫的,理論上磁盤設備滿足塊設備的隨機讀寫的要求,但是出于節約磁盤,提高效率的考慮,我們希望當磁頭處于某一個位置的時候,一起將最近需要寫在附近的數據寫入,而不是這寫一下。
IO調度就是將上層發下來的IO請求的順序進行重新排序以及對多個請求進行合并,這樣就可以實現上述的提高效率、節約磁盤的目的。這種解決問題的思路使用電梯算法,一個運行中的電梯,一個人從20樓->1樓,另外一個人從15->5樓,電梯不會先將第一個人送到1樓再去15樓接第二個人將其送到5樓,而是從20樓下來,到15樓的時候停下接人,到5樓將第二個放下,最后到達1樓。
一句話,電梯算法最終服務的優先順序并不按照按按鈕的先后順序。
Linux內核中提供了下面的幾種電梯算法來實現IO調度:
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即可
VS右邊是物理上的概念,磁盤中一個Sector是512Byte,SSD中一個Sector是4K
#核心數據結構
gendisk是一個物理磁盤或分區在內核中的描述
block_device_operations描述磁盤的操作方法集,block_device_operations之于gendisk,類似于file_operations之于cdev
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: 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); void?add_disk(struct?gendisk?*disk); void del_gendisk(struct gendisk *gp);上面幾個API是一個塊設備驅動中必不可少的部分,下面的兩個主要是用來內核對于設備管理用的,通常不要驅動來實現
struct kobject *get_disk(struct gendisk *disk);void put_disk(struct gendisk *disk);這兩個API最終回調用kobject *get_disk() 和kobject_put()來實現對設備的引用計數
#block_device_operations結構體
和字符設備一樣,如果使用/dev接口訪問塊設備,最終就會回調這個操作方法集的注冊函數
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;119120 struct hlist_node hash;126 union {127 struct rb_node rb_node;128 void *completion_data;129 };137 union {138 struct {139 struct io_cq *icq;140 void *priv[2];141 } elv;142143 struct {144 unsigned int seq;145 struct list_head list;146 rq_end_io_fn *saved_end_io;147 } flush;148 };149150 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;48 struct block_device *bi_bdev;49 unsigned long bi_flags;50 unsigned long bi_rw;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;91 unsigned short bi_max_vecs; 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;34 unsigned int bi_size;3536 unsigned int bi_idx;40 };#__rq_for_each_bio()
遍歷一個request中的每一個bio
?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
? 回復「?籃球的大肚子」進入技術群聊
回復「1024」獲取1000G學習資料
總結
以上是生活随笔為你收集整理的Linux块设备IO子系统的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 创建AD9361的vivado工程并导入
- 下一篇: 给你准备的Linux启动流程