多粒度时间戳

引言

历来,内核总是使用粗粒度时间值来标记 inode。该值每 jiffy 更新一次,因此在该 jiffy 内发生的任何更改都将具有相同的时间戳。

当内核标记 inode(由于读或写操作)时,它首先获取当前时间,然后将其与现有时间戳进行比较,以查看是否有任何变化。如果没有变化,则可以避免更新 inode 的元数据。

因此,从性能角度来看,粗粒度时间戳是好的,因为它们减少了元数据更新的需求,但从确定是否有任何变化的立场来看,它们是糟糕的,因为在一个 jiffy 内可能会发生很多事情。

它们在 NFSv3 中尤其麻烦,因为不变的时间戳使得难以判断是否需要使缓存失效。NFSv4 提供了一个专用的变更属性,应该总是显示可见的变更,但并非所有文件系统都正确实现了这一点,导致 NFS 服务器在许多情况下用 ctime 替代它。

多粒度时间戳旨在通过在文件最近被查询过时间戳,并且当前粗粒度时间未引起变化时,选择性地使用细粒度时间戳来弥补这一点。

Inode 时间戳

目前 inode 中有 3 个时间戳,它们根据不同的活动更新为当前的墙上时钟时间

ctime

inode 变更时间。每当 inode 的元数据发生变化时,它会标记为当前时间。请注意,此值无法从用户空间设置。

mtime

inode 修改时间。每当文件内容发生变化时,它会标记为当前时间。

atime

inode 访问时间。每当 inode 的内容被读取时,它会标记为当前时间。这被普遍认为是一个严重的错误。通常通过 noatime 或 relatime 等选项来避免。

更新 mtime 总是意味着 ctime 的变化,但由于读取请求而更新 atime 则不会。

多粒度时间戳仅针对 ctime 和 mtime 进行跟踪。atime 不受影响,并且总是使用粗粒度值(受制于下限)。

Inode 时间戳排序

除了提供有关单个文件变化的信息外,文件时间戳在“make”等应用程序中也起着重要作用。这些程序通过测量时间戳来确定源文件是否比缓存对象新。

像 make 这样的用户空间应用程序只能根据操作边界来确定排序。对于系统调用,这些是系统调用的入口和出口点。对于 io_uring 或 nfsd 操作,这些是请求提交和响应。在并发操作的情况下,用户空间无法确定事件发生的顺序。

例如,如果一个线程依次修改一个文件,然后再修改另一个文件,那么第二个文件必须显示等于或晚于第一个文件的 mtime。如果两个线程执行不重叠的类似操作,情况也是如此。

然而,如果两个线程有时间上重叠的竞态系统调用,那么就没有这样的保证,第二个文件可能显示为在第一个文件之前、之后或同时被修改,无论哪个先提交。

请注意,上述假设系统不会发生实时时钟向后跳变。如果这种情况发生在不合时宜的时间,那么时间戳可能会出现倒退,即使在一个正常运行的系统上也是如此。

多粒度时间戳实现

多粒度时间戳旨在确保对单个文件的更改始终可识别,同时不违反修改多个不同文件时的排序保证。这会影响 mtime 和 ctime,但 atime 将始终使用粗粒度时间戳。

它在 i_ctime_nsec 字段中使用一个未使用的位来指示 mtime 或 ctime 是否已被查询过。如果其中一个或两者都被查询过,那么内核会特别注意确保下一次时间戳更新将显示可见的变化。这确保了诸如 NFS 等用例的紧密缓存一致性,同时不牺牲当文件未被监视时减少元数据更新的好处。

Ctime 下限值

仅仅根据 mtime 或 ctime 是否已被查询来简单地使用细粒度或粗粒度时间戳是不够的。一个文件可能获得一个细粒度时间戳,而第二个文件稍后修改时可能获得一个粗粒度时间戳,这个时间戳却显得比第一个文件更早,这将打破内核的时间戳排序保证。

为了缓解这个问题,维护一个全局下限值,以确保这种情况不会发生。在上述示例中,这两个文件在这种情况下可能显示为同时被修改,但它们绝不会显示相反的顺序。为了避免实时时钟跳变问题,下限被管理为一个单调的 ktime_t 值,并且这些值在需要时会转换为实时时钟值。

实现注意事项

多粒度时间戳旨在供从本地时钟获取 ctime 值的本地文件系统使用。这与仅仅从服务器镜像时间戳值的网络文件系统等形成对比。

对于大多数文件系统,只需在 fstype->fs_flags 中设置 FS_MGTIME 标志即可选择启用,前提是 ctime 仅通过 inode_set_ctime_current() 设置。如果文件系统有一个没有调用 generic_fillattr 的 ->getattr 例程,那么它应该调用 fill_mg_cmtime() 来填充这些值。对于 setattr,它应该使用 setattr_copy() 来更新时间戳,或者以其他方式模仿其行为。