一文解析block io生命歷程
作為存儲(chǔ)業(yè)務(wù)的一個(gè)重要組成部分,block IO是非易失存儲(chǔ)的唯一路徑,它的生命歷程每個(gè)階段都直接關(guān)乎我們手機(jī)的性能、功耗、甚至壽命。本文試圖通過block IO的產(chǎn)生、調(diào)度、下發(fā)、返回的4個(gè)階段,闡述一個(gè)block IO的生命歷程。
一、什么是塊設(shè)備和塊設(shè)備層

從計(jì)算機(jī)誕生開始,就有了IO設(shè)備,IO設(shè)備大致分為兩類,塊設(shè)備和字符設(shè)備,塊設(shè)備的2個(gè)重要特性就是:塊存儲(chǔ)和可尋址。而塊設(shè)備層,就是通過組織管理,使得向塊設(shè)備下發(fā)的請求能夠高效合理的完成的一種軟件邏輯層。如上圖所示,在傳統(tǒng)的磁盤結(jié)構(gòu)中,減少吊桿/磁頭的移動(dòng)(機(jī)械動(dòng)作),容易找到目標(biāo)地址,就能提升IO性能。
塊設(shè)備層在整個(gè)存儲(chǔ)棧中的位置如下圖所示,上承文件系統(tǒng),下接具體的塊設(shè)備驅(qū)動(dòng)(UFS,EMMC驅(qū)動(dòng)):

通過blktrace這個(gè)開源工具可以用來分析IO軌跡和性能,從AQGP開始創(chuàng)建線程的plug,再到后面的AQM完成了線程內(nèi)部plug的merge合并,最后IUDC完成了線程內(nèi)部plug的下發(fā)和返回。

二、??block IO的產(chǎn)生
大到手機(jī)里面每一個(gè)應(yīng)用程序的打開,小到很多人學(xué)生時(shí)代寫過的一個(gè)C語言程序,都會(huì)伴隨block IO的產(chǎn)生。究其本質(zhì),只要調(diào)用了libc庫中的open, read, close,write, fsync, sync這些庫函數(shù),都可能產(chǎn)生blockIO。
1.?用戶態(tài)常用文件操作


2.文件系統(tǒng)IO~預(yù)讀

3.文件系統(tǒng)IO~臟頁回寫

4.文件系統(tǒng)IO~fsync & sync

5.文件系統(tǒng)IO~dm設(shè)備寫
通過下面的命令獲取dm設(shè)備(253:26)的軌跡:
./blktrace -d /dev/block/dm-26 -o - |./blkparse -i -

由于dm設(shè)備到真實(shí)的物理設(shè)備,有一層映射,對于同一個(gè)邏輯地址8898352通過下面的命令獲取針對這個(gè)邏輯地址的block IO在其映射的物理設(shè)備(259:41)的軌跡,在物理設(shè)備中,塊地址經(jīng)過remap從8898352變成了33294128。
./blktrace -d /dev/block/sdc57 -o - | ./blkparse -i -

這里以verity類型的dm設(shè)備為例,其block io的產(chǎn)生路徑:

6.direct-IO的產(chǎn)生路徑
上面的預(yù)讀,臟頁回寫,sync操作,都是經(jīng)過了page cache,有些跑分軟件如androbench不會(huì)經(jīng)過page cache,更關(guān)注直接的底層存儲(chǔ)性能,會(huì)采用direct-IO的方式。下圖是direct-IO的block IO產(chǎn)生路徑。

【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ??


三、block IO的調(diào)度
1.IO調(diào)度整體框架
調(diào)度在我們?nèi)粘I钪袝?huì)經(jīng)常遇到,如電梯,或者打車司機(jī)派單拼車,錯(cuò)峰吃飯,錯(cuò)峰上下班等,都是為了更好的整體性能和能耗。


前面列舉了一些block IO的產(chǎn)生場景,當(dāng)這些IO產(chǎn)生后,為了更好的整體性能和能耗,它們也需要合適的調(diào)度機(jī)制。從IO產(chǎn)生后,經(jīng)過軟隊(duì)列,調(diào)度器,硬隊(duì)列,最終完成派發(fā)。

