
Unix/Linux的體系架構

如上圖所示,從宏觀上來看,Linux操作系統的體系架構分為用戶態和內核態(或者用戶空間和內核空間)。內核從本質上看是一種軟件-----控制計算機的硬件資源,並提供上層應用程序運行的環境。
用戶態即上層應用程序的活動空間,應用程序的執行必須依托於內核提供的資源,包括CPU資源、存儲資源、I/O資源等。為了使上層應用能夠訪問到這些資源,內核必須為上層應用提供訪問的接口:系統調用。
簡單來說:
內核態:運行在內核空間的進程的狀態
用戶態:運行在用戶空間的進程的狀態
系統調用
系統調用是操作系統的最小功能單位,這些系統調用根據不同的應用場景可以進行擴展和裁剪,現在各種版本的Unix實現都提供了不同數量的系統調用,如Linux的不同版本提供了240-260個系統調用,FreeBSD大約提供了320個。
我們可以把系統調用看成是一種不能再化簡的操作(類似於原子操作,但是不同概念),有人把它比作一個漢字的一個“筆畫”,而一個“漢字”就代表一個上層應用,我覺得這個比喻非常貼切。一個漢字有很多筆畫組成,因此有時候如果要實現一個完整的漢字就必須調用很多的系統調用。這有時是一件很崩潰的事情,比如說這個字,你可能認識,但是有幾個人會寫呢?
系統庫函數:系統調用的封裝
應用程序直接使用系統調用,這勢必會加重程序員的負擔,良好的程序設計方法是:重視上層的業務邏輯操作,而盡可能避免底層復雜的實現細節。那么有沒有優化空間呢?庫函數正是為了將程序員從復雜的細節中解脫出來而提出的一種有效方法。它實現對系統調用的封裝,將簡單的業務邏輯接口呈現給用戶,方便用戶調用,從這個角度上看,庫函數就像是組成漢字的“偏旁”。這樣的一種組成方式極大增強了程序設計的靈活性,
對於簡單的操作,我們可以直接調用系統調用來訪問資源,如“人”;對於復雜操作,我們借助於庫函數來實現,如“仁”。庫函數依據不同的標准也可以有不同的實現版本,如ISOC 標准庫,POSIX標准庫等。
shell:系統調用的封裝
Shell是一個特殊的應用程序,俗稱命令行,本質上是一個命令解釋器,它下通系統調用,上通各種應用,通常充當着一種“膠水”的角色,來連接各個小功能程序,讓不同程序能夠以一個清晰的接口協同工作,從而增強各個程序的功能。同時,Shell是可編程的,它可以執行符合Shell語法的文本,這樣的文本稱為Shell腳本,通常短短的幾行Shell腳本就可以實現一個非常大的功能,原因就是這些Shell語句通常都對系統調用做了一層封裝。為了方便用戶和系統交互,一般,一個Shell對應一個終端,終端是一個硬件設備,呈現給用戶的是一個圖形化窗口。我們可以通過這個窗口輸入或者輸出文本。這個文本直接傳遞給shell進行分析解釋,然后執行。
總結一下,用戶態的應用程序可以通過三種方式來訪問內核態的資源:
1)系統調用
2)庫函數
3)Shell腳本
下圖是對上圖的一個細分結構,從這個圖上可以更進一步對內核所做的事有一個“全景式”的印象。主要表現為:向下控制硬件資源,向內管理操作系統資源:包括進程的調度和管理、內存的管理、文件系統的管理、設備驅動程序的管理以及網絡資源的管理,向上則向應用程序提供系統調用的接口。
從整體上來看,整個操作系統分為兩層:用戶態和內核態,這種分層的架構極大地提高了資源管理的可擴展性和靈活性,而且方便用戶對資源的調用和集中式的管理,帶來一定的安全性。

