1. 程式人生 > >寫給大忙人看的作業系統

寫給大忙人看的作業系統

文章主要結構圖如下

作業系統

現代計算機系統由一個或多個處理器、主存、印表機、鍵盤、滑鼠、顯示器、網路介面以及各種輸入/輸出裝置構成。

然而,程式設計師不會直接和這些硬體打交道,而且每位程式設計師不可能會掌握所有計算機系統的細節,這樣我們就不用再編寫程式碼了,所以在硬體的基礎之上,計算機安裝了一層軟體,這層軟體能夠通過響應使用者輸入的指令達到控制硬體的效果,從而滿足使用者需求,這種軟體稱之為 作業系統,它的任務就是為使用者程式提供一個更好、更簡單、更清晰的計算機模型。

我們一般常見的作業系統主要有 Windows、Linux、FreeBSD 或 OS X ,這種帶有圖形介面的作業系統被稱為 圖形使用者介面(Graphical User Interface, GUI)

,而基於文字、命令列的通常稱為 Shell。下面是我們所要探討的作業系統的部件

這是一個作業系統的簡化圖,最下面的是硬體,硬體包括晶片、電路板、磁碟、鍵盤、顯示器等我們上面提到的裝置,在硬體之上是軟體。大部分計算機有兩種執行模式:核心態使用者態,軟體中最基礎的部分是作業系統,它執行在 核心態 中,核心態也稱為 管態核心態,它們都是作業系統的執行狀態,只不過是不同的叫法而已。作業系統具有硬體的訪問權,可以執行機器能夠執行的任何指令。軟體的其餘部分執行在 使用者態 下。

使用者介面程式(shell 或者 GUI)處於使用者態中,並且它們位於使用者態的最低層,允許使用者執行其他程式,例如 Web 瀏覽器、電子郵件閱讀器、音樂播放器等。而且,越靠近使用者態的應用程式越容易編寫,如果你不喜歡某個電子郵件閱讀器你可以重新寫一個或者換一個,但你不能自行寫一個作業系統或者是中斷處理程式。這個程式由硬體保護,防止外部對其進行修改。

計算機硬體簡介

作業系統與執行作業系統的核心硬體關係密切。作業系統擴充套件了計算機指令集並管理計算機的資源。因此,作業系統因此必須足夠了解硬體的執行,這裡我們先簡要介紹一下現代計算機中的計算機硬體。

從概念上來看,一臺簡單的個人電腦可以被抽象為上面這種相似的模型,CPU、記憶體、I/O 裝置都和匯流排串聯起來並通過匯流排與其他裝置進行通訊。現代作業系統有著更為複雜的結構,會設計很多條匯流排,我們稍後會看到。暫時來講,這個模型能夠滿足我們的討論。

CPU

CPU 是計算機的大腦,它主要和記憶體進行互動,從記憶體中提取指令並執行它。一個 CPU 的執行週期是從記憶體中提取第一條指令、解碼並決定它的型別和運算元,執行,然後再提取、解碼執行後續的指令。重複該迴圈直到程式執行完畢。

每個 CPU 都有一組可以執行的特定指令集。因此,x86 的 CPU 不能執行 ARM 的程式並且 ARM 的 CPU 也不能執行 x86 的程式。由於訪問記憶體獲取執行或資料要比執行指令花費的時間長,因此所有的 CPU 內部都會包含一些暫存器來儲存關鍵變數和臨時結果。因此,在指令集中通常會有一些指令用於把關鍵字從記憶體中載入到暫存器中,以及把關鍵字從暫存器存入到記憶體中。還有一些其他的指令會把來自暫存器和記憶體的運算元進行組合,例如 add 操作就會把兩個運算元相加並把結果儲存到記憶體中。

除了用於儲存變數和臨時結果的通用暫存器外,大多數計算機還具有幾個特殊的暫存器,這些暫存器對於程式設計師是可見的。其中之一就是 程式計數器(program counter),程式計數器會指示下一條需要從記憶體提取指令的地址。提取指令後,程式計數器將更新為下一條需要提取的地址。

另一個暫存器是 堆疊指標(stack pointer),它指向記憶體中當前棧的頂端。堆疊指標會包含輸入過程中的有關引數、區域性變數以及沒有儲存在暫存器中的臨時變數。

還有一個暫存器是 PSW(Program Status Word) 程式狀態字暫存器,這個暫存器是由作業系統維護的8個位元組(64位) long 型別的資料集合。它會跟蹤當前系統的狀態。除非發生系統結束,否則我們可以忽略 PSW 。使用者程式通常可以讀取整個PSW,但通常只能寫入其某些欄位。PSW 在系統呼叫和 I / O 中起著重要作用。

作業系統必須瞭解所有的暫存器。在時間多路複用(time multiplexing) 的 CPU 中,作業系統往往停止執行一個程式轉而執行另外一個。每次當作業系統停止執行一個程式時,作業系統會儲存所有暫存器的值,以便於後續重新執行該程式。

為了提升效能, CPU 設計人員早就放棄了同時去讀取、解碼和執行一條簡單的指令。許多現代的 CPU 都具有同時讀取多條指令的機制。例如,一個 CPU 可能會有單獨訪問、解碼和執行單元,所以,當 CPU 執行第 N 條指令時,還可以對 N + 1 條指令解碼,還可以讀取 N + 2 條指令。像這樣的組織形式被稱為 流水線(pipeline)

比流水線更先進的設計是 超標量(superscalar)CPU,下面是超標量 CPU 的設計

在上面這個設計中,存在多個執行單元,例如,一個用來進行整數運算、一個用來浮點數運算、一個用來布林運算。兩個或者更多的指令被一次性取出、解碼並放入緩衝區中,直至它們執行完畢。只要一個執行單元空閒,就會去檢查緩衝區是否有可以執行的指令。如果有,就把指令從緩衝區中取出並執行。這種設計的含義是應用程式通常是無序執行的。在大多數情況下,硬體負責保證這種運算的結果與順序執行指令時的結果相同。

除了用在嵌入式系統中非常簡單的 CPU 之外,多數 CPU 都有兩種模式,即前面已經提到的核心態和使用者態。通常情況下,PSW 暫存器中的一個二進位制位會控制當前狀態是核心態還是使用者態。當執行在核心態時,CPU 能夠執行任何指令集中的指令並且能夠使用硬體的功能。在臺式機和伺服器上,作業系統通常以核心模式執行,從而可以訪問完整的硬體。在大多數嵌入式系統中,一部分執行在核心態下,剩下的一部分執行在使用者態下。

使用者應用程式通常執行在使用者態下,在使用者態下,CPU 只能執行指令集中的一部分並且只能訪問硬體的一部分功能。一般情況下,在使用者態下,有關 I/O 和記憶體保護的所有指令是禁止執行的。當然,設定 PSW 模式的二進位制位為核心態也是禁止的。

為了獲取作業系統的服務,使用者程式必須使用 系統呼叫(system call),系統呼叫會轉換為核心態並且呼叫作業系統。TRAP 指令用於把使用者態切換為核心態並啟用作業系統。當有關工作完成之後,在系統呼叫後面的指令會把控制權交給使用者程式。我們會在後面探討作業系統的呼叫細節。

