轉:
今天被朋友問及“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 <stdio.h>
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 <stdio.h>
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)。