我對計算機系統的理解

薰衣草的旋律發表於2016-03-06

計算機系統的組成

一個計算機系統是由軟體與硬體組成的,就硬體來說,當我們一般去電腦城配電腦的時候,一般會購買這些基本零部件:主機板,CPU,記憶體,磁碟,機箱,鍵盤滑鼠,顯示器。當然還有一些額外的部件,例如獨立顯示卡或者網路卡,音響等。如果除去非必要的部件來看,其實一個計算機系統主要由下面這些重要的部件組成:
CPU,儲存器(記憶體),磁碟,IO裝置(鍵鼠,顯示器),以及連線這些器件匯流排,只不過我們的成品電腦是用一塊電路板將這些部件連線在了一起。當然隨著電腦系統越來越強大,可能除了上述部件之外,還多了很多其他的部件,例如個人PC的主機板上可能還有南橋,北橋等等一系列的額外的晶片。但是我們這裡討論的是組成一個計算機系統最基本的部件,其他的部分,為了簡化說明,暫時省略。

CPU:是一個計算機最核心的部分,計算機的所有運算,程式的所有指令的執行是通過CPU來進行的,CPU是計算機的大腦。

儲存器:CPU所執行的指令,計算的中間結果,產生的資料都需要通過某種方式進行儲存,方便CPU讀取和存入,這就是儲存器的功能。

磁碟:提供永久儲存資料的辦法。

IO:也即是連線在計算機上的輸入輸出裝置,一般最常見的就是滑鼠,鍵盤和顯示器了,當然還有其他的一些輸入輸出裝置,例如印表機,USB外接裝置等。IO裝置主要提供給人們與計算機進行互動的時候使用。

匯流排:所有這些部件之間通過匯流排進行連線,有些裝置共享一組匯流排,有的裝置之間有專用的匯流排。

計算機的軟體組成:計算機除了硬體系統之外,在硬體系統之上執行著一系列的軟體,最重要的當然是作業系統軟體,另外還有各種驅動程式用於與特定的裝置進行通訊。除此之外還有各種執行在作業系統之上的使用者應用程式。

程式是如何在計算機上執行的

程式是什麼:
在計算機系統上執行著各種各樣的程式,包括作業系統本身也是程式,那麼程式到底是什麼呢,程式一般以檔案的形式存在於磁碟上,這些檔案是具有一定結構的二進位制檔案,這些檔案被以一種特定含義的格式進行組織,例如典型的二進位制的可執行檔案(不管是windows還是linux系統)以一定的結構描述,在這些描述中,該檔案被分成了多個段,每個段中的資料內容具有不同的含義,例如資料段,程式碼段,符號表等等。其中程式碼段中的二進位制資料,我們認為就是CPU直接執行的指令。

CPU的指令是什麼:
一條CPU的指令是一串不定長的二進位制的位串,而當CPU讀取這一個位串並執行的時候,一般是執行一個特定的功能,例如加法,減法,從記憶體讀取一個位元組到暫存器等等這樣的功能,而一個CPU能夠執行的所有二進位制位串(也就是指令)的集合則是CPU的指令集。不同的硬體廠商生產的CPU他們能夠識別執行的二進位制位串序列集(CPU指令)可能是不一樣的。而同樣的功能,例如加法功能,不同的CPU需要執行的二進位制位串可能是不一樣的。例如我們常見的PC機器的x86架構的CPU基本上是相容的,目前市面上主流的PC端的CPU由Intel和AMD生產,這兩個廠家生產的CPU的指令基本上是相容的,但是也可能各自有一些特定的指令是對方沒有的。另外的例如Arm或者mips的CPU的指令集則與x86體系是不同的,而前者多用於嵌入式系統。