需要注意的是作業系統在進行系統呼叫時會存在陷阱。大部分的陷阱會導致硬體發出警告,比如說試圖被零除或浮點下溢等你。在所有的情況下,作業系統都能得到控制權並決定如何處理異常情況。有時,由於出錯的原因,程式不得不停止。

多執行緒和多核晶片

Intel Pentinum 4也就是奔騰處理器引入了被稱為多執行緒(multithreading)超執行緒(hyperthreading, Intel 公司的命名) 的特性,x86 處理器和其他一些 CPU 晶片就是這樣做的。包括 SSPARC、Power5、Intel Xeon 和 Intel Core 系列 。近似地說,多執行緒允許 CPU 保持兩個不同的執行緒狀態並且在納秒級(nanosecond) 的時間完成切換。執行緒是一種輕量級的程序,我們會在後面說到。例如,如果一個程序想要從記憶體中讀取指令(這通常會經歷幾個時鐘週期),多執行緒 CPU 則可以切換至另一個執行緒。多執行緒不會提供真正的並行處理。在一個時刻只有一個程序在執行。

對於作業系統來講,多執行緒是有意義的,因為每個執行緒對作業系統來說都像是一個單個的 CPU。比如一個有兩個 CPU 的作業系統,並且每個 CPU 執行兩個執行緒,那麼這對於作業系統來說就可能是 4 個 CPU。

除了多執行緒之外,現在許多 CPU 晶片上都具有四個、八個或更多完整的處理器或核心。多核晶片在其上有效地承載了四個微型晶片,每個微型晶片都有自己的獨立CPU。

如果要說在絕對核心數量方面,沒有什麼能贏過現代 GPU(Graphics Processing Unit),GPU 是指由成千上萬個微核組成的處理器。它們擅長處理大量並行的簡單計算。

記憶體

計算機中第二個主要的元件就是記憶體。理想情況下,記憶體應該非常快速(比執行一條指令要快,從而不會拖慢 CPU 執行效率),而且足夠大且便宜,但是目前的技術手段無法滿足三者的需求。於是採用了不同的處理方式,儲存器系統採用一種分層次的結構

頂層的儲存器速度最高,但是容量最小,成本非常高,層級結構越向下,其訪問效率越慢,容量越大,但是造價也就越便宜。

暫存器

儲存器的頂層是 CPU 中的暫存器,它們用和 CPU 一樣的材料製成,所以和 CPU 一樣快。程式必須在軟體中自行管理這些暫存器(即決定如何使用它們)

快取記憶體

位於暫存器下面的是快取記憶體,它多數由硬體控制。主存被分割成快取記憶體行(cache lines) 為 64 位元組,記憶體地址的 0 - 63 對應快取記憶體行 0 ,地址 64 - 127 對應快取記憶體行的 1,等等。使用最頻繁的快取記憶體行儲存在位於 CPU 內部或非常靠近 CPU 的快取記憶體中。當應用程式需要從記憶體中讀取關鍵詞的時候,快取記憶體的硬體會檢查所需要的快取記憶體行是否在快取記憶體中。如果在的話,那麼這就是快取記憶體命中(cache hit)。快取記憶體滿足了該請求,並且沒有通過匯流排將記憶體請求傳送到主記憶體。快取記憶體命中通常需要花費兩個時鐘週期。快取未命中需要從記憶體中提取,這會消耗大量的時間。快取記憶體行會限制容量的大小因為它的造價非常昂貴。有一些機器會有兩個或者三個快取記憶體級別,每一級快取記憶體比前一級慢且容量更大。

快取在計算機很多領域都扮演了非常重要的角色,不僅僅是 RAM 快取行。

隨機儲存器(RAM): 記憶體中最重要的一種,表示既可以從中讀取資料,也可以寫入資料。當機器關閉時,記憶體中的資訊會 丟失

大量的可用資源被劃分為小的部分,這些可用資源的一部分會獲得比其他資源更頻繁的使用權,快取經常用來提升效能。作業系統無時無刻的不在使用快取。例如,大多數作業系統在主機記憶體中保留(部分)頻繁使用的檔案,以避免重複從磁碟重複獲取。舉個例子,類似於 /home/ast/projects/minix3/src/kernel/clock.c 這樣的場路徑名轉換成的檔案所在磁碟地址的結果也可以儲存快取中,以避免重複定址。另外,當一個 Web 頁面(URL) 的地址轉換為網路地址(IP地址)後,這個轉換結果也可以快取起來供將來使用。

在任何快取系統中,都會有下面這幾個噬需解決的問題

  • 何時把新的內容放進快取
  • 把新的內容應該放在快取的哪一行
  • 在需要空閒空間時,應該把哪塊內容從快取中移除
  • 應該把移除的內容放在某個較大儲存器的何處

並不是每個問題都與每種快取情況有關。對於 CPU 快取中的主存快取行,當有快取未命中時,就會調入新的內容。通常通過所引用記憶體地址的高位計算應該使用的快取行。

快取是解決問題的一種好的方式,所以現代 CPU 設計了兩種快取。第一級快取或者說是 L1 cache 總是位於 CPU 內部,用來將已解碼的指令調入 CPU 的執行引擎。對於那些頻繁使用的關鍵字,多數晶片有第二個 L1 cache 。典型的 L1 cache 的大小為 16 KB。另外,往往還設有二級快取,也就是 L2 cache,用來存放最近使用過的關鍵字,一般是兆位元組為單位。L1 cache 和 L2 cache 最大的不同在於是否存在延遲。訪問 L1 cache 沒有任何的延遲,然而訪問 L2 cache 會有 1 - 2 個時鐘週期的延後。

什麼是時鐘週期?計算機處理器或 CPU 的速度由時鐘週期來確定,該時鐘週期是振盪器兩個脈衝之間的時間量。一般而言,每秒脈衝數越高,計算機處理器處理資訊的速度就越快。 時鐘速度以 Hz 為單位測量,通常為兆赫(MHz)或千兆赫(GHz)。 例如,一個4 GHz處理器每秒執行4,000,000,000個時鐘週期。

計算機處理器可以在每個時鐘週期執行一條或多條指令,這具體取決於處理器的型別。 早期的計算機處理器和較慢的 CPU 在每個時鐘週期只能執行一條指令,而現代處理器在每個時鐘週期可以執行多條指令。

主存

在上面的層次結構中再下一層是主存,這是記憶體系統的主力軍,主存通常叫做 RAM(Random Access Memory),由於 1950 年代和 1960 年代的計算機使用微小的可磁化鐵氧體磁芯作為主儲存器,因此舊時有時將其稱為核心儲存器。所有不能再快取記憶體中得到滿足的記憶體訪問請求都會轉往主存中。

除了主存之外,許多計算機還具有少量的非易失性隨機存取儲存器。它們與 RAM 不同,在電源斷電後,非易失性隨機訪問儲存器並不會丟失內容。ROM(Read Only Memory) 中的內容一旦儲存後就不會再被修改。它非常快而且便宜。(如果有人問你,有沒有什麼又快又便宜的記憶體裝置,那就是 ROM 了)在計算機中,用於啟動計算機的引導載入模組(也就是 bootstrap )就存放在 ROM 中。另外,一些 I/O 卡也採用 ROM 處理底層裝置控制。

