cat 命令的原始碼進化史

一汀發表於2019-01-03

cat-command-linux

有一次,我跟我的親戚有一場爭論,是關於讀一個電腦科學的學位是否值得。當時是我在大學裡面臨是否選擇電腦科學專業的時候。我姑姑和一個表哥認為我不該選。他們覺得會程式設計當然是個既有用又合算的事情,但是他們也堅信,電腦科學更新太快了,當下學到的知識會很快被淘汰掉。所以最好是選一門程式設計的課程,然後主修經濟或者物理這種基本知識一輩子都適用的專業。

我並不相信他們的理論,並且選擇了主修計算機專業(抱歉了姑姑和表哥!)其實不難看出,為什麼常人會認為電腦科學,或者軟體工程這樣的專業,每幾年就會更新換代。先是誕生了私人計算機,然後是網路,手機,機器學習……科技永遠在變化,那麼其潛在的技術原理當然也在變化了。當然,最讓人驚訝的是,這些基礎技術原理,其實基本沒變。我相信大部分人要是知道他們計算機中重要軟體到底有多老,肯定會震驚。我並不是說軟體的表面,畢竟我自己用的最多的火狐瀏覽器,兩週前才更新過。但是如果你開啟幫助手冊檢視 grep 之類的工具,你會發現它的上一次更新還是在 2010 年(至少 Mac 系統是這樣)。grep 的初代誕生於 1974 年,那時候的計算機時代好比侏羅紀。現如今,人們(以及程式)在工作中仍然要依賴 grep 做很多事情。

我姑姑和表哥把計算機科技想象成一系列沙灘上的城堡,漲潮時潮水抹去舊的城堡,更加華麗的新城堡又會被建成。其實在現實中的很多領域,我們都是不斷地在現有的程式基礎上進行迭代。我們也許會時不時的修改這些程式來避免軟體崩潰,但是除此之外這些程式不需要額外的維護。grep 是一個簡單的程式,它所解決的問題現在也有意義,所以它至今還存在。很多應用程式的編寫都起始於一個很高的角度,就像是在金字塔頂端的基礎上構建,而金字塔本身是由曾經解決問題的答案所建成的。現在看來很陳舊的,三四十年前的想法與概念,在很多時候都融入到了你現在計算機上安裝了的應用程式裡。

我想仔細研究一個這樣的老程式,看看它從誕生到現在到底被修改了多少次,這肯定很有趣。我想用 cat 這個最簡單的 Unix 工具來作為例子。Ken Thompson 在 1969 年開發了初代 cat。如果我跟別人說我計算機裡有個 1969 年的程式,這準確嗎?cat 在這幾十年裡到底經歷了幾次迭代?我們計算機裡的程式到底有多古老?

幸好有這個程式碼倉庫,我們可以清晰地瞭解到,從 1969 年以來,cat 是如何進化的。我接下來會主要聚焦於我自己 Macbook 上 cat 程式的歷史實現方式。你會看到,cat 歷史從最初的 Unix 版本,到現在的 Mac 版本,這個程式被重寫了比你預想的還要多的次數,但是最終它所實現的功能幾乎跟五十年前一模一樣。

Unix實驗版本

1969 年,Ken Thompson 和 Dennis Ritchie 開始在 PDP 7 上開發 Unix。這是在 C 語言出現之前,所以早期的 Unix 程式都是用 PDP 7 上用匯編語言開發的。他們使用了專門針對於 Unix 的彙編版本,因為 Ken Thompson 開發了自己的彙編編譯器,他在 PDP 7 出廠商DEC 提供的編譯器基礎上新增了新的功能。Thompson 的改進文件在初始 Unix 程式設計手冊中有收錄,在 as 編譯器條目下面。

cat 的初代實現使用了 PDP 7 組合語言。我有新增一些註釋來解釋每行命令,但是除非你明白 Thompson 編寫彙編編譯器的一些擴充套件,不然這個程式還是很難理解。這裡有兩個重要的點。第一,字元 可以被用於分隔同一行的宣告語句。根據 sys 指令的描述, 通常被用於在同一行使用系統呼叫引數。第二,Thompson 新增了數字 0-9 用於支援“暫存標記”。這些標記可以被整個程式重用,這就像 Unix 程式設計手冊所描述的,“對於程式設計師思維和組合語言字元空間的縮減優化”。從手冊中,你可以使用 nf 來表示下一個標記 n,用 nb 來表示上一個標記 n。舉個例子,如果你有個標記為 1: 的程式碼塊,你可以從相距很遠的下方程式碼中使用 jmp 1b 來往上跳回標記程式碼。(但是你不能往下跳到標記程式碼,除非你使用jmp 1f。)

