寫給大忙人看的操作系統

文章主要結構圖如下

操作系統

現代計算機系統由一個或多個處理器、主存、打印機、鍵盤、鼠標、顯示器、網絡接口以及各種輸入/輸出設備構成。

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

我們一般常見的操作系統主要有 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 所指向的地址設置爲子進程的退出狀態(正常或異常終止以及退出值)。有各種可使用的選項,它們由第三個參數確定。例如,如果沒有已經退出的子進程則立刻返回。

那麼 shell 該如何使用 fork 呢?在鍵入一條命令後,shell 會調用 fork 命令創建一個新的進程。這個子進程會執行用戶的指令。通過使用 execve 系統調用可以實現系統執行,這個系統調用會引起整個核心映像被一個文件所替代,該文件由第一個參數給定。下面是一個簡化版的例子說明 fork、waitpid 和 execve 的使用

#define TRUE 1

/* 一直循環下去 */
while(TRUE){            

/* 在屏幕上顯示提示符 */
    type_prompt();          
  
  /* 從終端讀取輸入 */
    read_command(command,parameters)        
  
  /* fork 子進程 */
    if(fork() != 0){                                
  
        /* 父代碼 */
        /* 等待子進程執行完畢 */
        waitpid(-1, &status, 0);                                
    }else{
    
        /* 執行命令 */
        /* 子代碼 */
        execve(command,parameters,0)                    
    }
}

一般情況下,execve 有三個參數:將要執行的文件名稱,一個指向變量數組的指針,以及一個指向環境數組的指針。這裏對這些參數做一個簡要的說明。

先看一個 shell 指令

cp file1 file2

此命令把 file1 複製到 file2 文件中,在 shell 執行 fork 之後,子進程定位並執行文件拷貝,並將源文件和目標文件的名稱傳遞給它。

cp 的主程序(以及包含其他大多數 C 程序的主程序)包含聲明

main(argc,argv,envp)

其中 argc 是命令行中參數數目的計數,包括程序名稱。對於上面的例子,argc 是3。第二個參數argv 是數組的指針。該數組的元素 i 是指向該命令行第 i 個字符串的指針。在上面的例子中,argv[0] 指向字符串 cp,argv[1] 指向字符串 file1,argv[2] 指向字符串 file2。main 的第三個參數是指向環境的指針,該環境是一個數組,含有 name = value 的賦值形式,用以將諸如終端類型以及根目錄等信息傳送給程序。這些變量通常用來確定用戶希望如何完成特定的任務(例如,使用默認打印機)。在上面的例子中,沒有環境參數傳遞給 execve ,所以環境變量是 0 ,所以 execve 的第三個參數爲 0 。

可能你覺得 execve 過於複雜,這時候我要鼓勵一下你,execve 可能是 POSIX 的全部系統調用中最複雜的一個了,其他都比較簡單。作爲一個簡單的例子,我們再來看一下 exit ,這是進程在執行完成後應執行的系統調用。這個系統調用有一個參數,它的退出狀態是 0 - 255 之間,它通過 waitpid 系統調用中的 statloc 返回給父級。

UNIX 中的進程將內存劃分成三個部分:text segment,文本區,例如程序代碼,data segment,數據區,例如變量,stack segment,棧區域。數據向上增長而堆棧向下增長,如下圖所示

上圖能說明三個部分的內存分配情況,夾在中間的是空閒區,也就是未分配的區域,堆棧在需要時自動的擠壓空閒區域,不過數據段的擴展是顯示地通過系統調用 brk 進行的,在數據段擴充後,該系統調用指向一個新地址。但是,這個調用不是 POSIX 標準中定義的,對於存儲器的動態分配,鼓勵程序員使用 malloc 函數,而 malloc 的內部實現則不是一個適合標準化的主題,因爲幾乎沒有程序員直接使用它。

用於文件管理的系統調用

許多系統調用都與文件系統有關,要讀寫一個文件,必須先將其打開。這個系統調用通過絕對路徑名或指向工作目錄的相對路徑名指定要打開文件的名稱,而代碼 O_RDONLYO_WRONLYO_RDWR 的含義分別是隻讀、只寫或者兩者都可以,爲了創建一個新文件,使用 O_CREATE 參數。然後可使用返回的文件描述符進行讀寫操作。接着,可以使用 close 關閉文件,這個調用使得文件描述符在後續的 open 中被再次使用。

