雲栖号資訊:【 點選檢視更多行業資訊】
在這裡您可以找到不同行業的第一手的上雲資訊,還在等什麼,快來!

什麼是計算機系統
計算機系統(A computer system) 是由硬體和軟體組成的,它們協同工作運作程式。不同的系統可能會有不同實作,但是核心概念是一樣的,通用的。
“不同的系統有 Microsoft Windows、Apple Mac OS X、Linux 等。
所有的計算機系統都有相似的軟體和硬體組成,它們執行相似的功能。
你想要什麼
首先,問你一個問題,你想成為哪種程式員?
也就是
我一直想成為第一種工程師,即使我永遠成為不了,我也要越來越靠近它。不知道把這些書都吃透了會是什麼水準,姑且堅持吧。
回到正題
沒錯,我就想成為一種電源程式員
一段簡單的程式
這次真的言歸正傳了,下面是一道很簡單的 C 程式(不要管我的名字是 Java建設者還是什麼,Java建設者就不能學習 C 了嗎?雖然飯碗是 Java,但是 C 才是爸爸啊。)
這是用 C 語言輸出的一個 Hello,world 程式,盡管它是一個非常簡單的程式,但系統的每個部分都必須協同工作才能運作。
這段程式的生命周期就是程式員建立程式、在系統中運作這段程式、列印出一個簡單的消息然後終止。
程式員首先在文本中建立這段代碼,這個文本又被稱為源檔案或者源程式,然後儲存為 hello.c 檔案,源程式實際上就是一個由 0 和 1 組成的位(又稱為 比特,即 bit)。8 個 bit 成為一組,稱做 位元組。每個位元組又表示着一個文本字元,這些文本字元通常是由 ASCII 碼組成的,下面是 hello.c 程式的 ASCII 碼
hello.c 程式以位元組順序存儲在檔案中,每個位元組都對應一個整數值,也就是 8 位表示一個整數。比如第一個字元是 35,那這個 35 是從哪來的呢?這其實是有個 ASCII 碼的對照表(因為 ASCII 非常多,可以去 ASCII 官網查詢,這裡隻選取幾個作為參考哦)
每行都以不可見的 來結尾,它的 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 編譯器驅動從源檔案讀取 hello.c ,并把它翻譯成一個可執行檔案 hello。這個翻譯過程可用如下圖來表示
這就是一個完整的 hello world 程式執行過程,會涉及幾個核心元件:預處理器、編譯器、彙編器、連接配接器,下面我們逐個擊破。
預處理階段(Preprocessing phase),預處理器會根據開始的 # 字元,修改源 C 程式。#include
指令就會告訴預處理器去讀系統頭檔案 stdio.h 中的内容,并把它插入到程式作為文本。然後就得到了另外一個 C 程式hello.i,這個程式通常是以 .i為結尾。
然後是 編譯階段(Compilation phase),編譯器會把文本檔案 hello.i 翻譯成文本hello.s,它包括一段彙編語言程式(assembly-language program)。這個函數包含 main 函數的定義,如下
上面定義中的 2 - 7 描述了一種低級語言指令。彙編語言是非常有用的,因為它能夠針對不同進階語言來提供自己的一套标準輸出語言。
- 編譯完成之後是彙編階段(Assembly phase),這一步,彙編器 as會把 hello.s 翻譯成機器指令,把這些指令打包成可重定位的二進制程式(relocatable object program)放在 hello.o 檔案中。它包含的 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 應用程式中輸入
“這裡解釋下什麼是 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 程式将字元逐一讀入寄存器,再把它放到記憶體中,如下圖所示
當我們在鍵盤上敲擊Enter鍵的時候,shell 程式就知道我們已經結束了指令的輸入。然後 shell 執行一系列指令來加載可執行的 hello 檔案,這些指令将目标檔案中的代碼和資料從磁盤複制到主存。
利用 DMA(Direct Memory Access) 技術可以直接将磁盤中的資料複制到記憶體中,如下
一旦目标檔案中 hello 中的代碼和資料被加載到主存,處理器就開始執行 hello 程式的 main 程式中的機器語言指令。這些指令将 hello,world 字元串中的位元組從主存複制到寄存器檔案,再從寄存器中複制到顯示裝置,最終顯示在螢幕上。如下所示
高速緩存是關鍵
上面我們介紹完了一個 hello 程式的執行過程,系統花費了大量時間把資訊從一個地方搬運到另外一個地方。hello 程式的機器指令最初存儲在磁盤上。當程式加載後,它們會拷貝到主存中。當 CPU 開始運作時,指令又從記憶體複制到 CPU 中。同樣的,字元串資料 hello,world 最初也是在磁盤上,它被複制到記憶體中,然後再到顯示器裝置輸出。從程式員的角度來看,這種複制大部分是開銷,這減慢了程式的工作效率。是以,對于系統設計來說,最主要的一個工作是讓程式運作的越來越快。
由于實體定律,較大的儲存設備要比較小的儲存設備慢。而由于寄存器和記憶體的處理效率在越來越大,是以針對這種差異,系統設計者采用了更小更快的儲存設備,稱為高速緩存存儲器(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://yqh.aliyun.com/zhibo立即加入社群,與專家面對面,及時了解課程最新動态!
【雲栖号線上課堂 社群】
https://c.tb.cn/F3.Z8gvnK
原文釋出時間:2020-04-01
本文作者:cxuan
本文來自:“
Java建設者”,了解相關資訊可以關注“Java建設者”