前言
網(wǎng)上關(guān)于BIO和塊設(shè)備讀寫流程的文章何止千萬(wàn),但是能夠讓你徹底讀懂讀明白的文章實(shí)在難找,可以說(shuō)是越讀越糊涂!
我曾經(jīng)跨過(guò)山和大海 也穿過(guò)人山人海
我曾經(jīng)問(wèn)遍整個(gè)世界 從來(lái)沒(méi)得到答案
本文用一個(gè)最簡(jiǎn)單的read(fd, buf, 4096)的代碼,分析它從開始讀到讀結(jié)束,在整個(gè)Linux系統(tǒng)里面波瀾壯闊的一生。本文涉及到的代碼如下:
#include
#include
main()
{
int fd;
char buf[4096];
sleep(30); //run ./funtion.sh to trace vfs_read of this process
fd = open("file", O_RDONLY);
read(fd, buf, 4096);
read(fd, buf, 4096);
}
本文的寫作宗旨是:絕不裝逼,一定要簡(jiǎn)單,簡(jiǎn)單,再簡(jiǎn)單!
本文適合:已經(jīng)讀了很多亂七八糟的block資料,但是沒(méi)打通脈絡(luò)的讀者;
本文不適合:完全不知道block子系統(tǒng)是什么的讀者,和完全知道block子系統(tǒng)是什么的讀者
Page cache與預(yù)讀
在Linux中,內(nèi)存充當(dāng)硬盤的page cache,所以,每次讀的時(shí)候,會(huì)先check你讀的那一部分硬盤文件數(shù)據(jù)是否在內(nèi)存命中,如果沒(méi)有命中,才會(huì)去硬盤;如果已經(jīng)命中了,就直接從內(nèi)存里面讀出來(lái)。如果是寫的話,應(yīng)用如果是以非SYNC方式寫的話,寫的數(shù)據(jù)也只是進(jìn)內(nèi)存,然后由內(nèi)核幫忙在適當(dāng)?shù)臅r(shí)機(jī)writeback進(jìn)硬盤。
代碼中有2行read(fd, buf, 4096),第1行read(fd, buf, 4096)發(fā)生的時(shí)候,顯然”file”文件中的數(shù)據(jù)都不在內(nèi)存,這個(gè)時(shí)候,要執(zhí)行真正的硬盤讀,app只想讀4096個(gè)字節(jié)(一頁(yè)),但是內(nèi)核不會(huì)只是讀一頁(yè),而是要多讀,提前讀,把用戶現(xiàn)在不讀的也先讀,因?yàn)閮?nèi)核懷疑你讀了一頁(yè),接著要連續(xù)讀,懷疑你想讀后面的。與其等你發(fā)指令,不如提前先斬后奏(存儲(chǔ)介質(zhì)執(zhí)行大塊讀比多個(gè)小塊讀要快),這個(gè)時(shí)候,它會(huì)執(zhí)行預(yù)讀,直接比如讀4頁(yè),這樣當(dāng)你后面接著讀第2-4頁(yè)的硬盤數(shù)據(jù)的時(shí)候,其實(shí)是直接命中了。
所以這個(gè)代碼路徑現(xiàn)在是 :
當(dāng)你執(zhí)行完第一個(gè)read(fd, buf, 4096)后,”file”文件的0~16KB都進(jìn)入了pagecache,同時(shí)內(nèi)核會(huì)給第2頁(yè)標(biāo)識(shí)一個(gè)PageReadahead標(biāo)記,意思就是如果app接著讀第2頁(yè),就可以預(yù)判app在做順序讀,這樣我們?cè)赼pp讀第2頁(yè)的時(shí)候,內(nèi)核可以進(jìn)一步異步預(yù)讀。
第一個(gè)read(fd,buf, 4096)之前,page cache命中情況(都不命中):
第一個(gè)read(fd,buf, 4096)之后,page cache命中情況:
我們緊接著又碰到第二個(gè)read(fd, buf, 4096),它要讀硬盤文件的第2頁(yè)內(nèi)容,這個(gè)時(shí)候,第2頁(yè)是page cache命中的,這一次的讀,由于第2頁(yè)有PageReadahead標(biāo)記,讓內(nèi)核覺(jué)得app就是在順序讀文件,內(nèi)核會(huì)執(zhí)行更加激進(jìn)的異步預(yù)讀,比如讀文件的第16KB~48KB。
所以第二個(gè)read(fd,buf, 4096)的代碼路徑現(xiàn)在是 :
第二個(gè)read(fd,buf, 4096)之前,page cache命中情況:
第二個(gè)read(fd,buf, 4096)之后,page cache命中情況:
內(nèi)存到硬盤的轉(zhuǎn)換
剛才我們提到,第一次的read(fd, buf, 4096),變成了讀硬盤里面的16KB數(shù)據(jù),到內(nèi)存的4個(gè)頁(yè)面(對(duì)應(yīng)硬盤里面文件數(shù)據(jù)的第0~16KB)。但是我們還是不知道,硬盤里面文件數(shù)據(jù)的第0~16KB在硬盤的哪些位置?我們必須把內(nèi)存的頁(yè),轉(zhuǎn)化為硬盤里面真實(shí)要讀的位置。
在Linux里面,用于描述硬盤里面要真實(shí)操作的位置與page cache的頁(yè)映射關(guān)系的數(shù)據(jù)結(jié)構(gòu)是bio。相信大家已經(jīng)見到bio一萬(wàn)次了,但是就是和真實(shí)的案例對(duì)不上。
bio的定義如下(include/linux/blk_types.h):
struct bio_vec {
struct page *bv_page;
unsigned int bv_len;
unsigned int bv_offset;
};
struct bio {
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev;
…
struct bvec_iter bi_iter;
/* Number of segments in this BIO after
* physical address coalescing is performed.
*/
unsigned int bi_phys_segments;
…
bio_end_io_t *bi_end_io;
void *bi_private;
unsigned short bi_vcnt; /* how many bio_vec's */
atomic_t bi_cnt; /* pin count */
struct bio_vec *bi_io_vec; /* the actual vec list */
…
};
它是一個(gè)描述硬盤里面的位置與page cache的頁(yè)對(duì)應(yīng)關(guān)系的數(shù)據(jù)結(jié)構(gòu),每個(gè)bio對(duì)應(yīng)的硬盤里面一塊連續(xù)的位置,每一塊硬盤里面連續(xù)的位置,可能對(duì)應(yīng)著page cache的多頁(yè),或者一頁(yè),所以它里面會(huì)有一個(gè)bio_vec *bi_io_vec的表。
我們現(xiàn)在假設(shè)2種情況
第1種情況是page_cache_sync_readahead()要讀的0~16KB數(shù)據(jù),在硬盤里面正好是順序排列的(是否順序排列,要查文件系統(tǒng),如ext3、ext4),Linux會(huì)為這一次4頁(yè)的讀,分配1個(gè)bio就足夠了,并且讓這個(gè)bio里面分配4個(gè)bi_io_vec,指向4個(gè)不同的內(nèi)存頁(yè):
第2種情況是page_cache_sync_readahead()要讀的0~16KB數(shù)據(jù),在硬盤里面正好是完全不連續(xù)的4塊 (是否順序排列,要查文件系統(tǒng),如ext3、ext4),Linux會(huì)為這一次4頁(yè)的讀,分配4個(gè)bio,并且讓這4個(gè)bio里面,每個(gè)分配1個(gè)bi_io_vec,指向4個(gè)不同的內(nèi)存頁(yè)面:
當(dāng)然你還可以有第3種情況,比如0~8KB在硬盤里面連續(xù),8~16KB不連續(xù),那可以是這樣的:
其他的情況請(qǐng)類似推理…完成這項(xiàng)工作的史詩(shī)級(jí)的代碼就是mpage_readpages()。
mpage_readpages()會(huì)間接調(diào)用ext4_get_block(),真的搞清楚0~16KB的數(shù)據(jù),在硬盤里面的擺列位置,并依據(jù)這個(gè)信息,轉(zhuǎn)化出來(lái)一個(gè)個(gè)的bio。
bio和request的三進(jìn)三出
人生,說(shuō)到最后,簡(jiǎn)單得只有生死兩個(gè)字。但由于有了命運(yùn)的浮沉,由于有了人世的冷暖,簡(jiǎn)單的過(guò)程才變得跌宕起伏,紛繁復(fù)雜。小平三落三起,最終建立了不朽的功勛。曼德拉受非人待遇在監(jiān)獄服刑數(shù)十年,終成世界公認(rèn)的領(lǐng)袖。走向自由之路不會(huì)平坦,斗爭(zhēng)就是生活。與天斗,其樂(lè)無(wú)窮;與地斗,其樂(lè)無(wú)窮;與Linux斗,痛苦無(wú)窮!
bio產(chǎn)生后,到最終的完成,同樣經(jīng)歷了三進(jìn)三出的隊(duì)列,這個(gè)過(guò)程的艱辛和痛苦,讓人欲罷不能,欲說(shuō)還休,求生不得求死不能。
這三步是:
1.原地蓄勢(shì)
把bio轉(zhuǎn)化為request,把request放入進(jìn)程本地的plug隊(duì)列;蓄勢(shì)多個(gè)request后,再進(jìn)行泄洪。
2.電梯排序
進(jìn)程本地的plug隊(duì)列的request進(jìn)入到電梯,進(jìn)行再次的合并、排序,執(zhí)行QoS的排隊(duì),之后按照QoS的結(jié)果,分發(fā)給塊設(shè)備驅(qū)動(dòng)。電梯內(nèi)部的實(shí)現(xiàn),可以有各種各樣的隊(duì)列。
3.分發(fā)執(zhí)行
電梯分發(fā)的request,被設(shè)備驅(qū)動(dòng)的request_fn()挨個(gè)取出來(lái),派發(fā)真正的硬件讀寫命令到硬盤。這個(gè)分發(fā)的隊(duì)列,一般就是我們?cè)趬K設(shè)備驅(qū)動(dòng)里面見到的request_queue了。
下面我們?cè)僖灰怀尸F(xiàn),這三進(jìn)三出。
原地蓄勢(shì)
在Linux中,每個(gè)task_struct(對(duì)應(yīng)一個(gè)進(jìn)程,或輕量級(jí)進(jìn)程——線程),會(huì)有一個(gè)plug的list。什么叫plug呢?類似于葛洲壩和三峽,先蓄水,當(dāng)app需要發(fā)多個(gè)bio請(qǐng)求的時(shí)候,比較好的辦法是先蓄勢(shì),而不是一個(gè)個(gè)單獨(dú)發(fā)給最終的硬盤。
這個(gè)類似你現(xiàn)在有10個(gè)老師,這10個(gè)老師開學(xué)的時(shí)候都接受學(xué)生報(bào)名。然后有一個(gè)大的學(xué)生隊(duì)列,如果每個(gè)老師有一個(gè)學(xué)生報(bào)名的時(shí)候,都訪問(wèn)這個(gè)唯一的學(xué)生隊(duì)列,那么這個(gè)隊(duì)列的操作會(huì)變成一個(gè)重要的鎖瓶頸:
如果我們換一個(gè)方法,讓每個(gè)老師有學(xué)生報(bào)名的時(shí)候,每天的報(bào)名的學(xué)生掛在老師自己的隊(duì)列上面,老師的隊(duì)列上面掛了很多學(xué)生后,一天之后再泄洪,掛到最終的學(xué)生隊(duì)列,則可以避免這個(gè)問(wèn)題,最終小隊(duì)列融合進(jìn)大隊(duì)列的時(shí)候控制住時(shí)序就好。
你會(huì)發(fā)現(xiàn),代碼路徑是這樣的:
read_pages()函數(shù)先把閘門拉上,然后發(fā)起一系列bio后,再通過(guò)blk_finish_plug()的調(diào)用來(lái)泄洪。
在這個(gè)蓄勢(shì)的過(guò)程中,還要完成一項(xiàng)重要的工作,就是make request(造請(qǐng)求)。這個(gè)完成“造請(qǐng)求”的史詩(shī)級(jí)的函數(shù),一般是void blk_queue_bio(struct request_queue *q, struct bio *bio),位于block/blk-core.c。
它會(huì)嘗試把bio合并進(jìn)入一個(gè)進(jìn)程本地plug list里面的一個(gè)request,如果無(wú)法合并,則造一個(gè)新的request。request里面包含一個(gè)bio的list,這個(gè)list的bio對(duì)應(yīng)的硬盤位置,最終在硬盤上是連續(xù)存放的。
下面我們假設(shè)"file"的第0~16KB在硬盤的存放位置為:
根據(jù)我們前面"內(nèi)存到硬盤的轉(zhuǎn)換"一節(jié)舉的例子,這屬于在硬盤里面完全不連續(xù)的"情況2",于是這4塊數(shù)據(jù),會(huì)被史詩(shī)級(jí)的mpage_readpages()轉(zhuǎn)化為4個(gè)bio。
當(dāng)他們進(jìn)入進(jìn)程本地的plug list的時(shí)候,由于最開始plug list為空,100顯然無(wú)法與誰(shuí)合并,這樣形成一個(gè)新的request0。
Bio1也無(wú)法合并進(jìn)request0,于是得到新的request1。
Bio2正好可以合并進(jìn)request1,于是Bio1合并進(jìn)request1。
Bio3對(duì)應(yīng)硬盤的200塊,無(wú)法合并,于是得到新的request2。
現(xiàn)在進(jìn)程本地plug list上的request排列如下:
泄洪的時(shí)候,進(jìn)程本地的plug list的request,會(huì)通過(guò)調(diào)用elevator調(diào)度算法的elevator_add_req_fn() callback函數(shù),被加入電梯的隊(duì)列。
電梯排序
當(dāng)各個(gè)進(jìn)程本地的plug list里面的request被泄洪,以排山倒海之勢(shì)進(jìn)入的,不是最終的設(shè)備驅(qū)動(dòng)(不會(huì)直接被拍死在沙灘上的),而是一個(gè)電梯排隊(duì)算法,進(jìn)行再一次的排隊(duì)。這個(gè)電梯調(diào)度,其實(shí)目的3個(gè):
進(jìn)一步的合并request
把request對(duì)硬盤的訪問(wèn)變得順序化
執(zhí)行QoS
電梯的內(nèi)部實(shí)現(xiàn)可以非常靈活,但是入口是elevator_add_req_fn(),出口是elevator_dispatch_fn()。
合并和排序都好理解,下面我們重點(diǎn)解釋QoS(服務(wù)質(zhì)量)。想象你家里的寬帶,有迅雷,有在線電影,有機(jī)頂盒看電視。
當(dāng)你只用迅雷下電影的時(shí)候,你當(dāng)然可以全速的下電影,但是當(dāng)你還看電視,在線看電影,這個(gè)時(shí)候,你可能會(huì)對(duì)迅雷限流,以保證相關(guān)電視盒電影的服務(wù)質(zhì)量。
電梯調(diào)度里面也執(zhí)行同樣的邏輯,比如CFQ調(diào)度算法,可以根據(jù)進(jìn)程的ionice,調(diào)整不同進(jìn)程訪問(wèn)硬盤的時(shí)候的優(yōu)先級(jí)。比如,如下2個(gè)優(yōu)先級(jí)不同的dd
# ionice-c 2 -n 0 cat /dev/sda > /dev/null&
# ionice -c 2 -n 7 cat /dev/sda >/dev/null&
最終訪問(wèn)硬盤的速度是不一樣的,一個(gè)371M,一個(gè)只有72M。
所以當(dāng)泄洪開始,漫江碧透,百舸爭(zhēng)流,誰(shuí)能到中流擊水,浪遏飛舟?QoS是一個(gè)關(guān)于一將功成萬(wàn)骨枯的故事。
目前常用的IO電梯調(diào)度算法有:cfq, noop, deadline。詳細(xì)的區(qū)別不是本文的重點(diǎn),建議閱讀《劉正元:Linux 通用塊層之DeadLine IO調(diào)度器》從了解deadline的實(shí)現(xiàn)開始。
分發(fā)執(zhí)行
到了最后要交差的時(shí)刻了,設(shè)備驅(qū)動(dòng)的request_fn()通過(guò)調(diào)用電梯調(diào)度算法的elevator_dispatch_fn()取出經(jīng)過(guò)QoS排序后的request并發(fā)命令給最終的存儲(chǔ)設(shè)備執(zhí)行I/O動(dòng)作。
static void xxx_request_fn(struct request_queue *q)
{
struct request *req;
struct bio *bio;
while ((req = blk_peek_request(q)) != NULL) {
struct xxx_disk_dev *dev = req->rq_disk->private_data;
if (req->cmd_type != REQ_TYPE_FS) {
printk (KERN_NOTICE "Skip non-fs request ");
blk_start_request(req);
__blk_end_request_all(req, -EIO);
continue;
}
blk_start_request(req);
__rq_for_each_bio(bio, req)
xxx_xfer_bio(dev, bio);
}
}
request_fn()只是派發(fā)讀寫事件和命令,最終的完成一般是在另外一個(gè)上下文,而不是發(fā)起IO的進(jìn)程。request處理完成后,探知到IO完成的上下文會(huì)以blk_end_request()的形式,通知等待IO請(qǐng)求完成的本進(jìn)程。主動(dòng)發(fā)起IO的進(jìn)程的代碼序列一般是:
submit_bio()
io_schedule(),放棄CPU。
blk_end_request()一般把io_schedule()后放棄CPU的進(jìn)程喚醒。io_schedule()的這段等待時(shí)間,會(huì)計(jì)算到進(jìn)程的iowait時(shí)間上,詳見:《朱輝(茶水):Linux Kernel iowait 時(shí)間的代碼原理》。
用Ftrace抓所有流程
本文所涉及到的所有流程,都可以用ftrace跟蹤到。這樣可以了解更多更深刻的細(xì)節(jié)。
char buf[4096];
sleep(30); //run ./funtion.sh to trace vfs_read of this process
fd = open("file", O_RDONLY);
read(fd, buf, 4096);
在上述代碼的中間,我特意留下了30秒的延時(shí),在這個(gè)延時(shí)的空擋,你可以啟動(dòng)如下的腳本,來(lái)對(duì)整個(gè)過(guò)程進(jìn)行function graph的trace,抓取進(jìn)程對(duì)vfs_read()開始后的調(diào)用棧:
#!/bin/bash
debugfs=/sys/kernel/debug
echo nop > $debugfs/tracing/current_tracer
echo 0 > $debugfs/tracing/tracing_on
echo `pidof read` > $debugfs/tracing/set_ftrace_pid
echo function_graph > $debugfs/tracing/current_tracer
echo vfs_read > $debugfs/tracing/set_graph_function
echo 1 > $debugfs/tracing/tracing_on
筆者也是通過(guò)ftrace的結(jié)果,用vim打開,逐句分析的。關(guān)于ftrace使用的詳細(xì)方法,可以閱讀《宋寶華:關(guān)于Ftrace的一個(gè)完整案例》。
最后的話
本文描述的是主干,許多的細(xì)節(jié)和代碼分支沒(méi)有涉及,因?yàn)樵诒疚拿枋鎏嗟姆种В瑫?huì)讓讀者抓不住主干。很多分支都沒(méi)有介紹,比如unplug的泄洪,除了可以人為的blk_finish_plug()泄洪外,也會(huì)發(fā)生plug隊(duì)列較滿的時(shí)候,以及進(jìn)程睡眠schedule()的時(shí)候的自動(dòng)泄洪。另外,關(guān)于寫,后面的三進(jìn)三出的過(guò)程,基本與讀類似,但是寫有個(gè)page cache堆積和writeback的啟動(dòng)機(jī)制,是read所沒(méi)有的。
審核編輯:湯梓紅
-
硬盤
+關(guān)注
關(guān)注
3文章
1339瀏覽量
58486 -
Linux
+關(guān)注
關(guān)注
87文章
11512瀏覽量
213907 -
內(nèi)存
+關(guān)注
關(guān)注
8文章
3125瀏覽量
75294 -
Linux系統(tǒng)
+關(guān)注
關(guān)注
4文章
605瀏覽量
28638 -
代碼
+關(guān)注
關(guān)注
30文章
4900瀏覽量
70794
原文標(biāo)題:宋寶華:Linux文件讀寫(BIO)波瀾壯闊的一生
文章出處:【微信號(hào):LinuxDev,微信公眾號(hào):Linux閱碼場(chǎng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
Linux文件系統(tǒng)啟動(dòng)流程
Linux文件系統(tǒng)課程
文件I/O編程之文件讀寫及上鎖實(shí)驗(yàn)

《Linux設(shè)備驅(qū)動(dòng)開發(fā)詳解》第5章、Linux文件系統(tǒng)與設(shè)備文件系統(tǒng)

linux文件系統(tǒng)基礎(chǔ)
可以了解的Linux 文件系統(tǒng)結(jié)構(gòu)

需要了解的Linux內(nèi)核讀寫文件
Linux系統(tǒng)日志文件中的JFS文件系統(tǒng)

Linux文件系統(tǒng)解析

嵌入式linux系統(tǒng)中常用的文件系統(tǒng)

Linux I/O 接口的類型及處理流程

Linux的文件系統(tǒng)特點(diǎn)

評(píng)論