EEPROM(Electrically Erasable PROM,)快閃記憶體(flash memory) 也是非易失性的,但是與 ROM 相反,它們可以擦除和重寫。不過重寫它們需要比寫入 RAM 更多的時間,所以它們的使用方式與 ROM 相同,但是與 ROM 不同的是他們可以通過重寫欄位來糾正程式中出現的錯誤。

快閃記憶體也通常用來作為便攜性的儲存媒介。快閃記憶體是數碼相機中的膠捲,是行動式音樂播放器的磁碟。快閃記憶體的速度介於 RAM 和磁碟之間。另外,與磁碟儲存器不同的是,如果快閃記憶體擦除的次數太多,會出現磨損。

還有一類是 CMOS,它是易失性的。許多計算機都會使用 CMOS 儲存器保持當前時間和日期。

磁碟

下一個層次是磁碟(硬碟),磁碟同 RAM 相比,每個二進位制位的成本低了兩個數量級,而且經常也有兩個數量級大的容量。磁碟唯一的問題是隨機訪問資料時間大約慢了三個數量級。磁碟訪問慢的原因是因為磁碟的構造不同

磁碟是一種機械裝置,在一個磁碟中有一個或多個金屬碟片,它們以 5400rpm、7200rpm、10800rpm 或更高的速度旋轉。從邊緣開始有一個機械臂懸橫在盤面上,這類似於老式播放塑料唱片 33 轉唱機上的拾音臂。資訊會寫在磁碟一系列的同心圓上。在任意一個給定臂的位置,每個磁頭可以讀取一段環形區域,稱為磁軌(track)。把一個給定臂的位置上的所有磁軌合併起來,組成了一個柱面(cylinder)

每個磁軌劃分若干扇區,扇區的值是 512 位元組。在現代磁碟中,較外部的柱面比較內部的柱面有更多的扇區。機械臂從一個柱面移動到相鄰的柱面大約需要 1ms。而隨機移到一個柱面的典型時間為 5ms 至 10ms,具體情況以驅動器為準。一旦磁臂到達正確的磁軌上,驅動器必須等待所需的扇區旋轉到磁頭之下,就開始讀寫,低端硬碟的速率是50MB/s,而高速磁碟的速率是 160MB/s

需要注意,固態硬碟(Solid State Disk, SSD)不是磁碟,固態硬碟並沒有可以移動的部分,外形也不像唱片,並且資料是儲存在儲存器(快閃記憶體)中,與磁碟唯一的相似之處就是它也儲存了大量即使在電源關閉也不會丟失的資料。

許多計算機支援一種著名的虛擬記憶體機制,這種機制使得期望執行的儲存空間大於實際的物理儲存空間。其方法是將程式放在磁碟上,而將主存作為一部分快取,用來儲存最頻繁使用的部分程式,這種機制需要快速映像記憶體地址,用來把程式生成的地址轉換為有關位元組在 RAM 中的實體地址。這種映像由 CPU 中的一個稱為 儲存器管理單元(Memory Management Unit, MMU) 的部件來完成。

快取和 MMU 的出現是對系統的效能有很重要的影響,在多道程式系統中,從一個程式切換到另一個程式的機制稱為 上下文切換(context switch),對來自快取中的資源進行修改並把其寫回磁碟是很有必要的。

I/O 裝置

CPU 和儲存器不是作業系統需要管理的全部,I/O 裝置也與作業系統關係密切。可以參考上面這個圖片,I/O 裝置一般包括兩個部分:裝置控制器和裝置本身。控制器本身是一塊晶片或者一組晶片,它能夠控制物理裝置。它能夠接收作業系統的指令,例如,從裝置中讀取資料並完成資料的處理。

在許多情況下,實際控制裝置的過程是非常複雜而且存在諸多細節。因此控制器的工作就是為作業系統提供一個更簡單(但仍然非常複雜)的介面。也就是遮蔽物理細節。任何複雜的東西都可以加一層代理來解決,這是計算機或者人類社會很普世的一個解決方案

I/O 裝置另一部分是裝置本身,裝置本身有一個相對簡單的介面,這是因為介面既不能做很多工作,而且也已經被標準化了。例如,標準化後任何一個 SATA 磁碟控制器就可以適配任意一種 SATA 磁碟,所以標準化是必要的。ATA 代表 高階技術附件(AT Attachment),而 SATA 表示序列高階技術附件(Serial ATA)

AT 是啥?它是 IBM 公司的第二代個人計算機的高階技術成果,使用 1984 年推出的 6MHz 80286 處理器,這個處理器是當時最強大的。

像是高階這種詞彙應該慎用,否則 20 年後再回首很可能會被無情打臉。

現在 SATA 是很多計算機的標準硬碟介面。由於實際的裝置介面隱藏在控制器中,所以作業系統看到的是對控制器的介面,這個介面和裝置介面有很大區別。

每種型別的裝置控制器都是不同的,所以需要不同的軟體進行控制。專門與控制器進行資訊交流,發出命令處理指令接收響應的軟體,稱為 裝置驅動程式(device driver)。 每個控制器廠家都應該針對不同的作業系統提供不同的裝置驅動程式。

為了使裝置驅動程式能夠工作,必須把它安裝在作業系統中,這樣能夠使它在核心態中執行。要將裝置驅動程式裝入作業系統,一般有三個途徑

  • 第一個途徑是將核心與裝置啟動程式重新連線,然後重啟系統。這是 UNIX 系統採用的工作方式
  • 第二個途徑是在一個作業系統檔案中設定一個入口,通知該檔案需要一個裝置驅動程式,然後重新啟動系統。在重新系統時,作業系統回尋找有關的裝置啟動程式並把它裝載,這是 Windows 採用的工作方式
  • 第三個途徑是作業系統能夠在執行時接收新的裝置驅動程式並立刻安裝,無需重啟作業系統,這種方式採用的少,但是正變得普及起來。熱插拔裝置,比如 USB 和 IEEE 1394 都需要動態可裝載的裝置驅動程式。

每個裝置控制器都有少量用於通訊的暫存器,例如,一個最小的磁碟控制器也會有用於指定磁碟地址、記憶體地址、扇區計數的暫存器。要啟用控制器,裝置驅動程式回從作業系統獲取一條指令,然後翻譯成對應的值,並寫入裝置暫存器中,所有裝置暫存器的結合構成了 I/O 埠空間

在一些計算機中,裝置暫存器會被對映到作業系統的可用地址空間,使他們能夠向記憶體一樣完成讀寫操作。在這種計算機中,不需要專門的 I/O 指令,使用者程式可以被硬體阻擋在外,防止其接觸這些儲存器地址(例如,採用基址暫存器和變址暫存器)。在另一些計算機中,裝置暫存器被放入一個專門的 I/O 埠空間,每個暫存器都有一個埠地址。在這些計算機中,特殊的 INOUT 指令會在核心態下啟用,它能夠允許裝置驅動程式和暫存器進行讀寫。前面第一種方式會限制特殊的 I/O 指令但是允許一些地址空間;後者不需要地址空間但是需要特殊的指令,這兩種應用都很廣泛。

