編程環境中Runtime(運行時)的三個含義
轉自:https://www.zhihu.com/question/20607178 知乎答主@doodlewind
三個含義
實際上編程語境中的 runtime 至少有三個含義,分別是:
- 指「程序運行的時候」,即程序生命周期中的一個階段。例句:「Rust 比 C 更容易將錯誤發現在編譯時而非運行時。」
- 指「運行時庫」,即 glibc 這類原生語言的標準庫。例句:「C 程序的 malloc 函數實現需要由運行時提供。」
- 指「運行時系統」,即某門語言的宿主環境。例句:「Node.js 是一個 JavaScript 的運行時。」
下面簡單介紹一下個人的理解。
含義一:程序生命周期中的階段
一個程序從寫好代碼字符串(起點)到跑完退出(終點),有一整套標準化的生命周期(流程),可以被拆分為多個階段。這其中編譯階段是 compile time,鏈接階段是 link time,那運行起來的階段自然就是 run time 了。如果在前面的階段預先做了通常在后面才方便做的事,我們就管這個叫 ahead of time。
注意所謂 ahead of time 其實只是英語口語中的常見詞匯,并不是 AJAX 這種專有的技術概念。比如美軍參謀長在通共電話里說的這句:
If we’re going to attack, I’m going to call you ahead of time. It’s not going to be a surprise.[[1]]1
個人猜測 runtime 這個詞衍生出的定義應該就源于 run time,泛指那些「供代碼運行所需的最基礎的軟件」。下面的兩個定義其實也都沒有超出這個范疇。
作者:doodlewind
鏈接:https://www.zhihu.com/question/20607178/answer/2133648600
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
含義二:運行時庫(runtime library)
怎樣理解 runtime library 呢?要知道 C、C++ 和 Rust 這類「系統級語言」相比于 JavaScript 這類「應用級語言」最大的特點之一,就在于它們可以勝任嵌入式裸機、操作系統驅動等貼近硬件性質的開發——而所謂 runtime library,大致就是這時候你沒法用的東西。
回想一下,我們在 C 語言里是怎么寫 hello world 的呢?
#include <stdio.h> // 1int main(void) { // 2printf("Hello World!\n"); // 3
}
這里面除了最后一個括號,每行都和運行時庫有很大關系:
stdio.h
里的符號是 C 標準庫提供的 API,我們可以 include 進來按需使用(但注意運行時庫并不只是標準庫)。main
函數是程序入口,但難道可執行文件的機器碼一打開就是它嗎?這需要有一個復雜的啟動流程,是個從_start
開始的兔子洞。printf
是運行時庫提供的符號。可這里難道不是直接調操作系統的 API 嗎?實際上不管是 OS 的系統調用還是匯編指令,它們都不方便讓你直接把字符串畫到終端上,這些過程也要靠標準庫幫你封裝一下。
在缺少操作系統和標準庫的裸機環境下(例如 Rust 的 no_std),上面的代碼是跑不起來的。而這里的 stdio 只是標準庫的冰山一角,再舉幾個非常常見的例子:
- 負責數學運算的
math.h
:很多精簡指令集或嵌入式的低端 CPU 未必會提供做 sin 和 cos 這類三角函數運算的指令,這時它們需要軟件實現。 - 負責字符串的
string.h
:你覺得硬件和操作系統會內置「比較字符串長度」這種功能嗎?當然也是靠軟件實現啦。 - 負責內存分配的
stdlib.h
:直接通過mmap
這類 OS 系統調用來分配內存是過于底層的,一般也需要有人幫你封裝。分配內存的 malloc 雖然只是一個接受單個參數的函數,它的實現可遠沒有表面上的 API 那么簡單,建議翻一翻 @郭忠明 老師的回答。
換句話說,雖然 C 的 if、for 和函數等語言特性都可以很樸素且優雅地映射(lowering)到匯編,但必然會有些沒法直接映射到系統調用和匯編指令的常用功能,比如上面介紹的那幾項。對于這些臟活累活,它們就需要由運行時庫(例如 Linux 上的 glibc 和 Windows 上的 CRT)來實現。
如果你熟悉 JavaScript 但還不熟悉 C,我還有篇講「C 手動內存管理基礎入門」的教程應該適合你。
我們可以把「應用程序、運行時庫和 OS」三者間的關系大致按這樣來理解:
注意運行時庫并不只是標準庫,你就算不顯式 include 任何標準庫,也有一些額外的代碼會被編譯器插入到最后的可執行文件里。比如上面提到的 main 函數,它在真正執行前就需要大量來自運行時庫的輔助,一圖勝千言(具體細節推薦參考 Linux x86 Program Start Up):
除了加載和退出這些程序必備的地方以外,運行時庫還可以起到類似前端社區 polyfill 的作用,在程序執行過程中被隱式而「按需」地調用。例如 gcc 的 libgcc 和 clang 的 compiler-rt(后者還被移植成了 Rust 的 compiler-builtins ),這些庫都是特定于編譯器的,我們一般比較少聽到,但其實也很好理解。
舉個例子,我在移植 QuickJS 引擎到索尼 PSP 的時候,發現雖然把 libc 的靜態庫鏈接進來了,但鏈接時始終找不到 __truncdfsf2
這個符號。這非常讓人困惑,因為那個報錯位置的源碼簡單到了這種程度:
// 這是 QuickJS 相應位置的源碼
static double js_math_fround(double a)
{return (float)a;
}
我把這個函數在 .o
目標文件里反匯編以后的結果讀來讀去,也完全沒有看到 __truncdfsf2
這個東西。但其實是這樣的:double 到 float 的轉換并不能由 PSP 的 CPU 指令直接完成(PSP 刻意閹割了對雙精度浮點數的硬件支持),因此編譯 PSP 應用時需要通過軟件實現來兼容,這個軟浮點算法就叫 __truncdfsf2
,它本來應該由編譯器在鏈接出可執行文件時自動插入,但我用的 Rust 工具鏈恰好沒有實現它(Issue #327 · compiler-builtins),于是就有了這個報錯。最后我把找來的一個軟浮點函數的代碼貼進來,就可以正確完成鏈接了。這其實也是個人第一次意識到原來所謂「運行時庫」并不僅僅是 stdio.h 里提供的那些符號——哪有什么 include 進來一把梭的歲月靜好,還要有編譯器和運行時替你默默負重前行。
理解問題原因后再去看上面的 C 代碼,可以感受到這里運行時庫所起到的作用,跟 JavaScript 中用于支持新語法的 babel 轉譯產物頗有些相似之處。這還是挺有趣的。
總之,由于系統級語言被設計成既可以用來寫操作系統上的原生應用,也可以用來寫 bare metal 的裸機程序,因此這類語言需要的運行時(runtime)被設計成了可以按需使用的庫(library),于是我們就自然地得到了 runtime library 這個概念。
含義三:運行時系統(runtime system)
上面介紹的運行時庫,主要針對的是 C、C++ 和 Rust 這些「系統級語言」。只要將這個概念繼續推廣到其他高級語言,這時候的「運行時」指的就是 runtime system 了——如果討論某門高級語言的運行時,我們通常是在討論一個更重、更大而全的運行時庫。
比如 Java 的運行時是 JRE,C# 的運行時是 CLR。這兩者都相當于一個需要在 OS 上單獨安裝的軟件,借助它們來解釋執行相應語言的程序(編譯出的字節碼)。而對 JavaScript 來說,一般「JS 引擎」是個不帶 IO 支持的虛擬機,需要瀏覽器和 Node 這樣的「JS 運行時」才能讓它控制文件、網絡、圖形等硬件資源而真正實用。這些都是很經典的模型了。
典型的高級語言「運行時系統」里大概需要這些基礎組件:
- 一個解釋執行字節碼的虛擬機,多半得帶個垃圾回收器。
- 如果語言是源碼解釋執行,那么需要一個編譯器前端做詞法分析和語法分析。
- 如果運行時支持 JIT 優化,那么還得藏著個編譯器后端(動態生成機器碼)。
- IO 相關能力,比如 Node.js 的
fs.readFile
之類。
可以看到相比上面 C 語言的「運行時」,這已經是個復雜的基礎軟件系統了。
稍微再展開一點,注意上面的「運行時」里是不包含應用程序業務邏輯的。那么拿 JavaScript 舉例來說,如果我們把業務邏輯先編譯成字節碼,再把它和運行時一起編譯成一個可執行文件,那不就相當于「直接把 JavaScript 編譯成機器碼」了嗎?QuickJS 就可以這么做,但其實這時候業務邏輯解釋執行的天性不會變——難道真有黑科技能把弱類型的腳本直接靠靜態分析編譯達到系統級語言的水平?這更多地只是概念定義上的話術而已。
因此,理論上任意的弱類型動態語言都可以基于這種形式來 AOT 編譯成「原生機器碼」,你看 Dart、Swift 和 Java 都可以直接編譯成可執行文件,區別只是這個運行時的輕重量級不同——當然實際情況肯定沒有這么理想化,譬如哪怕編譯成了 ARM 機器碼,Flutter 里的 Dart 運行時也必然需要比 C 做更多的類型檢查和 stop the world 的 GC,這都是有成本的。但對于應用層開發來說,能做到這樣已經夠好了。
所以我們甚至可以激進地認為對于 OS 上的應用程序,各種編程語言都是或多或少地需要運行時的,大家只有運行時輕重的區別———「其 實 都 一 樣」。
綜上所述,runtime 在技術討論中有多個含義,我們經常用它作為 runtime library 和 runtime system 的簡稱,因此可能造成一些誤解。
推薦閱讀書目:
- 《程序員的自我修養》,這本書用了很大篇幅專門講運行時庫,很硬也很有料。
- 《Node.js:來一打C++擴展》,Node 是理解現代高級語言運行時系統的好例子。
- 《Javascript二十年》。
參考
- Top general was so fearful Trump might spark war that he made secret calls to his Chinese counterpart, new book says https://www.washingtonpost.com/politics/2021/09/14/peril-woodward-costa-trump-milley-china/