我想知道 Thunderbird 或 Firefox 等杀手级应用程序如何在运行时通过系统的包管理器进行更新。更新旧代码时会发生什么?当我想编写一个在运行时更新自身的程序 a.out 时,我该怎么办?
答案1
一般替换文件
首先,替换文件有几种策略:
打开现有的写入文件,将其截断为0长度,并写入新的内容。 (一种不太常见的变体是打开现有文件,用新内容覆盖旧内容,如果文件较短,则将文件截断为新长度。)用 shell 术语来说:
echo 'new content' >somefile
消除旧文件,并创建一个同名的新文件。用外壳术语来说:
rm somefile echo 'new content' >somefile
以临时名称写入新文件,然后移动将新文件更改为现有名称。该移动会删除旧文件。用外壳术语来说:
echo 'new content' >somefile.new mv somefile.new somefile
我不会列出这些策略之间的所有差异,我只会提到一些重要的差异。使用策略 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 .
对于策略 3,将新文件移动到现有名称的步骤会删除通向旧内容的目录条目,并创建通向新内容的目录条目。这是在一个原子操作中完成的,因此该策略具有一个主要优点:如果进程在任何时候打开文件,它将看到旧内容或新内容 - 不存在获得混合内容或文件不存在的风险。现存的。
替换可执行文件
如果您在 Linux 上使用正在运行的可执行文件尝试策略 1,您将收到错误。
cp /bin/sleep .
./sleep 999999 &
echo oops >|sleep
bash: sleep: Text file busy
“文本文件”是指包含可执行代码的文件由于不为人知的历史原因。 Linux 与许多其他 Unix 变体一样,拒绝覆盖正在运行的程序的代码;一些 UNIX 变体允许这样做,从而导致崩溃,除非新代码是对旧代码的彻底修改。
在 Linux 上,您可以覆盖动态加载库的代码。它可能会导致使用它的程序崩溃。 (您可能无法观察到这一点,sleep
因为它在启动时加载了所需的所有库代码。尝试一个更复杂的程序,该程序在睡眠后执行一些有用的操作,例如perl -e 'sleep 9; print lc $ARGV[0]'
。)
如果解释器正在运行脚本,则解释器会以普通方式打开脚本文件,因此无法防止覆盖脚本。一些解释器在开始执行第一行之前读取并解析整个脚本,另一些解释器则根据需要读取脚本。看如果在执行期间编辑脚本会发生什么?和Linux 如何处理 shell 脚本?更多细节。
策略 2 和 3 对于可执行文件也是安全的:尽管运行的可执行文件(和动态加载的库)在具有文件描述符的意义上不是打开文件,但它们的行为方式非常相似。只要某个程序正在运行该代码,即使没有目录条目,该文件也会保留在磁盘上。
升级应用程序
大多数包管理器使用策略 3 来替换文件,因为它有上面提到的主要优点——在任何时间点,打开文件都会得到它的有效版本。
应用程序升级可能会出现问题,虽然升级一个文件是原子性的,但如果应用程序由多个文件(程序、库、数据等)组成,则整个应用程序的升级就不是原子性的了。考虑以下事件顺序:
- 应用程序的实例已启动。
- 应用程序已升级。
- 正在运行的实例应用程序打开其数据文件之一。
在步骤 3 中,旧版本应用程序的运行实例正在打开新版本的数据文件。这是否有效取决于应用程序、它是哪个文件以及文件被修改了多少。
升级后,您会注意到旧程序仍在运行。如果要运行新版本,则必须退出旧程序并运行新版本。包管理器通常会在升级时终止并重新启动守护进程,但不影响最终用户应用程序。
一些守护进程有特殊的过程来处理升级,而不必终止守护进程并等待新实例重新启动(这会导致服务中断)。在以下情况下这是必要的在里面,无法被杀死; init 系统提供了一种请求正在运行的实例调用的方法execve
将其自身替换为新版本。
答案2
升级可以在程序运行的同时运行,但你看到的运行程序实际上是它的旧版本。旧的二进制文件保留在磁盘上,直到您关闭程序。
解释:在Linux系统上,一个文件只是一个inode,它可以有多个链接。例如。您看到的只是我系统上/bin/bash
的链接。您可以通过在链接上inode 3932163
发出命令来找到链接到哪个索引节点。ls --inode /path
仅当有零个链接指向文件(索引节点)且未被任何程序使用时,才会删除该文件(索引节点)。当包管理器升级时,例如。/usr/bin/firefox
,它首先取消链接(删除硬链接/usr/bin/firefox
),然后创建一个名为该文件的新文件/usr/bin/firefox
,该文件是到不同 inode(包含新版本的 inode firefox
)的硬链接。旧的索引节点现在被标记为空闲,可以重新用于存储新数据,但仍保留在磁盘上(索引节点仅在构建文件系统时创建,并且永远不会被删除)。下次启动时firefox
,将使用新的。
如果你想编写一个在运行时“升级”自身的程序,我能想到的唯一可能的解决方案是定期检查其自身二进制文件的时间戳,如果它比程序的启动时间新,则重新加载自身。
答案3
我想知道像 Thunderbird 或 Firefox 这样的杀手级应用程序如何在运行时通过系统的包管理器进行更新? 好吧,我可以告诉你,这并不能很好地工作......如果我在软件包更新运行时让 Firefox 打开,我会遇到非常严重的故障。有时我不得不强行杀死它并重新启动它,因为它太坏了我什至无法正确关闭它。
更新旧代码时会发生什么? 通常在 Linux 上,程序会加载到内存中,因此在程序运行时不需要或使用磁盘上的可执行文件。事实上,您甚至可以删除可执行文件,程序不应该关心...但是,某些程序可能需要可执行文件,并且某些操作系统(例如 Windows)将锁定可执行文件,防止删除甚至重命名/移动,而程序正在运行。 Firefox 崩溃了,因为它实际上相当复杂,并且使用一堆数据文件来告诉它如何构建 GUI(用户界面)。在软件包更新期间,这些文件会被覆盖(更新),因此当较旧的 Firefox 可执行文件(在内存中)尝试使用新的 GUI 文件时,可能会发生奇怪的事情......
当我想编写一个在运行时更新自身的程序 a.out 时,我该怎么办? 你的问题已经有很多答案了。看一下这个: https://stackoverflow.com/questions/232347/how-should-i-implement-an-auto-updater 顺便说一句,关于编程的问题最好在 StackOverflow 上问。