Linux下可以替換執行中的程式麼?

一見藍天發表於2009-01-04

今天被朋友問及“Linux下可以替換執行中的程式麼?”,以前依稀記得Linux下是可以的(而Windows就不讓),於是隨口答道“OK”。結果朋友發來一個執行結果:(test正在執行中)

# cp test2 test
cp: cannot create regular file `test`: Text file busy

看起來是程式被佔用,無法覆蓋。於是自己又再做了幾個實驗:

(1)先rm刪除正在執行的test,然後cp test2 test就沒有錯誤了。
(2)先mv改名正在執行的test,然後cp test2 test也沒有問題。

查了查資料並動手分析了一下,找到了比較滿意的解釋。cp並不改變目標檔案的inode,事實上它的實現是這樣的:

# strace cp test2 test  2>&1 | grep open.*test
open("test2", O_RDONLY|O_LARGEFILE)     = 3
open("test", O_WRONLY|O_TRUNC|O_LARGEFILE) = 4

我原以為cp的實現是“rm + open(O_CREAT)”,不過現在想想上面的實現方式才是最可靠的(保證了時序安全和目標檔案的屬性)。這也可以解釋為什麼cp的目標檔案會繼承被覆蓋檔案的屬性而非原始檔。

Linux由於Demand Paging機制的關係,必須確保正在執行中的程式映象(注意,並非檔案本身)不被意外修改,因此核心在啟動程式後會鎖定這個程式映象的inode。這就是為什麼cp在用“O_WRONLY|O_TRUNC”模式open目標檔案時會失敗。而先rm再cp的話,新檔案的inode其實已經改變了,原inode並沒有被真正刪除,直到核心釋放對它的引用。同理,mv只是改變了檔名,其inode不變,新檔案使用了新的inode。

問題到這裡已經水落石出,不過刨根究底的個性驅使我再做了以下一組實驗,沒想到結果完全出乎我意料之外!

寫了一個簡單的測試程式:

#include 

int main(int argc, char * argv[])
{
    foo();  // An export function by libtest.so.
    sleep(1000);
    return 0;
}

foo()是另一個測試動態庫libtest.so的匯出介面,只列印一行提示就返回。接下來我把上面對執行檔案的測試用例對動態庫又做了一遍:

(1)cp libtest2.so libtest.so可以直接覆蓋已載入的動態庫。
(2)先rm刪除已載入的libtest.so,然後cp libtest2.so libtest.so成功。
(3)先mv改名已載入的libtest.so,然後cp libtest2.so libtest.so成功。

除了第一個用例外,結果相同。這樣看來,動態庫被載入時難道ld並沒有鎖定inode?不過想想也可以寬恕,畢竟ld也是使用者態程式,沒有權利去鎖定inode,也不應與核心的檔案系統底層實現耦合。

到這裡都還算在情理之中,看起來Linux也都處理的很好。不過還剩下一個問題:動態庫被以cp的方式覆蓋後難道不會和Demand Paging機制產生衝突?

在思考這個問題的過程中,我意識到前面這個測試程式的一個致命漏洞,稍作修改如下:

#include 

int main(int argc, char * argv[])
{
loop:
    foo();  // An export function by libtest.so.
    sleep(1);
    goto loop;
    return 0;
}

這次,再執行上面的三個用例後發現,“cp libtest2.so libtest.so”雖然仍可直接覆蓋已載入的動態庫,但是測試程式馬上出現了“Segmentation fault”。而後兩個用例結果不變。由此可見,想要安全的替換已載入的動態庫,還是用“笨拙”的“rm + cp”吧,看似捷徑的“cp覆蓋”會直接葬送掉你的程式……

看來,我再一次低估了Linux的健壯性,看似符合邏輯的流程也可能會帶來災難性的後果;“rm & cp”與“cp覆蓋”背後所隱藏的底層差異卻可以成為你的救星。Linux用得越久越是讓人覺得這是一塊充滿了荊棘和陷阱的原始叢林,只有步步為營實踏前行才能走的更遠。

注:以上實驗基於SuSE Linux Enterprise Server 9 SP1(Linux 2.6.5 & glibc 2.3.3)。


相關文章