如何在程式運行時進行即時更新?

如何在程式運行時進行即時更新?

我想知道 Thunderbird 或 Firefox 等殺手級應用程式如何在運行時透過系統的套件管理器進行更新。更新舊程式碼時會發生什麼事?當我想寫一個在運行時更新自身的程式 a.out 時,我該怎麼辦?

答案1

一般替換文件

首先,替換文件有幾種策略:

  1. 打開現有的寫入文件,將其截斷為0長度,並寫入新的內容。 (一種不太常見的變體是開啟現有文件,用新內容覆蓋舊內容,如果文件較短,則將文件截斷為新長度。)用 shell 術語來說:

    echo 'new content' >somefile
    
  2. 消除舊文件,並建立一個同名的新文件。用殼術語來說:

    rm somefile
    echo 'new content' >somefile
    
  3. 以臨時名稱寫入新文件,然後移動將新文件更改為現有名稱。該移動會刪除舊檔案。用殼術語來說:

    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 來取代文件,因為它有上面提到的主要優點——在任何時間點,開啟文件都會得到它的有效版本。

應用程式升級可能會出現問題,雖然升級一個檔案是原子性的,但如果應用程式由多個檔案(程式、函式庫、資料等)組成,則整個應用程式的升級就不是原子性的了。考慮以下事件順序:

  1. 應用程式的實例已啟動。
  2. 應用程式已升級。
  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 上問。

相關內容