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