關於初代 cat 最有意思的是,它包含了兩個我們熟知的名字,分別是一個標記為是一個標記為 getc,和一個標記為 putc 的程式碼塊,這表示這倆名字要比標準 C 語言庫都要歷史久遠。初代 cat 實際上包含了這兩個方法的實現。這樣的實現方式使得輸入字元可以被寫入緩衝區,也就是說,讀和寫不需要以單個字元為單位完成。

初代 cat 並沒有存在很久。Ken Thompson 和 Dennis Ritchie 成功勸說了貝爾實驗室幫他們購入了一臺 PDP11,以便於他們對 Unix 系統進行擴充套件與提高。PDP 11 使用的是一種不同的指令集,因此他們不得不重寫 cat。對於 第二代 cat 程式碼我也加了註釋。第二代使用了針對於新指令集的新版彙編助記符,也利用了 PDP 11中不同的地址模式。(那些原始碼中的括號和 $ 符號,是被用來指代不同的地址模式的。)但是 cat 第二代中也同樣使用了初代中的 和暫存標記,這些功能一定是在 PDP 11 中移植 as 時被保留了下來。

cat 的第二代原始碼遠比初代要簡潔很多。第二代也更加的”Unix-y”,因為它不再需要一串檔名作為命令引數,而是與如今的 cat一樣,在沒有引數的情況下,從 stdin 讀取輸入。對於二代 cat,你也可以使用引數來指定從 stdin 讀取輸入資料。

1973 年,為了準備釋出第四版 Unix,很大一部分 Unix 系統都用 C 語言重寫了一遍。但是 C 語言版本的 cat 在 Unix 釋出後過了一段時間才出現。第一個 C 語言版本的 cat 只出現在第七版 Unix 系統中。這個實現方法非常值得一讀,因為它非常簡單明瞭。與其他版本比較,這一版最能作為代表 cat 的 K&R C 語言教育演示版本。這段程式的核心就是如下兩行:

當然還有更多的程式碼,但是除了這兩行以外,剩下的邏輯更多的是在確保使用者不會同時讀寫同一個檔案。另一個有意思的地方是,這個版本的 cat 只認得一個標記,-u。這個 -u 標記可以被用於關閉輸入輸出緩衝區,不然 cat 會預設快取 512 位元組。

伯克利軟體套件/BSD

在第七版之後,Unix 催生了各種各樣的衍生品。MacOS 是基於 Darwin 系統的,而 Darwin 是基於伯克利軟體套件(BSD),因此 BSD 是我們最感興趣的 Unix 分支。BSD 最初是作為Unix附加功能的軟體合集,但是它最終成為了一個完整的作業系統。BSD似乎一直在用cat的初代版本,一直到第四版 BSD 釋出為止。第四版 BSD 也就是 4BSD,它新增了對於新標記的支援。4BSD 版本的 cat 能明顯的看出是初代的衍生品,不過它新增了一些新的函式用來實現用新標記觸發的功能。4BSD 檔案系統的命名方法是基於 fflg 這個變數的,fflg 用於標記指令的輸入是從檔案,還是 stdin 讀取的。繼 fflg 之後,nflg、bflg、vflg、sflg、eflg 和 tflg 也被用於記錄程式中的標記是否被用到。這些命令列標記是 cat 新增的最後一批標記;如今至少在 Mac 系統中的 cat 命令列手冊有列出來這些標記。4BSD 是在 1980 年釋出的,所以這一系列的標記有 38 歲了。

cat 最後一次被重寫是為了 BSD Net/2,這主要是為了避免軟體許可證問題,因此所有 AT&T Unix 衍生程式碼都被替換為了新程式碼。BSD Net/2 在 1991 年釋出。最後一次重寫是由 Kevin Fall 完成的,Kevin Fall 於 1988 年畢業於伯克利,之後他花了一年的時間在計算機系統研究院(CSRG)工作了一年。Fall 告訴我,用 AT&T 程式碼寫的 Unix 工具集列表被掛在了 CSRG 的一面牆上,員工們被告知可以選擇感興趣的工具重寫。Fall 選擇了 cat 和 mknod。在如今 Mac 系統的預設 cat 版本中,Fall 的名字排在開發者名單前列。他所編寫的 cat,雖然是個很簡單的程式,但是直到今年還有數百萬的使用者在使用。

Fall 所寫的 cat 原始碼比我們之前看到的版本要長許多。除了支援 -? 幫助標記,這一版並沒有新增新的功能。理論上來說,這一版程式碼與 4BSD 版本非常相似。程式碼之所以長,是因為 Fall 分開了“舊版”和“新版”的邏輯。“舊版”是典型的 cat;它一個字元一個字元的輸出。“新版”的 cat 包括了 4BSD 命令列選項。這樣的分割很有道理,但是使得程式碼在第一眼看上去比實際複雜很多。程式碼的最後有個華麗的錯誤處理方程,這也增加了程式碼長度。

