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









































 
 
 

 
 
 
 