关注了就能看到更多这么棒的文章哦~
Toward a better definition for i_version
By Jonathan Corbet
August 26, 2022
DeepL assisted translation
https://lwn.net/Articles/905931/
文件系统里面维护了关于它们所持有的文件的很多 metadata 元数据;这些元数据大部分是供用户空间使用的。但是,有些是深埋在文件系统中的,在内核之外不可见。其中一个元数据就是文件的版本数,被称为 i_version。目前有一个改变 i_version 管理方式的工作(并使其对用户空间可见)引起了关于 i_version 实际含义和其行为应该是什么的辩论。
Early versions of i_version
内核的 0.99.7 版本于 1993 年 3 月 13 日发布。那是一个令人兴奋的时代;这个版本的众多功能中也包括了一个 mmap()系统调用的实现,根据年轻的 Linus Torvalds 所说,"finally starting to really happen"。这个版本还带来了由 Rémy Card 设计的新文件系统 "ext2fs",这是许多 Linux 系统目前使用的 ext4 文件系统的早期祖先。
作为 ext2fs 新增功能的一部分,内核的 inode 结构中增加了一个叫做 i_version 的字段,这个字段的注释中说是用于 NFS 文件系统的。直到当年 11 月发布的 0.99.14 版本增加了一个 ioctl() 调用来获取 i_version 之前,都没有代码真正使用这个字段。我们中很多在那些日子里勇敢地探索在 Linux 上使用 NFS 的人都应该还记得,当时 sever 是在用户空间运行的,所以这个 ioctl()调用对于 i_version 在 NFS 来说是有用的。
最初,每当一个给定的 inode 编号被重新用在一个新文件上时,i_version 就会被递增。这是一个 NFS server 需要知道的事件;否则,为一个文件创建的文件句柄可能会被用来访问另一个完全不相干的文件,仅仅是因为这个文件的 inode 号恰好相同的,这种后果谁都不想看到。1999 年的 2.2.3pre1 版本中增加了一个新的 i_generation 字段来代替这个目的,尽管它直到同年 5 月的 2.3.1pre1 开发版内核中才被实际使用起来。当 i_generation 接过这个角色时,i_version 就成了同一文件的多个版本的计数器,以文件系统特有的方式在每当有变动是就递增,当然这是指那些会对 i_version 进行管理的文件系统。
虽然 i_generation 对 NFS 服务器实现那个功能就足够了,也就是在文件被替换时产生可怕的 "stale file handle" 错误,但 i_version 仍有其作用。如果 NFS 能够在本地 cache 一些数据,它的性能会好得多,但要安全地做到这一点就需要了解文件内容何时发生了变动;i_version 可以用于这一目的。对细节感兴趣的人可以阅读 Neil Brown 的这篇文章(https://lwn.net/Articles/898262/ ),了解当前版本的 NFS 是如何保持缓存一致性的。
The trouble with i_version now
在 i_version 引入后的近 30 年里,对该字段的含义几乎没有正式的描述。在 2018 年,Jeff Layton 增加了一些描述 i_version 是如何被使用的说明,这澄清了一些细节。但事实证明,有些细节仍有待确定,而且现在正在导致麻烦。
Layton 的注释说:"如果自上次查询以来,inode 的数据或元数据发生了变动,那么 i_version 在观察者看来必须要有差异"。多年来,这一直是虚拟文件系统(VFS)层和文件系统之间的约定,但现在有人想改变这一点。i_version 当前的实现似乎正在导致一些性能问题。
如上所述,NFS 使用 i_version 来检测一个文件是否有变化。如果一个 NFS 客户端有文件的部分缓存,i_version 的改动就会导致它丢弃这些缓存,从而引发与服务器的更多通信。内核的完整性测量架构(IMA, integrity measurement architecture)通过跟可信的校验和进行比较,来确保文件没有被篡改,它也使用了 i_version;如果一个文件已经改变,在允许访问之前必须重新进行校验。在这两种情况下,如果 i_version 虚假变化的话,就会导致不必要的额外工作,损害性能。
事实证明,这些不应该发生的递增确实在出现了,其原因是一个古老的问题了:访问时间(atime)跟踪。默认情况下,Unix 文件系统会在文件的 atime 字段中记录每次读取文件的时间。这种记录方式把本来只读的操作变成了文件系统的写操作,本身就对性能不利;由于这个原因,有许多方式可以用来禁用 atime 更新。但是,如果它们被启用的话,每一次 atime 更新都会因为改变了文件的 inode 中的元数据而增加 i_version,从而产生上述的所有后果。
Rethinking i_version
Layton 决定对这个问题做些什么,从而提供了一些相关 patch。例如,这个 patch 使 i_version 在 statx() 系统调用中可见,首次将其暴露在用户空间(旧的 ext2 ioctl()命令仍然存在,但它返回 i_generation 而非 i_version)。其目的是为了更容易检测它的变动,并方便编写用户空间的 NFS 服务程序。另一个 patch 使 XFS 文件系统不因为 atime 更新而导致 i_version 的更新;对 ext4 也有一个类似的 patch。最后,对 i_version 的注释进行了更新,明确指出 atime 更新不应该增加该字段。
对这项工作的抵制主要来自 XFS 的开发者 Dave Chinner,他称改变后的 i_version 规则有些 "misguided"。他有一些抱怨,首先是 XFS 对 i_version 的看法相当不同,并且经常会对它进行更新:
不确定你有没有没有意识到,在一个 4kB block size 的文件系统上,XFS 可以为一个 1MB 的 write() 而对 iversion 递增 500 多次,而其中只有一次是由于最开始的 write()系统调用用来将数据复制到 page cache。其他 500 多次是我们在几十秒后将数据持久化存放到磁盘时可能出现的所有那些 extent allocation 以及 manipulation transaction。
他说,这种行为与 i_version 在磁盘上的存储方式有关,这意味着对其语义的改变需要被当作磁盘格式的改变来对待。他认为,这里所要求的,本质上是个 lazytime mount option,它是在 VFS 层面上实现的。他说,如果 NFS 的 i_version 需要类似 lazytime 的语义,那也应该在 VFS 层面上实现,这样所有的文件系统都会以同样的方式行事。
Layton 回应说,lazytime 的语义并没有真正提供帮助,因为它们只是推迟了 atime 的更新,仍然会导致不必要地修改 i_version。他还说,由于 i_version 的唯一使用者是在内核中,它的语义可以被改变而不会产生进一步的问题。Chinner 不同意这种说法,他说他的取证分析(forensic-analysis)工具会大量使用了磁盘镜像中的这个字段。在 XFS 中,如果不改变磁盘格式,可能无法改变 i_version 的行为。
尽管这样,Chinner 已经让大家知道他并不反对这种改变,除了一件事以外:他希望对 i_version 的行为进行严格的规范,尤其是如果它会被暴露给用户空间。Trond Myklebust 建议,i_version 应该只在响应明确的操作时改变,也就是那些用户空间要求改变文件的操作。相反,对 atime 的改变是隐性的,因为用户空间并没有要求进行这些改动,所以它们不应该导致 i_version 更新。Layton 说,它可以简单地被定义为任何会更新一个节点的 mtime 或 ctime 字段的操作。Neil Brown 有一个更复杂的建议,可以直接使用 ctime 字段,同时为 NFS 提供更高的粒度。
不过最后,Layton 认为,"为 i_version 编写规范的时间应该是在它被创建出来的时候",而且他正在尽最大的努力来解决那个时候之后的问题。但是,他说,"可能最好的办法是尽可能松散地定义,这样我们就可以使大量的文件系统更容易实现它"。偶尔的不必要地增长,并不是一个巨大的问题,但是由于 atime 更新引起的定期增长才是问题。解决这个问题应该就足够好了。
对于讨论中的所有观点来说,真正的分歧可能并不如看起来那么大。这是一个很好的机会,可以更好地理解这个有 30 年历史的成员的真正含义,并调整它的行为使 Linux 用户受益。下一步似乎是由 Layton 发布新版本的 patch,那时候我们将会基本明确是否有足够的共识可以达成相应的修改,并完成合入。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
欢迎分享、转载及基于现有协议再创作~
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~