MacOS

2001 年,蘋果公司釋出了 Mac OS X 系統。這次釋出對於蘋果公司來說非常重要,因為他們花了很多年,走了不少彎路,為了研發能夠取代存在了很多年的舊版 Mac OS 系統。蘋果公司內部曾經有過兩次研發新系統的嘗試,但是最終都沒能成功;後來,蘋果收購了史蒂夫·賈伯斯的公司 NeXT,他們公司開發了一款名為 NeXTSTEP 的,基於物件導向程式設計框架的作業系統。蘋果決定使用 NeXTSTEP 作為Mac OS X 的基礎。NeXTSTEP 的一部分是基於 BSD 開發的,所以用 NeXTSTEP 作為 Mac OS X 的基礎,同時也給蘋果系統帶來了 BSD 程式碼風格。

新發布的第一版 Mac OS X中包含了來自 NetBSD 專案的 cat 程式碼實現。NetBSD 專案如今仍在不斷開發中,它最初是來自 386BSD 的分支。而 386BSD 是直接基於 BSD Net/2 的。所以 Mac OS X 上的 cat 就是 Kevin Fall 所寫的 cat。唯一變化的是,Kevin Fall 寫的錯誤處理函式 err() 被替換成了 err.h 中的 err()err.h 是 BSD 基於 C 語言標準庫的擴充套件。

NetBSD 版本的 cat 在不久之後被 FreeBSD 版本取代了。根據維基百科,蘋果從 Mac OS X 10.3 (Panther)開始,使用 FreeBSD 來取代 NetBSD。但是 Mac OS X 版本的 cat,根據蘋果的開軟釋出記錄,一直到 2007 年釋出 Mac OS X 10.5 (Leopard) 才被取代。蘋果為了釋出 Leopard 而引進的 FreeBSD 的實現版本一直被沿用到了今天。從 2007 一直到 2018 年,這一版沒有做過任何升級或者改變。

所以說 Mac OS 中的 cat 是古老的。實際上 cat 的出現,比 2007 年的正式釋出時間還早兩年。2005 年的改動,在 FreeBSD 的Github 映象中可以看到,是 cat 被移植到 Mac OS X 之前 FreeBSD 版的最後一次更新。所以 Mac OS X 中 cat 實際上有 13 年的歷史了,它並沒有與 FreeBSD 的 cat 進行同步更新。這裡有過一個辯論,軟體到底被改動過幾次才算是一個新的軟體呢;就 cat 這個個例來看,它的原始碼從 2005 年開始就完全沒有改變過了。

如今 Mac OS 系統中的 cat 與 Fall 在 1991 年為 BSD Net/2 所寫的版本並沒有太多不同。最大的不同是新增了一個新的函式用來支援 Unix 上的套接字。一個 FreeBSD 的開發者認為 Fall 所寫的 raw_args() 函式應該與 cook_args() 合併為一個函式 scanfiles()。除此之外,最核心的部分還是 Fall 的程式碼。

我問過 Fall,有幾百萬蘋果使用者在使用你所寫的 cat,還有很多程式直接或者間接依賴 cat,對此你有什麼感想。如今已經是顧問兼最新版 TCP/IP 協議合作者的 Fall 表示,人們對他開發 cat 的經歷如此的感興趣,讓他覺得非常驚訝。Fall 曾經在計算領域工作過很久,並且有過很多有影響力的專案經歷。但是似乎人們對於他在 1989 年開發 cat 的那六個月更加感興趣。

百歲程式

縱觀歷史上各種偉大的發明,計算機的歷史並沒有很久。我們仍然在使用有著百年曆史的照片和膠捲。但是計算機軟體是另外一個類別——目前仍屬於高新科技。至少現在的軟體是這樣。隨著計算機產業日漸成熟,我們會不會有一天發現,我們在使用有著百年曆史的軟體呢?

計算機硬體最終也會更新換代,現在的軟體想必是沒法跑在一個世紀以後的硬體上。也許高階語言設計的進步,也會導致在將來沒有人會使用 C 語言,而 cat 也會被其他的語言重寫。(不過 C 語言已經存在了五十年了,估計短期內也不會被取代。)不考慮以上這些的話,不如我們就一直用現在這版 cat 吧。

我認為,cat 的歷史告訴我們,在電腦科學領域有一些思想是非常耐用的。實際上,對於 cat,它的程式碼和思想都是很多年前出現的。要說我計算機中的cat是1969年的其實並不準確。但如果說我計算機中的 cat 是 1989 年 Fall 開發的,就準確多了。很多軟體都很古老。也許我們不能單純的認為電腦科學和軟體開發是不斷更新換代的領域。我們所開發的系統都是基於歷史基礎的。在某些時候,我們在開發新程式碼的同時,也需要去花時間去理解和維護歷史程式碼。

相關文章