如何在程序运行时进行实时更新?

ubu*_*lex 22 executable upgrade files

我想知道像 Thunderbird 或 Firefox 这样的杀手级应用程序如何在它们仍在运行时通过系统的包管理器进行更新。旧代码在更新时会发生什么?当我想编写一个在运行时自我更新的程序 a.out 时,我必须做什么?

Gil*_*il' 28

一般替换文件

首先,有几种替换文件的策略:

  1. 打开已有的文件进行写入,将其截断为0长度,写入新的内容。(一个不太常见的变体是打开现有文件,用新内容覆盖旧内容,如果文件较短,则将文件截断为新长度。)在 shell 术语中:

    echo 'new content' >somefile
    
    Run Code Online (Sandbox Code Playgroud)
  2. 删除旧文件,并创建一个同名的新文件。在外壳术语中:

    rm somefile
    echo 'new content' >somefile
    
    Run Code Online (Sandbox Code Playgroud)
  3. 以临时名称写入新文件,然后新文件移动到现有名称。移动删除旧文件。在外壳术语中:

    echo 'new content' >somefile.new
    mv somefile.new somefile
    
    Run Code Online (Sandbox Code Playgroud)

我不会列出这些策略之间的所有差异,我只会在这里提及一些重要的差异。使用状态 1,如果任何进程当前正在使用该文件,则该进程会在更新内容时看到新内容。如果进程希望文件内容保持不变,这可能会导致一些混乱。请注意,这只是关于已打开的文件(如看见在过程lsof或;也有一个文件打开交互式应用(如打开在编辑器中的文件)通常不会保持文件打开,它们的加载过程中的文件内容“打开文档”操作,它们在“保存文档”操作期间替换文件(使用上述策略之一)。/proc/PID/fd/

使用策略 2 和 3,如果某个进程somefile打开了文件,则在内容升级期间旧文件保持打开状态。使用策略2,删除文件的步骤实际上只是删除了目录中的文件条目。文件本身只有在没有指向它的目录条目时才会被删除(在典型的 Unix 文件系统上,同一个文件可以有多个目录条目并且没有进程打开它。这是观察这一点的一种方法 - 只有在sleep进程被终止时才会删除文件(rm仅删除其目录条目)。

echo 'old content' >somefile
sleep 9999999 <somefile &
df .
rm somefile
df .
cat /proc/$!/fd/0
kill $!
df .
Run Code Online (Sandbox Code Playgroud)

使用策略 3,将新文件移动到现有名称的步骤将删除通向旧内容的目录条目并创建通向新内容的目录条目。这是在一个原子操作中完成的,所以这个策略有一个主要的优点:如果一个进程在任何时候打开文件,它要么看到旧内容,要么看到新内容——没有得到混合内容或文件没有的风险现存的。

替换可执行文件

如果您在 Linux 上使用正在运行的可执行文件尝试策略 1,则会出现错误。

cp /bin/sleep .
./sleep 999999 &
echo oops >|sleep
bash: sleep: Text file busy
Run Code Online (Sandbox Code Playgroud)

“文本文件”是指由于历史原因不明而包含可执行代码的文件。与许多其他 Unix 变体一样,Linux 拒绝覆盖正在运行的程序的代码;一些 unix 变体允许这样做,除非新代码是对旧代码的非常好的修改,否则会导致崩溃。

在 Linux 上,您可以覆盖动态加载的库的代码。它可能会导致正在使用它的程序崩溃。(你可能无法观察到这一点,sleep因为它在启动时加载了它需要的所有库代码。尝试一个更复杂的程序,在睡眠后做一些有用的事情,比如perl -e 'sleep 9; print lc $ARGV[0]'。)

如果解释器正在运行脚本,则解释器以普通方式打开脚本文件,因此没有防止覆盖脚本的保护。一些解释器在开始执行第一行之前读取并解析整个脚本,其他解释器根据需要读取脚本。请参阅如果在执行期间编辑脚本会发生什么?以及Linux 如何处理 shell 脚本?更多细节。

策略 2 和 3 对可执行文件也是安全的:虽然运行的可执行文件(和动态加载的库)在具有文件描述符的意义上不是打开的文件,但它们的行为方式非常相似。只要某个程序正在运行代码,即使没有目录条目,该文件也会保留在磁盘上。

升级应用程序

大多数包管理器使用策略 3 来替换文件,因为上面提到的主要优势——在任何时候,打开文件都会导致它的有效版本。

应用程序升级可能会中断的地方在于,虽然升级一个文件是原子性的,但如果应用程序由多个文件(程序、库、数据……)组成,则升级整个应用程序并非如此。考虑以下事件序列:

  1. 启动应用程序的一个实例。
  2. 应用程序已升级。
  3. 正在运行的实例应用程序打开其数据文件之一。

在步骤 3 中,旧版本应用程序的运行实例正在打开新版本的数据文件。这是否有效取决于应用程序,它是哪个文件以及文件被修改了多少。

升级后,您会注意到旧程序仍在运行。如果要运行新版本,则必须退出旧程序并运行新版本。包管理器通常会在升级时终止并重新启动守护进程,但不会影响最终用户应用程序。

一些守护进程有特殊的程序来处理升级,而不必终止守护进程并等待新实例重新启动(这会导致服务中断)。这在init的情况下是必要的,它不能被杀死;init 系统提供了一种方法来请求正在运行的实例调用execve将自身替换为新版本。


psi*_*mon 5

升级可以在程序运行的同时运行,但你看到的运行程序实际上是它的旧版本。旧的二进制文件保留在磁盘上,直到您关闭程序。

说明:在Linux系统上,一个文件只是一个inode,它可以有多个链接。例如。您/bin/bash看到的只是我系统上的链接inode 3932163ls --inode /path您可以通过在链接上发出命令来找到链接到哪个索引节点。仅当有零个链接指向文件(索引节点)且未被任何程序使用时,才会删除该文件(索引节点)。当包管理器升级时,例如。/usr/bin/firefox,它首先取消链接(删除硬链接/usr/bin/firefox),然后创建一个名为该文件的新文件,/usr/bin/firefox该文件是到不同 inode(包含新firefox版本的 inode)的硬链接。旧的索引节点现在被标记为空闲,可以重新用于存储新数据,但仍保留在磁盘上(索引节点仅在构建文件系统时创建,并且永远不会被删除)。下次启动时firefox,将使用新的。

如果你想编写一个在运行时“升级”自身的程序,我能想到的唯一可能的解决方案是定期检查其自身二进制文件的时间戳,如果它比程序的启动时间新,则重新加载自身。

  • APT(或者更确切地说,“dpkg”)不会覆盖文件。相反,它会取消它们的链接,并将新的链接放在同一名称下。请参阅我链接到的问题和答案以获取解释。 (2认同)
  • 这不仅仅是因为它仍在 RAM 中,而且还在磁盘上。直到程序的最后一个实例退出后,该文件才会真正被删除。 (2认同)
  • 该网站不允许我提出编辑建议(一旦您的代表足够高,您只需进行编辑,就不能再提出建议)。因此,作为评论:Unix 系统上的文件(inode)通常有一个名称。但如果你用“ln”(硬链接)添加名称,它可以有更多。您可以使用“rm”(取消链接)删除名称。您实际上不能直接删除文件,只能删除其名称。当文件没有名称并且未打开时,内核将删除它。正在运行的程序会打开正在运行的文件,因此即使删除所有名称后,该文件仍然存在。 (2认同)