新名詞|什麼是「電源」程序員?
- 2020 年 3 月 27 日
- 筆記
什麼是計算機系統
計算機系統(A computer system)
是由硬件和軟件組成的,它們協同工作運行程序。不同的系統可能會有不同實現,但是核心概念是一樣的,通用的。
不同的系統有 Microsoft Windows、Apple Mac OS X、Linux 等。
所有的計算機系統都有相似的軟件和硬件組成,它們執行相似的功能。
你想要什麼
首先,問你一個問題,你想成為哪種程序員?
這是我最近搜索到的一個很好的開源項目,它的路徑是 https://github.com/keithnull/TeachYourselfCS-CN/blob/master/TeachYourselfCS-CN.md
也就是
我也把它裏面涉及的中文/英文書籍都下載下來了,公眾號回復 計算機基礎
,即可領取。(圖中是馮·諾伊曼)
我一直想成為第一種工程師,即使我永遠成為不了,我也要越來越靠近它。
回到正題
沒錯,我就想成為一種電源程序員
一段簡單的程序
這次真的言歸正傳了,下面是一道很簡單的 C 程序(不要管我的名字是 Java建設者還是什麼,Java建設者就不能學習 C 了嗎?雖然飯碗是 Java,但是 C 才是爸爸啊。)
#include <stdio.h> int main(){ pritnf("hello, worldn"); return 0; }
這是用 C 語言輸出的一個 Hello,world 程序,儘管它是一個非常簡單的程序,但系統的每個部分都必須協同工作才能運行。
這段程序的生命周期就是程序員創建程序、在系統中運行這段程序、打印出一個簡單的消息然後終止。
程序員首先在文本中創建這段代碼,這個文本又被稱為源文件
或者源程序
,然後保存為 hello.c
文件,源程序實際上就是一個由 0 和 1 組成的位(又稱為 比特
,即 bit)。8 個 bit 成為一組,稱做 位元組
。每個位元組又表示着一個文本字符,這些文本字符通常是由 ASCII
碼組成的,下面是 hello.c
程序的 ASCII 碼
hello.c 程序以位元組順序存儲在文件中,每個位元組都對應一個整數值,也就是 8 位表示一個整數。比如第一個字符是 35,那這個 35 是從哪來的呢?這其實是有個 ASCII 碼的對照表(因為 ASCII 非常多,可以去 http://ascii.911cha.com/?year=%23 官網查詢,這裡只選取幾個作為參考哦)
每行都以不可見的 n
來結尾,它的 ASCII 碼值是 10。
注意;只由 ASCII 字符組成的諸如 hello.c 之類的文件稱為文本文件。 所有其他文件稱為二進制文件。
hello.c 的表示方法說明了一個基本思想:系統中所有的信息 — 包括磁盤文件、內存中的程序、內存中存放的數據以及網絡上傳輸的數據,都是由一串比特表示的。區分不同數據對象的唯一方法是我們讀取對象時的上下文,比如,在不同的上下文中,一個同樣的位元組序列可能表示一個整數、浮點數、字符串或者機器指令。
為什麼是 C
這裡插播一則新聞,為什麼我們要學 C 語言?學 Java 用不用懂 C 語言?這裡需要聊聊 C 語言的發家史了
C 語言起源於貝爾實驗室。美國國家標準學會 ANSI 在 1981 年頒佈了 ANSI C 的標準,後來 C 就被標準化了,這些標準定義了 C 語言和一系列函數庫,即所謂的
C 語言標準庫
,那麼 C 語言有什麼特點呢?
- C 語言與 Unix 操作系統密切關聯。C 從一開始就被開發為 UNIX 系統的編程語言,大部分 UNIX 內核(操作系統和核心部分)和工具,動態庫都是使用 C 編寫的。UNIX 成為 1970 – 1980 年代最火的操作系統,而 C 成為最火的編程語言
- C 是一種非常小巧,簡單的語言。並且 C 語言的簡單使他移植性比較強。
- C 語言是為實踐目的設計的。
我們上面提到了 C 語言的各種優勢,但是 C 語言也並非所有程序員都能熟練掌握並運用的,C 語言的指針經常讓很多程序員頭疼,C 語言還缺乏對抽象的良好支持,例如類、對象,但是 C++ 和 Java 都解決了這些問題。
程序被其他程序翻譯成不同的形式
C 語言程序成為高級語言的原因是它能夠讀取並理解人們的思想。然而,為了能夠在系統中運行 hello.c
程序,則各個 C 語句必須由其他程序轉換為一系列低級機器語言指令。這些指令被打包作為可執行對象程序
,存儲在二進制磁盤文件中。目標程序也稱為可執行目標文件。
在 UNIX 系統中,從源文件到對象文件的轉換是由編譯器
執行完成的。
gcc -o hello hello.c
gcc 編譯器驅動從源文件讀取 hello.c
,並把它翻譯成一個可執行文件 hello
。這個翻譯過程可用如下圖來表示
這就是一個完整的 hello world 程序執行過程,會涉及幾個核心組件:預處理器、編譯器、彙編器、連接器,下面我們逐個擊破。
預處理階段(Preprocessing phase)
,預處理器會根據開始的#
字符,修改源 C 程序。#include <stdio.h> 命令就會告訴預處理器去讀系統頭文件stdio.h
中的內容,並把它插入到程序作為文本。然後就得到了另外一個 C 程序hello.i
,這個程序通常是以.i
為結尾。- 然後是
編譯階段(Compilation phase)
,編譯器會把文本文件hello.i
翻譯成文本hello.s
,它包括一段彙編語言程序(assembly-language program)
。這個函數包含 main 函數的定義,如下
main: subq $8, %rsp movl $.LCO, %edi call puts movl &0, %eax addq $8, %rsp ret
上面定義中的 2 – 7 描述了一種低級語言指令。彙編語言是非常有用的,因為它能夠針對不同高級語言來提供自己的一套標準輸出語言。
- 編譯完成之後是
彙編階段(Assembly phase)
,這一步,彙編器 as
會把 hello.s 翻譯成機器指令,把這些指令打包成可重定位的二進制程序(relocatable object program)
放在 hello.c 文件中。它包含的 17 個位元組是函數 main 的指令編碼,如果我們在文本編輯器中打開 hello.c 將會看到一堆亂碼。 - 最後一個是
鏈接階段(Linking phase)
,我們的 hello 程序會調用printf
函數,它是 C 編譯器提供的 C 標準庫中的一部分。printf 函數位於一個叫做printf.o
文件中,它是一個單獨的預編譯好的目標文件,而這個文件必須要和我們的 hello.o 進行鏈接,連接器(ld)
會處理這個合併操作。結果是,hello 文件,它是一個可執行的目標文件(或稱為可執行文件),已準備好加載到內存中並由系統執行。
你需要理解編譯系統做了什麼
對於上面這種簡單的 hello 程序來說,我們可以依賴編譯系統(compilation system)
來提供一個正確和有效的機器代碼。然而,對於我們上面講的程序員來說,編譯器有幾大特徵你需要知道
優化程序性能(Optimizing program performance)
,現代編譯器是一種高效的用來生成良好代碼的工具。對於程序員來說,你無需為了編寫高質量的代碼而去理解編譯器內部做了什麼工作。然而,為了編寫出高效的 C 語言程序,我們需要了解一些基本的機器碼以及編譯器將不同的 C 語句轉化為機器代碼的過程。理解鏈接時出現的錯誤(Understanding link-time errors)
,在我們的經驗中,一些非常複雜的錯誤大多是由鏈接階段引起的,特別是當你想要構建大型軟件項目時。避免安全漏洞(Avoiding security holes)
,近些年來,緩衝區溢出(buffer overflow vulnerabilities)
是造成網絡和 Internet 服務的罪魁禍首,所以我們有必要去規避這種問題
處理器讀取、解釋內存中的指令
現在,我們的 hello.c 源程序已經被解釋成為了可執行的 hello 目標程序,它存儲在磁盤上。如果想要在 UNIX 操作系統中運行這個程序,我們需要在 shell 應用程序中輸入
cxuan $ ./hello hello, world cxuan $
這裡解釋下什麼是 shell,shell 其實就是一個命令解釋器,它輸出一個字符,等待用戶輸入一條命令,然後執行這個命令。如果命令行的第一個詞不是 shell 內置的命令,那麼 shell 就會假設這是一個可執行文件,它會加載並運行這個可執行文件。
系統硬件組成
為了理解 hello 程序在運行時發生了什麼,我們需要首先對系統的硬件有一個認識。下面這是一張 Intel 系統產品的模型,我們來對其進行解釋
總線(Buses)
:在整個系統中運行的是稱為總線的電氣管道的集合,這些總線在組件之間來回傳輸位元組信息。通常總線被設計成傳送定長的位元組塊,也就是字(word)
。字中的位元組數(字長)是一個基本的系統參數,各個系統中都不盡相同。現在大部分的字都是 4 個位元組(32 位)或者 8 個位元組(64 位)。
-
I/O 設備(I/O Devices)
:Input/Output 設備是系統和外部世界的連接。上圖中有四類 I/O 設備:用於用戶輸入的鍵盤和鼠標,用於用戶輸出的顯示器,一個磁盤驅動用來長時間的保存數據和程序。剛開始的時候,可執行程序就保存在磁盤上。每個I/O 設備連接 I/O 總線都被稱為
控制器(controller)
或者是適配器(Adapter)
。控制器和適配器之間的主要區別在於封裝方式。控制器是 I/O 設備本身或者系統的主印製板電路(通常稱作主板)上的芯片組。而適配器則是一塊插在主板插槽上的卡。無論組織形式如何,它們的最終目的都是彼此交換信息。 -
主存(Main Memory)
,主存是一個臨時存儲設備
,而不是永久性存儲,磁盤是永久性存儲
的設備。主存既保存程序,又保存處理器執行流程所處理的數據。從物理組成上說,主存是由一系列DRAM(dynamic random access memory)
動態隨機存儲構成的集合。邏輯上說,內存就是一個線性的位元組數組,有它唯一的地址編號,從 0 開始。一般來說,組成程序的每條機器指令都由不同數量的位元組構成,C 程序變量相對應的數據項的大小根據類型進行變化。比如,在 Linux 的 x86-64 機器上,short 類型的數據需要 2 個位元組,int 和 float 需要 4 個位元組,而 long 和 double 需要 8 個位元組。 -
處理器(Processor)
,CPU(central processing unit)
或者簡單的處理器,是解釋(並執行)存儲在主存儲器中的指令的引擎。處理器的核心大小為一個字的存儲設備(或寄存器),稱為程序計數器(PC)
。在任何時刻,PC 都指向主存中的某條機器語言指令(即含有該條指令的地址)。從系統通電開始,直到系統斷電,處理器一直在不斷地執行程序計數器指向的指令,再更新程序計數器,使其指向下一條指令。處理器根據其指令集體系結構定義的指令模型進行操作。在這個模型中,指令按照嚴格的順序執行,執行一條指令涉及執行一系列的步驟。處理器從程序計數器指向的內存中讀取指令,解釋指令中的位,執行該指令指示的一些簡單操作,然後更新程序計數器以指向下一條指令。指令與指令之間可能連續,可能不連續(比如 jmp 指令就不會順序讀取)
下面是 CPU 可能執行簡單操作的幾個步驟
-
加載(Load)
:從主存中拷貝一個位元組或者一個字到內存中,覆蓋寄存器先前的內容 -
存儲(Store)
:將寄存器中的位元組或字複製到主存儲器中的某個位置,從而覆蓋該位置的先前內容 -
操作(Operate)
:把兩個寄存器的內容複製到ALU(Arithmetic logic unit)
。把兩個字進行算術運算,並把結果存儲在寄存器中,重寫寄存器先前的內容。
算術邏輯單元(ALU)是對數字二進制數執行算術和按位運算的組合數字電子電路。
跳轉(jump)
:從指令中抽取一個字,把這個字複製到程序計數器(PC)
中,覆蓋原來的值
剖析 hello 程序的執行過程
前面我們簡單的介紹了一下計算機的硬件的組成和操作,現在我們正式介紹運行示例程序時發生了什麼,我們會從宏觀的角度進行描述,不會涉及到所有的技術細節
剛開始時,shell 程序執行它的指令,等待用戶鍵入一個命令。當我們在鍵盤上輸入了 ./hello
這幾個字符時,shell 程序將字符逐一讀入寄存器,再把它放到內存中,如下圖所示
當我們在鍵盤上敲擊回車鍵
的時候,shell 程序就知道我們已經結束了命令的輸入。然後 shell 執行一系列指令來加載可執行的 hello 文件,這些指令將目標文件中的代碼和數據從磁盤複製到主存。
利用 DMA(Direct Memory Access)
技術可以直接將磁盤中的數據複製到內存中,如下
一旦目標文件中 hello 中的代碼和數據被加載到主存,處理器就開始執行 hello 程序的 main 程序中的機器語言指令。這些指令將 hello,worldn
字符串中的位元組從主存複製到寄存器文件,再從寄存器中複製到顯示設備,最終顯示在屏幕上。如下所示
高速緩存是關鍵
上面我們介紹完了一個 hello 程序的執行過程,系統花費了大量時間把信息從一個地方搬運到另外一個地方。hello 程序的機器指令最初存儲在磁盤
上。當程序加載後,它們會拷貝
到主存中。當 CPU 開始運行時,指令又從內存複製到 CPU 中。同樣的,字符串數據 hello,world n
最初也是在磁盤上,它被複制到內存中,然後再到顯示器設備輸出。從程序員的角度來看,這種複製大部分是開銷,這減慢了程序的工作效率。因此,對於系統設計來說,最主要的一個工作是讓程序運行的越來越快。
由於物理定律,較大的存儲設備要比較小的存儲設備慢。而由於寄存器和內存的處理效率在越來越大,所以針對這種差異,系統設計者採用了更小更快的存儲設備,稱為高速緩存存儲器(cache memory, 簡稱為 cache 高速緩存)
,作為暫時的集結區域,存放近期可能會需要的信息。如下圖所示
圖中我們標出了高速緩存的位置,位於高速緩存中的 L1
高速緩存容量可以達到數萬位元組,訪問速度幾乎和訪問寄存器文件一樣快。容量更大的 L2
高速緩存通過一條特殊的總線鏈接 CPU,雖然 L2 緩存比 L1 緩存慢 5 倍,但是仍比內存要哦快 5 – 10 倍。L1 和 L2 是使用一種靜態隨機訪問存儲器(SRAM)
的硬件技術實現的。最新的、處理器更強大的系統甚至有三級緩存:L1、L2 和 L3。系統可以獲得一個很大的存儲器,同時訪問速度也更快,原因是利用了高速緩存的 局部性
原理。
局部性原理:在 cs 中,引用局部性,也稱為局部性原理,是 CPU 傾向於在短時間內重複訪問同一組內存的機制。
通過把經常訪問的數據存放在高速緩存中,大部分對內存的操作直接在高速緩存中就能完成。
存儲設備層次結構
上面我們提到了L1、L2、L3 高速緩存還有內存,它們都是用於存儲的目的,下面為你繪製了它們之間的層次結構
存儲器的主要思想就是上一層的存儲器作為低一層存儲器的高速緩存。因此,寄存器文件就是 L1 的高速緩存,L1 就是 L2 的高速緩存,L2 是 L3 的高速緩存,L3 是主存的高速緩存,而主存又是磁盤的高速緩存。這裡簡單介紹一下存儲器設備層次結構,具體的會在後面介紹。
操作系統如何管理硬件
再回到我們這個 hello 程序中,當 shell 加載並運行 hello 程序,以及 hello 程序輸出自己的消息時,shell 和 hello 程序都沒有直接訪問鍵盤、顯示器、磁盤或者主存,相反,它們會依賴操作系統(operating System)
做這項工作。操作系統是一種軟件,我們可以將操作系統視為介於應用程序和硬件之間的軟件層,所有想要直接對硬件的操作都會通過操作系統。
操作系統有兩項基本的功能:
- 操作系統能夠防止硬件被失控程序濫用
- 嚮應用程序提供簡單一致的機制來控制低級硬件設備。
那麼操作系統是通過什麼實現對硬件的操作的呢?無非是通過 進程、虛擬內存、文件 來實現這兩個功能。
文件是對 I/O 設備的抽象表示,虛擬內存是對主存和磁盤 I/O 設備的抽象表示,進程則是對處理器、主存和 I/O 設備的抽象表示。下面我們依次來探討一下
進程
進程
是操作系統中的核心概念,進程是對正在運行中的程序的一個抽象。操作系統的其他所有內容都是圍繞着進程展開的。即使只有一個 CPU,它們也支持(偽)並發
操作。它們會將一個單獨的 CPU 抽象為多個虛擬機的 CPU。我們可以把進程抽象為一種進程模型。
在進程模型中,一個進程就是一個正在執行的程序的實例,進程也包括程序計數器、寄存器和變量的當前值。從概念上來說,每個進程都有各自的虛擬 CPU,但是實際情況是 CPU 會在各個進程之間進行來回切換。
如上圖所示,這是一個具有 4 個程序的多道處理程序,在進程不斷切換的過程中,程序計數器也在不同的變化。
在上圖中,這 4 道程序被抽象為 4 個擁有各自控制流程(即每個自己的程序計數器)的進程,並且每個程序都獨立的運行。當然,實際上只有一個物理程序計數器,每個程序要運行時,其邏輯程序計數器會裝載到物理程序計數器中。當程序運行結束後,其物理程序計數器就會是真正的程序計數器,然後再把它放回進程的邏輯計數器中。
從下圖我們可以看到,在觀察足夠長的一段時間後,所有的進程都運行了,但在任何一個給定的瞬間僅有一個進程真正運行。
因此,當我們說一個 CPU 只能真正一次運行一個進程的時候,即使有 2 個核(或 CPU),每一個核也只能一次運行一個線程。
由於 CPU 會在各個進程之間來回快速切換,所以每個進程在 CPU 中的運行時間是無法確定的。並且當同一個進程再次在 CPU 中運行時,其在 CPU 內部的運行時間往往也是不固定的。
如下圖所示,從一個進程到另一個進程的轉換是由操作系統內核(kernel)
管理的。內核是操作系統代碼常駐
的部分。當應用程序需要操作系統某些操作時,比如讀寫文件,它就會執行一條特殊的 系統調用
指令。
注意:內核不是一個獨立的進程。相反,它是系統管理全部進程所用代碼和數據結構的集合。
我們會在後面具體介紹這些過程
線程
在傳統的操作系統中,每個進程都有一個地址空間和一個控制線程。事實上,這是大部分進程的定義。不過,在許多情況下,經常存在同一地址空間中運行多個控制線程的情形,這些線程就像是分離的進程。準確的說,這其實是進程模型和線程模型的討論,回答這個問題,可能需要分三步來回答
- 多線程之間會共享同一塊地址空間和所有可用數據的能力,這是進程所不具備的
- 線程要比進程
更輕量級
,由於線程更輕,所以它比進程更容易創建,也更容易撤銷。在許多系統中,創建一個線程要比創建一個進程快 10 – 100 倍。 - 第三個原因可能是性能方面的探討,如果多個線程都是 CPU 密集型的,那麼並不能獲得性能上的增強,但是如果存在着大量的計算和大量的 I/O 處理,擁有多個線程能在這些活動中彼此重疊進行,從而會加快應用程序的執行速度
進程中擁有一個執行的線程,通常簡寫為 線程(thread)
。線程會有程序計數器,用來記錄接着要執行哪一條指令;線程還擁有寄存器
,用來保存線程當前正在使用的變量;線程還會有堆棧,用來記錄程序的執行路徑。儘管線程必須在某個進程中執行,但是進程和線程完完全全是兩個不同的概念,並且他們可以分開處理。進程用於把資源集中在一起,而線程則是 CPU 上調度執行的實體。
線程給進程模型增加了一項內容,即在同一個進程中,允許彼此之間有較大的獨立性且互不干擾。在一個進程中並行運行多個線程類似於在一台計算機上運行多個進程。在多個線程中,各個線程共享同一地址空間和其他資源。在多個進程中,進程共享物理內存、磁盤、打印機和其他資源。因為線程會包含有一些進程的屬性,所以線程被稱為輕量的進程(lightweight processes)
。多線程(multithreading)
一詞還用於描述在同一進程中多個線程的情況。
下圖我們可以看到三個傳統的進程,每個進程有自己的地址空間和單個控制線程。每個線程都在不同的地址空間中運行
下圖中,我們可以看到有一個進程三個線程的情況。每個線程都在相同的地址空間中運行。
虛擬內存
虛擬內存的基本思想是,每個程序都有自己的地址空間,這個地址空間被劃分為多個稱為頁面(page)
的塊。每一頁都是連續
的地址範圍。這些頁被映射到物理內存,但並不是所有的頁都必須在內存中才能運行程序。當程序引用到一部分在物理內存中的地址空間時,硬件會立刻執行必要的映射。當程序引用到一部分不在物理內存中的地址空間時,由操作系統負責將缺失的部分裝入物理內存並重新執行失敗的指令。
在某種意義上來說,虛擬地址是對基址寄存器和變址寄存器的一種概述。8088 有分離的基址寄存器(但不是變址寄存器)用於放入 text 和 data 。
使用虛擬內存,可以將整個地址空間以很小的單位映射到物理內存中,而不是僅僅針對 text 和 data 區進行重定位。下面我們會探討虛擬內存是如何實現的。
虛擬內存很適合在多道程序設計系統中使用,許多程序的片段同時保存在內存中,當一個程序等待它的一部分讀入內存時,可以把 CPU 交給另一個進程使用。
文件
文件(Files)
是由進程創建的邏輯信息單元。一個磁盤會包含幾千甚至幾百萬個文件,每個文件是獨立於其他文件的。它是一種抽象機制,它提供了一種方式用來存儲信息以及在後面進行讀取。
網絡通信
現代系統是不會獨立存在的,因此經常通過網絡和其他系統連接到一起。從一個單獨的系統來看,網絡可以視為 I/O
設備,如下圖所示
當系統從主存複製一串位元組到網絡適配器時,數據流經過網絡到達另一台機器,而不是說到達本地磁盤驅動器。類似的,系統可以讀取其他系統發送過來的數據,把數據複製到自己的主存中。
隨着 internet 的出現,數據從一台主機複製到另一台主機的情況已經成為最重要的用途之一。比如,像電子郵件、即時通訊、FTP 和 telnet 這樣的應用都是基於網絡複製信息的功能。
全文完。
文章參考:
https://en.wikipedia.org/wiki/Locality_of_reference
https://en.wikipedia.org/wiki/Arithmetic_logic_unit
《深入理解計算機系統》第三版
https://github.com/keithnull/TeachYourselfCS-CN/blob/master/TeachYourselfCS-CN.md
http://ascii.911cha.com/?year=%23