我在开发时经常遇到这样的情况,我正在运行一个二进制文件,比如a.out
在后台运行,因为它做了一些冗长的工作。在此过程中,我对生成a.out
并再次编译的 C 代码进行了更改a.out
。到目前为止,我还没有遇到任何问题。正在运行的进程a.out
继续正常运行,不会崩溃,并且始终运行最初启动时的旧代码。
然而,假设a.out
这是一个巨大的文件,可能与 RAM 的大小相当。在这种情况下会发生什么?假设它链接到共享对象文件,libblas.so
如果我libblas.so
在运行时修改怎么办?会发生什么?
我的主要问题是 - 操作系统是否保证当我运行时a.out
,原始代码将始终正常运行,按照原来的二进制文件,无论它链接到的二进制文件或文件的大小如何.so
,即使这些文件.o
和.so
文件在运行时被修改?
我知道这些问题可以解决类似的问题: https://stackoverflow.com/questions/8506865/when-a-binary-file-runs-does-it-copy-its-entire-binary-data-into-memory-at-once 如果在执行期间编辑脚本会发生什么? 如何在程序运行时进行实时更新?
这帮助我更多地了解了这一点,但我不认为他们到底在问我想要什么,这是后果的一般规则在执行期间修改二进制文件
答案1
虽然 Stack Overflow 问题一开始似乎已经足够了,但从你的评论中我明白为什么你可能仍然对此有疑问。对我来说,这正是这样的危急时刻当两个 UNIX 子系统(进程和文件)通信时涉及到。
您可能知道,UNIX 系统通常分为两个子系统:文件子系统和进程子系统。现在,除非通过系统调用另有指示,否则内核不应让这两个子系统相互交互。然而有一个例外:将可执行文件加载到进程中'文本区域。当然,有人可能会说这个操作也是由系统调用(execve
)触发的,但这通常被认为是一进程子系统向文件子系统发出隐式请求的情况。
因为进程子系统自然无法处理文件(否则将整个事情分成两部分就没有意义),所以它必须使用文件子系统提供的任何内容来访问文件。这也意味着进程子系统将服从文件子系统针对文件编辑/删除采取的任何措施。关于这一点,我建议阅读吉尔斯的回答到这个 U&L 问题。我的其余答案基于吉尔斯的这一更笼统的答案。
首先应该注意的是,在内部,文件只能通过索引节点。如果给内核一个路径,它的第一步将是将其转换为用于所有其他操作的索引节点。当进程将可执行文件加载到内存中时,它通过其索引节点来执行此操作,该索引节点是在路径转换后由文件子系统提供的。索引节点可能与多个路径(链接)相关联,并且程序只能删除链接。为了删除文件及其索引节点,用户态必须删除该索引节点的所有现有链接,并确保它完全未使用。当满足这些条件时,内核将自动从磁盘删除该文件。
如果你看一下替换可执行文件吉尔斯答案的一部分,你会看到这取决于你如何编辑/删除对于文件,内核将以不同的方式做出反应/适应,始终通过文件子系统内实现的机制。
- 如果您尝试策略一(打开/截断为零/写入或者打开/写入/截断到新大小),你会发现内核不会费心处理你的请求。你会得到一个错误 26:文本文件忙(
ETXTBSY
)。没有任何后果。 - 如果您尝试策略二,第一步是删除可执行文件。然而,由于它正在被进程使用,文件子系统将启动并阻止文件(及其索引节点)被确实从磁盘中删除。从这一点来看,访问旧文件内容的唯一方法是通过其 inode 来完成,这就是进程子系统每当需要将新数据加载到其中时所做的事情文本部分(在内部,使用路径是没有意义的,除非将它们转换为索引节点)。即使你已经未链接的文件(删除了它的所有路径),该进程仍然可以使用它,就好像您什么也没做一样。使用旧路径创建新文件不会改变任何内容:新文件将被赋予一个全新的索引节点,而正在运行的进程对此一无所知。
策略 2 和 3 对于可执行文件也是安全的:尽管运行的可执行文件(和动态加载的库)在具有文件描述符的意义上不是打开文件,但它们的行为方式非常相似。只要某个程序正在运行该代码,即使没有目录条目,该文件也会保留在磁盘上。
- 策略三非常相似,因为该
mv
操作是原子操作。这可能需要使用rename
系统调用,并且由于进程在内核模式下无法中断,因此在完成(成功与否)之前没有任何东西可以干扰此操作。同样,旧文件的 inode 没有更改:创建了一个新文件,并且已在运行的进程不会知道它,即使它已与旧 inode 的链接之一关联。
使用策略 3,将新文件移动到现有名称的步骤会删除通向旧内容的目录条目,并创建通向新内容的目录条目。这是在一个原子操作中完成的,因此该策略具有一个主要优点:如果进程在任何时候打开文件,它将看到旧内容或新内容 - 不存在获得混合内容或文件不存在的风险。现存的。
重新编译文件:使用时gcc
(对于许多其他编译器的行为可能类似),您正在使用策略 2。您可以通过运行strace
编译器的一个进程来看到这一点:
stat("a.out", {st_mode=S_IFREG|0750, st_size=8511, ...}) = 0
unlink("a.out") = 0
open("a.out", O_RDWR|O_CREAT|O_TRUNC, 0666) = 3
chmod("a.out", 0750) = 0
- 编译器通过
stat
和lstat
系统调用检测到该文件已经存在。 - 该文件是未链接的。在这里,虽然不再可以通过 name 访问它
a.out
,但只要已在运行的进程正在使用它们,它的 inode 和内容就会保留在磁盘上。 - 创建一个新文件并以名称 赋予可执行权限
a.out
。这是一个全新的 inode 和全新的内容,已运行的进程无需关心这些内容。
现在,当涉及到共享库时,同样的行为也将适用。只要一个库对象被进程使用,它就不会从磁盘中删除,无论您如何更改其链接。每当需要将某些内容加载到内存中时,内核都会通过文件的索引节点来完成此操作,因此会忽略对其链接所做的更改(例如将它们与新文件关联)。
答案2
我的理解是,由于正在运行的进程的内存映射,内核不允许更新映射文件的保留部分。我想如果一个进程正在运行,那么它的所有文件都会被保留,因此会更新它,因为您编译了源代码的新版本实际上会导致创建一组新的索引节点。简而言之,旧版本的可执行文件仍然可以通过页面错误事件在磁盘上进行访问。因此,即使您更新一个巨大的文件,它应该保持可访问并且内核应该只要进程正在运行,仍然会看到未更改的版本。原始文件 inode不应该只要进程正在运行就可以重复使用。
这当然有待证实。
答案3
替换 .jar 文件时情况并非总是如此。在程序显式请求信息之前,不会从磁盘读取 Jar 资源和一些运行时反射类加载器。
这只是一个问题,因为 jar 只是一个存档,而不是映射到内存中的单个可执行文件。这有点偏离主题,但仍然是你的问题的一个分支,也是我搬起石头砸自己脚的事情。
所以对于可执行文件:是的。对于 jar 文件:也许(取决于实现)。