当你mmap
创建一个文件时,你基本上是在你的进程和内核的页面缓存之间直接共享内存——同一个缓存保存从磁盘读取的文件数据,或者等待写入磁盘。页面缓存中与磁盘上的页面不同的页面(因为它已被写入)称为“脏”。
有一个内核线程在几个参数的控制下扫描脏页并将它们写回磁盘。一项重要的是dirty_expire_centisecs
. 如果某个文件的任何页面的脏页时间超过dirty_expire_centisecs
了该文件的所有脏页,则该文件的所有脏页都将被写出。默认值为 3000 厘秒(30 秒)。
另一组变量是dirty_writeback_centisecs
、dirty_background_ratio
和dirty_ratio
。dirty_writeback_centisecs
控制内核线程检查脏页的频率,默认为 500(5 秒)。如果脏页的百分比(作为可用于缓存的内存的一部分)小于dirty_background_ratio
则什么也不会发生;如果大于dirty_background_ratio
,那么内核将开始将一些页面写入磁盘。最后,如果脏页的百分比超过dirty_ratio
,那么任何试图写入的进程都会阻塞,直到脏数据量减少。这样可以保证未写入的数据量不会无限制的增加;最终,产生数据的速度超过磁盘写入速度的进程将不得不放慢速度以匹配磁盘的速度。
mtime 如何更新的问题与内核如何首先知道页面是脏的问题有关。在 的情况下mmap
,答案是内核将映射的页面设置为只读。这并不意味着您不能编写它们,而是意味着您第一次这样做时,它会触发内存管理单元中的异常,该异常由内核处理。异常处理程序(至少)做了四件事:
- 将页面标记为脏,以便将其写回。
- 更新文件 mtime。
- 将页面标记为可读写,以便写入成功。
- 跳回到程序中写入
mmap
ed 页面的指令,这一次成功。
因此,当您将数据写入干净的页面时,它会导致 mtime 更新,但也会导致页面变为可读写,因此进一步的写入不会导致异常(或 mtime 更新)注 1。但是,当脏页被刷新到磁盘时,它变得干净,并且再次变为“只读”,因此任何进一步的写入都会触发另一个最终的磁盘写入,以及另一个 mtime 更新。
所以现在,有了一些假设,我们就可以开始拼凑这个谜题了。
首先,dirty_background_ratio
并且dirty_ratio
可能不会发挥作用。如果您的写入速度足以触发后台刷新,那么您很可能会在所有文件上看到“不规则”行为。
其次,“不规则”文件和“30 秒”文件之间的区别在于页面访问模式。我推测“不规则”文件正在以某种附加模式或循环缓冲区方式写入,因此您每隔几秒钟就开始写入一个新页面。每次您弄脏以前未触及的页面时,都会触发一次 mtime 更新。但是对于显示 30 秒模式的文件,您只能写入一页(可能它们的长度为一页或更短)。在这种情况下,mtime 会在第一次写入时更新,然后不会再次更新,直到文件超过dirty_expire_centisecs
30 秒刷新到磁盘。
注 1:从技术上讲,这种行为是错误的。这是不可预测的,但标准允许某种程度的不可预测性。但它们确实要求 mtime位于最后一次写入文件时或之后,以及msync
(如果有的话)之前或之前。如果页面在刷新到磁盘之前的时间间隔内被多次写入,则不会发生这种情况 - mtime 获取第一次写入的时间戳。这已经被讨论过了,但是一个可以修复它的补丁没有被接受。因此,在使用时mmap
,mtimes 可能会出错。dirty_expire_centisecs
某种程度上限制了该错误,但只是部分限制,因为其他磁盘流量可能导致刷新必须等待,从而延长写入窗口以进一步绕过 mtime。