用戶態和內核態的切換
因為操作系統的資源是有限的,如果訪問資源的操作過多,必然會消耗過多的資源,而且如果不對這些操作加以區分,很可能造成資源訪問的沖突。所以,為了減少有限資源的訪問和使用沖突,Unix/Linux的設計哲學之一就是:對不同的操作賦予不同的執行等級,就是所謂特權的概念。簡單說就是有多大能力做多大的事,與系統相關的一些特別關鍵的操作必須由最高特權的程序來完成。
Intel的X86架構的CPU提供了0到3四個特權級,數字越小,特權越高,Linux操作系統中主要采用了0和3兩個特權級,分別對應的就是內核態和用戶態。
運行於用戶態的進程可以執行的操作和訪問的資源都會受到極大的限制,而運行在內核態的進程則可以執行任何操作並且在資源的使用上沒有限制。很多程序開始時運行於用戶態,但在執行的過程中,一些操作需要在內核權限下才能執行,這就涉及到一個從用戶態切換到內核態的過程。比如C函數庫中的內存分配函數malloc(),它具體是使用sbrk()系統調用來分配內存,當malloc調用sbrk()的時候就涉及一次從用戶態到內核態的切換,類似的函數還有printf(),調用的是wirte()系統調用來輸出字符串,等等。

到底在什么情況下會發生從用戶態到內核態的切換,一般存在以下三種情況:
1)當然就是系統調用:原因如上的分析。
2)異常事件: 當CPU正在執行運行在用戶態的程序時,突然發生某些預先不可知的異常事件,這個時候就會觸發從當前用戶態執行的進程轉向內核態執行相關的異常事件,典型的如缺頁異常。
3)外圍設備的中斷:當外圍設備完成用戶的請求操作后,會像CPU發出中斷信號,此時,CPU就會暫停執行下一條即將要執行的指令,轉而去執行中斷信號對應的處理程序,如果先前執行的指令是在用戶態下,則自然就發生從用戶態到內核態的轉換。
注意:系統調用的本質其實也是中斷,相對於外圍設備的硬中斷,這種中斷稱為軟中斷,這是操作系統為用戶特別開放的一種中斷,如Linux int 80h中斷。所以,從觸發方式和效果上來看,這三種切換方式是完全一樣的,都相當於是執行了一個中斷響應的過程。但是從觸發的對象來看,系統調用是進程主動請求切換的,而異常和硬中斷則是被動的。
三、總結
本文僅是從宏觀的角度去理解Linux用戶態和內核態的設計,並沒有去深究它們的具體實現方式。從實現上來看,必須要考慮到的一點我想就是性能問題,因為用戶態和內核態之間的切換也會消耗大量資源。關於實現的細節,目前學藝不精不敢亂說,等日后補上。但知道了這一點,我相信對很多問題也就很容易理解了,比如說基於緩沖區的IO和無緩沖的IO,用戶進程和內核進程之間的切換,IO復用中的讀寫內核事件表,等等,這些知識之后會一一補上。
內核空間和用戶空間
對 32 位操作系統而言,它的尋址空間(虛擬地址空間,或叫線性地址空間)為 4G(2的32次方)。也就是說一個進程的最大地址空間為 4G。操作系統的核心是內核(kernel),它獨立於普通的應用程序,可以訪問受保護的內存空間,也有訪問底層硬件設備的所有權限。為了保證內核的安全,現在的操作系統一般都強制用戶進程不能直接操作內核。具體的實現方式基本都是由操作系統將虛擬地址空間划分為兩部分,一部分為內核空間,另一部分為用戶空間。
針對 Linux 操作系統而言,最高的 1G 字節(從虛擬地址 0xC0000000 到 0xFFFFFFFF)由內核使用,稱為內核空間。而較低的 3G 字節(從虛擬地址 0x00000000 到 0xBFFFFFFF)由各個進程使用,稱為用戶空間。
對上面這段內容我們可以這樣理解:
每個進程的4G地址空間中,最高1G都是一樣的,即內核空間。只有剩余的3G才歸進程自己使用。
換句話說就是, 最高 1G 的內核空間是被所有進程共享的!
下圖描述了每個進程 4G 地址空間的分配情況(此圖來自互聯網):