CPU的硬體本身可以理解為是一系列的電路,該電路實現一定的功能,該電路中又分成了很多的功能單元,例如在現代CPU的設計中,一條計算機指令在執行的時候,會經過:取指、譯碼、執行、訪存,寫回,更新PC等階段,而現代CPU被設計成以流水線的方式執行指令。如前所述,一條指令是若干個二進位制bit位的位串,在取指階段CPU根據程式計數器(PC)中的地址取出當前需要執行的指令。接著對該指令譯碼,所謂譯碼則是解析該指令,一般一條指令被分成了多個部分,每個部分代表不同的含義,一條IA32的指令是由1到15個位元組組成,運算元較少的指令所需要的位元組數少,那些不太常用的或者運算元較多的指令所需要的位元組數較多。在執行階段由CPU的算術/邏輯單元執行指令。訪存階段將資料寫入儲存器或者從儲存器中讀出資料。寫回階段將結果寫到暫存器中。更新PC階段則將PC計數器設定成下一條指令的地址。

程式的編譯與連結過程:
一個程式的生成流程可以大致描述為:程式文字檔案、編譯成彙編檔案(.s)、編譯成二進位制檔案(.obj)、連結成最終的可執行檔案。CPU只能識別並執行他的指令集中的位串,所以我們必須將程式設計師可以識別的文字檔案的程式程式碼轉換成CPU能夠執行的一系列的指令,這個轉換過程就是編譯過程。而連結是一個非常複雜的過程,其中包括靜態連結和動態連結,連結的過程是將所有模組中使用的符號替換成實際使用的記憶體地址的過程(而其實現代作業系統中程式訪問的記憶體地址並不是實際的實體地址,而是虛擬地址)。靜態連結是在編譯階段由連結器完成連結過程直接生成可執行程式的過程。動態連結是在程式執行階段動態將程式中的符號替換成為實際的記憶體地址的過程,例如我們的C程式裡面一般呼叫了C的標準庫函式,而C的標準庫函式是在其動態連結庫libc.so中的,在我們的程式執行的時候,會將libc.so所在的記憶體地址對映到程式的虛擬地址空間,並修改程式中所有訪問libc.so中的符號為正確的訪問地址的過程。這個過程就是動態連結。

程式的執行過程:
一個程式要想被執行,首先要將其檔案內容載入到記憶體中,然後CPU從程式的程式碼段的入口點的第一條指令開始執行,後面的所有執行過程都是按照程式中的指令進行的,一直到程式結束為止。這一整個過程實際上並沒有這幾句話描述的這麼簡單,首先程式被從磁碟讀取到記憶體這一過程,可能涉及到磁碟的IO中斷,DMA的拷貝,虛擬記憶體等,當程式被載入進記憶體之後,如果程式使用了動態連線庫,例如最簡單的C程式也可能連結了C的標準庫,所以還需要載入動態庫(一般是通過記憶體對映的方式),將動態庫載入進記憶體,並將其對映到程式的虛擬地址空間中。最後經過一些初始化之後(初始化暫存器,庫的初始化等),開始從程式的入口處的指令執行。

作業系統是什麼

什麼是作業系統呢,一般情況下我們提到作業系統就認為高深莫測,深不見底。其實作業系統的作用可以從這麼幾個方面來理解,首先作業系統的一個非常重要的功能即是提供使用者介面,也就是說當人們要使用計算機的時候,必須通過一種方式來控制計算機,那麼作業系統提供了這樣的方式來使人們可以控制計算機,例如命令列的或者是UI的方式,這種介面我們可以理解為使用計算機的入口。另外一種介面用於擴充套件計算機的功能,例如程式設計師編寫程式的時候就會使用作業系統提供的系統介面,這種介面是更底層的概念。

其實我們說的使用者介面不一定是作業系統的一部分,例如典型的linux型別的作業系統,其桌面環境是可以不用的,或者是可以隨意替換的,其shell命令列環境也可以自己替換,在linux中有多種可以使用的shell,而唯一不能改變的是linux核心,這麼說來linux的桌面環境以及shell命令列環境好像並不屬於作業系統,但是卻是計算機中不可缺少的。而在windows作業系統中,特別是windows xp 和windows NT 中其GUI介面是夾在核心中的,這樣一來其又是作業系統的一部分了。所以其實從功能上來說現代作業系統的軟體界限其實是越來越模糊了。