實現輸入和輸出的方式有三種。

  • 在最簡單的方式中,使用者程式會發起系統呼叫,核心會將其轉換為相應驅動程式的程式呼叫,然後裝置驅動程式啟動 I/O 並迴圈檢查該裝置,看該裝置是否完成了工作(一般會有一些二進位制位用來指示裝置仍在忙碌中)。當 I/O 呼叫完成後,裝置驅動程式把資料送到指定的地方並返回。然後作業系統會將控制權交給呼叫者。這種方式稱為 忙等待(busy waiting),這種方式的缺點是要一直佔據 CPU,CPU 會一直輪詢 I/O 裝置直到 I/O 操作完成。
  • 第二種方式是裝置驅動程式啟動裝置並且讓該裝置在操作完成時發生中斷。裝置驅動程式在這個時刻返回。作業系統接著在需要時阻塞呼叫者並安排其他工作進行。當裝置驅動程式檢測到該裝置操作完成時,它發出一個 中斷 通知操作完成。

在作業系統中,中斷是非常重要的,所以這需要更加細緻的討論一下。

如上圖所示,這是一個三步的 I/O 過程,第一步,裝置驅動程式會通過寫入裝置暫存器告訴控制器應該做什麼。然後,控制器啟動裝置。當控制器完成讀取或寫入被告知需要傳輸的位元組後,它會在步驟 2 中使用某些匯流排向中斷控制器傳送訊號。如果中斷控制器準備好了接收中斷訊號(如果正忙於一個優先順序較高的中斷,則可能不會接收),那麼它就會在 CPU 的一個引腳上面宣告。這就是步驟3

在第四步中,中斷控制器把該裝置的編號放在總線上,這樣 CPU 可以讀取匯流排,並且知道哪個裝置完成了操作(可能同時有多個裝置同時執行)。

一旦 CPU 決定去實施中斷後,程式計數器和 PSW 就會被壓入到當前堆疊中並且 CPU 會切換到核心態。裝置編號可以作為記憶體的一個引用,用來尋找該裝置中斷處理程式的地址。這部分記憶體稱作中斷向量(interrupt vector)。一旦中斷處理程式(中斷裝置的裝置驅動程式的一部分)開始後,它會移除棧中的程式計數器和 PSW 暫存器,並把它們進行儲存,然後查詢裝置的狀態。在中斷處理程式全部完成後,它會返回到先前使用者程式尚未執行的第一條指令,這個過程如下

  • 實現 I/O 的第三種方式是使用特殊的硬體:直接儲存器訪問(Direct Memory Access, DMA) 晶片。它可以控制記憶體和某些控制器之間的位流,而無需 CPU 的干預。CPU 會對 DMA 晶片進行設定,說明需要傳送的位元組數,有關的裝置和記憶體地址以及操作方向。當 DMA 晶片完成後,會造成中斷,中斷過程就像上面描述的那樣。我們會在後面具體討論中斷過程

當另一箇中斷處理程式正在執行時,中斷可能(並且經常)發生在不合宜的時間。 因此,CPU 可以禁用中斷,並且可以在之後重啟中斷。在 CPU 關閉中斷後,任何已經發出中斷的裝置,可以繼續保持其中斷訊號處理,但是 CPU 不會中斷,直至中斷再次啟用為止。如果在關閉中斷時,已經有多個裝置發出了中斷訊號,中斷控制器將決定優先處理哪個中斷,通常這取決於事先賦予每個裝置的優先順序,最高優先順序的裝置優先贏得中斷權,其他裝置則必須等待。

匯流排

上面的結構(簡單個人計算機的元件圖)在小型計算機已經使用了多年,並用在早期的 IBM PC 中。然而,隨著處理器核記憶體變得越來越快,單個匯流排處理所有請求的能力也達到了上線,其中也包括 IBM PC 匯流排。必須放棄使用這種模式。其結果導致了其他匯流排的出現,它們處理 I/O 裝置以及 CPU 到儲存器的速度都更快。這種演變的結果導致了下面這種結構的出現。

上圖中的 x86 系統包含很多匯流排,快取記憶體、記憶體、PCIe、PCI、USB、SATA 和 DMI,每條匯流排都有不同的傳輸速率和功能。作業系統必須瞭解所有的匯流排配置和管理。其中最主要的匯流排是 PCIe(Peripheral Component Interconnect Express) 匯流排。

Intel 發明的 PCIe 匯流排也是作為之前古老的 PCI 匯流排的繼承者,而古老的 PCI 匯流排也是為了取代古董級別的 ISA(Industry Standard Architecture) 匯流排而設立的。數十 Gb/s 的傳輸能力使得 PCIe 比它的前身快很多,而且它們本質上也十分不同。直到發明 PCIe 的 2004 年,大多數匯流排都是並行且共享的。共享匯流排架構(shared bus architeture) 表示多個裝置使用一些相同的電線傳輸資料。因此,當多個裝置同時傳送資料時,此時你需要一個決策者來決定誰能夠使用匯流排。而 PCIe 則不一樣,它使用專門的端到端鏈路。傳統 PCI 中使用的並行匯流排架構(parallel bus architecture) 表示通過多條電線傳送相同的資料字。例如,在傳統的 PCI 總線上,一個 32 位資料通過 32 條並行的電線傳送。而 PCIe 則不同,它選用了序列匯流排架構(serial bus architecture) ,並通過單個連線(稱為通道)傳送訊息中的所有位元資料,就像網路資料包一樣。這樣做會簡化很多,因為不再確保所有 32 位資料在同一時刻準確到達相同的目的地。通過將多個數據通路並行起來,並行性仍可以有效利用。例如,可以使用 32 條資料通道並行傳輸 32 條訊息。

在上圖結構中,CPU 通過 DDR3 匯流排與記憶體對話,通過 PCIe 匯流排與外圍圖形裝置 (GPU)對話,通過 DMI(Direct Media Interface)匯流排經整合中心與所有其他裝置對話。而整合控制中心通過序列匯流排與 USB 裝置對話,通過 SATA 匯流排與硬碟和 DVD 驅動器對話,通過 PCIe 傳輸乙太網絡幀。

不僅如此,每一個核

USB(Univversal Serial Bus) 是用來將所有慢速 I/O 裝置(比如鍵盤和滑鼠)與計算機相連的裝置。USB 1.0 可以處理總計 12 Mb/s 的負載,而 USB 2.0 將匯流排速度提高到 480Mb/s ,而 USB 3.0 能達到不小於 5Gb/s 的速率。所有的 USB 裝置都可以直接連線到計算機並能夠立刻開始工作,而不像之前那樣要求重啟計算機。

SCSI(Small Computer System Interface) 匯流排是一種高速匯流排,用在高速硬碟,掃描器和其他需要較大頻寬的裝置上。現在,它們主要用在伺服器和工作站中,速度可以達到 640MB/s 。

計算機啟動過程

那麼有了上面一些硬體再加上作業系統的支援,我們的計算機就可以開始工作了,那麼計算機的啟動過程是怎樣的呢?下面只是一個簡要版的啟動過程

