我知道当页面缓存页面被修改时,它会被标记为脏并需要写回,但是当出现以下情况时会发生什么:
设想: 文件 /apps/EXE 是一个可执行文件,被完全分页到页缓存中(它的所有页都在缓存/内存中)并由进程 P 执行
然后,持续发布将 /apps/EXE 替换为全新的可执行文件。
假设1: 我假设进程 P(以及具有引用旧可执行文件的文件描述符的任何其他人)将继续使用旧的内存 /apps/EXE 而不出现问题,并且尝试执行该路径的任何新进程都将获得新的可执行文件。
假设2: 我假设,如果不是文件的所有页面都映射到内存中,那么一切都会好起来,直到出现页面错误,需要文件中已被替换的页面,并且可能会发生段错误?
问题一: 如果您使用诸如 vmtouch 之类的东西 mlock 文件的所有页面,这会改变情况吗?
问题2: 如果 /apps/EXE 位于远程 NFS 上,会有什么不同吗? (我假设不是)
请纠正或验证我的两个假设并回答我的两个问题。
假设这是一个带有某种 3.10.0-957.el7 内核的 CentOS 7.6 机器
更新:进一步思考,我想知道这种情况是否与任何其他脏页情况没有什么不同。
我想写入新二进制文件的进程将进行读取并获取所有缓存页面,因为它已全部分页,然后所有这些页面将被标记为脏。如果它们被mlocked,那么在引用计数变为零后,它们将只是占用核心内存的无用页面。
我怀疑当当前正在执行的程序结束时,其他任何程序都会使用新的二进制文件。假设这一切都是正确的,我想只有当只有部分文件被分页时才有意义。
答案1
然后,持续发布将 /apps/EXE 替换为全新的可执行文件。
这是重要的部分。
发布新文件的方式是创建一个新文件(例如/apps/EXE.tmp.20190907080000
),写入内容,设置权限和所有权以及最后将其重命名(2)为最终名称/apps/EXE
,替换旧文件。
结果是新文件具有新的索引节点号(这意味着,实际上,它是一个不同的文件。)
并且旧文件有自己的索引节点号,实际上是犹在即使文件名不再指向它(或者不再有文件名指向该索引节点。)
因此,这里的关键是,当我们在 Linux 中谈论“文件”时,我们通常真正谈论的是“inode”,因为一旦打开文件,inode 就是我们保留的文件引用。
假设1:我假设进程 P(以及具有引用旧可执行文件的文件描述符的任何其他人)将继续使用旧的内存 /apps/EXE 而不出现问题,并且尝试执行该路径的任何新进程都将获得新的可执行的。
正确的。
假设2:我假设,如果不是文件的所有页面都映射到内存中,那么一切都会好起来,直到出现页面错误,需要文件中已替换的页面,并且可能会发生段错误?
不正确。旧的索引节点仍然存在,因此使用旧二进制文件的进程中的页面错误仍然能够在磁盘上找到这些页面。
您可以通过查看运行旧二进制文件的进程的/proc/${pid}/exe
符号链接(或等效地,输出)来看到这种现象的一些影响,它将显示名称不再存在,但 inode 仍然存在。lsof
/app/EXE (deleted)
您还可以看到,二进制文件使用的磁盘空间只会在进程终止后才会被释放(假设它是唯一打开该 inode 的进程)。检查df
终止进程之前和之后的输出,您会看到它的大小下降了您认为不再存在的旧二进制文件。
顺便说一句,这不仅适用于二进制文件,还适用于任何打开的文件。如果您在进程中打开文件并删除该文件,则该文件将保留在磁盘上,直到该进程关闭该文件(或终止)。与硬链接如何保存指向磁盘中 inode 的名称的计数器类似,文件系统驱动程序(在 Linux 内核中)保留一个计数器,记录该 inode 存在多少个引用在记忆中,并且只有在运行系统的所有引用都被释放后,才会从磁盘释放 inode。
问题1:如果您使用 vmtouch 之类的工具锁定文件的所有页面,会改变情况吗
这个问题基于错误的假设2,即不锁定页面会导致段错误。不会的。
问题2:如果 /apps/EXE 位于远程 NFS 上,会有什么不同吗? (我假设不是)
它是意思是大部分时间都以同样的方式工作,但是 NFS 存在一些“陷阱”。
有时,您可以看到删除 NFS 中仍打开的文件(在该目录中显示为隐藏文件。)
您还可以通过某种方式为 NFS 导出分配设备编号,以确保 NFS 服务器重新启动时这些设备编号不会被“重新洗牌”。
但主要思想是一样的。 NFS 客户端驱动程序仍使用 inode,并在仍引用 inode 时尝试保留文件(在服务器上)。
答案2
假设 2:我假设如果不是文件的所有页面都映射到内存中,那么一切都会很好,直到出现页面错误,需要文件中已替换的页面,并且可能会发生段错误?
不,这不会发生,因为内核不会让您打开并替换当前正在执行的文件中的任何内容。这样的操作将会失败ETXTBSY
[1]:
cp /bin/sleep sleep; ./sleep 3600 & echo none > ./sleep
[9] 5332
bash: ./sleep: Text file busy
当 dpkg 等更新二进制文件时,它不会覆盖它,而是使用rename(2)
它简单地将目录条目指向完全不同的文件,并且仍然具有旧文件的映射或打开句柄的任何进程将继续使用它而不会出现问题。
[1]保护ETXBUSY
不会扩展到也可以被视为“文本”(=实时代码/可执行文件)的其他文件:共享库、java 类等;在另一个进程映射时修改此类文件将要导致进程崩溃。在 Linux 上,动态链接器尽职尽责地将MAP_DENYWRITE
标志传递给mmap(2)
,但不要搞错——它没有任何效果。例子:
$ cc -xc - <<<'void lib(){}' -shared -o lib.so
$ cc -Wl,-rpath=. lib.so -include unistd.h -xc - <<<'
extern void lib();
int main(){ truncate("lib.so", 0); lib(); }
'
./a.out
Bus error
答案3
filbranden 的答案是正确的,假设连续发布过程通过rename
.如果没有,但就地修改了文件,情况就会有所不同。然而你的思维模式仍然是错误的。
磁盘上的内容不可能被修改并且与页面缓存不一致,因为页面缓存是规范版本以及经过修改的那个。对文件的任何写入都是通过页面缓存进行的。如果它已经存在,则现有页面将被修改。如果它还不存在,尝试修改部分页面将导致整个页面被缓存,然后进行修改,就好像它已经被缓存一样。跨越整个页面或更多页面的写入可以(并且几乎肯定会)优化对它们进行分页的读取步骤。在任何情况下,存在的文件只有一个规范的可修改版本(*),即页面缓存中的版本。
(*) 我稍微撒了谎。对于 NFS 和其他远程文件系统,可能有不止一种,并且它们通常(取决于使用哪一种以及使用的挂载和服务器端选项)无法正确实现写入的原子性和排序语义。这就是为什么我们很多人认为它们从根本上被破坏了,并且拒绝在使用时并发写入的情况下使用它们。