Linux块设备IO子系统
? ?
塊設(shè)備是Linux三大設(shè)備之一,其驅(qū)動(dòng)模型主要針對磁盤,Flash等存儲(chǔ)類設(shè)備,塊設(shè)備(blockdevice)是一種具有一定結(jié)構(gòu)的隨機(jī)存取設(shè)備,對這種設(shè)備的讀寫是按塊(所以叫塊設(shè)備)進(jìn)行的,他使用緩沖區(qū)來存放暫時(shí)的數(shù)據(jù),待條件成熟后,從緩存一次性寫入設(shè)備或者從設(shè)備一次性讀到緩沖區(qū)。作為存儲(chǔ)設(shè)備,塊設(shè)備驅(qū)動(dòng)的核心問題就是哪些page->segment->block->sector與哪些sector有數(shù)據(jù)交互,本文以3.14為藍(lán)本,探討內(nèi)核中的塊設(shè)備驅(qū)動(dòng)模型。
#框架
下圖是Linux中的塊設(shè)備模型示意圖,應(yīng)用層程序有兩種方式訪問一個(gè)塊設(shè)備:
/dev和文件系統(tǒng)掛載點(diǎn),前者和字符設(shè)備一樣,通常用于配置,后者就是我們mount之后通過文件系統(tǒng)直接訪問一個(gè)塊設(shè)備了。
read()系統(tǒng)調(diào)用最終會(huì)調(diào)用一個(gè)適當(dāng)?shù)腣FS函數(shù)(read()-->sys_read()-->vfs_read()),將文件描述符fd和文件內(nèi)的偏移量offset傳遞給它。
VFS會(huì)判斷這個(gè)SCI的處理方式,如果訪問的內(nèi)容已經(jīng)被緩存在RAM中(磁盤高速緩存機(jī)制),就直接訪問,否則從磁盤中讀取
為了從物理磁盤中讀取,內(nèi)核依賴映射層mapping layer,即上圖中的磁盤文件系統(tǒng)
確定該文件所在文件系統(tǒng)的塊的大小,并根據(jù)文件塊的大小計(jì)算所請求數(shù)據(jù)的長度。本質(zhì)上,文件被拆成很多塊,因此內(nèi)核需要確定請求數(shù)據(jù)所在的塊
映射層調(diào)用一個(gè)具體的文件系統(tǒng)的函數(shù),這個(gè)層的函數(shù)會(huì)訪問文件的磁盤節(jié)點(diǎn),然后根據(jù)邏輯塊號(hào)確定所請求數(shù)據(jù)在磁盤上的位置。
內(nèi)核利用通用塊層(generic block layer)啟動(dòng)IO操作來傳達(dá)所請求的數(shù)據(jù),通常,一個(gè)IO操作只針對磁盤上一組連續(xù)的塊。
IO調(diào)度程序根據(jù)預(yù)先定義的內(nèi)核策略將待處理的IO進(jìn)行重排和合并
塊設(shè)備驅(qū)動(dòng)程序向磁盤控制器硬件接口發(fā)送適當(dāng)?shù)闹噶?#xff0c;進(jìn)行實(shí)際的數(shù)據(jù)操作
#塊設(shè)備 VS 字符設(shè)備
作為一種存儲(chǔ)設(shè)備,和字符設(shè)備相比,塊設(shè)備有以下幾種不同:
| 1byte | 塊,硬件塊各有不同,但是內(nèi)核都使用512byte描述 |
| 順序訪問 | 隨機(jī)訪問 |
| 沒有緩存,實(shí)時(shí)操作 | 有緩存,不是實(shí)時(shí)操作 |
| 一般提供接口給應(yīng)用層 | 塊設(shè)備一般提供接口給文件系統(tǒng) |
| 是被用戶程序調(diào)用 | 由文件系統(tǒng)程序調(diào)用 |
此外,大多數(shù)情況下,磁盤控制器都是直接使用DMA方式進(jìn)行數(shù)據(jù)傳送。
#IO調(diào)度
就是電梯算法。我們知道,磁盤是的讀寫是通過機(jī)械性的移動(dòng)磁頭來實(shí)現(xiàn)讀寫的,理論上磁盤設(shè)備滿足塊設(shè)備的隨機(jī)讀寫的要求,但是出于節(jié)約磁盤,提高效率的考慮,我們希望當(dāng)磁頭處于某一個(gè)位置的時(shí)候,一起將最近需要寫在附近的數(shù)據(jù)寫入,而不是這寫一下。
IO調(diào)度就是將上層發(fā)下來的IO請求的順序進(jìn)行重新排序以及對多個(gè)請求進(jìn)行合并,這樣就可以實(shí)現(xiàn)上述的提高效率、節(jié)約磁盤的目的。這種解決問題的思路使用電梯算法,一個(gè)運(yùn)行中的電梯,一個(gè)人從20樓->1樓,另外一個(gè)人從15->5樓,電梯不會(huì)先將第一個(gè)人送到1樓再去15樓接第二個(gè)人將其送到5樓,而是從20樓下來,到15樓的時(shí)候停下接人,到5樓將第二個(gè)放下,最后到達(dá)1樓。
一句話,電梯算法最終服務(wù)的優(yōu)先順序并不按照按按鈕的先后順序。
Linux內(nèi)核中提供了下面的幾種電梯算法來實(shí)現(xiàn)IO調(diào)度:
No-op I/O scheduler只實(shí)現(xiàn)了簡單的FIFO的,只進(jìn)行最簡單的合并,比較適合基于Flash的存儲(chǔ)
Anticipatory I/O scheduler推遲IO請求(大約幾個(gè)微秒),以期能對他們進(jìn)行排序,獲得更高效率
Deadline I/O scheduler試圖把每次請求的延遲降到最低,同時(shí)也會(huì)對BIO重新排序,特別適用于讀取較多的場合,比如數(shù)據(jù)庫
CFQ I/O scheduler為系統(tǒng)內(nèi)所有的任務(wù)分配均勻的IO帶寬,提供一個(gè)公平的工作環(huán)境,在多媒體環(huán)境中,能保證音視頻及時(shí)從磁盤中讀取數(shù)據(jù),是當(dāng)前內(nèi)核默認(rèn)的調(diào)度器
我們可以通過內(nèi)核傳參的方式指定使用的調(diào)度算法
kernel elevator=deadline
或者,使用如下命令改變內(nèi)核調(diào)度算法
echo SCHEDULER > /sys/block/DEVICE/queue/scheduler
#Page->Segment->Block->Sector ?VS ?Sector
VS左面的是數(shù)據(jù)交互中的內(nèi)存部分。
Page就是內(nèi)存映射的最小單位;
Segment就是一個(gè)Page中我們要操作的一部分,由若干個(gè)相鄰的塊組成;
Block是邏輯上的進(jìn)行數(shù)據(jù)存取的最小單位,是文件系統(tǒng)的抽象,邏輯塊的大小是在格式化的時(shí)候確定的, 一個(gè) Block 最多僅能容納一個(gè)文件(即不存在多個(gè)文件同一個(gè)block的情況)。如果一個(gè)文件比block小,他也會(huì)占用一個(gè)block,因而block中空余的空間會(huì)浪費(fèi)掉。
而一個(gè)大文件,可以占多個(gè)甚至數(shù)十個(gè)成百上千萬的block。Linux內(nèi)核要求 Block_Size = Sector_Size ?* (2的n次方),并且Block_Size <= 內(nèi)存的Page_Size(頁大小), 如ext2 fs的block缺省是4k。若block太大,則存取小文件時(shí),有空間浪費(fèi)的問題;若block太小,則硬盤的 Block 數(shù)目會(huì)大增,而造成 inode 在指向 block 的時(shí)候的一些搜尋時(shí)間的增加,又會(huì)造成大文件讀寫方面的效率較差,block是VFS和文件系統(tǒng)傳送數(shù)據(jù)的基本單位。
block對應(yīng)磁盤上的一個(gè)或多個(gè)相鄰的扇區(qū),而VFS將其看成是一個(gè)單一的數(shù)據(jù)單元,塊設(shè)備的block的大小不是唯一的,創(chuàng)建一個(gè)磁盤文件系統(tǒng)時(shí),管理員可以選擇合適的扇區(qū)的大小,同一個(gè)磁盤的幾個(gè)分區(qū)可以使用不同的塊大小。此外,對塊設(shè)備文件的每次讀或?qū)懖僮魇且环N"原始"訪問,因?yàn)樗@過了磁盤文件系統(tǒng),內(nèi)核通過使用最大的塊(4096)執(zhí)行該操作。Linux對內(nèi)存中的block會(huì)被進(jìn)一步劃分為Sector,Sector是硬件設(shè)備傳送數(shù)據(jù)的基本單位,這個(gè)Sector就是512byte,和物理設(shè)備上的概念不一樣,如果實(shí)際的設(shè)備的sector不是512byte,而是4096byte(eg SSD),那么只需要將多個(gè)內(nèi)核sector對應(yīng)一個(gè)設(shè)備sector即可
VS右邊是物理上的概念,磁盤中一個(gè)Sector是512Byte,SSD中一個(gè)Sector是4K
#核心數(shù)據(jù)結(jié)構(gòu)
gendisk是一個(gè)物理磁盤或分區(qū)在內(nèi)核中的描述
block_device_operations描述磁盤的操作方法集,block_device_operations之于gendisk,類似于file_operations之于cdev
request_queue對象表示針對一個(gè)gendisk對象的所有請求的隊(duì)列,是相應(yīng)gendisk對象的一個(gè)域
request表示經(jīng)過IO調(diào)度之后的針對一個(gè)gendisk(磁盤)的一個(gè)"請求",是request_queue的一個(gè)節(jié)點(diǎn)。多個(gè)request構(gòu)成了一個(gè)request_queue
bio表示應(yīng)用程序?qū)σ粋€(gè)gendisk(磁盤)原始的訪問請求,一個(gè)bio由多個(gè)bio_vec,多個(gè)bio經(jīng)過IO調(diào)度和合并之后可以形成一個(gè)request。
bio_vec描述的應(yīng)用層準(zhǔn)備讀寫一個(gè)gendisk(磁盤)時(shí)需要使用的內(nèi)存頁page的一部分,即上文中的"段",多個(gè)bio_vec和bio_iter形成一個(gè)bio
bvec_iter描述一個(gè)bio_vec中的一個(gè)sector信息
#核心方法
set_capacity()設(shè)置gendisk對應(yīng)的磁盤的物理參數(shù)
blk_init_queue()分配+初始化+綁定一個(gè)有IO調(diào)度的gendisk的requst_queue,處理函數(shù)是void (request_fn_proc) (struct request_queue *q);類型
blk_alloc_queue() 分配+初始化一個(gè)沒有IO調(diào)度的gendisk的request_queue,
blk_queue_make_request()綁定處理函數(shù)到一個(gè)沒有IO調(diào)度的request_queue,處理函數(shù)函數(shù)是void (make_request_fn) (struct request_queue *q, struct bio *bio);類型
__rq_for_each_bio()遍歷一個(gè)request中的所有的bio
bio_for_each_segment()遍歷一個(gè)bio中所有的segment
rq_for_each_segment()遍歷一個(gè)request中的所有的bio中的所有的segment
最后三個(gè)遍歷算法都是用在request_queue綁定的處理函數(shù)中,這個(gè)函數(shù)負(fù)責(zé)對上層請求的處理。
#gendisk結(jié)構(gòu)體
同樣是面向?qū)ο蟮脑O(shè)計(jì)方法,Linux內(nèi)核使用gendisk對象描述一個(gè)系統(tǒng)的中的塊設(shè)備,類似于Windows系統(tǒng)中的磁盤分區(qū)和物理磁盤的關(guān)系,OS眼中的磁盤都是邏輯磁盤,也就是一個(gè)磁盤分區(qū),一個(gè)物理磁盤可以對應(yīng)多個(gè)磁盤分區(qū),在Linux中,這個(gè)gendisk就是用來描述一個(gè)邏輯磁盤,也就是一個(gè)磁盤分區(qū)。
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-->驅(qū)動(dòng)的主設(shè)備號(hào)
--170-->第一個(gè)次設(shè)備號(hào)
--171-->次設(shè)備號(hào)的數(shù)量,即允許的最大分區(qū)的數(shù)量,1表示不允許分區(qū)
--174-->設(shè)備名稱
--185-->分區(qū)表數(shù)組首地址
--186-->第一個(gè)分區(qū),相當(dāng)于part_tbl->part[0]
--188-->操作方法集指針
--189-->請求對象指針
--190-->私有數(shù)據(jù)指針
--193-->表示這是一個(gè)設(shè)備
gendisk是一個(gè)動(dòng)態(tài)分配的結(jié)構(gòu)體,所以不要自己手動(dòng)來分配,而是使用內(nèi)核相應(yīng)的API來分配,其中會(huì)做一些初始化的工作
struct?gendisk?*alloc_disk(int?minors); void?add_disk(struct?gendisk?*disk); void del_gendisk(struct gendisk *gp);上面幾個(gè)API是一個(gè)塊設(shè)備驅(qū)動(dòng)中必不可少的部分,下面的兩個(gè)主要是用來內(nèi)核對于設(shè)備管理用的,通常不要驅(qū)動(dòng)來實(shí)現(xiàn)
struct kobject *get_disk(struct gendisk *disk);void put_disk(struct gendisk *disk);這兩個(gè)API最終回調(diào)用kobject *get_disk() 和kobject_put()來實(shí)現(xiàn)對設(shè)備的引用計(jì)數(shù)
#block_device_operations結(jié)構(gòu)體
和字符設(shè)備一樣,如果使用/dev接口訪問塊設(shè)備,最終就會(huì)回調(diào)這個(gè)操作方法集的注冊函數(shù)
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-->當(dāng)應(yīng)用層打開一個(gè)塊設(shè)備的時(shí)候被回調(diào)
--1560-->當(dāng)應(yīng)用層關(guān)閉一個(gè)塊設(shè)備的時(shí)候被回調(diào)
--1562-->相當(dāng)于file_operations里的compat_ioctl,不過塊設(shè)備的ioctl包含大量的標(biāo)準(zhǔn)操作,所以在這個(gè)接口實(shí)現(xiàn)的操作很少
--1567-->在移動(dòng)塊設(shè)備中測試介質(zhì)是否改變的方法,已經(jīng)過時(shí),同樣的功能被check_event()實(shí)現(xiàn)
--1571-->即get geometry,獲取驅(qū)動(dòng)器的幾何信息,獲取到的信息會(huì)被填充在一個(gè)hd_geometry結(jié)構(gòu)中
--1574-->模塊所屬,通常填THIS_MODULE
#request_queue結(jié)構(gòu)體
每一個(gè)gendisk對象都有一個(gè)request_queue對象,前文說過,塊設(shè)備有兩種訪問接口,一種是/dev下,一種是通過文件系統(tǒng),后者經(jīng)過IO調(diào)度在這個(gè)gendisk->request_queue上增加請求,最終回調(diào)與request_queue綁定的處理函數(shù),將這些請求向下變成具體的硬件操作
294 struct request_queue {298 struct list_head queue_head;300 struct elevator_queue *elevator;472 }; struct request_queue--298-->請求隊(duì)列的鏈表頭
--300-->請求隊(duì)列使用的IO調(diào)度算法, 通過內(nèi)核啟動(dòng)參數(shù)來選擇: kernel elevator=deadline
request_queue_t和gendisk一樣需要使用內(nèi)核API來分配并初始化,里面大量的成員不要直接操作, 此外, 請求隊(duì)列如果要正常工作還需要綁定到一個(gè)處理函數(shù)中, 當(dāng)請求隊(duì)列不為空時(shí), 處理函數(shù)會(huì)被回調(diào), 這就是塊設(shè)備驅(qū)動(dòng)中處理請求的核心部分!
從驅(qū)動(dòng)模型的角度來說, 塊設(shè)備主要分為兩類需要IO調(diào)度的和不需要IO調(diào)度的, 前者包括磁盤, 光盤等, 后者包括Flash, SD卡等, 為了保證模型的統(tǒng)一性 , Linux中對這兩種使用同樣的模型但是通過不同的API來完成上述的初始化和綁定
#有IO調(diào)度類設(shè)備API
struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock _t *lock)#無IO調(diào)度類設(shè)備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
針對請求隊(duì)列的操作是塊設(shè)備的一個(gè)核心任務(wù), 其實(shí)質(zhì)就是對請求隊(duì)列操作函數(shù)的編寫, 這個(gè)函數(shù)的主要功能就是從請求隊(duì)列中獲取請求并根據(jù)請求進(jìn)行相應(yīng)的操作 內(nèi)核中已經(jīng)提供了大量的API供該函數(shù)使用
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-->將這個(gè)request掛接到鏈表的節(jié)點(diǎn)
--104-->這個(gè)request從屬的request_queue
--117-->組成這個(gè)request的bio鏈表的頭指針
--118-->組成這個(gè)request的bio鏈表的尾指針
--120-->內(nèi)核hash表頭指針
#bio
bio用來描述單一的I/O請求,它記錄了一次I/O操作所必需的相關(guān)信息,如用于I/O操作的數(shù)據(jù)緩存位置,,I/O操作的塊設(shè)備起始扇區(qū),是讀操作還是寫操作等等
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-->指向鏈表中下一個(gè)bio的指針bi_next
--50-->bi_rw低位表示讀寫READ/WRITE, 高位表示優(yōu)先級(jí)
--90-->bio對象包含bio_vec對象的數(shù)目
--91-->這個(gè)bio能承載的最大的io_vec的數(shù)目
--95-->該bio描述的第一個(gè)io_vec
--104-->表示這個(gè)bio包含的bio_vec變量的數(shù)組,即這個(gè)bio對應(yīng)的某一個(gè)page中的一"段"內(nèi)存
#bio_vec
描述指定page中的一塊連續(xù)的區(qū)域,在bio中描述的就是一個(gè)page中的一個(gè)"段"(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
用于記錄當(dāng)前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()
遍歷一個(gè)request中的每一個(gè)bio
?739?????????if?((rq->bio))??????????????????\740?????????????????for?(_bio?=?(rq)->bio;?_bio;?_bio?=?_bio->bi_next)#bio_for_each_segment()
遍歷一個(gè)bio中的每一個(gè)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()
遍歷一個(gè)request中的每一個(gè)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,綁定函數(shù)的一個(gè)必要的工作就是將request_queue中的數(shù)據(jù)取出, 所以遍歷是必不可少的, 針對有IO調(diào)度的設(shè)備, 我們需要從中提取請求再繼續(xù)操作, 對于沒有IO調(diào)度的設(shè)備, 我們可以直接從request_queue中提取bio進(jìn)行操作, 這兩種處理函數(shù)的接口就不一樣,下面的例子是對LDD3中的代碼進(jìn)行了修剪而來的,相應(yīng)的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
? 回復(fù)「?籃球的大肚子」進(jìn)入技術(shù)群聊
回復(fù)「1024」獲取1000G學(xué)習(xí)資料
總結(jié)
以上是生活随笔為你收集整理的Linux块设备IO子系统的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 创建AD9361的vivado工程并导入
- 下一篇: 华为突然宣布,对物联网下手了!