多粒度时间戳¶
引言¶
历来,内核总是使用粗粒度时间值来标记 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()
来更新时间戳,或者以其他方式模仿其行为。