到底什麼是系統程式設計?
此文翻譯自 Will Crichton 的部落格中 2018 年 9 月 9 日的一篇文章。
原文: ofollow,noindex">What is Systems Programming, Really?
翻譯者:nettee
譯者前言:Go 語言誕生也將近十年了,但它的發展並沒有很多人當時期望的那樣如火如荼。按照 Go 是 “系統語言”的說法以及 Go 的語法設計,它似乎是以取代 C/C++ 為目標的。然而現如今 Go 沒有撼動 C/C++ 一絲一毫的地位,倒是取代了一些 Python 在伺服器端的工作。但 Go 語言的作者仍然堅稱 Go 是“系統語言”。我們該如何看待“系統語言”這個術語?我們又該如何理解 Go 語言和最近流行的 Rust 語言的定位?我們究竟要不要學習它們?學習它們的什麼地方?這篇文章梳理了程式語言的發展歷史,能夠給我們一個思路。
前言:我對“系統程式設計” (systems programming) 這個詞不太滿意。對我而言,它實際上把兩個概念混為一談:“底層程式設計”(關注機器的實現細節),以及“系統設計”(建立並管理一系列複雜的互動的元件)。為什麼會這樣?這種情況持續了多久?重新定義“系統”這個概念,我們會得到什麼啟發?
1970 年代:彙編之上的改進
讓我們回到現代計算機系統誕生的時候,看看這個術語是如何改變的。我不知道是誰最先創造了這個詞語,但我發現對於“計算機系統”的定義始於 70 年代初期。在 系統程式語言 (Bergeron et al. 1972) 中,作者寫道:
系統軟體是一些子軟體的集合。這些子軟體形成一個整體,超過部分之和,並使整體有相當大的規模和複雜性。典型的例子包括 multiprogramming、翻譯、模擬、資訊管理、time sharing 的系統。[…] 下面是系統軟體的幾個特性。系統軟體不必包含所有的特性,而且一些特性也會出現在非系統軟體中。
- 需要解決的問題內涵廣泛,由許多(而且常常是多種多樣)的子問題組成。
- 系統程式可能是用於支援其他的軟體和應用程式,也可能本身就是一個完整的應用程式。
- 它是為持續的“產業”使用而設計,而不是針對單個應用程式問題的一次性解決方案。
- 它可以持續地演化,支援不同數量和種類的特性。
- 系統程式無論是模組內還是模組間(即“通訊”),都需要遵循一定的規則或結構。它常常是由多人設計並實現的。
這個定義還是比較合理的:計算機系統是大規模的、長期使用的、隨時間變化的。然而,雖然定義中大部分是描述性的,有一個關鍵點卻是限定性的:提倡區分底層語言和系統語言(在當時,也就是指區分彙編和 FORTRAN)。
系統程式語言的目標是可以在不過分關注底層細節的情況下使用,但編譯得到的底層程式碼不會比直接手寫的差。這種語言需要結合高階語言的簡潔性和可讀性,以及組合語言的時間空間效率、深入機器和作業系統的能力。它應該能在不帶來額外系統資源開銷的前提下,儘量減少設計、編寫、除錯的時間。
與此同時,CMU 的研究人員發表了 BLISS: 一個系統程式語言 (Wulf et al. 1972),將其描述為:
我們將 BLISS 稱為“實現語言” (implementation language)。這個術語有些模糊不清,因為實際上所有的計算機語言都是為了實現些什麼東西。對我們而言,這個詞意味著一種通用的、高階的程式語言,它首要關注的是編寫大型的生產軟體系統之類的特定應用程式。特殊用途的語言(例如編譯器的編譯器)不在這一類,這些語言也不需要機器無關性。我們在定義中強調“實現”一詞,而不是用“設計”或“文件”之類的詞,是因為我們我們的語言不希望成為描述大型系統的最初設計的原型語言,也不希望成為系統的專有文件。一些諸如機器無關性、使用同一套記號表達設計和實現、自我文件等概念,顯然都是可行的目標,也是我們評估各種語言的標準。
作者定義“實現語言”為彙編之上、“設計語言”之下的一種語言。這和前一篇文章的定義一致,提倡在設計系統和實現系統時使用不同的語言。
這兩篇文章都是研究中的產物,而我們要看的最後一篇文章(也是來自 1972 這個多產的年份!)是一篇學習系統程式設計的教學文章: 系統程式設計 (Donovan 1972)。
什麼是系統程式設計?你可以將計算機看成一種聽從任何命令的野獸。有人說,人類用金屬造出了計算機;也有人說,計算機用血和肉造出了人類。然而,當我們對計算機有點了解,就會發現它們是遵循非常具體和原始的指令的機器。在計算機的早期,人們使用代表原始指令的開關來與計算機通訊。有些人想描述更復雜的指令。比如他們想表達:X = 30 * Y,當 Y = 10 時,X 是多少?如果沒有系統語言的幫助,現代計算機無法理解這種語言。系統軟體(編譯器、loader、巨集處理器、作業系統等)是讓計算機能更好地適應使用者的需要。然後,人們希望在程式設計的時候有更多機制的幫助。
我想這個定義提醒了我們,系統是為人服務的,即使是不直接面對終端使用者的基礎架構系統。
1990 年代:指令碼語言的興起
在七八十年代,似乎研究人員都將系統語言看成和組合語言對立的事物。這時候根本沒有其他適合搭建系統的工具。(我不太確定 Lisp 是算什麼?我讀的材料中沒有提到 Lisp 的,儘管我隱約記得 Lisp 機器曾短暫存在過。)
然而,在 90 年代中期,動態指令碼語言的興起給程式語言帶來的巨大的改變。對早期 Bash 一類的 shell 指令碼語言改進之後,Perl (1987), Tcl (1988), Python (1990), Ruby (1995), PHP (1995), 以及 Javascript (1995) 等一系列語言開始成為主流。這帶來了一篇有影響力的文章“ 指令碼:21 世紀的高階程式語言 ” (Ousterhout 1998)。這篇文章表達了“系統程式語言”與“指令碼語言”的“Outsterhout 二分法”。
指令碼語言是針對與系統程式語言不同的任務而設計的,這導致了語言間的根本差異。系統語言是要從最原始的計算機元素(如記憶體中的字)開始,從頭構造資料結構和演算法。而指令碼語言是為黏合而設計:假設已經存在一些功能強大的元件,指令碼語言主要用於將元件連線在一起。系統語言是強型別的,以管理複雜性;而指令碼語言的型別較弱,以簡化元件間的連線,提供快速的應用程式開發。[…] 近年來,隨著機器速度變快,指令碼語言變得更好,圖形使用者介面、元件架構的重要性提高,以及網際網路的發展,指令碼語言的適用性變得越來越廣。
從技術層面看,Ousterhout 從“型別安全性”和“每個語句的資訊量”兩個緯度比較了指令碼語言與系統語言。從設計層面而言,他為兩類語言描述的新的角色:系統語言用於建立元件,而指令碼語言用於黏合元件。
大約在這個時候,靜態型別但支援垃圾回收的語言開始流行。Java (1995) 和 C# (2000) 成為我們今天所知的巨頭。雖然這兩個語言傳統上不被認為是“系統語言”,但他們已經設計出了許多超大型的軟體系統。Ousterhout 甚至明確提出:“在正在成形的網際網路世界裡,Java 就是系統程式設計的語言。”
2010 年代:界線開始模糊
在過去十年中,指令碼語言好系統程式語言之間的界線開始變得模糊。像 Dropbox 這樣的公司能夠只用 Python 就構建出巨大而且可擴充套件的系統。Javascript 也用來在數以萬計的網頁中渲染實時、複雜的介面。漸進型別系統 (gradual typing) 在 Python , JavaScript 等指令碼語言中已經成為主流。開發者可以使用這種型別系統,在“原型”程式碼上逐步新增靜態型別資訊來過渡到“生產”程式碼。
與此同時,大量工程資源被用於開發 JIT 編譯器,既包括靜態語言的(如 Java 的 HotSpot),也包括動態語言的(如 Lua 的 LuaJIT,JavaScript 的 V8,Python 的 PyPy)。這些 JIT 編譯器可以使語言的效能 和傳統的系統程式語言(C,C++)相媲美 。像 Spark 這樣大型可擴充套件的分散式系統也是用 Scala 寫的。而像 Julia, Swift 和 Go 這樣的新語言也在不斷提升垃圾回收類語言的效能上限。
這裡有一個叫做 2014 年及以後的系統程式設計 的座談會,其中的幾位嘉賓都是當今幾個系統語言(自稱)的創造者:C++ 的作者 Bjarne Stroustrup、Go 的作者 Rob Pike、D 開發者 Andrei Alexandrescu,以及 Rust 開發者 Niko Matsakis。當被問道“什麼是如今的系統程式語言”時,他們回答到(整理稿):
- Niko Matsakis : 寫客戶端應用程式用到的語言。這和 Go 面向的領域正好相反。客戶端程式需要滿足延遲、安全等標準,其中很多在伺服器端是不會遇到的。
- Bjarne Stroustrup : 系統程式設計最開始是需要處理硬體的情況,不過後來應用程式變得更加複雜。你需要管理複雜性。如果你遇到了重大的資源限制,或者你需要細粒度的控制,那麼你進入了系統程式設計的領域。程式設計中的限制決定了它是否是系統程式設計。記憶體夠用嗎?時間夠用嗎?
- Rob Pike : 我們釋出 Go 的時候將其稱為系統程式語言。這個詞似乎不太恰當,因為很多人以為它是寫作業系統的語言。我們覺得它更應該叫做“寫伺服器的語言”。現在 Go 成為了雲平臺語言,這樣系統語言又可以定義為“跑在雲上的語言”。
- Andrei Alexandrescu : 我有幾個檢驗一個語言是不是系統語言的方法。系統語言必須能讓你寫出自己的記憶體分配器。系統語言應當能讓你將一個整數轉換為指標,因為硬體實際上就是這麼工作的。
那麼,系統語言意味著高效能?資源限制?硬體控制?雲平臺?概括說來,C, C++, Rust, D 這些語言因為對於機器的抽象層次而可以分為一類。這些語言暴露了底層硬體的細節,如記憶體分配/佈局,以及細粒度的資源管理。
從另一個角度來看,當你遇到了效率問題,你有多大的自由度來解決它?底層程式語言的好處在於,當你發現了效率問題時,你可以通過仔細控制機器細節(如並行化指令,調整資料結構大小以保證其在快取中,等等)來消除效能瓶頸。正如靜態型別可以讓你對“我要加的兩個東西肯定都是整數” 更有信心 ,底層語言可以讓你對“這段程式碼在機器上肯定會像我指定的一樣執行” 更有信心 。
相比之下,優化指令碼語言就 相當難以捉摸 。你很難知道程式碼執行時是否和你期望的一致。自動並行化編譯器遇到的是同樣的問題——“自動並行化並不是一個程式設計模型”(參見 ispc 的故事 )。就像在 Python 中寫一個介面,你想的是 “我希望呼叫這個函式的人一定要傳進一個整數”。
今天:那麼到底什麼是系統程式設計呢?
這又讓我想起了一開始那個問題。我認為很多人只是把系統程式設計看作是底層程式設計——一種暴露底層機器細節的方式。那麼,系統又究竟如何定義呢?回顧 1972 年的那個定義:
- 需要解決的問題內涵廣泛,由許多(而且常常是多種多樣)的子問題組成。
- 系統程式可能是用於支援其他的軟體和應用程式,也可能本身就是一個完整的應用程式。
- 它是為持續的“產業”使用而設計,而不是針對單個應用程式問題的一次性解決方案。
- 它可以持續地演化,支援不同數量和種類的特性。
- 系統程式無論是模組內還是模組間(即“通訊”),都需要遵循一定的規則或結構。它常常是由多人設計並實現的。
這幾點看上去更像是軟體工程的問題(模組化、重用、程式碼演化),而不是底層的效能問題。這意味著,任何注重於解決這些軟體工程問題的程式語言,其實都可以叫做系統語言!當然,這不是說所有的語言都是系統語言——動態語言似乎仍然離系統語言很遠,因為動態型別以及“請求寬恕,而不是許可”的格言(譯註:Python等語言中一種“先進行處理,再解決異常”的程式設計風格)不利於良好的程式碼質量。
那麼這個定義給我們帶來了什麼?有一個激進的觀點: 像 OCaml 和 Haskell 這樣的函式式語言,相比 C 或 C++ 這樣的底層語言,其實更加面向系統 (system-oriented) 。當我們向本科生教授系統程式設計的時候,我們應該引入函數語言程式設計的原則,例如不變性 (immutability) 的價值、豐富的型別系統對於提升介面設計的影響、以及高階函式的作用。學校裡應該既教授系統程式設計,又教授底層程式設計。
系統程式設計真的和好的軟體工程是不同的嗎?這不一定。但是問題在於,軟體工程和底層程式設計通常是孤立地教授的。雖然大部分的軟體工程課程是以 Java 為中心的 “書寫良好的介面和測試”,但我們也應該教給學生如何設計有重大資源限制的系統。或許我們將底層程式設計叫做“系統程式設計”,是因為很多有趣的軟體系統是底層的(如資料庫、網路、作業系統等等)。由於底層系統有很多的限制,因此需要設計者進行創造性的思考。
另一個可以得出的結論是,底層程式設計師應該試圖理解系統設計中的哪些想法可以應用於現代硬體上。我覺得 Rust 社群在這方面非常有創新性,尋找將好的軟體設計/函數語言程式設計的原則(如 記憶體安全 、 future 、 錯誤處理 )應用到底層問題的方法。
總而言之,我們所謂的“系統程式設計”我認為應該叫做“底層程式設計”。“設計計算機系統”這個重要的領域,其實應該有自己獨特的名字。通過將“系統程式設計”和“底層程式設計”兩個概念區分開,我們就能在程式語言設計時的概念更加清晰。這也為系統和機器間共享見解提供了可能:我們如何為機器設計系統,我們又如何為系統設計機器?