作業系統除了提供使用者介面之外,還是計算機系統的管理者,負責管理各種軟硬體資源,制定計算機世界的規則,例如作業系統管理CPU,排程各種程式,執行緒來使CPU執行指令。管理記憶體,提供程式訪問記憶體的規則,還有磁碟,外設等裝置的管理。如果沒有這些管理,我們的程式可以亂執行,誰先執行,誰後執行? 執行多久?記憶體區域可以亂寫,我可以破壞你的記憶體,你可以破壞我的記憶體。就像在道路上沒有警察去約束,那交通肯定是一片混亂。

什麼是驅動程式呢,驅動程式嚴格的來說並不是一個單獨的程式,也不是一個獨立執行的程式,實際上我們可以理解驅動程式是一個程式片段,或者是一個符合一定約定提供約定介面的程式庫,作業系統呼叫該庫中提供的介面,去操作特定的硬體。該庫實現了對特定硬體的操作,並提供一個對外的介面供作業系統呼叫。例如對於磁碟來說,生產磁碟的硬體廠商一定會提供從磁碟讀取或者寫入磁碟資料的辦法,該辦法(可能是一系列的底層C介面)可能很複雜,需要很多個步驟,定址,尋道等。而且不同的磁碟廠商提供的底層介面並不一定相同,那麼磁碟驅動程式封裝了這些底層的操作磁碟的功能,並對外提供統一的操作介面(該介面規範是作業系統規定的,這樣作業系統才能統一呼叫)。那麼符合規定介面的該軟體庫我們可以認為就是驅動,有的驅動程式是隨作業系統啟動的時候進行載入的,而有的可以直接即時安裝進行動態載入,這有賴於動態連結庫的執行時載入功能。

程式與執行緒

什麼是程式呢,我們的程式實際上是一個二進位制檔案存放在磁碟中,當將程式載入到記憶體中,並從第一條指令開始執行,一直到最後一條指令結束該程式檔案中的指令不再被CPU執行,那麼該程式的這一次的執行過程,我們可以理解為是一個程式。也就是說一個程式是一個二進位制的程式中的指令從第一條指令被CPU執行,一直到最後一條指令被CPU執行的這整個過程。程式是一個磁碟中的程式執行的例項。

一個程式檔案是一個具有一定結構的二進位制檔案,一般情況下不同的作業系統的二進位制檔案的結構是不同的,但是其大體概念是相似的,例如linux和windows程式檔案都包含有程式碼段,資料段,符號表等等區域。程式檔案中除了程式碼段中的二進位制資料是CPU真正要執行的指令以外,其他的段要麼是在CPU執行過程中需要使用到的記憶體資訊(例如變數),要麼是編譯器除錯的時候需要用到的輔助資訊(例如符號表)。

在作業系統中有一些資訊是與一個執行的程式,也就是程式相關聯的,一般我們稱之為PCB(程式控制塊),該資訊中描述了程式在執行過程與該程式關聯的資訊,例如程式的ID,程式計數器,堆疊等等。

通常每一個程式有一個地址空間和一個控制執行緒(主執行緒),當然一個程式中可以有多個控制執行緒,這些執行緒就像分離的程式一樣,只不過他們共享同一個程式的地址空間以及其他資源。在記憶體中也有一個資訊結構來記錄與每個執行緒關聯的資訊,例如程式計數器,堆疊資訊等。

程式是資源分配的最小單位,執行緒是CPU排程的最小單位。

單執行緒與多執行緒程式、併發

