《Linux内核设计与实现》读书笔记(十四)- 块I/O层
最近太忙,居然過了2個月才更新第十四章。。。。
主要內(nèi)容:
- 塊設(shè)備簡介
- 內(nèi)核訪問塊設(shè)備的方法
- 內(nèi)核I/O調(diào)度程序
?
1. 塊設(shè)備簡介
I/O設(shè)備主要有2類:
- 字符設(shè)備:只能順序讀寫設(shè)備中的內(nèi)容,比如 串口設(shè)備,鍵盤
- 塊設(shè)備:能夠隨機讀寫設(shè)備中的內(nèi)容,比如 硬盤,U盤
字符設(shè)備由于只能順序訪問,所以應用場景也不多,這篇文章主要討論塊設(shè)備。
塊設(shè)備是隨機訪問的,所以塊設(shè)備在不同的應用場景中存在很大的優(yōu)化空間。
?
塊設(shè)備中最重要的一個概念就是塊設(shè)備的最小尋址單元。
塊設(shè)備的最小尋址單元就是扇區(qū),扇區(qū)的大小是2的整數(shù)倍,一般是 512字節(jié)。
扇區(qū)是物理上的最小尋址單元,而邏輯上的最小尋址單元是塊。
為了便于文件系統(tǒng)管理,塊的大小一般是扇區(qū)的整數(shù)倍,并且小于等于頁的大小。
?
查看扇區(qū)和I/O塊的方法:
[wangyubin@localhost]$ sudo fdisk -lWARNING: GPT (GUID Partition Table) detected on '/dev/sda'! The util fdisk doesn't support GPT. Use GNU Parted. Disk /dev/sda: 500.1 GB, 500107862016 bytes, 976773168 sectors Units = sectors of 1 * 512 = 512 bytes Sector size (logical/physical): 512 bytes / 4096 bytes I/O size (minimum/optimal): 4096 bytes / 4096 bytes Disk identifier: 0x00000000上面的 Sector size 就是扇區(qū)的值,I/O size就是 塊的值
從上面顯示的結(jié)果,我們發(fā)現(xiàn)有個奇怪的地方,扇區(qū)的大小有2個值,邏輯大小是 512字節(jié),而物理大小卻是 4096字節(jié)。
其實邏輯大小 512字節(jié)是為了兼容以前的軟件應用,而實際物理大小 4096字節(jié)是由于硬盤空間越來越大導致的。
具體的來龍去脈請參考:4KB扇區(qū)的原因
?
2. 內(nèi)核訪問塊設(shè)備的方法
內(nèi)核通過文件系統(tǒng)訪問塊設(shè)備時,需要先把塊讀入到內(nèi)存中。所以文件系統(tǒng)為了管理塊設(shè)備,必須管理[塊]和內(nèi)存頁之間的映射。
內(nèi)核中有2種方法來管理 [塊] 和內(nèi)存頁之間的映射。
- 緩沖區(qū)和緩沖區(qū)頭
- bio
?
2.1 緩沖區(qū)和緩沖區(qū)頭
每個 [塊] 都是一個緩沖區(qū),同時對每個 [塊] 都定義一個緩沖區(qū)頭來描述它。
由于 [塊] 的大小是小于內(nèi)存頁的大小的,所以每個內(nèi)存頁會包含一個或者多個 [塊]
?
緩沖區(qū)頭定義在 <linux/buffer_head.h>: include/linux/buffer_head.h
struct buffer_head {unsigned long b_state; /* 表示緩沖區(qū)狀態(tài) */struct buffer_head *b_this_page;/* 當前頁中緩沖區(qū) */struct page *b_page; /* 當前緩沖區(qū)所在內(nèi)存頁 */sector_t b_blocknr; /* 起始塊號 */size_t b_size; /* buffer在內(nèi)存中的大小 */char *b_data; /* 塊映射在內(nèi)存頁中的數(shù)據(jù) */struct block_device *b_bdev; /* 關(guān)聯(lián)的塊設(shè)備 */bh_end_io_t *b_end_io; /* I/O完成方法 */void *b_private; /* 保留的 I/O 完成方法 */struct list_head b_assoc_buffers; /* 關(guān)聯(lián)的其他緩沖區(qū) */struct address_space *b_assoc_map; /* 相關(guān)的地址空間 */atomic_t b_count; /* 引用計數(shù) */ };?
整個 buffer_head 結(jié)構(gòu)體中的字段是減少過的,以前的內(nèi)核中字段更多。
各個字段的含義通過注釋都很明了,只有 b_state 字段比較復雜,它涵蓋了緩沖區(qū)可能的各種狀態(tài)。
enum bh_state_bits {BH_Uptodate, /* 包含可用數(shù)據(jù) */BH_Dirty, /* 該緩沖區(qū)是臟的(說明緩沖的內(nèi)容比磁盤中的內(nèi)容新,需要回寫磁盤) */BH_Lock, /* 該緩沖區(qū)正在被I/O使用,鎖住以防止并發(fā)訪問 */BH_Req, /* 該緩沖區(qū)有I/O請求操作 */BH_Uptodate_Lock,/* 由內(nèi)存頁中的第一個緩沖區(qū)使用,使得該頁中的其他緩沖區(qū) */BH_Mapped, /* 該緩沖區(qū)是映射到磁盤塊的可用緩沖區(qū) */BH_New, /* 緩沖區(qū)是通過 get_block() 剛剛映射的,尚且不能訪問 */BH_Async_Read, /* 該緩沖區(qū)正通過 end_buffer_async_read() 被異步I/O讀操作使用 */BH_Async_Write, /* 該緩沖區(qū)正通過 end_buffer_async_read() 被異步I/O寫操作使用 */BH_Delay, /* 緩沖區(qū)還未和磁盤關(guān)聯(lián) */BH_Boundary, /* 該緩沖區(qū)處于連續(xù)塊區(qū)的邊界,下一個塊不在連續(xù) */BH_Write_EIO, /* 該緩沖區(qū)在寫的時候遇到 I/O 錯誤 */BH_Ordered, /* 順序?qū)?*/BH_Eopnotsupp, /* 該緩沖區(qū)發(fā)生 “不被支持” 錯誤 */BH_Unwritten, /* 該緩沖區(qū)在磁盤上的位置已經(jīng)被申請,但還有實際寫入數(shù)據(jù) */BH_Quiet, /* 該緩沖區(qū)禁止錯誤 */BH_PrivateStart,/* 不是表示狀態(tài),分配給其他實體的私有數(shù)據(jù)區(qū)的第一個bit */ };?
在2.6之前的內(nèi)核中,主要就是通過緩沖區(qū)頭來管理 [塊] 和內(nèi)存之間的映射的。
用緩沖區(qū)頭來管理內(nèi)核的 I/O 操作主要存在以下2個弊端,所以在2.6開始的內(nèi)核中,緩沖區(qū)頭的作用大大降低了。
- 弊端 1
對內(nèi)核而言,操作內(nèi)存頁是最為簡便和高效的,所以如果通過緩沖區(qū)頭來操作的話(緩沖區(qū) 即[塊]在內(nèi)存中映射,可能比頁面要小),效率低下。
而且每個 [塊] 對應一個緩沖區(qū)頭的話,導致內(nèi)存的利用率降低(緩沖區(qū)頭包含的字段非常多)
- 弊端 2
每個緩沖區(qū)頭只能表示一個 [塊],所以內(nèi)核在處理大數(shù)據(jù)時,會分解為對一個個小的 [塊] 的操作,造成不必要的負擔和空間浪費。
?
2.2 bio
bio結(jié)構(gòu)體的出現(xiàn)就是為了改善上面緩沖區(qū)頭的2個弊端,它表示了一次 I/O 操作所涉及到的所有內(nèi)存頁。
/** I/O 操作的主要單元,針對 I/O塊和更低級的層 (ie drivers and* stacking drivers)*/ struct bio {sector_t bi_sector; /* 磁盤上相關(guān)扇區(qū) */struct bio *bi_next; /* 請求列表 */struct block_device *bi_bdev; /* 相關(guān)的塊設(shè)備 */unsigned long bi_flags; /* 狀態(tài)和命令標志 */unsigned long bi_rw; /* 讀還是寫 */unsigned short bi_vcnt; /* bio_vecs的數(shù)目 */unsigned short bi_idx; /* bio_io_vect的當前索引 *//* Number of segments in this BIO after* physical address coalescing is performed.* 結(jié)合后的片段數(shù)目*/unsigned int bi_phys_segments;unsigned int bi_size; /* 剩余 I/O 計數(shù) *//** To keep track of the max segment size, we account for the* sizes of the first and last mergeable segments in this bio.* 第一個和最后一個可合并的段的大小*/unsigned int bi_seg_front_size;unsigned int bi_seg_back_size;unsigned int bi_max_vecs; /* bio_vecs數(shù)目上限 */unsigned int bi_comp_cpu; /* 結(jié)束CPU */atomic_t bi_cnt; /* 使用計數(shù) */struct bio_vec *bi_io_vec; /* bio_vec 鏈表 */bio_end_io_t *bi_end_io; /* I/O 完成方法 */void *bi_private; /* bio結(jié)構(gòu)體創(chuàng)建者的私有方法 */ #if defined(CONFIG_BLK_DEV_INTEGRITY)struct bio_integrity_payload *bi_integrity; /* data integrity */ #endifbio_destructor_t *bi_destructor; /* bio撤銷方法 *//** We can inline a number of vecs at the end of the bio, to avoid* double allocations for a small number of bio_vecs. This member* MUST obviously be kept at the very end of the bio.* 內(nèi)嵌在結(jié)構(gòu)體末尾的 bio 向量,主要為了防止出現(xiàn)二次申請少量的 bio_vecs*/struct bio_vec bi_inline_vecs[0]; };幾個重要字段說明:
- bio 結(jié)構(gòu)體表示正在執(zhí)行的 I/O 操作相關(guān)的信息。
- bio_io_vec 鏈表表示當前 I/O 操作涉及到的內(nèi)存頁
- bio_vec 結(jié)構(gòu)體表示 I/O 操作使用的片段
- bi_vcnt bi_io_vec鏈表中bi_vec的個數(shù)
- bi_idx 當前的 bi_vec片段,通過 bi_vcnt(總數(shù))和 bi_idx(當前數(shù)),就可以跟蹤當前 I/O 操作的進度
?
bio_vec 結(jié)構(gòu)體很簡單,定義如下:
struct bio_vec {struct page *bv_page; /* 對應的物理頁 */unsigned int bv_len; /* 緩沖區(qū)大小 */unsigned int bv_offset; /* 緩沖區(qū)開始的位置 */ };每個 bio_vec 都是對應一個頁面,從而保證內(nèi)核能夠方便高效的完成 I/O 操作
?
2.3 2種方法的對比
緩沖區(qū)頭和bio并不是相互矛盾的,bio只是緩沖區(qū)頭的一種改善,將以前緩沖區(qū)頭完成的一部分工作移到bio中來完成。
bio中對應的是內(nèi)存中的一個個頁,而緩沖區(qū)頭對應的是磁盤中的一個塊。
對內(nèi)核來說,配合使用bio和緩沖區(qū)頭 比 只使用緩沖區(qū)頭更加的方便高效。
bio相當于在緩沖區(qū)上又封裝了一層,使得內(nèi)核在 I/O操作時只要針對一個或多個內(nèi)存頁即可,不用再去管理磁盤塊的部分。
?
使用bio結(jié)構(gòu)體還有以下好處:
- bio結(jié)構(gòu)體很容易處理高端內(nèi)存,因為它處理的是內(nèi)存頁而不是直接指針
- bio結(jié)構(gòu)體既可以代表普通頁I/O,也可以代表直接I/O
- bio結(jié)構(gòu)體便于執(zhí)行分散-集中(矢量化的)塊I/O操作,操作中的數(shù)據(jù)可以取自多個物理頁面
?
3. 內(nèi)核I/O調(diào)度程序
緩沖區(qū)頭和bio都是內(nèi)核處理一個具體I/O操作時涉及的概念。
但是內(nèi)核除了要完成I/O操作以外,還要調(diào)度好所有I/O操作請求,盡量確保每個請求能有個合理的響應時間。
?
下面就是目前內(nèi)核中已有的一些 I/O 調(diào)度算法。
3.1 linus電梯
為了保證磁盤尋址的效率,一般會盡量讓磁頭向一個方向移動,等到頭了再反過來移動,這樣可以縮短所有請求的磁盤尋址總時間。
磁頭的移動有點類似于電梯,所有這個 I/O 調(diào)度算法也叫電梯調(diào)度。
linux中的第一個電梯調(diào)度算法就是 linus本人所寫的,所有也叫做 linus 電梯。
?
linus電梯調(diào)度主要是對I/O請求進行合并和排序。
當一個新請求加入I/O請求隊列時,可能會發(fā)生以下4種操作:
?
linus電梯調(diào)度程序在2.6版的內(nèi)核中被其他調(diào)度程序所取代了。
?
3.2 最終期限I/O調(diào)度
linus電梯調(diào)度主要考慮了系統(tǒng)的全局吞吐量,對于個別的I/O請求,還是有可能造成饑餓現(xiàn)象。
而且讀寫請求的響應時間要求也是不一樣的,一般來說,寫請求的響應時間要求不高,寫請求可以和提交它的應用程序異步執(zhí)行,
但是讀請求一般和提交它的應用程序時同步執(zhí)行,應用程序等獲取到讀的數(shù)據(jù)后才會接著往下執(zhí)行。
因此在 linus 電梯調(diào)度程序中,還可能造成 寫-饑餓-讀(wirtes-starving-reads)這種特殊問題。
?
為了盡量公平的對待所有請求,同時盡量保證讀請求的響應時間,提出了最終期限I/O調(diào)度算法。
最終期限I/O調(diào)度 算法給每個請求設(shè)置了超時時間,默認情況下,讀請求的超時時間500ms,寫請求的超時時間是5s
但一個新請求加入到I/O請求隊列時,最終期限I/O調(diào)度和linus電梯調(diào)度相比,多出了以下操作:
?
最終期限I/O調(diào)度 算法也不能嚴格保證響應時間,但是它可以保證不會發(fā)生請求在明顯超時的情況下仍得不到執(zhí)行。
最終期限I/O調(diào)度 的實現(xiàn)參見: block/deadline-iosched.c
?
3.3 預測I/O調(diào)度
最終期限I/O調(diào)度算法優(yōu)先考慮讀請求的響應時間,但系統(tǒng)處于寫操作繁重的狀態(tài)時,會大大降低系統(tǒng)的吞吐量。
因為讀請求的超時時間比較短,所以每次有讀請求時,都會打斷寫請求,讓磁盤尋址到讀的位置,完成讀操作后再回來繼續(xù)寫。
這種做法保證讀請求的響應速度,卻損害了系統(tǒng)的全局吞吐量(磁頭先去讀再回來寫,發(fā)生了2次尋址操作)
?
預測I/O調(diào)度算法是為了解決上述問題而提出的,它是基于最終期限I/O調(diào)度算法的。
但有一個新請求加入到I/O請求隊列時,預測I/O調(diào)度與最終期限I/O調(diào)度相比,多了以下操作:
?
預測I/O調(diào)度算法中最重要的是保證等待期間不要浪費,也就是提高預測的準確性,
目前這種預測是依靠一系列的啟發(fā)和統(tǒng)計工作,預測I/O調(diào)度程序會跟蹤并統(tǒng)計每個應用程序的I/O操作習慣,以便正確預測應用程序的讀寫行為。
?
如果預測的準確率足夠高,那么預測I/O調(diào)度和最終期限I/O調(diào)度相比,既能提高讀請求的響應時間,又能提高系統(tǒng)吞吐量。
預測I/O調(diào)度的實現(xiàn)參見: block/as-iosched.c
?
注:預測I/O調(diào)度是linux內(nèi)核中缺省的調(diào)度程序。
?
3.4 完全公正的排隊I/O調(diào)度
完全公正的排隊(Complete Fair Queuing, CFQ)I/O調(diào)度 是為專有工作負荷設(shè)計的,它和之前提到的I/O調(diào)度有根本的不同。
CFQ I/O調(diào)度 算法中,每個進程都有自己的I/O隊列,
CFQ I/O調(diào)度程序以時間片輪轉(zhuǎn)調(diào)度隊列,從每個隊列中選取一定的請求數(shù)(默認4個),然后進行下一輪調(diào)度。
?
CFQ I/O調(diào)度在進程級提供了公平,它的實現(xiàn)位于: block/cfq-iosched.c
?
3.5 空操作的I/O調(diào)度
空操作(noop)I/O調(diào)度幾乎不做什么事情,這也是它這樣命名的原因。
空操作I/O調(diào)度只做一件事情,當有新的請求到來時,把它與任一相鄰的請求合并。
?
空操作I/O調(diào)度主要用于閃存卡之類的塊設(shè)備,這類設(shè)備沒有磁頭,沒有尋址的負擔。
空操作I/O調(diào)度的實現(xiàn)位于: block/noop-iosched.c
?
3.6 I/O調(diào)度程序的選擇
2.6內(nèi)核中內(nèi)置了上面4種I/O調(diào)度,可以在啟動時通過命令行選項 elevator=xxx 來啟用任何一種。
elevator選項參數(shù)如下:
| 參數(shù) | I/O調(diào)度程序 |
| as | 預測 |
| cfq | 完全公正排隊 |
| deadline | 最終期限 |
| noop | 空操作 |
如果啟動預測I/O調(diào)度,啟動的命令行參數(shù)中加上 elevator=as
轉(zhuǎn)載于:https://www.cnblogs.com/wang_yb/p/3299092.html
與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的《Linux内核设计与实现》读书笔记(十四)- 块I/O层的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL学习笔记_2_MySQL创建数
- 下一篇: Linux常用指令总结二~~