[toc]
fs/libfs.c 文件系统库函数(Filesystem Library Functions) 构建简单伪文件系统的工具集
历史与背景
这项技术是为了解决什么特定问题而诞生的?
这项技术以及fs/libfs.c
文件中的代码,是为了解决在Linux内核中重复开发简单的、基于内存的(或称为“伪”)文件系统的问题。
- 消除样板代码(Boilerplate):在
libfs
出现之前,如果一个内核开发者想要创建一个简单的文件系统来导出一组调试信息(像debugfs
)或配置接口(像configfs
),他们通常需要从一个现有的简单文件系统(如ramfs
)中复制大量代码。这些代码处理了与虚拟文件系统(VFS)交互的所有通用逻辑,如创建超级块(superblock)、inode、dentry,以及实现基本的目录查找操作。这种复制粘贴的做法导致了大量重复、冗余且难以维护的代码。 - 降低开发门槛:从头开始编写一个文件系统,即使是简单的,也需要对VFS的复杂内部机制有深入的了解。
libfs
通过提供一套高级、易于使用的API,将这些复杂的VFS交互细节封装起来,极大地降低了创建新伪文件系统的难度。 - 保证一致性和正确性:通过提供一个集中的、经过良好测试的通用实现,
libfs
确保了所有使用它的简单文件系统在与VFS交互时都遵循了相同的正确模式,减少了因实现不当而引入的bug。
它的发展经历了哪些重要的里程碑或版本迭代?
libfs
的发展是一个有机的、逐步抽象和完善的过程,而不是通过大的版本迭代。
- 起源:它的概念起源于对
ramfs
等早期内存文件系统的观察。开发者们注意到,这些文件系统的绝大部分代码都是在处理相同的VFS交互逻辑。 - 功能抽象:
libfs.c
的诞生就是将这些通用逻辑(特别是与超级块、inode和dentry管理相关的逻辑)抽象成可重用的函数。关键的函数如simple_fill_super
、simple_lookup
、simple_get_tree
等被创建出来。 - 逐步采用:随着
libfs
的成熟,内核中越来越多新的和现有的伪文件系统被重构,以使用libfs
提供的函数。例如,debugfs
,securityfs
,pipefs
,hugetlbfs
等都构建在libfs
之上。这本身就是其发展和成功的重要里程碑。
目前该技术的社区活跃度和主流应用情况如何?
libfs
是内核VFS层一个非常基础、稳定且被广泛依赖的组件。
- 社区活跃度:其代码库非常稳定,不会有频繁的功能性变更。相关的修改通常是进行细微的优化、代码清理或适应VFS层其他部分的变化。
- 主流应用:它构成了内核中大量非磁盘文件系统的骨架。它不是一个用户可以直接“使用”的文件系统,而是一个供内核其他部分使用的开发工具包。其应用实例包括:
- debugfs:用于内核调试,驱动程序可以在此创建文件来导出内部状态。
- securityfs:供Linux安全模块(LSMs)如SELinux、AppArmor使用,以导出策略和状态信息。
- configfs:一个基于文件的内核对象管理接口。
- hugetlbfs:用于管理和使用大页内存(Huge Pages)。
- tracefs:Ftrace跟踪框架的接口。
核心原理与设计
它的核心工作原理是什么?
fs/libfs.c
的核心工作原理是为实现一个简单的、层次结构固定的、基于内存的文件系统提供一套通用的VFS回调函数和辅助工具。它本身不是一个文件系统,而是一个函数库。
- 超级块填充:它提供了
simple_fill_super
函数。一个文件系统在被挂载时,其mount
回调函数可以调用simple_fill_super
。这个函数会处理所有创建超级块、根inode和根dentry的繁琐工作。文件系统开发者只需要提供一个包含文件和目录定义的tree
结构,libfs
就会自动在内存中构建出对应的dentry和inode层次结构。 - 通用目录操作:它提供了
simple_lookup
、simple_readdir
等函数,这些函数实现了在内存中进行目录查找和读取的标准算法(通常是线性遍历一个链表)。使用libfs
的文件系统可以直接将它们的inode_operations
中的.lookup
和.readdir
指针指向这些通用函数。 - 简化文件操作:它提供了一系列辅助函数来创建文件,如
simple_create_file
。文件系统开发者需要做的,是定义好自己文件的file_operations
结构(即实现read
,write
等函数来提供或接收数据),然后调用libfs
的函数将这个文件节点“挂”在目录树上。 - 内存管理:
libfs
处理了其创建的所有VFS对象(inode, dentry)的生命周期和内存管理,开发者无需手动分配和释放它们。
它的主要优势体现在哪些方面?
- 代码重用:极大地减少了创建新伪文件系统时需要编写的重复代码。
- 简洁性:让文件系统的实现者可以专注于内容(即通过文件导出什么数据或提供什么功能),而将结构(如何与VFS集成)交给
libfs
处理。 - 开发速度:使用
libfs
可以在很短的时间内创建一个功能完备的伪文件系统。 - 健壮性:依赖于一个集中的、经过充分测试的库,而不是各自为政的实现,提高了代码的整体质量。
它存在哪些已知的劣势、局限性或在特定场景下的不适用性?
- 仅限内存:
libfs
的设计完全基于文件系统的所有元数据和结构都存在于内存中的假设。它没有任何与块设备、持久化存储相关的逻辑。 - 性能非最优:其目录操作(如
simple_lookup
)通常采用简单的线性链表扫描。这对于只有少量条目的目录(debugfs
中的典型情况)来说足够快,但如果一个目录中有成千上万个文件,其性能会急剧下降。 - 功能有限:它只提供了一套基本的文件系统功能。不支持高级特性,如扩展属性(xattrs)、访问控制列表(ACLs)、日志(Journaling)、快照等。
使用场景
在哪些具体的业务或技术场景下,它是首选解决方案?请举-例说明。
当需要在内核中创建一个非持久化的、主要用于信息展示或配置的、目录结构相对简单的文件系统时,libfs
是绝对的首选解决方案。
- 内核调试接口(debugfs):一个I2C设备驱动的开发者,想要在运行时查看该设备的内部寄存器值。他可以在驱动的初始化代码中,使用
debugfs_create_file()
(一个基于libfs
的上层API)在/sys/kernel/debug/
下创建一个文件。他只需要提供一个.read
回调函数,该函数被调用时会去读取硬件寄存器并返回其值。所有VFS的复杂性都由debugfs
和libfs
处理了。 - 安全策略管理(securityfs):SELinux需要一个接口让管理员可以查看和加载安全策略。它通过
securityfs
(基于libfs
)创建文件,管理员通过向这些文件写入数据来加载新策略。 - 特殊内存管理(hugetlbfs):为了让应用程序能方便地使用大页内存,内核提供了
hugetlbfs
。用户可以像操作普通文件一样,通过mmap
一个hugetlbfs
中的文件来获得大页内存。libfs
负责管理这个文件系统的目录结构。
是否有不推荐使用该技术的场景?为什么?
- 通用磁盘文件系统:绝对不能用于实现需要在硬盘、SSD等持久化介质上存储数据的文件系统(如ext4, XFS, FAT)。这些文件系统需要复杂的块分配、元数据管理、日志或写时复制等机制,
libfs
完全没有提供这些功能。 - 大规模目录结构:不适合用于需要存储大量文件(如数万个)在同一个目录下的场景,因为其目录查找性能会成为瓶颈。
对比分析
请将其 与 其他相似技术 进行详细对比。
对比一:使用 libfs
vs. 从零开始实现伪文件系统
特性 | 使用 fs/libfs.c |
从零开始实现 |
---|---|---|
开发工作量 | 极低。开发者只需关注特定文件的file_operations 。 |
极高。需要手动实现所有与VFS的接口,包括超级块管理、inode/dentry的创建和缓存、目录操作等。 |
代码量 | 非常少。通常只需要几十行代码来定义文件和操作。 | 非常大。需要数百甚至上千行样板代码。 |
VFS集成正确性 | 高。依赖于内核提供的标准、健壮的实现。 | 依赖开发者经验。很容易在复杂的VFS交互中引入细微的bug,如引用计数错误、锁问题等。 |
灵活性 | 有限。受限于libfs 提供的功能集。 |
完全灵活。可以实现任何自定义的行为和高级功能。 |
对比二:libfs
vs. shmem.c
(tmpfs
)
这两者角色不同,但功能有相近之处,容易混淆。
特性 | fs/libfs.c (工具库) |
mm/shmem.c (tmpfs实现) |
---|---|---|
定位 | 一个开发工具包(SDK),用于帮助开发者创建新的文件系统。 | 一个完整、功能齐全的文件系统,供最终用户直接挂载和使用。 |
核心功能 | 提供结构化的VFS接口封装,简化目录和文件的创建。 | 提供一个可交换的、有大小限制的内存存储后端。它关注的是如何高效地管理内存页作为文件内容。 |
依赖关系 | 许多伪文件系统(如debugfs)依赖libfs 。 |
tmpfs 不使用libfs ,因为它有更复杂的需求(如特殊的inode用于交换),需要自己实现完整的VFS接口。 |
使用场景 | 供内核开发者在代码中调用,以创建调试或配置接口。 | 供系统管理员或用户通过mount 命令使用,以获得一个高速的临时存储空间。 |
通用伪文件系统创建助手:为内核对象提供VFS框架
本代码片段是Linux内核VFS层中的一个通用辅助框架,用于简化创建基于内存的、无物理后端的“伪”文件系统。其核心功能是提供一个标准化的入口函数init_pseudo
和一套回调操作,来处理这类文件系统的挂载流程,自动完成超级块(superblock)、根inode和根dentry的创建与初始化。像dmabuf
、sockfs
(套接字文件系统)、pipefs
(管道文件系统)等众多内核子系统都利用这个框架,来为它们的内部对象(如DMA缓冲区、套接字、管道)提供一个文件接口,从而复用VFS强大的生命周期管理和命名空间机制。
实现原理分析
该框架的实现原理是将创建一个简单内存文件系统的通用步骤进行封装,并通过一个上下文结构体(pseudo_fs_context
)来接收少量的定制化参数。
上下文初始化 (
init_pseudo
): 这是外部子系统(如DMA-BUF)调用的入口点。它负责:- 分配一个
pseudo_fs_context
结构体。 - 将
fs_context
的操作函数表指向pseudo_fs_context_ops
,从而接管后续的挂载流程。 - 将分配的
ctx
存入fc->fs_private
,以便后续回调函数可以访问。 - 设置一些通用标志,如
SB_NOUSER
,表示该文件系统不能由用户空间直接挂载。
- 分配一个
挂载树获取 (
pseudo_fs_get_tree
): 当VFS需要为这个文件系统构建一个实例树时,会调用这个函数。它不做具体工作,而是直接调用通用的get_tree_nodev
辅助函数(适用于无块设备的文件系统),并将真正的核心逻辑pseudo_fs_fill_super
作为回调函数传递过去。超级块填充 (
pseudo_fs_fill_super
): 这是框架的核心。当get_tree_nodev
分配了一个新的super_block
结构后,会调用此函数来填充它:- 设置通用的文件系统属性,如块大小、魔数(magic number)等。
- 创建一个根inode(
new_inode
),并将其设置为一个基本的目录模式。 - 通过
d_make_root
为根inode创建一个根dentry,并将其挂载到s->s_root
。 - 关键定制点: 调用
set_default_d_op(s, ctx->dops)
。它将init_pseudo
时传入的dentry_operations
(例如上一节分析的dma_buf_dentry_ops
)设置为该文件系统中所有dentry的默认操作。正是这一步,将特定子系统(如DMA-BUF)的生命周期管理逻辑(如dma_buf_release
)注入到了这个通用的文件系统中。
代码分析
1 | // simple_super_operations: 一套最简化的超级块操作函数。 |
Inode版本查询:确保文件变更的可检测性
本代码片段定义了用于查询inode
版本号(i_version
)的核心函数。i_version
是一个64位计数器,每当文件的元数据或数据发生改变时,它都会被递增。这比时间戳更可靠地用于检测文件变更。inode_query_iversion
函数不仅读取这个版本号,还实现了一个关键的“查询即标记”机制:它在i_version
值本身内部原子地设置一个I_VERSION_QUERIED
标志,以确保下一次文件变更必定会产生一个新的、可区分的版本号,从而解决了因两次变更过于接近而可能无法被检测到的问题。
实现原理分析
该功能通过一个无锁的、基于原子操作的乐观循环(Optimistic Loop)来实现,兼顾了高性能和正确性。
底层原子读取 (
inode_peek_iversion_raw
):- 这是一个基础的辅助函数,它简单地使用
atomic64_read
来原子地读取64位的i_version
值。它返回的是“原始”值,包含了可能已设置的I_VERSION_QUERIED
标志位。
- 这是一个基础的辅助函数,它简单地使用
查询并标记 (
inode_query_iversion
):- 乐观循环: 函数的核心是一个
do-while
循环。这种模式首先假定i_version
不会在操作期间被其他任务改变(乐观),然后尝试执行操作,最后验证假设是否成立。 - 初始读取: 循环开始前,先用
inode_peek_iversion_raw
读取当前的版本号cur
。 - 优化检查: 循环内部首先检查
cur
是否已经设置了I_VERSION_QUERIED
标志。如果已经设置了,说明之前已有进程查询过,我们的目标(确保下一次变更是可区分的)已经达成。此时无需再进行昂贵的原子写操作,只需放置一个内存屏障(smp_mb
)确保有序性,然后跳出循环。 - 尝试原子更新: 如果标志未被设置,函数会计算出期望的新值
new
(即cur
按位或上I_VERSION_QUERIED
)。然后,它调用atomic64_try_cmpxchg
。这是一个“比较并交换”(Compare-and-Swap, CAS)操作,它会原子地执行以下逻辑:
a. 比较inode->i_version
的当前值是否仍然等于我们之前读取的cur
。
b. 如果是,说明没有其他任务修改过它,就将inode->i_version
更新为new
,并返回true
,循环结束。
c. 如果不是,说明在我们读取cur
之后,有其他任务(或其他CPU)修改了i_version
。此时更新失败,函数返回false
,并且将inode->i_version
的最新值写回cur
。 - 循环重试: 如果
cmpxchg
失败,while
条件为真,循环会利用被更新后的cur
值重新开始,再次尝试“读取-修改-交换”的过程。 - 返回干净值: 无论循环如何结束,最终返回的是
cur >> I_VERSION_QUERIED_SHIFT
。cur
要么是最初读取的值,要么是cmpxchg
失败时获取的更新值。通过右移操作,将I_VERSION_QUERIED
标志位移出,返回给调用者一个纯净的版本号。
- 乐观循环: 函数的核心是一个
代码分析
1 | // inode_peek_iversion_raw: “原始地”窥探iversion的值。 |
include/linux/iversion.h Inode版本(i_version)管理:提供可靠的文件变更检测机制
本头文件 linux/iversion.h
定义了Linux内核中用于管理inode
版本号(i_version
)的完整API和底层机制。i_version
是一个64位计数器,其核心目的是提供一个比传统时间戳(mtime
, ctime
)更可靠的方式来检测文件的内容或元数据是否发生了变化。这对于NFS等网络文件系统客户端维护缓存一致性至关重要。该头文件不仅定义了如何查询和比较版本号,还通过一个巧妙的“已查询”标志位,实现了一种按需递增的优化策略,平衡了性能与可靠性。
实现原理分析
i_version
机制的设计围绕几个核心概念展开:
位域封装:
i_version
并不是一个纯粹的64位计数器。它的最低位(bit 0)被用作I_VERSION_QUERIED
标志,而剩下的63位(bits 1-63)才构成实际的版本号。I_VERSION_QUERIED
(1ULL << 0
): 一个标志,表示该i_version
自上次变更以来,是否被inode_query_iversion()
查询过。I_VERSION_INCREMENT
(1ULL << 1
): 版本号的最小增量单位。递增操作实际上是加上这个值,这确保了递增不会影响到标志位。- API隔离: 内核提供两套API:一套是”raw” API(如
inode_peek_iversion_raw
),它操作包含标志位的完整64位值;另一套是面向普通用户的API(如inode_peek_iversion
),它会自动处理标志位的屏蔽和移位,返回一个纯净的版本号。
按需递增优化: 这是该机制的核心优化。
- 当文件发生变更时,内核会调用
inode_maybe_inc_iversion
。 - 此函数会检查
I_VERSION_QUERIED
标志。如果标志未被设置,说明自上次变更后,没有任何进程查询过版本号。因此,即使文件再次变更,我们也可以安全地不递增版本号,因为没有观察者会错过这次更新。这避免了对inode的不必要修改和写回操作。 - 如果标志已被设置,说明有进程正在“观察”这个版本号。此时,任何新的变更都必须递增版本号,以确保观察者能检测到变化。递增后,内核会清除
I_VERSION_QUERIED
标志。
- 当文件发生变更时,内核会调用
查询即标记:
inode_query_iversion
函数(在之前的分析中已详述)在被调用时,会原子地设置I_VERSION_QUERIED
标志。这就是上述优化得以实现的前提。文件系统角色划分:
- 内核管理 (Kernel-managed): 对于设置了
SB_I_VERSION
超级块标志的本地文件系统(如ext4),VFS会自动处理对普通文件的写操作导致的i_version
递增。文件系统自身只需处理目录操作(如创建/删除文件)导致的i_version
变更。 - 自我管理 (Self-managed): 对于NFS、Ceph等网络或分布式文件系统,
i_version
的值由服务器提供,它们不设置SB_I_VERSION
标志。这些文件系统使用”raw” API(如inode_set_iversion_raw
)来直接存取服务器提供的、不透明的版本值。
- 内核管理 (Kernel-managed): 对于设置了
持久化鲁棒性:
- 为了应对系统崩溃,当文件系统从磁盘加载一个
inode
时,它被要求调用inode_set_iversion_queried
。这会假定磁盘上存储的版本号在崩溃前可能已经被查询过,因此强制在加载时就设置I_VERSION_QUERIED
标志,确保下一次变更是可检测的。
- 为了应对系统崩溃,当文件系统从磁盘加载一个
代码分析
1 | // I_VERSION_QUERIED_SHIFT: 定义标志位和版本号数值部分的分界线。版本号左移1位。 |
您的提问非常深刻,触及了该机制设计的核心思想。
1. 这个版本号机制是什么作用?
i_version
机制的核心作用是提供一个高可靠性的、单调递增的“文件变更”信号,以解决传统时间戳(mtime
, ctime
)的粒度问题(Granularity Problem)。
传统时间戳的缺陷:
计算机系统的时间不是连续的,而是由离散的时钟节拍(tick)驱动的。假设一个系统的时钟精度是10毫秒,那么在同一个10毫秒的窗口内,对一个文件进行两次快速的修改,这两次修改的mtime
或ctime
可能是完全相同的。
这会带来致命问题,尤其是在以下场景:
- NFS客户端缓存:NFS客户端为了性能会大量缓存文件数据。当它需要验证缓存是否有效时,会向服务器请求文件的元数据。如果文件的
ctime
没变,客户端就认为它的缓存是有效的。但如果因为粒度问题,服务器上的文件实际已经变了但ctime
没更新,客户端就会继续使用过期的缓存数据,导致数据损坏或程序逻辑错误。
i_version
如何解决这个问题:i_version
是一个简单的64位计数器。内核保证,任何会导致ctime
变更的显式文件修改,都必须导致i_version
的数值发生变化(通常是递增)。因为它是一个整数计数器,不受时钟精度的限制,所以即使在一毫秒内发生一百万次修改,它也能产生一百万个不同的版本号。
总结作用:
它为NFS客户端等“观察者”提供了一个绝对可靠的凭证,来判断文件自上次检查以来是否发生了任何变化,从而完美地解决了缓存一致性问题。
2. 是否是多读少写情况的一种实现方案?
您的直觉非常准确,但这需要更精确地描述。它不是为“多读少写”设计的,而是为**“多写,但少查询版本号”的场景做了极致的优化。它实现的是一种按需更新(On-Demand Update)或惰性更新(Lazy Update)**的策略。
让我们回顾一下I_VERSION_QUERIED
标志位的逻辑:
场景A:无人关心版本号
一个文件被连续修改了100次,但在此期间,没有任何进程调用inode_query_iversion()
来查询它的版本号。
结果:I_VERSION_QUERIED
标志一直为false
。根据inode_maybe_inc_iversion
的逻辑,i_version
计数器一次都不会递增。内核避免了100次对inode
的修改和可能的磁盘写回,性能开销极低。场景B:有人关心版本号
- 一个NFS客户端查询了文件的版本号,得到
V1
。此时,inode_query_iversion()
会将I_VERSION_QUERIED
标志设为true
。 - 文件被修改了第一次。内核检查到
I_VERSION_QUERIED
为true
,必须将版本号递增到V2
,然后清除该标志。 - 文件又被连续修改了99次。在此期间,没有新的查询。
- 结果:由于
I_VERSION_QUERIED
标志在第一次修改后被清除了,后续的99次修改都不会再递增i_version
。版本号将一直保持在V2
。 - NFS客户端再次查询,它会得到
V2
。由于V2 != V1
,它知道文件已过期,就会去服务器重新获取。
- 一个NFS客户端查询了文件的版本号,得到
结论:
这个机制的核心是:只有在“可能存在观察者”的情况下,才付出递增版本号的代价。 它为那些不会被频繁检查版本号的文件(即使这些文件被频繁写入)提供了巨大的性能优化。所以,您的“多读少写”可以理解为“对版本号的多读少写”(即“少查询”)。
3. 这种方案叫啥?
这种方案融合了多种计算机科学中的概念,可以从不同层面来命名:
从实现手法上看:无锁编程 (Lock-Free Programming)
它更新i_version
时使用的do-while
循环配合atomic64_try_cmpxchg
(比较并交换)是一种典型的乐观循环(Optimistic Loop)。它不使用互斥锁来保护数据,而是乐观地假设没有冲突,然后通过原子操作来验证。如果冲突发生,就简单地重试。这是高性能并发编程中的常见模式。从设计策略上看:按需更新 (On-Demand Update) 或 惰性更新 (Lazy Update)
正如上面分析的,版本号并不是在每次修改时都更新,而是在需要被观察到时才更新。这种推迟工作的思想就是惰性或按需策略。从它所属的更大概念上看:乐观并发控制 (Optimistic Concurrency Control - OCC)
在数据库和分布式系统中,OCC是一种非常重要的并发控制思想。其基本流程是:- 读取数据和它的版本号。
- 在本地进行修改(不加锁)。
- 提交更新时,检查服务器上数据的版本号是否仍与当初读取时相同。
- 如果相同,说明没有冲突,提交成功。
- 如果不同,说明有冲突,放弃本次修改并重试。
i_version
机制正是为实现这种OCC模式提供了最关键的组件——那个可靠的版本号。因此,可以说整个i_version
机制是乐观并发控制思想在文件系统层面的一个关键实现。
您的这个问题非常精准,完全抓住了i_version
机制最核心、最巧妙的设计思想!
您的理解非常接近,但有一个关键的时间点需要精确一下。i_version
的写入修改不是发生在被查询的那个时刻,而是在被查询之后的下一次修改时刻。
让我们把这个逻辑链条梳理得更清晰一些:
4. 精确机制:查询是“预告”,修改是“执行”
可以把i_version
的I_VERSION_QUERIED
标志位想象成一个“有人在看,下次修改请留痕迹”的通知牌。
初始状态:文件有一个版本号(比如
100
),通知牌是放下的(I_VERSION_QUERIED = false
)。发生N次修改:
- 文件被修改了第一次、第二次…第N次。
- 在每一次修改前,内核都会去看一下那个通知牌。
- 内核发现通知牌是放下的,它就认为“没人关心版本号”,于是它什么都不做。
- 结果:经过N次修改,文件的版本号完全没有改变,仍然是
100
。这是关键的性能优化,避免了不必要的写操作。
发生一次查询:
- 一个NFS客户端(或其他程序)调用了
inode_query_iversion()
来查询版本号。 - 这个函数做了两件事:
a. 读取当前的版本号100
,并准备把它返回给客户端。
b. 原子地把通知牌“竖起来”(将I_VERSION_QUERIED
标志设为true
)。 - 结果:客户端得到了版本号
100
并记录下来。现在内核里,文件的版本号仍然是100
,但通知牌是竖起来的。
- 一个NFS客户端(或其他程序)调用了
查询后的第一次修改:
- 文件又被修改了(这是第N+1次修改)。
- 在修改前,内核又去看了那个通知牌。
- 内核发现通知牌是“竖起来”的! 它立刻明白:“有人在观察!这次修改必须留下痕迹!”
- 于是内核做了两件事:
a. 执行写入修改:将版本号从100
递增到102
(增量是2,以避开标志位)。
b. 再次把通知牌“放下”(清除I_VERSION_QUERIED
标志)。 - 结果:文件的版本号现在是
102
,通知牌又回到了放下的状态,等待下一次查询。
结论
所以,您的描述可以精确地修正为:
一个文件经过N次修改,
i_version
本身不会改变。直到它被查询一次之后,在下一次(即第N+1次)修改发生时,i_version
才会被真正地写入修改(递增一次),并且这个“通知”机制会重置,等待下一次查询。
这正是这种按需更新 (On-Demand Update) 或 惰性更新 (Lazy Update) 方案的精髓所在:只在绝对必要时(即有人观察时)才付出更新的代价。