深入解析linux IO Block layer
早期的 Block 框架是單隊列(single-queue)架構,適用于“硬件單隊列”的存儲設備(比如機械磁盤),隨著存儲器件技術的發(fā)展,支持“硬件多隊列”的存儲器件越來越常見(比如 NVMe SSD),傳統(tǒng)的單隊列架構也因此被改成了多隊列(multi-queue)架構。早在 3.13 內(nèi)核就已經(jīng)加入了多隊列代碼,但是還不太穩(wěn)定,經(jīng)過多年的發(fā)展 multi-queue 越來越穩(wěn)定,linux 5.0+ 已經(jīng)默認使用 multi-queue。本篇文章介紹 Block 層框架及調度器相關知識,讓讀者對 Block 層有一個宏觀的認識。
一、Block 層的作用
用戶發(fā)起讀寫操作時,并不是直接操作存儲設備,而是需要經(jīng)過較長的 IO 棧才能完成數(shù)據(jù)的讀寫。讀寫操作大體上需依次經(jīng)過虛擬文件系統(tǒng) vfs、磁盤文件系統(tǒng)、block 層、設備驅動層,最后到達存儲器件,器件處理完成后發(fā)送中斷通知驅動程序,流程見圖 1。

圖1 IO 棧
備注:page cache 機制用來提高性能。在內(nèi)存資源不緊張的情況下,用戶訪問過的數(shù)據(jù)不會被丟棄,而是緩存在內(nèi)存中,下次可以訪問快速的內(nèi)存中數(shù)據(jù),無需訪問慢速的存儲設備。mapper layer 用來將用戶操作文件偏移量轉換成磁盤文件系統(tǒng)的 block 偏移量。
Block 層連接著文件系統(tǒng)層和設備驅動層,從 submit_bio 開始,bio 就進入了 block 層,這些 bio 被 Block 層抽象成 request 管理,在適當?shù)臅r候這些 request 離開 Block 層進入設備驅動層。IO 請求完成后,Block 層的軟中斷負責處理 IO 完成后的工作。Block 層主要負責:
管理 IO 請求
IO 請求暫存、合并,以及決定以何種順序處理IO請求。這里面涉及到 single-queue、multi-queue 框架以及具體的 IO 調度器。
IO 統(tǒng)計
主要是task io accounting統(tǒng)計各個進程的讀寫情況,統(tǒng)計信息見struct task_io_accounting。
注意,雖然 Block 層中存放著很多的 request,但正常情況下 Block 層不會主動“下發(fā)” request 給設備驅動程序(在線切換 IO 調度器、存儲器件 offline 場景會主動下發(fā) request)。當設備空閑時,設備驅動程序從 Block 層的“分發(fā)隊列”頭部依次取 request 進行處理,設備驅動程序拿到 request 后,根據(jù) request 中的信息及器件協(xié)議生成 cmd 命令交由器件處理。
二、Block 框架演變
Block 層軟件設計與存儲器件的特性緊密相關,大致經(jīng)歷了 2 個階段。