在每臺計算機上有一塊雙親板,也就是母板,母板也就是主機板,它是計算機最基本也就是最重要的部件之一。主機板一般為矩形電路板,上面安裝了組成計算機的主要電路系統,一般有 BIOS 晶片、I/O 控制晶片、鍵盤和麵板控制開關介面、指示燈插接件、擴充插槽、主機板及插卡的直流電源供電接外掛等元件。

在母板上有一個稱為 基本輸入輸出系統(Basic Input Output System, BIOS)的程式。在 BIOS 內有底層 I/O 軟體,包括讀鍵盤、寫螢幕、磁碟I/O 以及其他過程。如今,它被儲存在快閃記憶體中,它是非易失性的,但是當BIOS 中發現錯誤時,可以由作業系統進行更新。

在計算機啟動(booted)時,BIOS 開啟,它會首先檢查所安裝的 RAM 的數量,鍵盤和其他基礎裝置是否已安裝並且正常響應。接著,它開始掃描 PCIe 和 PCI 匯流排並找出連在上面的所有裝置。即插即用的裝置也會被記錄下來。如果現有的裝置和系統上一次啟動時的裝置不同,則新的裝置將被重新配置。

藍後,BIOS 通過嘗試儲存在 CMOS 儲存器中的裝置清單嘗試啟動裝置

CMOS是 Complementary Metal Oxide Semiconductor(互補金氧半導體)的縮寫。它是指製造大規模積體電路晶片用的一種技術或用這種技術製造出來的晶片,是電腦主機板上的一塊可讀寫的 RAM 晶片。因為可讀寫的特性,所以在電腦主機板上用來儲存 BIOS 設定完電腦硬體引數後的資料,這個晶片僅僅是用來存放資料的。

而對 BIOS 中各項引數的設定要通過專門的程式。BIOS 設定程式一般都被廠商整合在晶片中,在開機時通過特定的按鍵就可進入 BIOS 設定程式,方便地對系統進行設定。因此 BIOS 設定有時也被叫做 CMOS 設定。

使用者可以在系統啟動後進入一個 BIOS 配置程式,對裝置清單進行修改。然後,判斷是否能夠從外部 CD-ROM 和 USB 驅動程式啟動,如果啟動失敗的話(也就是沒有),系統將從硬碟啟動,boots 裝置中的第一個扇區被讀入記憶體並執行。該扇區包含一個程式,該程式通常在引導扇區末尾檢查分割槽表以確定哪個分割槽處於活動狀態。然後從該分割槽讀入第二個啟動載入程式,該載入器從活動分割槽中讀取作業系統並啟動它。

然後作業系統會詢問 BIOS 獲取配置資訊。對於每個裝置來說,會檢查是否有裝置驅動程式。如果沒有,則會向用戶詢問是否需要插入 CD-ROM 驅動(由裝置製造商提供)或者從 Internet 上下載。一旦有了裝置驅動程式,作業系統會把它們載入到核心中,然後初始化表,建立所需的後臺程序,並啟動登入程式或GUI。

作業系統博物館

作業系統已經存在了大半個世紀,在這段時期內,出現了各種型別的作業系統,但並不是所有的作業系統都很出名,下面就羅列一些比較出名的作業系統

大型機作業系統

高階一些的作業系統是大型機作業系統,這些大型作業系統可在大型公司的資料中心找到。這些計算機的 I/O 容量與個人計算機不同。一個大型計算機有 1000 個磁碟和數百萬 G 位元組的容量是很正常,如果有這樣一臺個人計算機朋友會很羨慕。大型機也在高階 Web 伺服器、大型電子商務服務站點上。

伺服器作業系統

下一個層次是伺服器作業系統。它們執行在伺服器上,伺服器可以是大型個人計算機、工作站甚至是大型機。它們通過網路為若干使用者服務,並且允許使用者共享硬體和軟體資源。伺服器可提供列印服務、檔案服務或 Web 服務。Internet 服務商執行著許多臺伺服器機器,為使用者提供支援,使 Web 站點儲存 Web 頁面並處理進來的請求。典型的伺服器作業系統有 Solaris、FreeBSD、Linux 和 Windows Server 201x

多處理器作業系統

獲得大型計算能力的一種越來越普遍的方式是將多個 CPU 連線到一個系統中。依據它們連線方式和共享方式的不同,這些系統稱為平行計算機,多計算機或多處理器。他們需要專門的作業系統,不過通常採用的作業系統是配有通訊、連線和一致性等專門功能的伺服器作業系統的變體。

個人計算機中近來出現了多核晶片,所以常規的桌上型電腦和膝上型電腦作業系統也開始與小規模多處理器打交道,而核的數量正在與時俱進。許多主流作業系統比如 Windows 和 Linux 都可以執行在多核處理器上。

個人計算機系統

接下來一類是個人計算機作業系統。現代個人計算機作業系統支援多道處理程式。在啟動時,通常有幾十個程式開始執行,它們的功能是為單個使用者提供良好的支援。這類系統廣泛用於字處理、電子表格、遊戲和 Internet 訪問。常見的例子是 Linux、FreeBSD、Windows 7、Windows 8 和蘋果公司的 OS X 。

掌上計算機作業系統

隨著硬體越來越小化,我們看到了平板電腦、智慧手機和其他掌上計算機系統。掌上計算機或者 PDA(Personal Digital Assistant),個人數字助理 是一種可以握在手中操作的小型計算機。這部分市場已經被谷歌的 Android 系統和蘋果的 IOS主導。

嵌入式作業系統

嵌入式作業系統用來控制裝置的計算機中執行,這種裝置不是一般意義上的計算機,並且不允許使用者安裝軟體。典型的例子有微波爐、汽車、DVD 燒錄機、行動電話以及 MP3 播放器一類的裝置。所有的軟體都執行在 ROM 中,這意味著應用程式之間不存在保護,從而獲得某種簡化。主要的嵌入式系統有 Linux、QNX 和 VxWorks

感測器節點作業系統

有許多用途需要配置微小感測器節點網路。這些節點是一種可以彼此通訊並且使用無線通訊基站的微型計算機。這類感測器網路可以用於建築物周邊保護、國土邊界保衛、森林火災探測、氣象預測用的溫度和降水測量等。

每個感測器節點是一個配有 CPU、RAM、ROM 以及一個或多個環境感測器的實實在在的計算機。節點上執行一個小型但是真是的作業系統,通常這個作業系統是事件驅動的,可以響應外部事件。

實時作業系統

另一類作業系統是實時作業系統,這些系統的特徵是將時間作為關鍵引數。例如,在工業過程控制系統中,工廠中的實時計算機必須收集生產過程的資料並用有關資料控制機器。如果某個動作必須要在規定的時刻發生,這就是硬實時系統。可以在工業控制、民用航空、軍事以及類似應用中看到很多這樣的系統。另一類系統是 軟實時系統,在這種系統中,雖然不希望偶爾違反最終時限,但仍可以接受,並不會引起任何永久性損害。數字音訊或多媒體系統就是這類系統。智慧手機也是軟實時系統。

智慧卡作業系統

