為什麼解釋型的腳本語言(如Lua、Python)可以熱更新,而編譯型的語言(如C、C++)不能呢?
本人知道怎麼熱更新腳本語言,但不清楚為什麼腳本可以做到而c/c++不可以,求大牛們解釋一下背後原理
不問是不是就問為什麼系列。Lua/Python等腳本可當作數據,重新讀取這些數據並編譯、解譯它們,比較簡單。C/C++也可以熱更新,例如在Windows上是可利用LoadLibrary()和FreeLibrary()實現。現實例子是Unreal引擎4用這種方式使用C++做「腳本」。
C/C++ 在理論上可以熱更新,實際上,或者說在更嚴格的理論上是無法熱更新的。
說「在更嚴格的理論上」是因為 C/C++ 的 runtime 標準里沒有 compiler/linker/loader。而 Lua/Python 的 runtime 就是直接集成 compiler/loader 的。所以 Lua/Python 的熱更新代碼是完全符合標準的。而 C/C++ 寫出這樣的代碼就是就是不合標準的。
實際上當然可以採用操作系統的特殊方式來通過特定的 loader 來更新。
不過以上討論的熱更新是指在一個進程內更新代碼。實際中,需要熱更新的系統都是多進程集群。只要輪流停運更新可執行文件就行了。有那個功夫寫 OS-specific 的進程內熱更新代碼,還不如解決集群可靠性的時候順手就把熱更新的功能加進去。C++也可以,把需要hot swapping的部分放入動態鏈接庫,用dlsym讀出具體實現的函數指針,創建wrapper調用函數指針。hot swapping時重新載入.so,重新設置函數指針。Linux下的例子:
////// a.cc
#include &
#include &
#include &
void *hb, *hc;
void *ptr_foo, *ptr_bar;
// wrapper of function implemented in b.so
int foo() { return ((typeof(foo))ptr_foo)(); }
// wrapper of function implemented in c.so
int bar() { return ((typeof(bar))ptr_bar)(); }
void reload()
{
if (hb) dlclose(hb);
if (hc) dlclose(hc);
hb = dlopen("./b.so", RTLD_LAZY | RTLD_GLOBAL);
hc = dlopen("./c.so", RTLD_LAZY | RTLD_GLOBAL);
ptr_foo = dlsym(hb, "foo_impl");
ptr_bar = dlsym(hc, "bar_impl");
}
void sigint(int)
{
reload();
}
int used_by_b_so() { return 1; } // -rdynamic to export to dynamic symbol table
int main()
{
signal(SIGINT, sigint);
reload();
printf("foo()=%d bar()=%d
", foo(), bar());
// wait for changes of b.so/c.so
sleep(100); // would be interrupted by SIGINT
puts("after reload");
printf("foo()=%d bar()=%d
", foo(), bar());
}
////// b.cc
int used_by_b_so(); // from main program
extern "C" int foo_impl() { return 0x12131213 + used_by_b_so(); }
////// c.cc
// functions implemented in other libraries should be called indirectly
int foo();
extern "C" int bar_impl() { return foo() + 13; }
# Makefile
CXXFLAGS := -g3
all: a b.so c.so
a: a.cc
a: LDLIBS += -ldl # for dlopen,dlsym
a: LDFLAGS += -rdynamic # genereate dynamic symbol `used_by_b_so`
%.so: %.cc
$(LINK.cc) -shared -fPIC $&< -o $@
互動式的例子可以參考 ElfHacks/hot-swapping at master · MaskRay/ElfHacks · GitHub
CC++也是可以做熱更新的.
把需要支持熱更新的介面做到動態庫裡面,需要熱更新的時候重新載入動態庫即可.因為在這些語言看來,所謂的函數不過是內存裡面的數據罷了.
至於如何選擇熱更新的時機,Linux下面可以使用inotify這樣的機制,監控動態庫所在的目錄,發現有變動的時候就通知應用程序重新載入動態庫.只是非越獄的ios系統不能熱更
熱更的原理很簡單,將需要替換或者新增的二進位代碼和資源載入到內存,然後運行他,這種事情在windows上有很多方便的方式(例如dll)實現,而在Android雖然沒有直接提供簡單的方式仍然可以將動態鏈接庫(so)當作數據讀入到內存,然後執行之。
但是,作為沒有越獄的ios系統,蘋果因為安全或者其他原因,啟動了CPU的No eXecute bit,大致就是將appstore審核過的代碼加入簽名文件,然後ios運行app的時候會為appstore審核過的代碼開闢專用的內存空間,而其他app中的數據或者通過代碼從線上下載的數據載入的時候會將存放的內存空間定義描述符定義為禁止運行,這樣ip寄存器將不能夠跳轉到該空間,因此這部分代碼不能運行。這種安全技術應該在某些系統(比如安全要求很高的銀行計算機,猜測)也是使用的的,不過一般我們接觸到的電子產品也就只有ios使用了這種技術(不知道遊戲主機有沒有使用,木有玩過主機)
而為什麼lua等腳本語言可以熱更,因為lua在不開啟JIT的情況下是解釋運行,也就是通過軟體cpu來執行這些代碼,而模擬器代碼在提交的時候已經通過了appstore之類的審核是可以被cpu執行的,你熱更的lua腳本只是一種數據,被模擬器載入了而已,因此不會被No eXecute bit技術所限制。
當然想其他童鞋說的那樣,使用C++解釋器仍然是可以熱更的。
其實使用lua的主要原因是lua的位元組碼執行比較快,並且模擬器(虛擬機)運行比較穩定,維護足夠好。同事lua模擬器足夠輕量,方便擴展。多年以前跟某人一起接了個給某遊戲寫外掛的活,協議加密什麼的都搞定了,然後倒在了動態下載二進位代碼運行掃描所有運行進程的 warden 模塊面前。
都可以~~題主的困惑也許更多是因iOS平台而來吧。iOS不可動態下發可執行代碼(關於這個問題寫過一篇小博:誰偷了我的熱更新?Mono,JIT,IOS),但是比如JS這樣的解釋型腳本語言是可以通過蘋果JavaScriptCore.framework或WebKit執行的。
C/C++當然可以,因為C/C++的運行時就是操作系統內核。你去利用操作系統內核的功能(進程切換和I/O重定向)就可以了
正好是我以前的論文方向,多說幾句。C/C++也可以熱更新(術語是動態更新)。如果不考慮硬體架構、軟體架構支持,僅從語言層面討論,首先需要選擇:
選擇動態更新的顆粒度:程序級?模塊級?函數級?
然後是另外3個關鍵問題:怎麼維護狀態一致性?尋找程序的熱更時機,可更新性檢查這些問題處理完之後,就能做到動態的熱更新。
read+eval+dynamic type +function object
一般語言都能做。只是有的做了成本太高,有的做了成本很低。那些做這事成本太高的語言,被我們稱為不能做;做這事兒成本很低的語言,被我們統稱為能做。蛤蛤。
可執行文件 (executable file) 指的是可以由操作系統進行載入執行的文件(百度百科)。
相對上面的來說(注意這裡說的是相對),腳本語言指的是可以有其他可執行文件進行載入執行的文件。比如py腳本實際執行是操作系統載入執行文件python,python載入py腳本並解釋執行。操作系統,為保證系統執行的安全性,通常是不允許在執行過程中刪除文件實體的。所以必須按照操作系統支持的方式盡心熱更新,比如樓上 @Milo Yip 提到的「Windows上是可利用LoadLibrary()和FreeLibrary()」,標準卸載後,覆蓋二進位文件實體,然後重新載入。
上面解釋過了,腳本語言並不是由操作系統直接載入執行,所以沒有上述限制,比如python就完全自己定義一套自己的腳本載入模式和腳本重新載入模式。但在運行過程中的python程序本身是無法實現熱更新的,如果你想升級python版本,就必須終止python-&>升級-&>啟動python的過程實現,因為python自身是一個可執行文件 (executable file)。
最後補充:如果想查深層次原理的話,可以去查Windows操作系統PE文件載入執行原理(包括執行程序在內存中的存儲模式),以及python reload執行原理(包括python對腳本內容載入模式)。其實無論windows和python對大批量程序/腳本執行時都有優化操作,也不全是一次性全部載入,具體細節可討論的還有很多(但無論如何操作系統層的優化和應用程序層的優化也還是有根本性的不同的,這點一定要注意)。tomcat載入Servlet就可以
JAVA應該符合你說的編譯型語言為什麼我覺得編譯型語言(或者說靜態數據類型語言),動態熱更的關鍵難點是數據而不是代碼!編譯型語言熱更新是有局限的!這個局限來自於數據,你說把一個函數的邏輯從foo()換到foo()的新實現沒有問題,動態鏈接庫即可解決問題。你想修改什麼函數就修改什麼函數,添加、修改、刪除都沒有問題。
但是這裡的前提的foo()操作的數據結構沒有變化。假設foo()操作從數據結構發生了變化,在運行時刻的程序卻並不會知道這個變化,依舊把舊的數據結構的指針傳入foo函數,那麼程序就掛了。你說更新數據結構,那你估計得想想如何發現舊的數據結構了。這個任務近乎不可完成。所以編譯型語言或者說靜態數據結構的語言,在熱更新時是有巨大局限的。
【但是數據呢,舊的內存中的數據如何解決?如何更新!?】其實動態語言在解決數據熱更新時也是有很多問題的,但是比靜態語言略微好一些……Lua python 相當於一個在線的編譯器,文本代碼被lua的程序讀取、解析編譯成它自己能認識的東西,然後可以被調用運行。
一般C++程序需要通過離線的編譯器製作出二進位文件,由操作系統載入運行。也有項目做成像lua那樣的解釋執行c++代碼的,比如這個 CINT | ROOT。你寫了一個C程序,這個程序從某個文本文件中讀出每一個字元,每一個字元根據你的規則來處理,比如當讀到一個print字元串,你就調用C語言printf函數輸出點什麼。你再寫一個函數,叫reload,它的作用是重新讀一次文件,以確保你對文件做的修改能正確反映到當前正在運行的C程序中,這就是所謂的熱更新。看起來這個東西跟什麼操作系統沒關係,只要你的程序能正確被運行,熱更新則總是有效。而現在切換一下視角,你寫的這個C語言程序,其實也是類似一個文本的文件,解析並運行你這個文件的宿主程序則變成了操作系統。操作系統能不能讓你更換這個文件的內容,當然可以呀。你只要保證你知道你在幹啥就可以了
另外一個例子就是TCC編譯器。他是C編譯器,同時也可以作為庫使用。你可以在C代碼裡面調用libtcc去編譯生成代碼,然後直接調用。TCC : Tiny C Compiler
C可以熱更新,例如 Linux kernel 的 ksplice
lua本身就是c寫的,你用c寫一個lua類似的東西,也就是c的動態熱更新了.而且lua裡面load binary module本身就是用loadlibrary實現的
可以啊。
你把C/C++ 的代碼文本文件也當成腳本,然後給他們找一個runtime,或者解釋器就可以啊。
我依稀記得TCC可以當做C語言的解釋器,你的熱更新就實現了嘛。推薦閱讀:
※C++ 析構函數問題?
※只是為了建立一點編程的思想思維,學哪個語言最好?
※學習編程語言最好的方法是什麼?
※關於函數式語言的編譯優化,有沒有好的學習資料?
※怎麼樣可以很好地理解編程中的遞歸呢?