在前文《read文件一個字節實際會發生多大的磁盤IO?》寫完以後,原本想着偷個懶,只經過讀操做來讓你們瞭解下Linux IO棧的各個模塊就好了。但不少同窗表示再讓我寫一篇關於寫操做的。既然很多人都有這個需求,那我就寫一下吧。node
Linux內核真的是太複雜了,源代碼的行數已經從1.0版本時的幾萬行,到如今已是千萬行的一個龐然大物了。直接鑽進去的話,很容易在各類眼花繚亂的各類調用中迷失了本身,再也鑽不出來了。我分享給你們一個我在琢磨內核的方法。通常我本身先想一個本身很想搞清楚的問題。無論在代碼裏咋跳來跳去,時刻都要記得本身的問題,無關的部分儘可能少去發散,只要把本身的問題搞清楚了就好了。linux
如今我想搞明白的問題是,在最經常使用的方式下,不開O_DIRECT、不開O_SYNC(寫文件的方法有不少,有sync模式、direct模式、mmap內存映射模式),write是怎麼寫的。c的代碼示例以下:後端
#include <fcntl.h> int main() { char c = 'a'; int out; out = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC); write(out,&c,1); ... }
進一步細化個人問題,咱們對打開的問題寫入一個字節後服務器
咱們在討論的過程當中不可避免地要涉及到內核代碼,我使用的內核版本是3.10.1。若是有須要,你能夠到這裏來下載。https://mirrors.edge.kernel.org/pub/linux/kernel/v3.x/。異步
我花了不短的時候跟蹤write寫到ext4文件系統時的各類調用和返回,大體理出來了一個交互圖。固然爲了突出重點,我拋棄了很多細節,好比DIRECT IO、ext4日誌記錄啥的都沒有體現出來,只抽取出來了一些我認爲關鍵的調用。函數
在上面的流程圖裏,全部的寫操做最終到哪兒了呢?在最後面的__block_commit_write中,只是make dirty。而後大部分狀況下你的函數調用就返回了(稍後再說balance_dirty_pages_ratelimited)。數據如今還在內存中的PageCache裏,並無真正寫到硬盤。工具
爲何要這樣實現,不直接寫硬盤呢?緣由就在於硬盤尤爲是機械硬盤,性能是在是太慢了。一塊服務器級別的萬轉盤,最壞隨機訪問平均延遲都是毫秒級別的,換算成IOPS只有100多不到200。設想一下,假如你的後端接口裏每一個用戶來訪問都須要一次隨機磁盤IO,無論你多牛的服務器,每秒200的qps都將直接打爆你的硬盤,相信做爲爲百萬/千萬/過億用戶提供接口的你,這個是你絕對不能忍的。性能
Linux這麼搞也是有反作用的,若是接下來服務器發生掉電,內存裏東西全丟。因此Linux還有另一個「補丁」-延遲寫,幫咱們緩解這個問題。注意下,我說的是緩解,並無完全解決。this
再說下balance_dirty_pages_ratelimited,雖然絕大部分狀況下,都是直接寫到Page Cache裏就返回了。但在一種狀況下,用戶進程必須得等待寫入完成才能夠返回,那就是對balance_dirty_pages_ratelimited的判斷若是超出限制了。該函數判斷當前髒頁是否已經超過髒頁上限dirty_bytes、dirty_ratio,超過了就必須得等待。這兩個參數只有一個會生效,另外1個是0。拿dirty_ratio來講,若是設置的是30,就說明若是髒頁比例超過內存的30%,則write函數調用就必須等待寫入完成才能返回。能夠在你的機器下的/proc/sys/vm/目錄來查看這兩個配置。線程
# cat /proc/sys/vm/dirty_bytes 0 # cat /proc/sys/vm/dirty_ratio 30
內核是何時真正把數據寫到硬盤中呢?爲了快速摸清楚全貌,我想到的辦法是用systemtap工具,找到內核寫IO過程當中的一個關鍵函數,而後在其中把函數調用堆棧打出來。查了半天資料之後,我決定用do_writepages這個函數。
#!/usr/bin/stap probe kernel.function("do_writepages") { printf("--------------------------------------------------------\n"); print_backtrace(); printf("--------------------------------------------------------\n"); }
systemtab跟蹤之後,打印信息以下:
0xffffffff8118efe0 : do_writepages+0x0/0x40 [kernel] 0xffffffff8122d7d0 : __writeback_single_inode+0x40/0x220 [kernel] 0xffffffff8122e414 : writeback_sb_inodes+0x1c4/0x490 [kernel] 0xffffffff8122e77f : __writeback_inodes_wb+0x9f/0xd0 [kernel] 0xffffffff8122efb3 : wb_writeback+0x263/0x2f0 [kernel] 0xffffffff8122f35c : bdi_writeback_workfn+0x1cc/0x460 [kernel] 0xffffffff810a881a : process_one_work+0x17a/0x440 [kernel] 0xffffffff810a94e6 : worker_thread+0x126/0x3c0 [kernel] 0xffffffff810b098f : kthread+0xcf/0xe0 [kernel] 0xffffffff816b4f18 : ret_from_fork+0x58/0x90 [kernel]
從上面的輸出咱們能夠看出,真正的寫文件過程操做是由worker內核線程發出來的(和咱們本身的應用程序進程沒有半毛錢關係,此時咱們的應用程序的write函數調用早就返回了)。這個worker線程寫回是週期性執行的,它的週期取決於內核參數dirty_writeback_centisecs的設置,根據參數名也大概能看出來,它的單位是百分之一秒。
# cat /proc/sys/vm/dirty_writeback_centisecs 500
我查看到個人配置是500,就是說每5秒會週期性地來執行一遍。回顧咱們的問題,咱們最關心的問題的啥時候寫入的,圍繞這個思路不過多發散。因而沿着這個調用棧不斷地跟蹤,跳轉,終於找到了下面的代碼。以下代碼裏咱們看到,若是是for_background模式,且over_bground_thresh
判斷成功,就會開始回寫了。
static long wb_writeback(struct bdi_writeback *wb, struct wb_writeback_work *work) { work->older_than_this = &oldest_jif; ... if (work->for_background && !over_bground_thresh(wb->bdi)) break; ... if (work->for_kupdate) { oldest_jif = jiffies - msecs_to_jiffies(dirty_expire_interval * 10); } else ... } static long wb_check_background_flush(struct bdi_writeback *wb) { if (over_bground_thresh(wb->bdi)) { ... return wb_writeback(wb, &work); } }
那麼over_bground_thresh
函數判斷的是啥呢?其實就是判斷當前的髒頁是否是超過內核參數裏dirty_background_ratio或dirty_background_bytes的配置,沒超過的話就不寫了(代碼位於fs/fs-writeback.c:1440,限於篇幅我就不貼了)。這兩個參數只有一個會真正生效,其中dirty_background_ratio配置的是比例、dirty_background_bytes配置的是字節。
在個人機器上的這兩個參數配置以下,表示髒頁比例超過10%就開始回寫。
# cat /proc/sys/vm/dirty_background_bytes 0 # cat /proc/sys/vm/dirty_background_ratio 10
那若是髒頁一直都不超過這個比例怎麼辦呢,就不寫了嗎? 不是的。在上面的wb_writeback函數中咱們看到了,若是是for_kupdate模式,會記錄一個過時標記到work->older_than_this,再日後面的代碼中把符合這個條件的頁面也寫回了。dirty_expire_interval這個變量是從哪兒來的呢? 在kernel/sysctl.c裏,咱們發現了蛛絲馬跡。哦,原來它是來自/proc/sys/vm/dirty_expire_centisecs這個配置。
1158 { 1159 .procname = "dirty_expire_centisecs", 1160 .data = &dirty_expire_interval, 1161 .maxlen = sizeof(dirty_expire_interval), 1162 .mode = 0644, 1163 .proc_handler = proc_dointvec_minmax, 1164 .extra1 = &zero, 1165 },
在個人機器上,它的值是3000。單位是百分之一秒,因此就是髒頁過了30秒就會被內核線程認爲須要寫回到磁盤了。
# cat /proc/sys/vm/dirty_expire_centisecs 3000
咱們demo代碼中的寫入,其實絕大部分狀況都是寫入到PageCache中就返回了,這時並無真正寫入磁盤。咱們的數據會在以下三個時機下被真正發起寫磁盤IO請求:
若是對以上配置不滿意,你能夠本身經過修改/etc/sysctl.conf來調整,修改完了別忘了執行sysctl -p。
最後咱們要認識到,這套write pagecache+回寫的機制第一目標是性能,不是保證不丟失咱們寫入的數據的。若是這時候掉電,髒頁時間未超過dirty_expire_centisecs的就真的丟了。若是你作的是和錢相關很是重要的業務,必須保證落盤完成才能返回,那麼你就可能須要考慮使用fsync。
開發內功修煉之硬盤篇專輯:
個人公衆號是「開發內功修煉」,在這裏我不是單純介紹技術理論,也不僅介紹實踐經驗。而是把理論與實踐結合起來,用實踐加深對理論的理解、用理論提升你的技術實踐能力。歡迎你來關注個人公衆號,也請分享給你的好友~~~