当你mmap
一个文件,基本上是在进程和内核的页面缓存之间直接共享内存——该缓存保存已从磁盘读取或等待写入磁盘的文件数据。页面缓存中与磁盘上的页面不同(因为它已被写入)的页面被称为“脏”页面。
有一个内核线程在几个参数的控制下扫描脏页并将其写回磁盘。其中一个重要的就是dirty_expire_centisecs
。如果文件的任何页面脏的时间超过dirty_expire_centisecs
那么该文件的所有脏页都将被写出。默认值为 3000 厘秒(30 秒)。
另一组变量是dirty_writeback_centisecs
, dirty_background_ratio
, and dirty_ratio
. dirty_writeback_centisecs
控制内核线程检查脏页的频率,默认为 500(5 秒)。如果脏页的百分比(作为可用于缓存的内存的一部分)小于dirty_background_ratio
然后什么也没有发生;如果超过dirty_background_ratio
,然后内核将开始将一些页面写入磁盘。最后,如果脏页百分比超过dirty_ratio
,那么任何尝试写入的进程都会阻塞,直到脏数据量减少。这样保证了未写入的数据量不会无限制的增加;最终,产生数据的速度快于磁盘写入速度的进程将不得不放慢速度以匹配磁盘的速度。
mtime 如何更新的问题首先与内核如何知道页面是脏的问题相关。如果是mmap
,答案是内核将映射的页面设置为只读。这并不意味着您不能编写它们,而是意味着第一次这样做时,它会触发内存管理单元中的异常,该异常由内核处理。异常处理程序(至少)做了四件事:
- 将页面标记为脏页,以便将其写回。
- 更新文件 mtime.
- 将页面标记为可读写,以便写入能够成功。
- 跳回到程序中写入的指令
mmap
ed 页面,这次成功了。
So when you write data to a clean page, it causes an mtime update, but it also causes the page to become read-write, so that further writes don't cause an exception (or an mtime update)note 1. However, when the dirty page gets flushed to disk, it becomes clean, and also becomes "read-only" again, so that any further writes to it will trigger another eventual disk write, and also another mtime update.
现在,通过一些假设,我们可以开始拼凑这个难题。
First, dirty_background_ratio
and dirty_ratio
可能不会发挥作用。如果您的写入速度足够快以触发后台刷新,那么您很可能会在所有文件上看到“不规则”行为。
其次,“不规则”文件和“30秒”文件之间的区别在于页面访问模式。我推测“不规则”文件是以某种追加模式或循环缓冲区方式写入的,这样您就开始每隔几秒写入一个新页面。每次弄脏之前未触及的页面时,都会触发一次实时更新。但对于显示 30 秒模式的文件,您只能写入一页(也许它们的长度为一页或更短)。在这种情况下,mtime 在第一次写入时更新,然后不会再次更新,直到文件刷新到磁盘超过dirty_expire_centisecs
,即 30 秒。
Note 1:从技术上讲,这种行为是错误的。这是不可预测的,但标准允许一定程度的不可预测性。但他们确实要求 mtime 是某个时间最后一次写入时或之后到一个文件,并且在msync
(如果有的话)。如果页面在刷新到磁盘之前的时间间隔内被多次写入,则不会发生这种情况 - mtime 获取该页面的时间戳first写。这已经讨论过了,但是一个可以修复它的补丁 https://lwn.net/Articles/564120/没有被接受。因此,当使用mmap
,m次可能会出错。dirty_expire_centisecs
某种程度上限制了该错误,但只是部分限制,因为其他磁盘流量可能会导致刷新必须等待,从而进一步延长写入时间以绕过 mtime。