一個程式到底是單執行緒結構好還是多執行緒好呢?從CPU的角度上來看,如果是單核CPU在任何時刻,只能執行一個程式,也就是說只能排程一個執行緒(如果是單執行緒的程式則執行程式的時候實際上排程的是該程式的主執行緒),單核CPU的多工併發實際上是偽併發,任何時刻只有一個程式處於執行中,只不過CPU的執行很快,多個程式可以輪番得到CPU,看起來好像是同時執行的一樣。執行緒是CPU排程的最小單位,任何時刻只有一個執行緒獲得CPU執行,所以理論上多個執行緒執行的時候多了執行緒上下文切換的開銷,應該比單執行緒的程式效率低。但是由於複雜程式的業務邏輯,單執行緒程式中如果有阻塞的邏輯,則反而因為阻塞,該執行緒被作業系統切換出去,不能得到CPU,而降低了程式得到CPU的時間,另外單執行緒程式的設計往往比較複雜,由程式設計帶來的複雜性可能會抵消多執行緒上下文切換的損失。所以一般情況下當程式中有比較耗時的操作時,單執行緒程式是不合適的,應該用多執行緒比較好。如果是CPU密集型的程式,加上良好的程式設計,則使用單執行緒的結構效率比較高(單執行緒的程式一般使用狀態機來驅動程式的執行)。而即使多執行緒的程式有自己的優點,也並不是執行緒越多越好,當執行緒非常多的時候執行緒上下文切換的開銷就比較可觀了。現代計算機一般都擁有多核CPU,從這點上來講多執行緒的程式更能取得比較高的CPU利用率。

儲存管理

儲存器是計算機系統中的重要組成部分,一個程式要執行,必須先從磁碟拷貝到記憶體上,然後從記憶體中讀取計算機指令進行執行。而計算機儲存的使用歷史也是伴隨著計算機以及作業系統的發展變化的。

最開始的作業系統是單任務的作業系統,某一時刻只有一個程式在執行,也沒有執行緒的概念。所以程式中使用的地址都是實際的實體地址,而該實體地址是編譯器在編譯階段分配的。程式在執行之前被載入記憶體中然後被執行。

隨著使用者不滿足於只執行一個程式,多個程式開始被同時執行,那麼此時程式中的地址就不可能使用實際的實體地址了,因為要保證兩個程式同時執行互相不產生干擾的話,他們就不應該同時訪問相同的實體地址,而如果此時程式中使用的地址還是實際的實體地址並且需要保證程式訪問的地址空間不會重疊,在編譯階段編譯器幾乎不可能實現。編譯器可能的實現方式是使用相對地址,而程式只需要在執行階段知道一個基地址,程式中的其他地址則通過基地址加上偏移地址(也就是相對地址由編譯階段填到程式中)的方式來實現。當然也會有一個暫存器去記錄程式訪問記憶體的最大邊界,以防止程式訪問越界。這種方式解決了多個程式同時執行的記憶體分配問題,使用統一的方式來分配記憶體,也降低了編譯器編譯的複雜度。

如果記憶體空間無限大,使用基地址加上偏移地址的方式即可解決記憶體的分配問題。而實際上雖然即使現在記憶體空間越來越大,但是隨著程式越來越大,使用者要求同時執行的程式越來越多,要想在程式執行之前將所有程式載入到記憶體,以現在的記憶體空間恐怕是辦不到的,特別是有的程式動則以G為單位。為了適應這樣的需求,出現了記憶體交換的技術,其實現過程是這樣的,例如有3個程式需要執行,先載入程式A執行,再載入程式B執行,此時再載入C執行,然而剩餘的記憶體空間不夠載入C程式,此時可以選擇將B程式執行的所有記憶體映像交換到磁碟空間儲存,這樣B程式佔用的記憶體空間被釋放,然後載入C程式執行,如此反覆。這種方式就是記憶體交換技術。但是即使採用記憶體交換技術,也只能解決一部分問題,另外即使現代磁碟的傳輸速度越來越快,但是頻繁的記憶體交換,仍然會大大影響程式的效能,特別是當執行的程式很大的時候,例如將1G的程式交換到磁碟上,即使SATA磁碟傳輸的峰值是100MB/s也仍然需要10秒才能完成交換操作,而將該程式載入到記憶體中重新執行也需要同樣的時間。

現代作業系統採取另外一種方式來做這樣的操作,即將程式分割成很多的小塊,每一小塊的大小或是4K,或是8K,16K,32K等,每次載入程式的時候以塊為單位載入。這些分塊操作全部由計算機完成,不需要程式設計師干預,這種方式被稱為虛擬記憶體。每個程式有自己的地址空間,這個空間被分割成多個塊,每個塊稱為一個頁或者頁面,每一頁有連續的地址範圍。這些頁被對映到實體記憶體,而並不要求所有的頁都在記憶體中才能執行程式,當程式引用到的地址在實際的實體記憶體中有對應的對映的時候,由硬體來翻譯該地址為實際的實體地址,如果沒有對應的對映,則會引發一個缺頁中斷,由作業系統負責將缺失的部分載入到實體記憶體並重新執行失敗的指令。