最常用的調用還是 readwrite,我們再前面探討過 read 調用,write 具有與 read 相同的參數。

儘管多數程序頻繁的讀寫文件,但是仍有一些應用程序需要能夠隨機訪問一個文件的任意部分。與每個文件相關的是一個指向文件當前位置的指針。在順序讀寫時,該指針通常指向要讀出(寫入)的下一個字節。Iseek 調用可以改變該位置指針的值,這樣後續的 read 或 write 調用就可以在文件的任何地方開始。

Iseek 有三個參數,position = iseek(fd,offset,whence),第一個是文件描述符,第二個是文件位置,第三個是說明該文件位置是相對於文件起始位置,當前位置還是文件的結尾。在修改了指針之後,Iseek 所返回的值是文件中的絕對位置。

UNIX 爲每個文件保存了該文件的類型(普通文件、特殊文件、目錄等)、大小,最後修改時間以及其他信息,程序可以通過 stat 系統調用查看這些信息。s = stat(name,&buf),第一個參數指定了被檢查的文件;第二個參數是一個指針,該指針指向存放這些信息的結構。對於一個打開的文件而言,fstat 調用完成同樣的工作。

用於目錄管理的系統調用

下面我們探討目錄和整個文件系統的系統調用,上面探討的是和某個文件有關的系統調用。 mkdirrmdir 分別用於創建s = mkdir(nname,mode) 和刪除 s = rmdir(name) 空目錄,下一個調用是 s = link(name1,name2) 它的作用是允許同一個文件以兩個或者多個名稱出現,多數情況下是在不同的目錄中使用 link ,下面我們探討一下 link 是如何工作的

圖中有兩個用戶 astjim,每個用戶都有他自己的一個目錄和一些文件,如果 ast 要執行一個包含下面系統調用的應用程序

link("/usr/jim/memo", "/usr/ast/note");

jim 中的 memo 文件現在會進入到 ast 的目錄中,在 note 名稱下。此後,/usr/jim/memo/usr/ast/note 會有相同的名稱。

用戶目錄是保存在 /usr,/user,/home 還是其他位置,都是由本地系統管理員決定的。

要理解 link 是如何工作的需要清楚 link 做了什麼操作。UNIX 中的每個文件都有一個獨一無二的版本,也稱作 i - number,i-編號,它標示着不同文件的版本。這個 i - 編號是 i-nodes,i-節點 表的索引。每個文件都會表明誰擁有這個文件,這個磁盤塊的位置在哪,等等。目錄只是一個包含一組(i編號,ASCII名稱)對應的文件。UNIX 中的第一個版本中,每個目錄項都會有 16 個字節,2 個字節對應 i - 編號和 14 個字節對應其名稱。現在需要一個更復雜的結構需要支持長文件名,但是從概念上講一個目錄仍是一系列(i-編號,ASCII 名稱)的集合。在上圖中,mail 的 i-編號爲 16,依此類推。link 只是利用某個已有文件的 i-編號,創建一個新目錄項(也許用一個新名稱)。在上圖 b 中,你會發現有兩個相同的 70 i-編號的文件,因此它們需要有相同的文件。如果其中一個使用了 unlink 系統調用的話,其中一個會被移除,另一個將保留。如果兩個文件都移除了,則 UNIX 會發現該文件不存在任何沒有目錄項(i-節點中的一個域記錄着指向該文件的目錄項),就會把該文件從磁盤中移除。

就像我們上面提到過的那樣,mount 系統 s = mount(special,name,flag) 調用會將兩個文件系統合併爲一個。通常的情況是將根文件系統分佈在硬盤(子)分區上,並將用戶文件分佈在另一個(子)分區上,該根文件系統包含常用命令的二進制(可執行)版本和其他使用頻繁的文件。然後,用戶就會插入可讀取的 USB 硬盤。

通過執行 mount 系統調用,USB 文件系統可以被添加到根文件系統中,

如果用 C 語言來執行那就是

mount("/dev/sdb0","/mnt",0)