最小的作業系統執行在智慧卡上。智慧卡是一種包含一塊 CPU 晶片的信用卡。它有非常嚴格的執行能耗和儲存空間的限制。有些卡具有單項功能,如電子支付;有些智慧卡是面向 Java 的。這意味著在智慧卡的 ROM 中有一個 Java 虛擬機器(Java Virtual Machine, JVM)直譯器。

作業系統概念

大部分作業系統提供了特定的基礎概念和抽象,例如程序、地址空間、檔案等,它們是需要理解的核心內容。下面我們會簡要介紹一些基本概念,為了說明這些概念,我們會不時的從 UNIX 中提出示例,相同的示例也會存在於其他系統中,我們後面會進行介紹。

程序

作業系統一個很關鍵的概念就是 程序(Process)。程序的本質就是作業系統執行的一個程式。與每個程序相關的是地址空間(address space),這是從某個最小值的儲存位置(通常是零)到某個最大值的儲存位置的列表。在這個地址空間中,程序可以進行讀寫操作。地址空間中存放有可執行程式,程式所需要的資料和它的棧。與每個程序相關的還有資源集,通常包括暫存器(registers)(暫存器一般包括程式計數器(program counter)堆疊指標(stack pointer))、開啟檔案的清單、突發的報警、有關的程序清單和其他需要執行程式的資訊。你可以把程序看作是容納執行一個程式所有資訊的一個容器。

對程序建立一種直觀感覺的方式是考慮建立一種多程式的系統。考慮下面這種情況:使用者啟動一個視訊編輯程式,指示它按照某種格式轉換視訊,然後再去瀏覽網頁。同時,一個檢查電子郵件的後臺程序被喚醒並開始執行,這樣,我們目前就會有三個活動程序:視訊編輯器、Web 瀏覽器和電子郵件接收程式。作業系統週期性的掛起一個程序然後啟動執行另一個程序,這可能是由於過去一兩秒鐘程式用完了 CPU 分配的時間片,而 CPU 轉而執行另外的程式。

像這樣暫時中斷程序後,下次應用程式在此啟動時,必須要恢復到與中斷時刻相同的狀態,這在我們使用者看起來是習以為常的事情,但是作業系統內部卻做了巨大的事情。這就像和足球比賽一樣,一場完美精彩的比賽是可以忽略裁判的存在的。這也意味著在掛起時該程序的所有資訊都要被儲存下來。例如,程序可能打開了多個檔案進行讀取。與每個檔案相關聯的是提供當前位置的指標(即下一個需要讀取的位元組或記錄的編號)。當程序被掛起時,必須要儲存這些指標,以便在重新啟動程序後執行的 read呼叫將能夠正確的讀取資料。在許多作業系統中,與一個程序有關的所有資訊,除了該程序自身地址空間的內容以外,均存放在作業系統的一張表中,稱為 程序表(process table),程序表是陣列或者連結串列結構,當前存在每個程序都要佔據其中的一項。

所以,一個掛起的程序包括:程序的地址空間(往往稱作磁芯映像, core image,紀念過去的磁芯儲存器),以及對應的程序表項(其中包括暫存器以及稍後啟動該程序所需要的許多其他資訊)。

與程序管理有關的最關鍵的系統呼叫往往是決定著程序的建立和終止的系統呼叫。考慮一個典型的例子,有一個稱為 命令直譯器(command interpreter)shell 的程序從終端上讀取命令。此時,使用者剛鍵入一條命令要求編譯一個程式。shell 必須先建立一個新程序來執行編譯程式,當編譯程式結束時,它執行一個系統呼叫來終止自己的程序。

如果一個程序能夠建立一個或多個程序(稱為子程序),而且這些程序又可以建立子程序,則很容易找到程序數,如下所示

上圖表示一個程序樹的示意圖,程序 A 建立了兩個子程序 B 和程序 C,子程序 B 又建立了三個子程序 D、E、F。

合作完成某些作業的相關程序經常需要彼此通訊來完成作業,這種通訊稱為程序間通訊(interprocess communication)。我們在後面會探討程序間通訊。

其他可用的程序系統呼叫包括:申請更多的記憶體(或釋放不再需要的記憶體),等待一個子程序結束,用另一個程式覆蓋該程式。

有時,需要向一個正在執行的程序傳遞資訊,而該程序並沒有等待接收資訊。例如,一個程序通過網路向另一臺機器上的程序傳送訊息進行通訊。為了保證一條訊息或訊息的應答不丟失。傳送者要求它所在的作業系統在指定的若干秒後傳送一個通知,這樣如果對方尚未收到確認訊息就可以進行重新發送。在設定該定時器後,程式可以繼續做其他工作。

在限定的時間到達後,作業系統會向程序傳送一個 警告訊號(alarm signal)。這個訊號引起該程序暫時掛起,無論該程序正在做什麼,系統將其暫存器的值儲存到堆疊中,並開始重新啟動一個特殊的訊號處理程,比如重新發送可能丟失的訊息。這些訊號是軟體模擬的硬體中斷,除了定時器到期之外,該訊號可以通過各種原因產生。許多由硬體檢測出來的陷阱,如執行了非法指令或使用了無效地址等,也被轉換成該訊號並交給這個程序。

系統管理器授權每個程序使用一個給定的 UID(User IDentification)。每個啟動的程序都會有一個作業系統賦予的 UID,子程序擁有與父程序一樣的 UID。使用者可以是某個組的成員,每個組也有一個 GID(Group IDentification)

在 UNIX 作業系統中,有一個 UID 是 超級使用者(superuser),或者 Windows 中的管理員(administrator),它具有特殊的權利,可以違背一些保護規則。在大型系統中,只有系統管理員掌握著那些使用者可以稱為超級使用者。

地址空間

每臺計算機都有一些主存用來儲存正在執行的程式。在一個非常簡單的作業系統中,僅僅有一個應用程式執行在記憶體中。為了執行第二個應用程式,需要把第一個應用程式移除才能把第二個程式裝入記憶體。

複雜一些的作業系統會允許多個應用程式同時裝入記憶體中執行。為了防止應用程式之間相互干擾(包括作業系統),需要有某種保護機制。雖然此機制是在硬體中實現,但卻是由作業系統控制的。

上述觀點涉及對計算機主存的管理和保護。另一種同等重要並與儲存器有關的內容是管理程序的地址空間。通常,每個程序有一些可以使用的地址集合,典型值從 0 開始直到某個最大值。一個程序可擁有的最大地址空間小於主存。在這種情況下,即使程序用完其地址空間,記憶體也會有足夠的記憶體執行該程序。

但是,在許多 32 位或 64 位地址的計算機中,分別有 2^32 或 2^64 位元組的地址空間。如果一個程序有比計算機擁有的主存還大的地址空間,而且該程序希望使用全部的記憶體,那該怎麼處理?在早期的計算機中是無法處理的。但是現在有了一種虛擬記憶體的技術,正如前面講到過的,作業系統可以把部分地址空間裝入主存,部分留在磁碟上,並且在需要時來回交換它們。

檔案

