上一篇 計算機系統002 - 數值運算
前一篇中粗略講述了二進位制加法運算的過程,其中假定資料是從暫存器直接裝載到加法器兩端,加法器產生的結果也同樣儲存在另一暫存器中,而沒有在意資料是如何從外界傳遞到了暫存器裡,也並未給出暫存器中結果將以何種方式呈現給加法操作的真正使用者。
因此本章將從計算機體系結構入手,介紹自然語言與機器語言間的共識。
1. 問題表現形式
當我們希望求助他人幫忙解決某一問題時,首先要做的是用雙方共同支援的自然語言(如普通話、英語)來描述問題,等待對方提出解決問題的方法或是協同完成。然而如果希望將某一任務交付給計算機來完成,那麼很抱歉,計算機並不會思考,他只是流程的接收和執行者,也就是說,如何形成流程,仍然需要我們來完成。
也可能有人會講,不是啊,當我開啟計算機,雙擊一個視訊檔案,計算機就會幫我播放,我並沒有告訴它如何播放,用什麼播放吧。但你回過頭來看看這段文字,就可以發現其中還是含有通用流程,和你要尋找食物必須開啟冰箱一樣。至於視訊播放的真正過程,後面會講到,此處先給一個概念。
所有的資訊都是以電訊號儲存於儲存器中,讀取後得到的是二進位制位元組流,至於該位元組流描述成什麼具體內容需要依賴上下文,以及直譯器。比如同樣一句“不好吧”使用不同語氣,對於不同接收者而言可能獲取到不同的含義。同樣,一串位元組流,使用音訊播放器可能被解析為聲音,使用notepad可能被處理為文字,甚至如果想象力再豐富一點,也可以假象成這是一個文字解密遊戲。
因此通常會在位元組流中埋下一些記號,標明資料型別,這些稱為後設資料。在後設資料基礎上,不同視訊格式會有不同資料組合方式,通過列舉不同格式嘗試解析視訊檔案,如可以匹配為某格式,就按照該格式進行播放;如不能,則報錯不能正確播放某檔案。
綜上所述,要想託管任務給計算機執行,那麼計算機必須有兩個充分條件:
- 接收任務描述:支援以程式(流程)方式表述處理過程
- 具有執行能力:能夠執行程式
計算機能夠執行的任務歸根結底是數學計算,而對於如何進行統一、可行的任務描述,人們做了大量嘗試,直到圖靈機概念的出現。
1.1 圖靈機
圖靈為了給出計算機的清晰數學描述,他觀察人們計算式採用的方法和行為,然後將其抽象成一種統一的表達機制。圖靈將人們用紙筆進行數學運算的過程抽象成下列兩種簡單動作:
- 在紙上寫上或擦除某個符號
- 把注意力從紙的一個位置移動到另一個位置
為了模擬人的運算過程,圖靈造出一臺假想的機器,該機器按照讀取規則讀取紙帶上數值,然後進行計算。
這臺機器主要由如下幾個部分組成:
- 一條無限長的紙帶TAPE
- 一個讀寫頭HEAD
- 一套控制規則TABLE
- 一個狀態暫存器
簡而言之就是TABLE中規則控制讀寫頭HEAD在無限長紙帶TAPE上讀寫,根據讀寫內容進行運算,並選擇是否儲存結果至紙帶特定位置中。如執行過程中發生狀態變化,則保留在狀態暫存器內。
1.2 七層轉換
圖靈機是思想指導,迴歸到實踐而言,對於任務描述的方法可表達為七層轉換。《計算機系統概論》1.7節中提出,要控制電子器件按照我們的意圖工作,需經歷如下七個完整過程:
6 問題
通常我們採用“自然語言”來描述問題,如英語、漢語等,存在的問題是有很多語言組成部分具有二義性,而計算機指令很可能因為無法確定語句的準確含義而採取錯誤措施,導致錯誤結果。因此,剩下的幾層轉換,均可以視為消除二義性的步驟。5 演算法
演算法描述的特定是流程化、步驟清晰,並確保該流程能終止。具體如下:- 確定性,表明每個操作步驟的描述是清晰的、可定義的
- 可計算性,表示每一步的描述都可被計算機執行
- 有限性,表示過程是會終止的
4 語言(開發語言)
有了人類可以理解的計算方法,還需要轉達計算機如何計算。計算機只能識別“機械語言”,這種語言具有嚴格的順序方式,以便讓計算機順序地執行指令序列。這一層的語言通常稱為開發語言,如C/C++,Java,Python等,由於開發語言需要程式設計師編寫,因此仍具有一定可讀性。3 機器(ISA)結構
然而到開發語言一層仍然不夠,因為開發語言可能是面向多種硬體環境的,為了在特定的硬體平臺上也能夠順利執行,還要做一次翻譯,即將程式由開發語言翻譯成為特定計算機的指令集。該部分功能通常由“編譯器”或“直譯器”來完成。
指令集結構實際上就是程式和計算機硬體之間介面的一個完整定義。
2 微結構
有了介面,就可以順利完成任務描述,而真正要調動硬體的執行能力,還需要將各介面進行實現。比如很多處理器都使用了ISA X86,但具體實現卻可以各不相同。1 電路
微結構的概念最終要落實到一組組簡單的邏輯電路,同樣一個功能,如加法器,可以使用多種實現方式,不同的實現方式也同樣帶來了效能和成本上的差異。需要設計者從全域性上進行衡量取捨。0 器件
最後,相同的邏輯電路也仍然可以使用器件技術來實現,如材料、規格等等。
綜上所述,從人類自然語言表述的問題到最終執行的單元器件,需要做七層轉換。對於為什麼要分層以及為什麼是七層,通常來講,高層通常是低層的抽象結果,意在忽略部分無關細節,提供分析問題的更高層次的視野。不過到目前為止表述的方法都是理論性的,接下來就看一下歷史中實踐的結果。
2. 馮·諾依曼結構
馮·諾依曼結構是一種將程式指令儲存器和資料儲存器合併在一起的電腦設計概念結構,它用於實現通用圖靈機和一種相對於平行計算的序列式結構參考模型。
接下來我們對馮·諾依曼結構進行細化,如下圖所示。
從圖中可以看出,馮·諾依曼結構主要有如下5個組成部分:
記憶體
記憶體存放程式,訪問記憶體的第一步,是想記憶體提供被訪問記憶體單元的地址。以讀寫操作為例:- 讀:將訪問記憶體單元的地址放入MAR;傳送讀訊號通知記憶體;記憶體將該單元中存放的資料傳送至MDR
- 寫:將訪問記憶體單元的地址放入MAR;將要寫入的資料放入MDR,傳送寫訊號通知記憶體
處理單元
ALU所能處理的量化大小通常稱為計算機的字長,如X86,X64。設計中通常會在ALU附近配置少量儲存器,以便存放最近生成的中間計算結果。輸入
通過計算機解決問題的前提是能夠將問題描述成電訊號,轉換層次可參考1.2小節。輸出
計算結果要具有可讀性就必須通過裝置進行輸出,或是顯示器、或是音響等可將電訊號轉換為自然語言的裝置。控制單元
控制單元負責指令的有序執行,其中具有兩個特殊暫存器。- 指令暫存器(IR),儲存當前執行的指令
- PC暫存器,指示下一條待處理的指令,也可視為“指令指標”
哈佛結構
雖然馮·諾依曼結構是現代計算機的主要結構,但我們也必須知道,還有一個結構叫做哈佛結構。它和馮·諾依曼結構之間最大的差異在於資料在記憶體中排列方式。馮·諾依曼結構中允許指令和資料混合儲存在同一儲存模組中,而哈佛結構必須使用獨立的儲存模組來分別儲存指令和資料。
2.1 硬體實現(0 器件,1 電路)
本系列的第一篇中主要介紹了電學知識,為的就是在理解計算機時能夠形成完整通路,而不是隻能觸及作業系統這一層,視底層硬體為黑盒,不知所以然。從前面我們知道硬體只能識別電訊號,無論是組合邏輯電路還是時序邏輯電路,輸出的產生終歸需要輸入的參與,而在託管任務時,任務就是我們需要的輸入。
然而我們不可能在託管任務後還要守在一旁切換電訊號以提供準確輸入換取輸出,既不高效也不可理智。因此最好能將任務描述使用一種能夠最終轉換為電訊號的語言進行描述,而硬體在收到任務描述後,根據其中規則和資料自動完成計算,產出結果。同時為了保證任務描述的精確性,對於電訊號我們進行了一次抽象,將訊號幅度抽象成有無,對應為0和1,即所謂的二進位制。
2.2 指令結構及實現
馮·諾依曼結構在現有基礎上提出計算過程應該是:程式和資料都是以bit流的方式存放在計算機記憶體中,程式在控制單元的控制下,依次完成指令的讀取和執行。
二進位制在歷史悠久的人類自然語言面前是過於蒼白的,為了能夠將二進位制位和人類想表達的操作對應起來,這一次,抽象的任務落在了自然語言上。既然計算機實質上是負責計算任務,因此可以不直接參考自然語言,而是選擇抽象數學運算的基本規則(如加減乘除),形成指令。
目前通用計算機中指令是其執行的最小單位,指令本身本身由操作碼和運算元組成。操作碼標明其行為,運算元標明行為作用物件。
指令集架構還定義了其他內容,現存的指令集結構也各不相同,如Intel和AMD系的。但通常一條指令包含內容如下:
指令的處理過程是在控制單元控制下一步步完成的,這裡執行的步驟順序稱為指令週期,每一步稱為節拍,通常一個指令週期包含6個節拍,分別如下:
取指令FETCH
從記憶體中讀取下一條待執行的指令,並將其裝入控制單元的指令暫存器IR中。- 將PC暫存器的內容裝入MAR暫存器
- 該地址對應記憶體單元的內容(即下一條指令)被裝入MDR
- 控制單元將MDR內容裝入IR暫存器
- PC暫存器內容自增
譯碼DECODE
譯碼操作的任務是分析、檢查指令的型別,並確定對應微結構操作細節。
換句人話就是將幾個訊號通過們電路擴充套件為多個單個訊號,代表多個模式,而譯碼器後的器件可根據不同模式執行預設的不同操作。地址計算EVALUATE ADDRESS
如運算元不是立即數(如123),而是暫存器地址等涉及定址的表達方式時,需要進行地址計算以得到運算元的真正地址。
定址方式通常有如下幾種:
立即定址 | 運算元為立即數 | MOV BX,8080H |
---|---|---|
暫存器定址 | 運算元為暫存器 | MOV BX,AX |
直接定址 | 運算元為地址 | MOV AX,[1234H] |
暫存器間接定址 | 運算元為SI/DI/BX/BP之一 | MOV BX,[DI] |
暫存器相對定址 | 運算元為SI/DI/BX/BP之一,加上偏移量 | MOV BX,[DI+100H] |
基址加變址定址 | 運算元是BX/BP之一,加上SI/DI之一 | MOV BX,[BX+DI] |
相對基址加變址定址 | 運算元是BX/BP之一,加上SI/DI之一,加上偏移量 | MOV BX,[BX+DI+100H] |
取運算元FETCH OPERAND
讀取指令處理所需要的源運算元,分為兩個步驟:- 將地址計算節拍中結果值裝入MAR
- 從MDR中獲取讀自記憶體的源運算元
執行EXECUTE
負責指令的執行操作,不同操作碼在該節拍的操作也各不相同。存放結果STORE RESULT
將執行結果寫入目的暫存器,該節拍結束後,迴圈進入第一個取指令節拍,由於PC暫存器在連續空間內指令上移動,因此也稱為順序執行。有順序執行自然就有非順序執行,這裡不去深究跳轉的具體實現,只要知道,滿足條件後需要跳轉前,將會修改PC暫存器中內容,跳轉完成後,繼續順序執行連續指令,直到下一次跳轉的來臨。
2.3 從問題到開發語言
終於現在有了指令集,它定義了處理器可以執行的所有操作,而我們剩下要做的就是將問題描述成一條條順序或條件控制的跳轉執行指令。對於簡單的、小規模的任務,通過編寫彙編函式,通常也能實現功能,除了效率略低,移植性不佳(平臺支援的指令集可能不同)等。但對於複雜、大規模的任務,使用指令集語言就略顯力不從心了,畢竟所有操作如MOV等都需要小心翼翼地不停叮囑,這時候人們又想到了抽象。
雖然指令集介面各家、各平臺互不相同,那能不能想出一套更高層次的,不在意平臺差異的開發語言來描述問題。這樣一來,還可以順便把一些繁瑣的、囉嗦的地址處理指令模組化,將任務描述這件事情從細節中解脫?答案是肯定的,現在我們有了更貼近底層的C/C++,主打一次編譯處處執行的Java,甚至不要編譯純靠直譯器在執行時逐字逐句翻譯的Python等指令碼語言,這些都讓我們能夠更關注問題本身,而不是事無鉅細的實現。
當然從開發語言到指令集並不是省略了那些繁瑣的細節,而是通過編譯器、直譯器、虛擬化執行環境等模組代勞。軟體開發中有一句話和東北燒烤哲學類似,沒有什麼問題是一頓燒烤不能解決的,如果有,那就兩頓;同樣沒有什麼問題是一箇中間層不能解決的,如果有,那就兩個。
3. 總結
到目前為止,我們闡述了從問題本身出發,形成任務描述(指令),並由電路執行運算。這些都是整體的概念,或者說,對於實現細節,都是做了較多的抽象,為的是從巨集觀上有所瞭解。後續將對體系中各組成部分進行分別深入,力求淺出,再不濟也要知道一些關鍵概念。
這裡或者本系列也均不會進行任務無謂的語言之爭,本系列文章尋求的是問題的解決方法、思維,而不是各實現細節之間的差異。
追尋知識融會貫通後的快感。