這裏,第一個參數是 USB 驅動器 0 的塊特殊文件名稱,第二個參數是被安裝在樹中的位置,第三個參數說明將要安裝的文件系統是可讀寫的還是隻讀的。

當不再需要一個文件系統時,可以使用 umount 移除之。

其他系統調用

除了進程、文件、目錄系統調用,也存在其他系統調用的情況,下面我們來探討一下。我們可以看到上面其他系統調用只有四種,首先來看第一個 chdir,chdir 調用更改當前工作目錄,在調用

chdir("/usr/ast/test");

後,打開 xyz 文件,會打開 /usr/ast/test/xyz 文件,工作目錄的概念消除了總是需要輸入長文件名的需要。

在 UNIX 系統中,每個文件都會有保護模式,這個模式會有一個讀-寫-執行位,它用來區分所有者、組和其他成員。chmod 系統調用提供改變文件模式的操作。例如,要使一個文件除了對所有者之外的用戶可讀,你可以執行

chmod("file",0644); 

kill 系統調用是用戶和用戶進程發送信號的方式,如果一個進程準備好捕捉一個特定的信號,那麼在信號捕捉之前,會運行一個信號處理程序。如果進程沒有準備好捕捉特定的信號,那麼信號的到來會殺掉該進程(此名字的由來)。

POSIX 定義了若干時間處理的進程。例如,time 以秒爲單位返回當前時間,0 對應着 1970 年 1月 1日。在一臺 32 位字的計算機中,time 的最大值是 (2^32) - 1秒,這個數字對應 136 年多一點。所以在 2106 年,32 位的 UNIX 系統會發飆。如果讀者現在有 32 位 UNIX 系統,建議在 2106 年更換位 64 位操作系統(偷笑~)。

Win 32 API

上面我們提到的都是 UNIX 系統調用,現在我們來聊聊 Win 32 中的系統調用。Windows 和 UNIX 在各自的編程方式上有着根本的不同。UNIX 程序由執行某些操作或執行其他操作的代碼組成,進行系統調用以執行某些服務。Windows 系統則不同,Windows 應用程序通常是由事件驅動的。主程序會等待一些事件發生,然後調用程序去處理。最簡單的事件處理是鍵盤敲擊和鼠標滑過,或者是鼠標點擊,或者是插入 USB 驅動,然後操作系統調用處理器去處理事件,更新屏幕和更新程序內部狀態。這是與 UNIX 不同的設計風格。

當然,Windows 也有系統調用。在 UNIX 中,系統調用(比如 read)和系統調用所使用的調用庫(例如 read)幾乎是一對一的關係。而在 Windows 中,情況則大不相同。首先,函數庫的調用和實際的系統調用幾乎是不對應的。微軟定義了一系列過程,稱爲 Win32應用編程接口(Application Programming Interface),程序員通過這套標準的接口來實現系統調用。這個接口支持從 Windows 95 版本以來所有的 Windows 版本。

Win32 API 調用的數量是非常巨大的,有數千個多。但這些調用並不都是在內核態的模式下運行時,有一些是在用戶態的模型下運行。Win32 API 有大量的調用,用來管理視窗、幾何圖形、文本、字體、滾動條、對話框、菜單以及 GUI 的其他功能。爲了使圖形子系統在內核態下運行,需要系統調用,否則就只有函數庫調用。

我們把關注點放在和 Win32 系統調用中來,我們可以簡單看一下 Win32 API 中的系統調用和 UNIX 中有什麼不同(並不是所有的系統調用)

UNIX Win32 說明
fork CreateProcess 創建一個新進程
waitpid WaitForSingleObject 等待一個進程退出
execve none CraeteProcess = fork + servvice
exit ExitProcess 終止執行
open CreateFile 創建一個文件或打開一個已有的文件
close CloseHandle 關閉文件
read ReadFile 從單個文件中讀取數據
write WriteFile 向單個文件寫數據
lseek SetFilePointer 移動文件指針
stat GetFileAttributesEx 獲得不同的文件屬性
mkdir CreateDirectory 創建一個新的目錄
rmdir RemoveDirectory 移除一個空的目錄
link none Win32 不支持 link
unlink DeleteFile 銷燬一個已有的文件
mount none Win32 不支持 mount
umount none Win32 不支持 mount,所以也不支持mount
chdir SetCurrentDirectory 切換當前工作目錄
chmod none Win32 不支持安全
kill none Win32 不支持信號
time GetLocalTime 獲取當前時間

