kernel笔记——块I/O
Linux下,I/O處理的層次可分為4層:
? 1. 系統(tǒng)調(diào)用層,應用程序使用系統(tǒng)調(diào)用指定讀寫哪個文件,文件偏移是多少
?2. 文件系統(tǒng)層,寫文件時將用戶態(tài)中的buffer拷貝到內(nèi)核態(tài)下,并由cache緩存該部分數(shù)據(jù)
? ? ? ? 3. 塊層,管理塊設備I/O隊列,對I/O請求進行合并、排序
? ? ? ? 4. 設備層,通過DMA與內(nèi)存直接交互,將數(shù)據(jù)寫到磁盤
?
下圖清晰地說明了Linux I/O層次結構:
?
寫文件過程
寫文件的過程包含了讀的過程,文件先從磁盤載入內(nèi)存,存到cache中,磁盤內(nèi)容與物理內(nèi)存頁間建立起映射關系。用于寫文件的write函數(shù)的聲明如下:
ssize_t write(int fd, const void *buf, size_t count);其中fd對應進程的file結構, buf指向寫入的數(shù)據(jù)。內(nèi)核從cache中找出與被寫文件相應的物理頁,write決定寫內(nèi)存的第幾個頁面,例如"echo 1 > a.out"(底層調(diào)用write)寫入的是a.out文件的第0個位置,write將寫相應內(nèi)存的第一頁。
?
write函數(shù)修改內(nèi)存內(nèi)容之后,相應的內(nèi)存頁、inode被標記為dirty,此時write函數(shù)返回。注意至此尚未往磁盤寫數(shù)據(jù),只是cache中的內(nèi)容被修改。
?
那什么時候內(nèi)存中的內(nèi)容會刷到磁盤中呢?
把臟數(shù)據(jù)刷到磁盤的工作由內(nèi)核線程flush完成,flush搜尋內(nèi)存中的臟數(shù)據(jù),按設定將臟數(shù)據(jù)寫到磁盤,我們可以通過sysctl命令查看、設定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í)行時寫入磁盤,“dirty_background_ratio = 10”指示若臟頁占總物理內(nèi)存10%以上,則觸發(fā)flush把臟數(shù)據(jù)寫回磁盤。
?
flush找出了需要寫回磁盤的臟數(shù)據(jù),那存儲臟數(shù)據(jù)的物理頁又與磁盤的哪些扇區(qū)對應呢?
物理頁與扇區(qū)的對應關系由文件系統(tǒng)定義,文件系統(tǒng)定義了一個內(nèi)存頁(4KB)與多少個塊對應,對應關系在格式化磁盤時設定,運行時由buffer_head保存對應關系:
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層寫哪個設備,具體哪個塊,執(zhí)行以下命令后,我們可以在/var/log/messages中看到文件系統(tǒng)層下發(fā)到塊層的讀寫請求:
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請求,bio中主要保存了需要往磁盤刷數(shù)據(jù)的物理頁信息,以及對應磁盤上的扇區(qū)信息。
?
塊I/O層為每一個磁盤設備維護了一條I/O請求隊列,請求隊列在內(nèi)核中由struct request_queue表示。每一個讀或寫請求都需經(jīng)過submit_bio函數(shù)處理,submit_bio將讀寫請求放入相應I/O請求隊列中。該層起到最主要的作用就是對I/O請求進行合并和排序,這樣減少了實際的磁盤讀寫次數(shù)和尋道時間,達到優(yōu)化磁盤讀寫性能的目的。
?
使用crash解析vmcore文件,執(zhí)行"dev -d"命令,可以看到塊設備請求隊列的相關信息:
crash > dev -d MAJOR GENDISK NAME REQUEST QUEUE TOTAL ASYNC SYNC DRV8 0xffff880119e85800 sda 0xffff88011a6a6948 10 0 0 108 0xffff880119474800 sdb 0xffff8801195632d0 0 0 0 0執(zhí)行"struct request_queue 0xffff88011a6a6948",可對以上sda設備相應的request_queue請求隊列結構進行解析。
執(zhí)行以下命令,可以查看sda設備的請求隊列大小:
linux # cat /sys/block/sda/queue/nr_requests 128?
如何對I/O請求進行合并、排序,那就是I/O調(diào)度算法完成的工作,Linux支持多種I/O調(diào)度算法,通過以下命令可以查看:
linux # cat /sys/block/sda/queue/scheduler noop anticipatory deadline [cfq]?
塊I/O層的另一個作用就是對I/O讀寫情況進行統(tǒng)計,執(zhí)行iostat命令,看到的就是該層提供的統(tǒng)計信息:
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 %utilsda 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分別指示了每秒寫請求、讀請求的合并次數(shù)。
?
task_io_account_read函數(shù)用于統(tǒng)計各個進程發(fā)起的讀請求量, 由該函數(shù)得到的是進程讀請求量的準確值。而對于寫請求,由于數(shù)據(jù)寫入cache后write調(diào)用就返回,因而在內(nèi)核的層面無法統(tǒng)計到一個進程發(fā)起的準確寫請求量,讀時進程會等buff可用,而寫則寫入cache后返回,讀是同步的,寫卻不一定同步,這是讀寫實現(xiàn)上的最大區(qū)別。
?
再往下就是設備層,設備從隊列中取出I/O請求,scsi的scsi_request_fn函數(shù)就是完成取請求并處理的任務。scsi層最終將處理請求轉化為指令,指令下發(fā)后進行DMA(direct memory access)映射,將內(nèi)存的部分cache映射到DMA,這樣設備繞過cpu直接操作主存。
?
設備層完成內(nèi)存數(shù)據(jù)到磁盤拷貝后,該消息將一層層上報,最后內(nèi)核去除原臟頁的dirty位標志。
?
以上為寫磁盤的大致實現(xiàn)過程,對于讀磁盤,內(nèi)核首先在緩存中查找對應內(nèi)容,若命中則不會進行磁盤操作。若進程讀取一個字節(jié)的數(shù)據(jù),內(nèi)核不會僅僅返回一個字節(jié),其以頁面為單位(4KB),最少返回一個頁面的數(shù)據(jù)。另外,內(nèi)核會預讀磁盤數(shù)據(jù),執(zhí)行以下命令可以看到能夠預讀的最大數(shù)據(jù)量(以KB為單位):
linux # cat /sys/block/sda/queue/read_ahead_kb 512?
下面我們通過一段systemtap代碼,了解內(nèi)核的預讀機制:
//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)} }?
以上代碼指示當dd命令讀寫inode號為5234的文件、經(jīng)過內(nèi)核函數(shù)submit_bio時,輸出inode號、操作方式(讀或寫)、文件所在設備名、讀寫大小、扇區(qū)號信息。執(zhí)行以下代碼安裝探測模塊:
stap test.stp &?
之后我們使用dd命令讀取inode號為5234的文件(可通過stat命令取得文件inode號):
dd if=airport.txt of=/dev/null bs=1 count=10000000?
以上命令故意將bs設為1,即每次讀取一個字節(jié),以此觀察內(nèi)核預讀機制。執(zhí)行該命令的過程中,我們在終端中可以看到以下輸出:
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?
由以上輸出可知,預讀從16384字節(jié)(16KB)逐漸增大,最后變?yōu)?24288字節(jié)(512KB),可見內(nèi)核會根據(jù)讀的情況動態(tài)地調(diào)整預讀的數(shù)據(jù)量。?
?
由于讀、寫磁盤均要經(jīng)過submit_bio函數(shù)處理,submit_bio之后讀、寫的底層實現(xiàn)大致相同。
?
直接I/O
當我們以O_DIRECT標志調(diào)用open函數(shù)打開文件時,后續(xù)針對該文件的read、write操作都將以直接I/O(direct I/O)的方式完成;對于裸設備,I/O方式也為直接I/O。
?
直接I/O跳過了文件系統(tǒng)這一層,但塊層仍發(fā)揮作用,其將內(nèi)存頁與磁盤扇區(qū)對應上,這時不再是建立cache到DMA映射,而是進程的buffer映射到DMA。進行直接I/O時要求讀寫一個扇區(qū)(512bytes)的整數(shù)倍,否則對于非整數(shù)倍的部分,將以帶cache的方式進行讀寫。
?
使用直接I/O,寫磁盤少了用戶態(tài)到內(nèi)核態(tài)的拷貝過程,這提升了寫磁盤的效率,也是直接I/O的作用所在。而對于讀操作,第一次直接I/O將比帶cache的方式快,但因帶cache方式后續(xù)再讀時將從cache中讀,因而后續(xù)的讀將比直接I/O快。有些數(shù)據(jù)庫使用直接I/O,同時實現(xiàn)了自己的cache方式。
?
異步I/O
Linux下有兩種異步I/O(asynchronous I/O)方式,一種是aio_read/aio_write庫函數(shù)調(diào)用,其實現(xiàn)方式為純用戶態(tài)的實現(xiàn),依靠多線程,主線程將I/O下發(fā)到專門處理I/O的線程,以此達到主線程異步的目的。
?
另一種是io_submit,該函數(shù)是內(nèi)核提供的系統(tǒng)調(diào)用,使用io_submit也需要指定文件的打開方式為O_DIRECT,并且讀寫需按扇區(qū)對齊。
?
Reference: Chapter 14 - The Block I/O Layer, Linux kernel development.3rd.Edition
轉載于:https://www.cnblogs.com/felixzh/p/9039519.html
總結
以上是生活随笔為你收集整理的kernel笔记——块I/O的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 索引和慢查询优化
- 下一篇: reactNative 计算时间差