幾乎所有作業系統都支援的另一個關鍵概念就是檔案系統。如前所述,作業系統的一項主要功能是遮蔽磁碟和其他 I/O 裝置的細節特性,給程式設計師提供一個良好、清晰的獨立於裝置的抽象檔案模型。建立檔案、刪除檔案、讀檔案和寫檔案 都需要系統呼叫。在檔案可以讀取之前,必須先在磁碟上定位和開啟檔案,在檔案讀過之後應該關閉該檔案,有關的系統呼叫則用於完成這類操作。

為了提供儲存檔案的地方,大多數個人計算機作業系統都有目錄(directory) 的概念,從而可以把檔案分組。比如,學生可以給每個課程都建立一個目錄,用於儲存該學科的資源,另一個目錄可以存放電子郵件,再有一個目錄可以存放全球資訊網主頁。這就需要系統呼叫建立和刪除目錄、將已有檔案放入目錄中,從目錄中刪除檔案等。目錄項可以是檔案或者目錄,目錄和目錄之間也可以巢狀,這樣就產生了檔案系統

程序和檔案層次都是以樹狀的結構組織,但這兩種樹狀結構有不少不同之處。一般程序的樹狀結構層次不深(很少超過三層),而檔案系統的樹狀結構要深一些,通常會到四層甚至五層。程序樹層次結構是暫時的,通常最多存在幾分鐘,而目錄層次則可能存在很長時間。程序和檔案在許可權保護方面也是有區別的。一般來說,父程序能控制和訪問子程序,而在檔案和目錄中通常存在一種機制,使檔案所有者之外的其他使用者也能訪問該檔案。

目錄層結構中的每一個檔案都可以通過從目錄的頂部即 根目錄(Root directory) 開始的路徑名(path name) 來確定。絕對路徑名包含了從根目錄到該檔案的所有目錄清單,它們之間用斜槓分隔符分開,在上面的大學院系檔案系統中,檔案 CS101 的路徑名是 /Faculty/Prof.Brown/Courses/CS101。最開始的斜槓分隔符代表的是根目錄 /,也就是檔案系統的絕對路徑。

出於歷史原因,Windows 下面的檔案系統以 \ 來作為分隔符,但是 Linux 會以 / 作為分隔符。

在上面的系統中,每個程序會有一個 工作目錄(working directory),對於沒有以斜線開頭給出絕對地址的路徑,將在這個工作目錄下尋找。如果 /Faculty/Prof.Brown 是工作目錄,那麼 /Courses/CS101 與上面給定的絕對路徑名錶示的是同一個檔案。程序可以通過使用系統呼叫指定新的工作目錄,從而變更其工作目錄。

在讀寫檔案之前,首先需要開啟檔案,檢查其訪問許可權。若許可權許可,系統將返回一個小整數,稱作檔案描述符(file descriptor),供後續操作使用。若禁止訪問,系統則返回一個錯誤碼。

在 UNIX 中,另一個重要的概念是 特殊檔案(special file)。提供特殊檔案是為了使 I/O 裝置看起來像檔案一般。這樣,就像使用系統呼叫讀寫檔案一樣,I/O 裝置也可以通過同樣的系統呼叫進行讀寫。特殊檔案有兩種,一種是塊兒特殊檔案(block special file)字元特殊檔案(character special file)。塊特殊檔案指那些由可隨機存取的塊組成的裝置,如磁碟等。比如開啟一個塊特殊檔案,然後讀取第4塊,程式可以直接訪問裝置的第4塊而不必考慮存放在該檔案的檔案系統結構。類似的,字元特殊檔案用於印表機、調製解調起和其他接受或輸出字元流的裝置。按照慣例,特殊檔案儲存在 /dev 目錄中。例如,/devv/lp 是印表機。

還有一種與程序和檔案相關的特性是管道,管道(pipe) 是一種虛檔案,他可以連線兩個程序

如果 A 和 B 希望通過管道對話,他們必須提前設定管道。當程序 A 相對程序 B 傳送資料時,它把資料寫到管道上,相當於管道就是輸出檔案。這樣,在 UNIX 中兩個程序之間的通訊就非常類似於普通檔案的讀寫了。

保護

計算機中含有大量的資訊,使用者希望能夠對這些資訊中有用而且重要的資訊加以保護,這些資訊包括電子郵件、商業計劃等,管理這些資訊的安全性完全依靠作業系統來保證。例如,檔案提供授權使用者訪問。

比如 UNIX 作業系統,UNIX 作業系統通過對每個檔案賦予一個 9 位二進位制保護程式碼,對 UNIX 中的檔案實現保護。該保護程式碼有三個位子段,一個用於所有者,一個用於與所有者同組(使用者被系統管理員劃分成組)的其他成員,一個用於其他人。每個欄位中有一位用於讀訪問,一位用於寫訪問,一位用於執行訪問。這些位就是著名的 rwx位。例如,保護程式碼 rwxr-x--x 的含義是所有者可以讀、寫或執行該檔案,其他的組成員可以讀或執行(但不能寫)此檔案、而其他人可以執行(但不能讀和寫)該檔案。

shell

作業系統是執行系統呼叫的程式碼。編輯器、編譯器、彙編程式、連結程式、使用程式以及命令解釋符等,儘管非常重要,非常有用,但是它們確實不是作業系統的組成部分。下面我們著重介紹一下 UNIX 下的命令提示符,也就是 shell,shell 雖然有用,但它也不是作業系統的一部分,然而它卻能很好的說明作業系統很多特性,下面我們就來探討一下。

shell 有許多種,例如 sh、csh、ksh 以及 bash等,它們都支援下面這些功能,最早起的 shell 可以追溯到 sh

使用者登入時,會同時啟動一個 shell,它以終端作為標準輸入和標準輸出。首先顯示提示符(prompt),它可能是一個美元符號($),提示使用者 shell 正在等待接收命令,假如使用者輸入

date

shell 會建立一個子程序,並執行 date 做為子程序。在該子程序執行期間,shell 將等待它結束。在子程序完成時,shell 會顯示提示符並等待下一行輸入。

使用者可以將標準輸出重定向到一個檔案中,例如

date > file

同樣的,也可以將標準輸入作為重定向

sort <file1> file2

這會呼叫 sort 程式來接收 file1 的內容並把結果輸出到 file2。

可以將一個應用程式的輸出通過管道作為另一個程式的輸入,因此有

cat file1 file2 file3 | sort > /dev/lp

這會呼叫 cat 應用程式來合併三個檔案,將其結果輸送到 sort 程式中並按照字典進行排序。sort 應用程式又被重定向到 /dev/lp ,顯然這是一個列印操作。

系統呼叫

我們已經可以看到作業系統提供了兩種功能:為使用者提供應用程式抽象和管理計算機資源。對於大部分在應用程式和作業系統之間的互動主要是應用程式的抽象,例如建立、寫入、讀取和刪除檔案。計算機的資源管理對使用者來說基本上是透明的。因此,使用者程式和作業系統之間的介面主要是處理抽象。為了真正理解作業系統的行為,我們必須仔細的分析這個介面。

