write文件一個字節後什麼時候發起寫磁盤IO

在前文《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);
    ...
}

進一步細化個人問題,咱們對打開的問題寫入一個字節後服務器

  • write函數在內核裏是怎麼執行的?
  • 數據在什麼時機真正能寫入到磁盤上?

咱們在討論的過程當中不可避免地要涉及到內核代碼,我使用的內核版本是3.10.1。若是有須要,你能夠到這裏來下載。https://mirrors.edge.kernel.org/pub/linux/kernel/v3.x/異步

write函數實現剖析

我花了不短的時候跟蹤write寫到ext4文件系統時的各類調用和返回,大體理出來了一個交互圖。固然爲了突出重點,我拋棄了很多細節,好比DIRECT IO、ext4日誌記錄啥的都沒有體現出來,只抽取出來了一些我認爲關鍵的調用。函數

file

在上面的流程圖裏,全部的寫操做最終到哪兒了呢?在最後面的__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請求:

  • 第一種狀況,若是write系統調用時,若是發現PageCache中髒頁佔比太多,超過了dirty_ratio或dirty_bytes,write就必須等待了。
  • 第二種狀況,write寫到PageCache就已經返回了。worker內核線程異步運行的時候,再次判斷髒頁佔比,若是超過了dirty_background_ratio或dirty_background_bytes,也發起寫回請求。
  • 第三種狀況,這時一樣write調用已經返回了。worker內核線程異步運行的時候,雖然系統內臟頁一直沒有超過dirty_background_ratio或dirty_background_bytes,可是髒頁在內存中呆的時間超過dirty_expire_centisecs了,也會發起會寫。

若是對以上配置不滿意,你能夠本身經過修改/etc/sysctl.conf來調整,修改完了別忘了執行sysctl -p。

最後咱們要認識到,這套write pagecache+回寫的機制第一目標是性能,不是保證不丟失咱們寫入的數據的。若是這時候掉電,髒頁時間未超過dirty_expire_centisecs的就真的丟了。若是你作的是和錢相關很是重要的業務,必須保證落盤完成才能返回,那麼你就可能須要考慮使用fsync。


file


開發內功修煉之硬盤篇專輯:


個人公衆號是「開發內功修煉」,在這裏我不是單純介紹技術理論,也不僅介紹實踐經驗。而是把理論與實踐結合起來,用實踐加深對理論的理解、用理論提升你的技術實踐能力。歡迎你來關注個人公衆號,也請分享給你的好友~~~

相關文章
相關標籤/搜索