2.關(guān)鍵數(shù)據(jù)結(jié)構(gòu)bio,request,page,sector的關(guān)系
一個(gè)或多個(gè)bio最后合并為一個(gè)request,每個(gè)request作為面向存儲(chǔ)器件驅(qū)動(dòng)的直接數(shù)據(jù)結(jié)構(gòu),里面攜帶了內(nèi)存頁虛擬地址和存儲(chǔ)介質(zhì)邏輯地址,它是易失介質(zhì)和非易失介質(zhì)進(jìn)行數(shù)據(jù)交互的橋梁。
每個(gè)bio里面包含了一個(gè)bio_vec數(shù)組,每個(gè)數(shù)組元素指向一個(gè)內(nèi)存page。
每個(gè)bio里面包含了一個(gè)bvec_iter,包含了這個(gè)io指向的存儲(chǔ)介質(zhì)的sector。
內(nèi)存page沒有連續(xù)的要求,但是存儲(chǔ)介質(zhì)的sector必須是首尾相連的,因?yàn)樵隍?qū)動(dòng)代碼中,sglist可以包含多個(gè)離散點(diǎn),而存儲(chǔ)介質(zhì)中的sector地址不連續(xù),那么就會(huì)導(dǎo)致磁頭反復(fù)調(diào)整,極大降低性能。而sglist由于是內(nèi)存總線尋址訪問,不存在性能的問題。在flash介質(zhì),雖然不涉及磁頭調(diào)整,但如果不連續(xù),編程速度也會(huì)大大降低。

3.bio -> request
一個(gè)bio經(jīng)歷split,plug merge,電梯merge,最后獲取merge到一個(gè)已有的request或者獲取一個(gè)全新的request。

4.block IO所在dev的remap
為了更好的理解block IO的remap,merge操作,這里說一下塊設(shè)備和分區(qū)表的概念。每個(gè)設(shè)備(比如LU0,LU1)都有一個(gè)gendisk的結(jié)構(gòu)體,但是一個(gè)LU(Logical Unit)經(jīng)常會(huì)被分割為多個(gè)分區(qū)。gendisk包含了這個(gè)設(shè)備的分區(qū)表,對于每個(gè)被分割的分區(qū),都可以獨(dú)立掛載自己的文件系統(tǒng),并在文件系統(tǒng)內(nèi)從0開始尋址。當(dāng)形成IO下發(fā)到器件時(shí),由于對于器件內(nèi)部的地址管理是以LU為單位,因此,就需要通過找到先找到改分區(qū)在分區(qū)表中的偏移,再加上文件系統(tǒng)內(nèi)部的偏移,才構(gòu)成面向LU的尋址邏輯地址。


5.bio的split
一個(gè)bio有自己可以承受著的最大數(shù)據(jù)量,如果超過,則會(huì)被拆分,下圖是512KB為邊界的一個(gè)拆分示意圖。

6.??bio的merge
bio會(huì)嘗試在本線程自身plug中merge一次,如果沒有merge成功,則繼續(xù)嘗試本隊(duì)列對應(yīng)的電梯隊(duì)列里面進(jìn)行merge,對于mq(multiqueue),也可以在soft-queue里面嘗試merge。

7.線程的plug和unplug
Merge和plug都是蓄流,減少請求向存儲(chǔ)器件下發(fā)的頻率,plug的等待也會(huì)增加merge的機(jī)會(huì),結(jié)伴而行才能提升整體IO性能。在schedule, io_schedule,或線程顯性調(diào)用blk_finish_plug的時(shí)候,才會(huì)開閘。

8.??電梯調(diào)度算法
每個(gè)隊(duì)列可以配置一個(gè)io scheduler,即IO調(diào)度器,常見的有noop, deadline, cfq等,電梯調(diào)度進(jìn)一步把request請求進(jìn)行合并和排序,根據(jù)所選擇的算法(根據(jù)時(shí)間片,進(jìn)程優(yōu)先級,同步異步等因素),決定下一個(gè)dispatch的request請求。

9.??queues之間的關(guān)系
Linux塊設(shè)備層已逐步切換到multiqueue , Linux5.0以后單隊(duì)列代碼已被完全移除。multiqueue核心思路是為每個(gè)CPU分配一個(gè)軟件隊(duì)列,為存儲(chǔ)設(shè)備的每個(gè)硬件隊(duì)列分配一個(gè)硬件派發(fā)隊(duì)列,大大減少鎖競爭。在整個(gè)block IO的生命歷程中,有3個(gè)常見的隊(duì)列類型,分別是: request_queue, blk_mq_ctx, blk_mq_hw_ctx。每個(gè)塊設(shè)備有1個(gè)request_queue,包含設(shè)備相關(guān)的隊(duì)列屬性,可以理解為隊(duì)列大管家。

每個(gè)塊設(shè)備有1個(gè)或者多個(gè)硬隊(duì)列,用于面向底層驅(qū)動(dòng),如tag的管理。request_queue,blk_mq_ctx,blk_mq_hw_ctx之間可以相互遍歷。


四、?block IO的下發(fā)
1.生產(chǎn)者和消費(fèi)者模型
經(jīng)歷過漫長的產(chǎn)生,調(diào)度環(huán)節(jié),一個(gè)request終于開始向器件下發(fā)了。這里以常見的生產(chǎn)者消費(fèi)者模型抽象一下,每個(gè)設(shè)備都有自己的生產(chǎn)者隊(duì)列(電梯隊(duì)列,software queue),但消費(fèi)者隊(duì)列(hardwardqueue)卻可能是共享的,在單硬件隊(duì)列場景中(當(dāng)前UFS,eMMC)。