圖2 single-queue與multi-queue 架構
*引用自Linux Block IO: Introducing Multi-queue SSD Access on Multi-core Systems
single-queue 框架
早期的存儲設備是磁盤,特點是機械運動尋址、且不支持多硬件隊列并發(fā)處理 io,所以代碼邏輯自然地設計了一個軟件分發(fā)隊列,這種軟件邏輯上只有一個分發(fā)隊列的架構稱作 single-queue 架構,由于軟件本身的開銷(多核訪問 request queue 需要獲取 request_queue->queue_lock 等原因),single-queue 的 IOPS 能達到百萬到千萬級別的數(shù)據(jù)量,由于早期存儲器件速度慢,百萬的 IOPS 已經(jīng)完全能夠滿足需求。
multi-queue 架構
當支持多隊列的高速存儲器件出現(xiàn)后,器件端處理的時間變短,single-queue 引入的軟件開銷變得突出,軟件成為性能瓶頸,導致性能瓶頸的因素有 3 個:
1) 所有 cpu 共享一個 request queue,對 request_queue->queue_lock 的競爭比較多。
2) 大多數(shù)情況下,完成一次 io 需要兩次中斷,一個是硬件中斷,一個是 IPI 核間中斷用于觸發(fā)其他 cpu 上的軟中斷。
3) 如果提交 io 請求的 cpu 不是接收到硬件中斷的 cpu,還存在遠端內(nèi)存訪問的問題。
Jens Axboe(block maintainer) 針對 single-queue 存在的問題,提出了 multi-queue 架構,這種架構為每個 cpu 分配一個軟件隊列(稱為 soft context dispatch q),又根據(jù)存儲器件的硬件隊列(hardware q)數(shù)量分配了相同數(shù)量的硬件上下文分發(fā)隊列(hard context dispatch q,這是軟件邏輯上的隊列),通過固定的映射關系,將 1 個或多個 soft context dispatch q 映射到 1 個 hard context dispatch q,再將 hard context dispatch q 與存儲器件的 hardware q 一一對應起來,達到并發(fā)處理的效果,提升 IO 性能。
【文章福利】小編推薦自己的Linux內(nèi)核技術交流群:【749907784】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦!?。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)? ?


三、數(shù)據(jù)結構
相關的數(shù)據(jù)結構可以分成兩大類,一是 IO 請求本身,二是管理 IO 請求用到的隊列,理解這些數(shù)據(jù)結構是了解 block 層設計邏輯的基礎,數(shù)據(jù)結構描述如下。
1.? IO 請求
按照 IO 請求的生命周期,IO 請求被抽象成了 bio、request(簡稱 rq)、cmd,見圖 3。訪問存儲器件上相鄰區(qū)域的 bio、request 可能會被合并,稱為 bio merge、request merge。若 bio 的長度超過軟件或者硬件的限制,bio 會被拆分成多個,稱為 bio split。Block 層接收到一個 bio 后,這個 bio 將生成一個新的 request,或者合并到已有的 request 中。圖 3 中 bio2 被拆分,bio3、bio4 合并到一個 request 中。

圖3 IO 請求的生命周期
bio 是描述 io 請求的最小單位,bio 描述了數(shù)據(jù)的位置屬性,表示為:

表1 bio結構體說明
POSIX標準定義了scatter-gather I/O,這種IO通過一個讀/寫系統(tǒng)調用可以往多個不連續(xù)的內(nèi)存段中讀/寫,從而提高IO性能并且能確保訪問多段內(nèi)存的原子性。linux系統(tǒng)調用readv、writev支持scatter-gather I/O,所以bio的內(nèi)存端需用多個[ page地址, 頁內(nèi)偏移, 長度 ]描述不連續(xù)的內(nèi)存段,每一個[ page地址, 頁內(nèi)偏移, 長度 ]在linux中稱為bio_vector。bio結構體的示意圖見下圖。

圖4 bio 結構體示意圖
request是IO調度的最小單位,多個bio訪問存儲器件上相鄰的區(qū)域數(shù)據(jù)并且是同種類型的(讀/寫),則會被合并到一個request中,所以一個request可能包含多個bio。系統(tǒng)中request數(shù)量是有限制的,不是無限多的,否則當io請求太多,而存儲器件來不及處理時,就會出現(xiàn)struct request結構體占用太多內(nèi)存的情況。
在single-queue中用rq資源池管理request的申請釋放,一個存儲器件的讀寫請求數(shù)量各自限制最多q->nr_requests個(默認的q->nr_requests = BLKDEV_MAX_RQ = 128)。當該器件上待處理的讀或寫請求數(shù)超過

=7/8 * q->nr_requests時進入擁塞狀態(tài),此時會限制新生成request的速度。比如讀請求擁塞時,page_cache_async_readahead函數(shù)中關閉預讀功能以較少新生成的request。當存儲器件待處理讀或寫的request比

低時退出擁塞狀態(tài)。以默認q->nr_requests = 128個請求為例:

圖5 single-queue request 數(shù)量管理
在multi-queue中,沒有上述的congestion state邏輯,因為multi-queue用于支持多硬件隊列存儲器件的場景(當然也可以用于單硬件隊列的存儲器件),這些存儲器件的速度很快,不需要過多考慮器件處理慢的問題(但是也不能一味地任由rqeuest增長)。在multi-queue中request最大數(shù)量與調度器的tag數(shù)量有關,同single-queue一樣,默認值也是q->nr_requests = BLKDEV_MAX_RQ = 128,當存儲器件待處理的讀/寫超過調度器tag數(shù)量時,申請rq的task睡眠,當有rq處理完成被釋放后,再喚醒當前的task。默認的調度器tag數(shù)量可以通過sysfs接口修改,內(nèi)核里通過blk_mq_update_nr_requests更新。
cmd是設備驅動處理的IO請求,設備驅動程序根據(jù)器件協(xié)議,將request轉換成cmd,然后發(fā)送給器件處理。cmd已經(jīng)不屬于block層管理了,所以這篇文章不做描述。
2.IO隊列
上面的IO請求需要經(jīng)過多級緩沖隊列管理,見圖6。

圖6 IO隊列
注:multi-queue如果器件支持hardware multi q,plug list功能關閉
所有的bio都由submit_bio提交到block層,bio依次經(jīng)過下面隊列:
進程私有的plug list
隊列中存放的是io請求(rq),引入這個緩沖隊列的目的是為了性能。進程提交一個bio后,短時間類很可能還會有新的bio,這些bio被暫存在plug list中,因為這個隊列只有本進程能操作,所以不用加鎖就可以進行bio merge操作(在后面提到的調度器隊列中做merge需要加鎖)。
調度器隊列elevator q
隊列中存放的是io請求(rq)。single-queue的調度器有noop、cfq;multi-queue的調度器有mq-deadline、bfq、kyber。每個調度器有都實現(xiàn)了專門的數(shù)據(jù)結構管理rq(鏈表、紅黑樹等),這里統(tǒng)以elevator q稱呼。
系統(tǒng)中的調度器隊列可能有很多,比如cfq為每個進程維護各自的同步請求隊列,又為所有進程維護了公用的異步請求隊列。調度器需要決定先處理哪個隊列以及隊列中的哪個rq。一般情況下,調度器不會主動將rq移到設備分發(fā)隊列中,而是由設備驅動程序主動來取rq。
設備分發(fā)隊列device dispatch q(也可以稱作hardware dispatch q)
這是軟件實現(xiàn)的隊列。存儲器件空閑時,其設備驅動程序主動從調度器中拉取一個rq存在設備分發(fā)隊列中,分發(fā)隊列中的rq按照先進先出順序被封裝成cmd下發(fā)給器件。
對于multi-queue,設備分發(fā)隊列包中還額外包含per-core軟件隊列,它是為硬件分發(fā)隊列服務的,可以把它理解成設備分發(fā)隊列中的一部分。
硬件隊列HW q
隊列中存放的是按器件協(xié)議封裝的cmd,一些器件是單HW隊列,比如UFS內(nèi)部是一個隊列深度為32的HW q,NVMe SSD最大支持的隊列數(shù)量為64K、隊列深度64K。
四、常用調度器
single-queue用到的調度器有noop,deadline,cfq。
multi-queue用到的調度器有none(類似于noop),mq-deadline(類似于deadline),bfq(類似于cfq),kyber。這里選取幾個具有代表意義的調度器對比分析。
noop調度器
最簡單的調度器,IO請求放入一個FIFO隊列,逐個執(zhí)行這些IO請求(rq)。
noop調度器基本上對rq不做額外,僅僅在將rq插入到調度器隊列時,將rq與已有的rq做前向、后向合并(2個rq的sector連續(xù))。從調度器隊列中發(fā)送一個rq給設備驅動程序代碼如下:

