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


开发内功修炼之硬盘篇专辑:


个人公众号是「开发内功修炼」,在这里我不是单纯介绍技术理论,也不仅介绍实践经验。而是把理论与实践结合起来,用实践加深对理论的理解、用理论提升你的技术实践能力。欢迎你来关注个人公众号,也请分享给你的好友~~~

相关文章
相关标签/搜索