2. scsi子系統(tǒng)和ufs設(shè)備
ufs設(shè)備采用了scsi協(xié)議定義的通用的命令集(讀,寫,unmap等),以及scsi子系統(tǒng)通用的異常處理等各種流程,所以ufs驅(qū)動(dòng)在初始化后注冊到scsi子系統(tǒng)里面,作為一個(gè)scsi設(shè)備。

前面看到的sdc,sdc1,sdc2就是在上面的sd_probe函數(shù)中完成的。

sd_probe中同時(shí)也會(huì)解析出這個(gè)設(shè)備對應(yīng)的分區(qū)表,并且把這個(gè)設(shè)備對應(yīng)的每個(gè)分區(qū)添加到塊設(shè)備里面。
3. scsi設(shè)備的關(guān)鍵結(jié)構(gòu)
每個(gè)scsi_device共用一個(gè)host,通過這個(gè)host可以找到所有的scsi設(shè)備。
???Ufs注冊的scsi設(shè)備scsi_device中,所有的scsi_device共用一個(gè)hostdata,即ufs_hba。
不同scsi_device有自己的request_queue。
另外1個(gè)scsi_device對應(yīng)1個(gè)LU,如下圖的W-LUN和LUN屬于不同的scsi_device。
以某平臺(tái)的6個(gè)scsi_device(3個(gè)LUN+3個(gè)W-LUN)為例,其數(shù)據(jù)結(jié)構(gòu)對應(yīng)關(guān)系:


4. IO的獲取和下發(fā)
作為消費(fèi)者,從生產(chǎn)者中獲取request請求,這個(gè)獲取的過程會(huì)有個(gè)優(yōu)先級排序,先從哪個(gè)鏈表里面獲取,取決于不同的策略。

5. mmc和ufs底層驅(qū)動(dòng)對接
每個(gè)塊設(shè)備在生成時(shí),會(huì)設(shè)置自己的request_queue及其屬性,回調(diào)函數(shù),比如mmc和ufs設(shè)備分別設(shè)定了mmc_request_fn和scsi_request_fn作為這個(gè)request_queue的request_fn,從而實(shí)現(xiàn)了block層和設(shè)備驅(qū)動(dòng)層的解耦。

6. scsi_device busy等異常路徑處理
請求進(jìn)入到scsi層時(shí),會(huì)對scsi target和scsi host的各種狀態(tài)進(jìn)行判斷,檢查其是否滿足下發(fā)的條件。當(dāng)出現(xiàn)IO出現(xiàn)異常時(shí),從block層的timeout回調(diào)開始,調(diào)用到scsi層,進(jìn)一步再調(diào)到ufs驅(qū)動(dòng)層的異常處理函數(shù),如ufshcd_abort,ufshcd_eh_device_reset_handler。

五、?block IO的返回
1.?控制器的硬中斷
存儲(chǔ)器件處理完request請求后,由controller向GIC上報(bào)中斷,進(jìn)入中斷上半部處理,再到softirq的raise,整體流程如下。有些request請求指定了某個(gè)cpu,這里會(huì)涉及當(dāng)前上半部接受中斷cpu和其指定cpu之間的cache共享問題,如果共享,即便兩者不同,也會(huì)在當(dāng)前上半部接受所在cpu觸發(fā)當(dāng)前cpu的softirq,主要是考慮性能問題。

2. 軟中斷

3.??文件系統(tǒng)回調(diào)
每個(gè)進(jìn)程在下發(fā)IO請求后,會(huì)把自己置于wait隊(duì)列,當(dāng)IO請求返回時(shí),通過自下而上的層層回調(diào),最后調(diào)到wait_on_page_locked把當(dāng)前page的waiter進(jìn)程喚醒,從而完成了整個(gè)block IO的生命歷程。

六、總結(jié)
性能問題中經(jīng)常會(huì)遇到某個(gè)進(jìn)程iowait時(shí)間過長的問題,那么這個(gè)時(shí)間的究竟是怎么統(tǒng)計(jì)的,涵蓋了哪個(gè)范圍?從cpu角度看,就是該進(jìn)程被dequeue到被重新enqueue的時(shí)間范圍。從block IO角度看,就是涵蓋了該進(jìn)程所發(fā)起的整個(gè)block IO的生命歷程的時(shí)間范圍。通過這個(gè)blockIO的4個(gè)生命歷程階段,可以進(jìn)一步細(xì)化了解iowait這種耗時(shí)的分布。

原文作者:內(nèi)核工匠