上表中是 UNIX 調用大致對應的 Win32 API 系統調用,簡述一下上表。CreateProcess 用於創建一個新進程,它把 UNIX 中的 fork 和 execve 兩個指令合成一個,一起執行。它有許多參數用來指定新創建進程的性質。Windows 中沒有類似 UNIX 中的進程層次,所以不存在父進程和子進程的概念。在進程創建之後,創建者和被創建者是平等的。WaitForSingleObject 用於等待一個事件,等待的事件可以是多種可能的事件。如果有參數指定了某個進程,那麼調用者將等待指定的進程退出,這通過 ExitProcess 來完成。

然後是6個文件操作,在功能上和 UNIX 的調用類似,然而在參數和細節上是不同的。和 UNIX 中一樣,文件可以打開,讀取,寫入,關閉。SetFilePointerGetFileAttributesEx 設置文件的位置並取得文件的屬性。

Windows 中有目錄,目錄分別用 CreateDirectory 以及 RemoveDirectory API 調用創建和刪除。也有對當前的目錄的標記,這可以通過 SetCurrentDirectory 來設置。使用GetLocalTime 可獲得當前時間。

Win32 接口中沒有文件的鏈接、文件系統的 mount、umount 和 stat ,當然, Win32 中也有大量 UNIX 中沒有的系統調用,特別是對 GUI 的管理和調用。

操作系統結構

下面我們會探討操作系統的幾種結構,主要包括單體結構、分層系統、微內核、客戶-服務端系統、虛擬機和外核等。下面以此來探討一下

單體系統

到目前爲止,在大多數系統中,整個系統在內核態以單一程序的方式運行。整個操作系統是以程序集合來編寫的,鏈接在一塊形成一個大的二進制可執行程序。使用此技術時,如果系統中的每個過程都提供了前者所需的一些有用的計算,則它可以自由調用任何其他過程。在單體系統中,調用任何一個所需要的程序都非常高效,但是上千個不受限制的彼此調用往往非常臃腫和笨拙,而且單體系統必然存在單體問題,那就是隻要系統發生故障,那麼任何系統和應用程序將不可用,這往往是災難性的。

在單體系統中構造實際目標程序時,會首先編譯所有單個過程(或包含這些過程的文件),然後使用系統鏈接器將它們全部綁定到一個可執行文件中

對於單體系統,往往有下面幾種建議

  • 需要有一個主程序,用來調用請求服務程序
  • 需要一套服務過程,用來執行系統調用
  • 需要一套服務程序,用來輔助服務過程調用

在單體系統中,對於每個系統調用都會有一個服務程序來保障和運行。需要一組實用程序來彌補服務程序需要的功能,例如從用戶程序中獲取數據。可將各種過程劃分爲一個三層模型

除了在計算機初啓動時所裝載的核心操作系統外,許多操作系統還支持額外的擴展。比如 I/O 設備驅動和文件系統。這些部件可以按需裝載。在 UNIX 中把它們叫做 共享庫(shared library),在 Windows 中則被稱爲 動態鏈接庫(Dynamic Link Library,DLL)。他們的擴展名爲 .dll,在 C:\Windows\system32 目錄下存在 1000 多個 DLL 文件,所以不要輕易刪除 C 盤文件,否則可能就炸了哦。

分層系統

分層系統使用層來分隔不同的功能單元。每一層只與該層的上層和下層通信。每一層都使用下面的層來執行其功能。層之間的通信通過預定義的固定接口通信。

分層系統是由 E.W.Dijkstar 和他的學生在荷蘭技術學院所開發的 THE 系統。

把上面單體系統進一步通用化,就變爲了一個層次式結構的操作系統,它的上層軟件都是在下層軟件的基礎之上構建的。該系統分爲六層,如下所示

層號 功能
5 操作員
4 用戶程序
3 輸入/輸出管理
2 操作員-進程通信
1 存儲器和磁鼓管理
0 處理器分配和多道程序編程