程式使用的地址空間叫虛擬地址空間,虛擬地址空間中的地址都是虛擬地址,其地址範圍可以從0到最大的實體記憶體地址,例如實體記憶體是4G,那麼程式的虛擬地址空間是0-4G,每個程式都是這樣。那麼如果同時執行多個程式會怎樣呢,這樣兩個程式都可以訪問到最大的4G記憶體,如果他們訪問的是同一個地址會怎麼樣?在實際執行過程中,虛擬地址會被一個叫做MMU(記憶體管理單元)的設施翻譯成為實際的實體地址,也就是說每一個程式使用的虛擬地址都會被對映到實際的實體地址,而兩個程式中相同虛擬地址會被對映到不同的實體地址,這樣就不存在訪問衝突的問題了。程式的虛擬地址空間是相同的,這給編譯器的編寫帶來了極大的方便,生成的程式程式碼使用統一的地址空間模型,地址的翻譯工作交給了MMU與作業系統。

以上實際上是現代計算機採用的記憶體執行模型,實際上這是最基本的原理,還有一些其他的細節,例如程式的虛擬地址空間被分成多個頁,每個頁會對映到實際的實體地址頁,而實體地址頁我們稱為頁框,頁與頁框的大小一般是相同的,而頁與頁框的對映關係表則稱為頁表。關於頁到頁框的解析也是一個複雜過程,在頁表項中的各種二進位制位用於解析頁到頁框的對映。

當發生缺頁中斷的時候,由作業系統負責載入缺失的頁面到實體記憶體中,當然如果記憶體夠的話是沒有任何問題的,如果在記憶體不夠的情況下,則必須要做出選擇將記憶體中的其他頁交換到磁碟中,這樣騰出空間給即將載入的頁,當作業系統成功載入需要的頁之後,會重新修改頁表中的對映關係,並執行引發缺頁中斷的指令,繼續原來程式的執行過程。那麼作業系統如何選擇將哪些頁面交換到磁碟以騰出記憶體空間呢,這種選擇的策略則是頁面置換演算法的內容。現代作業系統有多種頁面置換演算法,並不會單純的採用某一種演算法,而是綜合使用。例如最優頁面置換,最近未使用頁面置換,先進先出頁面置換,時鐘頁面置換,最近最少使用頁面置換等等。

因為有了虛擬記憶體的存在,可以將同一個實體記憶體對映到兩個不同程式的虛擬地址空間中,這樣兩個程式可以同時讀寫該塊實體記憶體區域,這就是共享記憶體。同樣可以將虛擬地址空間對映到一個檔案,這樣程式可以像操作記憶體一樣方便的讀寫一個檔案,這是記憶體對映檔案。

檔案系統

什麼是檔案系統呢,在說明這個概念之前,我們先說說檔案是怎麼儲存的,我們以儲存介質硬碟為例,硬碟的物理結構是由很多碟片組成的,每一個碟片有兩面,每一面由一圈一圈的同心圓組成的磁軌,而將多個碟片疊在一起之後,所有疊在一起的碟片上的同一個位置的同心圓組成一個圓柱體,這個由所有碟片上相同的磁軌組成圓柱稱為柱面。而每一個碟片上的每一個磁軌被分割成一些等分的圓弧,這些圓弧叫扇區。在物理上通過電磁的原理從磁碟讀寫資料,我們可以認為可以將0,1這兩種不同的狀態記錄到磁碟的磁軌上,而這些連續的0,1的狀態就是計算機中的二進位制位流。所以可以認為在磁軌上儲存的就是表示不同狀態的二進位制位流資料。那麼整個磁碟是由多個碟片組成的,每個碟片上有很多磁軌,如何組織這些資料呢?就必須要對所有的碟片,柱面,扇區,磁軌進行標識,並且方便查詢與讀寫。也就是說需要用一個結構或者一組規則來組織他們,這就是檔案系統。