cfq調度器(Completely Fair Queuing)
CFQ公平對待每個進程,給每個進程分配相同的“虛擬”時間片,在時間片內(nèi)進程可以訪問存儲器件,時間片用完后,選擇下一個進程運行。
1)cfq支持“優(yōu)先級”策略
“虛擬”時間片 = 實際訪問存儲設備時間*優(yōu)先級系數(shù),優(yōu)先級越高實際獲得的時間片越長,優(yōu)先級越低實際獲得的時間片越短。
cfqq用完時間片后,通過cfq_resor_rr_list調整cfqq在紅黑樹中的位置,由于紅黑樹key值rb_key近似等于jiffies,所以cfqq近似于按照round-roubin執(zhí)行,代碼如下。



圖7 cfqq round-roubin策略
2)cfq支持“權重策略”
多個cfqq可以歸屬于一個group,這些group按照占用存儲設備的時長(cfq中稱作vdisktime)組織在紅黑樹中。當group被調度運行結束后,cfq_group_served更新group 的vdisktime(vdisktime增長量=實際占用disk時間*權重系數(shù)),權重越高vdisktime增長的越慢,權重越低vdisktime增長的越快。
cfq優(yōu)先選擇vdisktime小的group執(zhí)行,所以權重越大,group被調度的越頻繁。
cfq_group_served更新vdisktime的代碼如下:
vfr表示在group與其他group構成的父group中,group在父group中的權重占比。
cfqg_scale_charge將group使用存儲設備的時長(以charge表示)做個虛擬轉換,本質上等價于cfqg->vdisktime = charge / vfr

bfq調度器(Budget Fair Queuing)
bfq從cfq演變過來,大部分代碼也借鑒cfq的。cfq在各個進程間平分存儲器件時間來達到公平,這樣做是有問題的,一個隨機訪問存儲設備的進程與一個順序訪問存儲設備的進程,雖然占用存儲器件的時間是一樣的,但二者訪問的數(shù)據(jù)量有很大差距,難以保證公平。
bfq通過budget(就是block sector)確保公平,不管進程占用了存儲設備多長時間,只管進程訪問存儲設備的數(shù)據(jù)量。
bfq的這種思想帶來了一個優(yōu)勢,即提高了交互式進程的響應性。因為交互式進程每次IO的數(shù)據(jù)量很少,而batch類進程數(shù)據(jù)量很大,為了確保budget公平(訪問相同數(shù)量的block sector),必須頻繁調度交互式進程運行,從而提高了交互式進程的響應性。

圖8 bfqq紅黑樹
bfq同cfq一樣,也為每個進程分配一個隊列稱作bfqq,通過時間戳維護在紅黑樹中(cfq是通過vdisktime維護在紅黑樹中)。與cfqq輪詢機制不同,bfqq紅黑樹中只有eligible狀態(tài)的bfqq才會被選擇調度。我們定義如下變量:


與cfqq權重的作用不一樣,cfqq中權重越大時間片越長,但bfqq中權重與時間片無關,與調度頻率有關,假設有3個進程P1~P3,每次訪問100個budget,權重比例P1:P2:P3 = 2:1:1,bfqq執(zhí)行效果如下(綠色表示eligible,灰色表示不可選):

表2 bffq調度
從上面執(zhí)行步驟可以看出,P權重占比50%,第一次到第三次下來,P1執(zhí)行2次,P2、P3累計執(zhí)行2次,執(zhí)行的頻率與各進程的權重占比相等。
以上3個調度器的差異對比如下:

表3 noop/cfq/bfq對比
五、總結
Block主要涉及框架和調度器兩部分,都是為了吞吐量合IO響應性設計的??蚣艽a與存儲器件緊密相關,從慢速的存儲設備到高速的存儲設備,Block框架變成了multi-queue架構,軟件、硬件的緊密結合才能把存儲器件性能發(fā)揮到最大,期待未來新存儲器件的出現(xiàn),將存儲性能再提高一個級別。調度器也越來越智能,能夠兼顧交互進程的響應性和batch類進程的吞吐量,用戶體驗在Block新框架、新調度器的支持下將會越來越好。
原文作者:內(nèi)核工匠
轉載地址:深入解析linux IO Block layer
