Linux下馆匿,I/O處理的層次可分為4層:
- 系統(tǒng)調(diào)用層鸦概,應(yīng)用程序使用系統(tǒng)調(diào)用指定讀寫哪個(gè)文件,文件偏移是多少
- 文件系統(tǒng)層堕伪,寫文件時(shí)將用戶態(tài)中的buffer拷貝到內(nèi)核態(tài)下,并由cache緩存該部分?jǐn)?shù)據(jù)
- 塊層栗菜,管理塊設(shè)備I/O隊(duì)列欠雌,對(duì)I/O請(qǐng)求進(jìn)行合并、排序
- 設(shè)備層疙筹,通過(guò)DMA與內(nèi)存直接交互富俄,將數(shù)據(jù)寫到磁盤
下圖清晰地說(shuō)明了Linux I/O層次結(jié)構(gòu):
寫文件過(guò)程
寫文件的過(guò)程包含了讀的過(guò)程,文件先從磁盤載入內(nèi)存而咆,存到cache中霍比,磁盤內(nèi)容與物理內(nèi)存頁(yè)間建立起映射關(guān)系。用于寫文件的write函數(shù)的聲明如下:
ssize_t write(int fd, const void *buf, size_t count);
其中fd對(duì)應(yīng)進(jìn)程的file結(jié)構(gòu)暴备, buf指向?qū)懭氲臄?shù)據(jù)悠瞬。內(nèi)核從cache中找出與被寫文件相應(yīng)的物理頁(yè),write決定寫內(nèi)存的第幾個(gè)頁(yè)面涯捻,例如"echo 1 > a.out"(底層調(diào)用write)寫入的是a.out文件的第0個(gè)位置浅妆,write將寫相應(yīng)內(nèi)存的第一頁(yè)。
write函數(shù)修改內(nèi)存內(nèi)容之后障癌,相應(yīng)的內(nèi)存頁(yè)凌外、inode被標(biāo)記為dirty,此時(shí)write函數(shù)返回涛浙。注意至此尚未往磁盤寫數(shù)據(jù)康辑,只是cache中的內(nèi)容被修改摄欲。
那什么時(shí)候內(nèi)存中的內(nèi)容會(huì)刷到磁盤中呢?
把臟數(shù)據(jù)刷到磁盤的工作由內(nèi)核線程flush完成疮薇,flush搜尋內(nèi)存中的臟數(shù)據(jù)蒿涎,按設(shè)定將臟數(shù)據(jù)寫到磁盤,我們可以通過(guò)sysctl命令查看惦辛、設(shè)定flush刷臟數(shù)據(jù)的策略:
linux # sysctl -a | grep centi
vm.dirty_writeback_centisecs = 500
vm.dirty_expire_centisecs = 3000
linux # sysctl -a | grep background_ratio
vm.dirty_background_ratio = 10
以上數(shù)值單位為1/100秒,“dirty_writeback_centisecs = 500”指示flush每隔5秒執(zhí)行一次仓手,“dirty_expire_centisecs = 3000” 指示內(nèi)存中駐留30秒以上的臟數(shù)據(jù)將由flush在下一次執(zhí)行時(shí)寫入磁盤胖齐,“dirty_background_ratio = 10”指示若臟頁(yè)占總物理內(nèi)存10%以上,則觸發(fā)flush把臟數(shù)據(jù)寫回磁盤嗽冒。
flush找出了需要寫回磁盤的臟數(shù)據(jù)呀伙,那存儲(chǔ)臟數(shù)據(jù)的物理頁(yè)又與磁盤的哪些扇區(qū)對(duì)應(yīng)呢?
物理頁(yè)與扇區(qū)的對(duì)應(yīng)關(guān)系由文件系統(tǒng)定義添坊,文件系統(tǒng)定義了一個(gè)內(nèi)存頁(yè)(4KB)與多少個(gè)塊對(duì)應(yīng)剿另,對(duì)應(yīng)關(guān)系在格式化磁盤時(shí)設(shè)定,運(yùn)行時(shí)由buffer_head保存對(duì)應(yīng)關(guān)系:
linux # cat /proc/slabinfo | grep buffer_head
buffer_head 12253 12284 104 37 1 : tunables 120 60 8 : slabdata 332 332 0
文件系統(tǒng)層告知塊I/O層寫哪個(gè)設(shè)備贬蛙,具體哪個(gè)塊雨女,執(zhí)行以下命令后,我們可以在/var/log/messages中看到文件系統(tǒng)層下發(fā)到塊層的讀寫請(qǐng)求:
linux # echo 1 > /proc/sys/vm/block_dump
linux # tail -n 3 /var/log/messages
Aug 7 00:50:31 linux-q62c kernel: [ 7523.602144] bash(5466): READ block 1095792 on sda1
Aug 7 00:50:31 linux-q62c kernel: [ 7523.622857] bash(5466): dirtied inode 27874 (tail) on sda1
Aug 7 00:50:31 linux-q62c kernel: [ 7523.623213] tail(5466): READ block 1095824 on sda1
塊I/O層使用struct bio記錄文件系統(tǒng)層下發(fā)的I/O請(qǐng)求阳准,bio中主要保存了需要往磁盤刷數(shù)據(jù)的物理頁(yè)信息氛堕,以及對(duì)應(yīng)磁盤上的扇區(qū)信息。
塊I/O層為每一個(gè)磁盤設(shè)備維護(hù)了一條I/O請(qǐng)求隊(duì)列野蝇,請(qǐng)求隊(duì)列在內(nèi)核中由struct request_queue表示讼稚。每一個(gè)讀或?qū)懻?qǐng)求都需經(jīng)過(guò)submit_bio函數(shù)處理,submit_bio將讀寫請(qǐng)求放入相應(yīng)I/O請(qǐng)求隊(duì)列中绕沈。該層起到最主要的作用就是對(duì)I/O請(qǐng)求進(jìn)行合并和排序锐想,這樣減少了實(shí)際的磁盤讀寫次數(shù)和尋道時(shí)間,達(dá)到優(yōu)化磁盤讀寫性能的目的乍狐。
使用crash解析vmcore文件赠摇,執(zhí)行"dev -d"命令,可以看到塊設(shè)備請(qǐng)求隊(duì)列的相關(guān)信息:
crash > dev -d
MAJOR GENDISK NAME REQUEST QUEUE TOTAL ASYNC SYNC DRV
8 0xffff880119e85800 sda 0xffff88011a6a6948 10 0 0 10
8 0xffff880119474800 sdb 0xffff8801195632d0 0 0 0 0
執(zhí)行"struct request_queue 0xffff88011a6a6948"澜躺,可對(duì)以上sda設(shè)備相應(yīng)的request_queue請(qǐng)求隊(duì)列結(jié)構(gòu)進(jìn)行解析蝉稳。
執(zhí)行以下命令,可以查看sda設(shè)備的請(qǐng)求隊(duì)列大芯虮伞:
linux # cat /sys/block/sda/queue/nr_requests
128
如何對(duì)I/O請(qǐng)求進(jìn)行合并耘戚、排序,那就是I/O調(diào)度算法完成的工作操漠,Linux支持多種I/O調(diào)度算法收津,通過(guò)以下命令可以查看:
linux # cat /sys/block/sda/queue/scheduler
noop anticipatory deadline [cfq]
塊I/O層的另一個(gè)作用就是對(duì)I/O讀寫情況進(jìn)行統(tǒng)計(jì)饿这,執(zhí)行iostat命令,看到的就是該層提供的統(tǒng)計(jì)信息:
linux # iostat -x -k -d 1
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await svctm %util
sda 0.00 9915.00 1.00 90.00 4.00 34360.00 755.25 11.79 120.57 6.33 57.60
其中rrqm/s撞秋、wrqm/s分別指示了每秒寫請(qǐng)求长捧、讀請(qǐng)求的合并次數(shù)。
task_io_account_read函數(shù)用于統(tǒng)計(jì)各個(gè)進(jìn)程發(fā)起的讀請(qǐng)求量吻贿, 由該函數(shù)得到的是進(jìn)程讀請(qǐng)求量的準(zhǔn)確值串结。而對(duì)于寫請(qǐng)求,由于數(shù)據(jù)寫入cache后write調(diào)用就返回舅列,因而在內(nèi)核的層面無(wú)法統(tǒng)計(jì)到一個(gè)進(jìn)程發(fā)起的準(zhǔn)確寫請(qǐng)求量肌割,讀時(shí)進(jìn)程會(huì)等buff可用,而寫則寫入cache后返回帐要,讀是同步的把敞,寫卻不一定同步,這是讀寫實(shí)現(xiàn)上的最大區(qū)別榨惠。
再往下就是設(shè)備層奋早,設(shè)備從隊(duì)列中取出I/O請(qǐng)求,scsi的scsi_request_fn函數(shù)就是完成取請(qǐng)求并處理的任務(wù)赠橙。scsi層最終將處理請(qǐng)求轉(zhuǎn)化為指令耽装,指令下發(fā)后進(jìn)行DMA(direct memory access)映射,將內(nèi)存的部分cache映射到DMA简烤,這樣設(shè)備繞過(guò)cpu直接操作主存剂邮。
設(shè)備層完成內(nèi)存數(shù)據(jù)到磁盤拷貝后,該消息將一層層上報(bào)横侦,最后內(nèi)核去除原臟頁(yè)的dirty位標(biāo)志挥萌。
以上為寫磁盤的大致實(shí)現(xiàn)過(guò)程,對(duì)于讀磁盤枉侧,內(nèi)核首先在緩存中查找對(duì)應(yīng)內(nèi)容引瀑,若命中則不會(huì)進(jìn)行磁盤操作。若進(jìn)程讀取一個(gè)字節(jié)的數(shù)據(jù)榨馁,內(nèi)核不會(huì)僅僅返回一個(gè)字節(jié)憨栽,其以頁(yè)面為單位(4KB),最少返回一個(gè)頁(yè)面的數(shù)據(jù)翼虫。另外屑柔,內(nèi)核會(huì)預(yù)讀磁盤數(shù)據(jù),執(zhí)行以下命令可以看到能夠預(yù)讀的最大數(shù)據(jù)量(以KB為單位):
linux # cat /sys/block/sda/queue/read_ahead_kb
512
下面我們通過(guò)一段systemtap代碼珍剑,了解內(nèi)核的預(yù)讀機(jī)制:
//test.stp
probe kernel.function("submit_bio") {
if(execname() == "dd" && __bio_ino($bio) == 5234)
{
printf("inode %d %s on %s %d bytes start %d\n",
__bio_ino($bio),
bio_rw_str($bio),
__bio_devname($bio),
$bio->bi_size,
$bio->bi_sector)
}
}
以上代碼指示當(dāng)dd命令讀寫inode號(hào)為5234的文件掸宛、經(jīng)過(guò)內(nèi)核函數(shù)submit_bio時(shí),輸出inode號(hào)招拙、操作方式(讀或?qū)?唧瘾、文件所在設(shè)備名措译、讀寫大小、扇區(qū)號(hào)信息饰序。執(zhí)行以下代碼安裝探測(cè)模塊:
stap test.stp &
之后我們使用dd命令讀取inode號(hào)為5234的文件(可通過(guò)stat命令取得文件inode號(hào)):
dd if=airport.txt of=/dev/null bs=1 count=10000000
以上命令故意將bs設(shè)為1领虹,即每次讀取一個(gè)字節(jié),以此觀察內(nèi)核預(yù)讀機(jī)制求豫。執(zhí)行該命令的過(guò)程中塌衰,我們?cè)诮K端中可以看到以下輸出:
inode 5234 R on sda2 16384 bytes start 70474248
inode 5234 R on sda2 32768 bytes start 70474280
inode 5234 R on sda2 32768 bytes start 70474352
inode 5234 R on sda2 131072 bytes start 70474416
inode 5234 R on sda2 262144 bytes start 70474672
inode 5234 R on sda2 524288 bytes start 70475184
由以上輸出可知,預(yù)讀從16384字節(jié)(16KB)逐漸增大蝠嘉,最后變?yōu)?24288字節(jié)(512KB)猾蒂,可見(jiàn)內(nèi)核會(huì)根據(jù)讀的情況動(dòng)態(tài)地調(diào)整預(yù)讀的數(shù)據(jù)量。
由于讀是晨、寫磁盤均要經(jīng)過(guò)submit_bio函數(shù)處理,submit_bio之后讀舔箭、寫的底層實(shí)現(xiàn)大致相同罩缴。
直接I/O
當(dāng)我們以O(shè)_DIRECT標(biāo)志調(diào)用open函數(shù)打開(kāi)文件時(shí),后續(xù)針對(duì)該文件的read层扶、write操作都將以直接I/O(direct I/O)的方式完成箫章;對(duì)于裸設(shè)備,I/O方式也為直接I/O镜会。
直接I/O跳過(guò)了文件系統(tǒng)這一層檬寂,但塊層仍發(fā)揮作用,其將內(nèi)存頁(yè)與磁盤扇區(qū)對(duì)應(yīng)上戳表,這時(shí)不再是建立cache到DMA映射桶至,而是進(jìn)程的buffer映射到DMA。進(jìn)行直接I/O時(shí)要求讀寫一個(gè)扇區(qū)(512bytes)的整數(shù)倍匾旭,否則對(duì)于非整數(shù)倍的部分镣屹,將以帶cache的方式進(jìn)行讀寫。
使用直接I/O价涝,寫磁盤少了用戶態(tài)到內(nèi)核態(tài)的拷貝過(guò)程女蜈,這提升了寫磁盤的效率,也是直接I/O的作用所在色瘩。而對(duì)于讀操作伪窖,第一次直接I/O將比帶cache的方式快,但因帶cache方式后續(xù)再讀時(shí)將從cache中讀居兆,因而后續(xù)的讀將比直接I/O快覆山。有些數(shù)據(jù)庫(kù)使用直接I/O,同時(shí)實(shí)現(xiàn)了自己的cache方式史辙。
異步I/O
Linux下有兩種異步I/O(asynchronous I/O)方式汹买,一種是aio_read/aio_write庫(kù)函數(shù)調(diào)用佩伤,其實(shí)現(xiàn)方式為純用戶態(tài)的實(shí)現(xiàn),依靠多線程晦毙,主線程將I/O下發(fā)到專門處理I/O的線程生巡,以此達(dá)到主線程異步的目的。
另一種是io_submit见妒,該函數(shù)是內(nèi)核提供的系統(tǒng)調(diào)用孤荣,使用io_submit也需要指定文件的打開(kāi)方式為O_DIRECT,并且讀寫需按扇區(qū)對(duì)齊须揣。