多數現代作業系統都有功能相同但是細節不同的系統呼叫,引發作業系統的呼叫依賴於計算機自身的機制,而且必須用匯編程式碼表達。任何單 CPU 計算機一次執行執行一條指令。如果一個程序在使用者態下執行使用者程式,例如從檔案中讀取資料。那麼如果想要把控制權交給作業系統控制,那麼必須執行一個異常指令或者系統呼叫指令。作業系統緊接著需要引數檢查找出所需要的呼叫程序。作業系統緊接著進行引數檢查找出所需要的呼叫程序。然後執行系統呼叫,把控制權移交給系統呼叫下面的指令。大致來說,系統呼叫就像是執行了一個特殊的過程呼叫,但是隻有系統呼叫能夠進入核心態而過程呼叫則不能進入核心態。

為了能夠了解具體的呼叫過程,下面我們以 read 方法為例來看一下呼叫過程。像上面提到的那樣,會有三個引數,第一個引數是指定檔案、第二個是指向緩衝區、第三個引數是給定需要讀取的位元組數。就像幾乎所有系統呼叫一樣,它通過使用與系統呼叫相同的名稱來呼叫一個函式庫,從而從C程式中呼叫:read。

count = read(fd,buffer,nbytes);

系統呼叫在 count 中返回實際讀出的位元組數。這個值通常與 nbytes 相同,但也可能更小。比如在讀過程中遇到了檔案尾的情況。

如果系統呼叫不能執行,不管是因為無效的引數還是磁碟錯誤,count 的值都會被置成 -1,然後在全域性變數 errno 中放入錯誤訊號。程式應該進場檢查系統呼叫的結果以瞭解是否出錯。

系統呼叫是通過一系列的步驟實現的,為了更清楚的說明這個概念,我們還以 read 呼叫為例,在準備系統呼叫前,首先會把引數壓入堆疊,如下所示

C 和 C++ 編譯器使用逆序(必須把第一個引數賦值給 printf(格式字串),放在堆疊的頂部)。第一個引數和第三個引數都是值呼叫,但是第二個引數通過引用傳遞,即傳遞的是緩衝區的地址(由 & 指示),而不是緩衝的內容。然後是 C 呼叫系統庫的 read 函式,這也是第四步。

在由組合語言寫成的庫過程中,一般把系統呼叫的編號放在作業系統所期望的地方,如暫存器(第五步)。然後執行一個 TRAP 指令,將使用者態切換到核心態,並在核心中的一個固定地址開始執行第六步。TRAP 指令實際上與過程呼叫指令非常相似,它們後面都跟隨一個來自遠處位置的指令,以及供以後使用的一個儲存在棧中的返回地址。

TRAP 指令與過程呼叫指令存在兩個方面的不同

  • TRAP 指令會改變作業系統的狀態,由使用者態切換到核心態,而過程呼叫不改變模式
  • 其次,TRAP 指令不能跳轉到任意地址上。根據機器的體系結構,要麼跳轉到一個單固定地址上,或者指令中有一 8 位長的欄位,它給定了記憶體中一張表格的索引,這張表格中含有跳轉地址,然後跳轉到指定地址上。

跟隨在 TRAP 指令後的核心程式碼開始檢查系統呼叫編號,然後dispatch給正確的系統呼叫處理器,這通常是通過一張由系統呼叫編號所引用的、指向系統呼叫處理器的指標表來完成第七步。此時,系統呼叫處理器執行第八步,一旦系統呼叫處理器完成工作,控制權會根據 TRAP 指令後面的指令中返回給函式呼叫庫第九步。這個過程接著以通常的過程呼叫返回的方式,返回到客戶應用程式,這是第十步。然後呼叫完成後,作業系統還必須清除使用者堆疊,然後增加堆疊指標(increment stackpointer),用來清除呼叫 read 之前壓入的引數。從而完成整個 read 呼叫過程。

在上面的第九步中我們說道,控制可能返回 TRAP 指令後面的指令,把控制權再移交給呼叫者這個過程中,系統呼叫會發生阻塞,從而避免應用程式繼續執行。這麼做是有原因的。例如,如果試圖讀鍵盤,此時並沒有任何輸入,那麼呼叫者就必須被阻塞。在這種情形下,作業系統會檢查是否有其他可以執行的程序。這樣,當有使用者輸入 時候,程序會提醒作業系統,然後返回第 9 步繼續執行。

下面,我們會列出一些常用的 POSIX 系統呼叫,POSIX 系統呼叫大概有 100 多個,它們之中最重要的一些呼叫見下表

程序管理

呼叫 說明
pid = fork() 建立與父程序相同的子程序
pid = waitpid(pid, &statloc,options) 等待一個子程序終止
s = execve(name,argv,environp) 替換一個程序的核心映像
exit(status) 終止程序執行並返回狀態

檔案管理

呼叫 說明
fd = open(file, how,...) 開啟一個檔案使用讀、寫
s = close(fd) 關閉一個開啟的檔案
n = read(fd,buffer,nbytes) 把資料從一個檔案讀到緩衝區中
n = write(fd,buffer,nbytes) 把資料從緩衝區寫到一個檔案中
position = iseek(fd,offset,whence) 移動檔案指標
s = stat(name,&buf) 取得檔案狀態資訊

目錄和檔案系統管理

呼叫 說明
s = mkdir(nname,mode) 建立一個新目錄
s = rmdir(name) 刪去一個空目錄
s = link(name1,name2) 建立一個新目錄項 name2,並指向 name1
s = unlink(name) 刪去一個目錄項
s = mount(special,name,flag) 安裝一個檔案系統
s = umount(special) 解除安裝一個檔案系統

其他

呼叫 說明
s = chdir(dirname) 改變工作目錄
s = chmod(name,mode) 修改一個檔案的保護位
s = kill(pid, signal) 傳送訊號給程序
seconds = time(&seconds) 獲取從 1970 年1月1日至今的時間

上面的系統呼叫引數中有一些公共部分,例如 pid 系統程序 id,fd 是檔案描述符,n 是位元組數,position 是在檔案中的偏移量、seconds 是流逝時間。

從巨集觀角度上看,這些系統調所提供的服務確定了多數作業系統應該具有的功能,下面分別來對不同的系統呼叫進行解釋

用於程序管理的系統呼叫

在 UNIX 中,fork 是唯一可以在 POSIX 中建立程序的途徑,它建立一個原有程序的副本,包括所有的檔案描述符、暫存器等內容。在 fork 之後,原有程序以及副本(父與子)就分開了。在 fork 過程中,所有的變數都有相同的值,雖然父程序的資料通過複製給子程序,但是後續對其中任何一個程序的修改不會影響到另外一個。fork 呼叫會返回一個值,在子程序中該值為 0 ,並且在父程序中等於子程序的 程序識別符號(Process IDentified,PID)。使用返回的 PID,就可以看出來哪個是父程序和子程序。

在多數情況下, 在 fork 之後,子程序需要執行和父程序不一樣的程式碼。從終端讀取命令,建立一個子程序,等待子程序執行命令,當子程序結束後再讀取下一個輸入的指令。為了等待子程序完成,父程序需要執行 waitpid 系統呼叫,父程序會等待直至子程序終止(若有多個子程序的話,則直至任何一個子程序終止)。waitpid 可以等待一個特定的子程序,或者通過將第一個引數設為 -1 的方式,等待任何一個比較老的子程序。當 waitpid 完成後,會將第二個引數 statloc 所指向的地址設定為子程序的退出狀態(