處理器在 0 層運行,當中斷髮生或定時器到期時,由該層完成進程切換;在第 0 層之上,系統由一些連續的進程組成,編寫這些進程時不用再考慮在單處理器上多進程運行的細節。內存管理在第 1 層,它分配進程的主存空間。第 1 層軟件保證一旦需要訪問某一頁面,該頁面必定已經在內存中,並且在頁面不需要的時候將其移出。

第 2 層處理進程與操作員控制檯(即用戶)之間的通信。第 3 層管理 I/O 設備和相關的信息流緩衝區。第 4 層是用戶程序層,用戶程序不用考慮進程、內存、控制檯或 I/O 設備管理等細節。系統操作員在第 5 層。

微內核

在分層方式中,設計者要確定在哪裏劃分 內核-用戶 的邊界。傳統上,所有的層都在內核中,但是這樣做沒有必要。事實上,儘可能減少內核態中功能可能是更好的做法。因爲內核中的錯誤很難處理,一旦內核態中出錯誤會拖累整個系統。

所以,爲了實現高可靠性,將操作系統劃分成小的、層級之間能夠更好定義的模塊是很有必要的,只有一個模塊 --- 微內核 --- 運行在內核態,其餘模塊可以作爲普通用戶進程運行。由於把每個設備驅動和文件系統分別作爲普通用戶進程,這些模塊中的錯誤雖然會使這些模塊崩潰,但是不會使整個系統死機。

MINIX 3 是微內核的代表作,它的具體結構如下

在內核的外部,系統的構造有三層,它們都在用戶態下運行,最底層是設備驅動器。由於它們都在用戶態下運行,所以不能物理的訪問 I/O 端口空間,也不能直接發出 I/O 命令。相反,爲了能夠對 I/O 設備編程,驅動器構建一個結構,指明哪個參數值寫到哪個 I/O 端口,並聲稱一個內核調用,這樣就完成了一次調用過程。

位於用戶態的驅動程序上面是服務器層,包含有服務器,它們完成操作系統的多數工作。由一個或多個文件服務器管理着文件系統,進程管理器創建、銷燬和管理進程。服務器中有一個特殊的服務器稱爲 再生服務器(reincarnation server),它的任務就是檢查服務器和驅動程序的功能是否正確,一旦檢查出來錯誤,它就會補上去,無需用戶干預。這種方式使得系統具有可恢復性,並具有較高的可靠性。

微內核中的內核還具有一種 機制策略 分離的思想。比如系統調度,一個比較簡單的調度算法是,對每個進程賦予一個優先級,並讓內核執行具有最高優先級的進程。這裏,內核機制就是尋找最高的優先級進程並運行。而策略(賦予進程優先級)可以在用戶態中的進程完成。在這種模式中,策略和機制是分離的,從而使內核變得更小。

客戶-服務器模式

微內核思想的策略是把進程劃分爲兩類:服務器,每個服務器用來提供服務;客戶端,使用這些服務。這個模式就是所謂的 客戶-服務器模式。

客戶-服務器模式會有兩種載體,一種情況是一臺計算機既是客戶又是服務器,在這種方式下,操作系統會有某種優化;但是普遍情況下是客戶端和服務器在不同的機器上,它們通過局域網或廣域網連接。

客戶通過發送消息與服務器通信,客戶端並不需要知道這些消息是在本地機器上處理,還是通過網絡被送到遠程機器上處理。對於客戶端而言,這兩種情形是一樣的:都是發送請求並得到迴應。

越來越多的系統,包括家裏的 PC,都成爲客戶端,而在某地運行的大型機器則成爲服務器。許多 web 就是以這種方式運行的。一臺 PC 向某個服務器請求一個 Web 頁面,服務器把 Web 頁面返回給客戶端,這就是典型的客服-服務器模式

文章參考:

《現代操作系統》第四版

https://baike.baidu.com/item/操作系統/192?fr=aladdin

《Modern Operating System》forth edition

http://faculty.cs.niu.edu/~hutchins/csci360/hchnotes/psw.htm

https://www.computerhope.com/jargon/c/clockcyc.htm

《B站-操作系統》

https://www.bilibili.com/video/av9555596?from=search&seid=8107077283516919308

https://en.wikipedia.org/wiki/System_call

http://c.biancheng.net/cpp/html/238.html

http://www.dossier-andreas.net/software_architecture/layers.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章