当您使用
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
页面的指令,这次成功执行。
因此,当您向干净的页面写入数据时,会导致 mtime 更新,但它还会使页面变得可写,以便进一步的写入不会引发异常(或 mtime 更新)注1。但是,当脏页面被刷新到磁盘时,它会变得干净,并再次变成“只读”,因此对其进行任何进一步的写入都将触发另一个最终的磁盘写入,以及另一个 mtime 更新。
所以现在,在做出一些假设的情况下,我们可以开始拼凑这个谜题了。
首先,
dirty_background_ratio
和
dirty_ratio
可能并没有发挥作用。如果您的写入速度足够快以触发后台刷新,则很可能会在所有文件上看到“不规则”行为。
其次,“不规则”文件与“30秒”文件之间的区别在于页面访问模式。我推测,“不规则”文件是以某种追加模式或循环缓冲区方式进行写入的,因此您每隔几秒钟就开始写入新的页面。每次脏一个以前未触及的页面,它都会触发mtime更新。但对于显示30秒模式的文件,您只写入一个页面(也许长度为一页或更少)。在这种情况下,mtime在第一次写入时更新,然后直到文件通过超过
dirty_expire_centisecs
被刷新到磁盘时才会再次更新,该值为30秒。
注意1:从技术上讲,这种行为是错误的。它是不可预测的,但标准允许一定程度的不可预测性。但是,它们要求mtime在文件的最后一次写入时或之后,在
msync
(如果有)之前或之后的某个时间点上。在一个页面在刷新到磁盘之前的时间段内被多次写入的情况下,不会发生这种情况 - mtime会获得第一个写入的时间戳。这已经被讨论过,但是
修复该问题的补丁并未被接受。因此,在使用
mmap
时,mtime可能存在错误。
dirty_expire_centisecs
在某种程度上限制了该错误,但仅在其他磁盘流量可能导致刷新需要等待的情况下,进一步延长了写入绕过mtime的时间窗口。
mtime
行为的理论很有趣...但也有点令人困惑,因为具有不规则mtime
步骤的文件确实是以追加方式进行编写,但速度是恒定的(每秒一个固定大小记录)。如果像文本日志文件那样不断添加,我可以理解为什么会在不规则的时间间隔遇到新页面,但在我的情况下,写入速率是恒定的(对于那些特定的文件)。具有一致时间步长的文件确实较小且随机。 - John Zwinck