檔案系統是作業系統用於明確儲存裝置或分割槽上的檔案的方法和資料結構,也就是儲存裝置上組織檔案的方法,從系統角度看,檔案系統是對檔案儲存裝置的空間進行組織和分配,負責檔案儲存並對存入的檔案進行保護和檢索的系統。具體的說,它負責為使用者建立檔案,存入、讀出、修改、轉儲檔案,控制檔案的存取,當使用者不再使用時撤銷檔案等。

如果沒有檔案系統這樣的組織規則,那麼我們看磁碟則是一連串的無意義的二進位制位流資料,我們分不清楚哪裡是開始,哪裡是結束,這些二進位制位流資訊的含義。就好像一個doc文件,我們使用二進位制編輯軟體開啟之後看到的就是一連串毫無意義的二進位制位流一樣,然而使用doc文件的結構規則來解析這些位流資訊的含義,我們可以解析出該doc文件中的文字,圖片,以及格式等資訊。而檔案系統也起到類似的作用,檔案系統本身也佔用磁碟空間,是存放在磁碟上的。多數磁碟劃分為一個或多個分割槽,每個分割槽中都可以有一個獨立的檔案系統。磁碟的0號扇區稱為主開機記錄(MBR),用來引導計算機。在MBR的結尾是分割槽表。該表給出了每個分割槽的起始和結束地址。表中的一個分割槽被標記為活動分割槽,在計算機被引導時,BIOS讀入並執行MBR。MBR做的第一件事是確定活動分割槽,讀入它的第一個塊,稱為引導塊,並執行之。引導塊中的程式將裝載該分割槽中的作業系統。

每一個磁碟分割槽由引導塊、超級塊、空閒空間管理、i節點、根目錄、其他檔案和目錄的順序組成。超級塊包含檔案系統的所有關鍵引數,在計算機啟動的時候或者該檔案系統首次使用時,把超級塊讀入記憶體。超級塊中的典型資訊包括:確定檔案系統型別用的魔數、檔案系統中資料塊的數量以及其他重要的管理資訊。

在讀檔案前必須先開啟檔案,開啟檔案時,作業系統利用使用者給出的路徑找到相應的目錄項,目錄項中提供了查詢檔案磁碟塊所需要的資訊。而檔案的屬性資訊可以存放在目錄項中或者存放在檔案的i節點的資訊中。而這些目錄項以及i節點存放在物理磁碟的哪個柱面,哪個磁軌,哪個扇區的資訊是可以在i節點資訊以及從根目錄根據路徑層層查詢的目錄項資訊中得到的。

光碟的儲存方式與磁碟類似,只不過光碟使用的介質並不是磁介質,而是其他型別的介質,所以光碟沒有磁軌的概念,光碟實際上是通過一個螺旋狀的類似於磁軌的軌跡來儲存資料的,在這條軌跡上記錄了0,1兩種不同狀態的位流資料。由於光碟的儲存特性,光碟上的檔案系統與磁碟使用的檔案系統是有不同的,具體可以查閱光碟相關的格式與檔案系統文件,但是其思路與磁碟檔案系統是類似的。

輸入輸出(IO)

作業系統管理者各種各樣的連線到計算機上的輸入輸出裝置,IO裝置可以分為兩類,塊裝置和字元裝置,塊裝置把資訊儲存在固定大小的塊中,每個塊有自己的地址。通常塊的大小在512位元組和32768位元組之間。所有傳輸以一個或多個完整的塊為單位。塊裝置的基本特徵是每個塊都能獨立於其他的塊進行讀寫。硬碟,CD-ROM,USB盤是常見的塊裝置。另一種裝置是字元裝置,字元裝置以字元為單位傳送或接受一個字元流,而不考慮任何塊結構。字元裝置是不可定址的,也沒有尋道操作。印表機,網路介面,滑鼠,以及大多數與磁碟不同的裝置都可以看作是字元裝置。