為什么需要區分內核空間與用戶空間
在 CPU 的所有指令中,有些指令是非常危險的,如果錯用,將導致系統崩潰,比如清內存、設置時鍾等。如果允許所有的程序都可以使用這些指令,那么系統崩潰的概率將大大增加。
所以,CPU 將指令分為特權指令和非特權指令,對於那些危險的指令,只允許操作系統及其相關模塊使用,普通應用程序只能使用那些不會造成災難的指令。比如 Intel 的 CPU 將特權等級分為 4 個級別:Ring0~Ring3。
其實 Linux 系統只使用了 Ring0 和 Ring3 兩個運行級別(Windows 系統也是一樣的)。當進程運行在 Ring3 級別時被稱為運行在用戶態,而運行在 Ring0 級別時被稱為運行在內核態。
內核態與用戶態
好了我們現在需要再解釋一下什么是內核態、用戶態:
當進程運行在內核空間時就處於內核態,而進程運行在用戶空間時則處於用戶態。
在內核態下,進程運行在內核地址空間中,此時 CPU 可以執行任何指令。運行的代碼也不受任何的限制,可以自由地訪問任何有效地址,也可以直接進行端口的訪問。
在用戶態下,進程運行在用戶地址空間中,被執行的代碼要受到 CPU 的諸多檢查,它們只能訪問映射其地址空間的頁表項中規定的在用戶態下可訪問頁面的虛擬地址,且只能對任務狀態段(TSS)中 I/O 許可位圖(I/O Permission Bitmap)中規定的可訪問端口進行直接訪問。
對於以前的 DOS 操作系統來說,是沒有內核空間、用戶空間以及內核態、用戶態這些概念的。可以認為所有的代碼都是運行在內核態的,因而用戶編寫的應用程序代碼可以很容易的讓操作系統崩潰掉。
對於 Linux 來說,通過區分內核空間和用戶空間的設計,隔離了操作系統代碼(操作系統的代碼要比應用程序的代碼健壯很多)與應用程序代碼。即便是單個應用程序出現錯誤也不會影響到操作系統的穩定性,這樣其它的程序還可以正常的運行(Linux 可是個多任務系統啊!)。
所以,區分內核空間和用戶空間本質上是要提高操作系統的穩定性及可用性。
如何從用戶空間進入內核空間
其實所有的系統資源管理都是在內核空間中完成的。比如讀寫磁盤文件,分配回收內存,從網絡接口讀寫數據等等。我們的應用程序是無法直接進行這樣的操作的。但是我們可以通過內核提供的接口來完成這樣的任務。
比如應用程序要讀取磁盤上的一個文件,它可以向內核發起一個 "系統調用" 告訴內核:"我要讀取磁盤上的某某文件"。其實就是通過一個特殊的指令讓進程從用戶態進入到內核態(到了內核空間),在內核空間中,CPU 可以執行任何的指令,當然也包括從磁盤上讀取數據。具體過程是先把數據讀取到內核空間中,然后再把數據拷貝到用戶空間並從內核態切換到用戶態。此時應用程序已經從系統調用中返回並且拿到了想要的數據,可以開開心心的往下執行了。
簡單說就是應用程序把高科技的事情(從磁盤讀取文件)外包給了系統內核,系統內核做這些事情既專業又高效。
對於一個進程來講,從用戶空間進入內核空間並最終返回到用戶空間,這個過程是十分復雜的。舉個例子,比如我們經常接觸的概念 "堆棧",其實進程在內核態和用戶態各有一個堆棧。運行在用戶空間時進程使用的是用戶空間中的堆棧,而運行在內核空間時,進程使用的是內核空間中的堆棧。所以說,Linux 中每個進程有兩個棧,分別用於用戶態和內核態。
下圖簡明的描述了用戶態與內核態之間的轉換:

既然用戶態的進程必須切換成內核態才能使用系統的資源,那么我們接下來就看看進程一共有多少種方式可以從用戶態進入到內核態。概括的說,有三種方式:系統調用、軟中斷和硬件中斷。這三種方式每一種都涉及到大量的操作系統知識,所以這里不做展開。
整體結構
接下來我們從內核空間和用戶空間的角度看一看整個 Linux 系統的結構。它大體可以分為三個部分,從下往上依次為:硬件 -> 內核空間 -> 用戶空間。如下圖所示(此圖來自互聯網):

在硬件之上,內核空間中的代碼控制了硬件資源的使用權,用戶空間中的代碼只有通過內核暴露的系統調用接口(System Call Interface)才能使用到系統中的硬件資源。其實,不光是 Linux,Windows 操作系統的設計也是大同小異。
總結
現代的操作系統大都通過內核空間和用戶空間的設計來保護操作系統自身的安全性和穩定性。所以在我們閱讀有關操作系統的資料時經常遇到內核空間、用戶空間和內核態、用戶態等概念,希望本文能夠幫助您理解這些基本的概念。
轉載文章,轉載至:
https://www.cnblogs.com/bakari/p/5520860.html
https://www.jianshu.com/p/a77613045601