IO裝置一般由機械部件和電子部件兩部分組成,電子部件稱作裝置控制器或者介面卡。在個人計算機上它經常以主機板上的晶片的形式出現,或者以插入(PCI)擴充套件槽中的印刷電路板的形式出現。機械部件則是裝置本身。

控制器與裝置之間的介面是一個很低層次的介面。以磁碟裝置為例,從裝置中出來的是一個序列的位流,控制器的任務是把序列的位流轉換為位元組塊,並進行必要的錯誤校正工作,位元組塊通常首先在控制器內部的一個緩衝區按位進行組裝,然後進行校驗證明位元組塊沒有錯誤之後,將它複製到主存中。

CPU如何從裝置中讀取資料(例如磁碟)或寫資料到裝置中呢? CPU從磁碟讀寫資料是直接與磁碟的裝置控制器(也就是磁碟介面卡)進行資料互動的,每個裝置控制器中有幾個暫存器用來與CPU進行通訊,通過寫入這些暫存器,作業系統可以命令裝置傳送資料,接收資料,開啟或者關閉,或者執行其他操作。通過讀取這些暫存器的值,作業系統可以知道裝置當前的狀態。除了裝置控制器中的暫存器之外,許多裝置還有一個作業系統可以讀寫的資料緩衝區,可以供程式或者作業系統讀寫資料。那麼作業系統要操作這些控制暫存器,就需要有一個地址來標識這些控制暫存器,有兩種方式對控制暫存器進行地址編號,一種是為每個控制暫存器分配一個IO埠號,所有IO埠形成IO埠空間,該埠空間獨立於記憶體的地址空間,是完全不同的。IO埠空間受到保護使普通的使用者程式不能訪問,只有作業系統可以訪問。CPU可以通過這些IO埠號來讀寫控制暫存器的內容,從而達到與裝置控制器通訊的目的。另一種是將IO埠號對映到記憶體的地址空間,每個控制暫存器被分配一個記憶體地址,這樣的系統稱為記憶體對映IO。通常分配給控制暫存器的地址位於記憶體地址的頂端,並且該地址不會再分配給應用程式。在這種模式下,IO埠的地址與記憶體地址是一個地址空間,並不是獨立的。這種模式的好處是,不需要使用特定的指令去操作控制暫存器,只需要普通的訪問記憶體的指令即可以對控制暫存器進行讀寫操作,就像程式中普通讀寫記憶體一樣。

上面講述了CPU如何與裝置進行通訊的原理,那麼讀寫檔案的具體過程是怎麼樣的呢? 一般情況是這樣的,在程式中我們一般通過系統提供的read,write函式對檔案進行讀寫,當然在讀寫之前一般會有一個open操作(傳遞一個檔案路徑,以及開啟的方式),讀寫完成之後一般會有一個close操作關閉檔案。當使用open操作開啟一個檔案的時候,最終會呼叫到檔案系統的相關介面,並且返回的檔案描述符與檔案的inode節點資訊在作業系統的核心中肯定有對應的關聯關係,此後在read或者write的時候傳遞進去的檔案描述符,最後應該被檔案系統關聯到檔案相關的底層結構例如inode資訊,通過inode資訊就可以操作該檔案在磁碟上的塊了(檔案系統應該有查詢檔案的第幾個位元組在磁碟的哪個塊上的能力)。在write或者read的時候,CPU先通知磁碟裝置控制器,裝置控制器從磁碟尋道,定址,一位一位的讀取某一個塊(或者是以扇區為單位),直到將整個塊讀入到控制器的內部緩衝區中,計算校驗和,保證沒有錯誤。然後控制器產生一箇中斷,等到作業系統響應的時候,CPU從裝置控制器的緩衝中一個位元組一個位元組的讀取資料,並拷貝到記憶體中(read的時候會指定一個buffer,也就是資料要讀到的記憶體地址)。這種模式是IO操作模式中的一種。下面說說IO實現的三種方法。

第一種方法是程式控制IO,程式需要輪詢IO裝置的狀態,以檢視IO裝置是否準備好,例如印表機,程式需要輪詢印表機中的暫存器,以檢視印表機是否準備好接受列印的字元,如果準備好,程式將需要列印的字元拷貝到印表機的緩衝區中,如果印表機此時開始列印一個字元,並同時設定印表機的狀態為非就緒,在印表機列印該字元的過程中,程式必須一直查詢並等待印表機完成該字元的列印直到印表機完成該字元的列印過程之後,其暫存器中表示印表機狀態的值被改為就緒狀態,程式拷貝第二個需要列印的字元到印表機的緩衝區中。如此迴圈,直到所有列印完成為止。這種模式浪費了大量的CPU時間,因為印表機列印的過程是一個耗時的過程,而在CPU等待印表機列印字元的過程,將大量的CPU週期浪費在輪詢印表機的狀態上,這就出現了第二種方式。

第二種方式是中斷驅動的IO,當印表機開始列印第一個字元的時候,CPU去做其他事情(與當前列印相關的程式被排程到阻塞狀態,其他的程式或者執行緒獲得CPU),印表機完成第一個字元的列印之後,發出一箇中斷,中斷處理程式執行,之前被掛起的程式重新開始執行,拷貝第二個需要列印的字元給印表機,如此反覆。在這種模型下,由於IO裝置的操作很慢(例如印表機的列印操作),在IO裝置執行操作的時候CPU不必等在那裡,而是可以做其他事情,等IO操作完成之後,以中斷的方式通知,這樣大大的節約了CPU的時鐘週期。這種方式的缺點是每一次IO裝置的讀寫都會產生中斷,中斷髮生在每個字元上。而且中斷是需要花費時間的,所以這一方法將浪費一定量的CPU時間。

第三種方式是使用DMA(直接儲存器存取),讓DMA控制器來操作IO而不是CPU。本質上DMA是程式控制IO,只不過是由DMA控制器而不是主CPU做全部工作。其實我們可以想象成DMA是一個特定功能的CPU,該特定功能也即是對IO裝置的操作。將原來需要CPU進行IO操作的部分,放到DMA控制器上實現。而對於IO裝置來說,例如磁碟控制器並不知道或者並不關心,磁碟的讀寫請求是來自CPU還是DMA控制器。當程式需要將檔案中的某些位元組拷貝到記憶體中的一塊區域時,首先CPU告訴DMA控制器需要將什麼資料傳送到什麼地方,而該程式(或者當前請求IO的執行緒)被掛起阻塞,接著DMA控制器向磁碟控制器發出命令,磁碟控制器尋道,定址,將資料拷貝到磁碟控制器的緩衝區中。接著DMA控制器在匯流排上發出一個讀請求給磁碟控制器,開始DMA傳送。傳輸完成之後DMA控制器將中斷CPU,以讓CPU知道傳輸已經完成。接著中斷服務程式開始執行,之前被掛起的程式(執行緒)繼續開始執行,而此時檔案的內容已經讀到了指定的記憶體地址了。這期間沒有佔用CPU的時鐘週期。

總結與其他

計算機系統是一個非常複雜與龐大的系統,以上的內容僅僅是一個入門的指引,限於篇幅很多細枝末節的內容都沒有提到,如果有任何錯誤歡迎大家指出。另外計算機網路本身是一個非常大的課題所以我希望用單獨的一篇文章進行說明。另外留下一個問題,也是我有疑惑的地方,如果大家有答案,可以在留言區域討論。

問題:在單核CPU的架構下,多執行緒之間的鎖其實相對比較容易實現,因為任何時刻其實只有一個程式的一個執行緒在佔用CPU,所謂的併發實際上是偽併發,真正的多核CPU的併發叫並行。單核CPU情況下,由於CPU輪轉的非常快,讓我們看起來是同時執行了多個程式,那麼問題是,如果在多個CPU的情況下,原來寫的多執行緒程式的互斥與同步還有效果嗎?因為很可能一個程式中的兩個執行緒同時在兩個不同CPU上執行,他們訪問同一個變數,這個加鎖還有效果嗎?

相關文章