內核態和用戶態是針對CPU狀態的描述。在內核態可以執行一切特權代碼,在用戶態只能執行那些受限級別的代碼。如果需要調用特權代碼需要進行內核態切換。
一、內核態和用戶態概況
內核態:
- 系統中既有操作系統的程序,也有普通用戶程序。為了安全性和穩定性,操作系統的程序不能隨便訪問,這就是內核態。即需要執行操作系統的程序就必須轉換到內核態才能執行
- 內核態可以使用計算機所有的硬件資源
用戶態:
- 不能直接使用系統資源,也不能改變 CPU 的工作狀態,并且只能訪問這個用戶程序自己的存儲空間!
為什么要區分內核態和用戶態?
本質意義是為了進行權限保護。限定用戶的程序不能亂搞操作系統,如果人人都能任意讀寫任意地址空間軟件管理就亂套了。例如:
- 在CPU的所有指令中,有一些指令是非常危險的,如果錯用,將導致整個系統崩潰。比如:清內存、設置時鐘等。如果所有的程序都能使用這些指令,那么你的系統一天死機N回就不足為奇了。
- 所以,CPU將指令分為特權指令和非特權指令,對于那些危險的指令,只允許操作系統及其相關模塊使用,普通的應用程序只能使用那些不會造成災難的指令。
二、CPU指令集權限
指令集是CPU實現軟件指揮硬件執行的媒介,具體來說每一條匯編語句都對應了一條CPU指令,而非常非常多的CPU指令在一起,可以組成一個、甚至多個集合,指令的集合叫CPU指令集。
同時CPU指令集有權限分級,大家試想,CPU指令集可以直接操作硬件的,要是因為指令操作的不規范,造成的錯誤會影響整個計算機系統的。好比你寫程序,因為對硬件操作不熟悉,導致操作系統內核、及其他所有正在運行的程序,都可能會因為操作失誤而受到不可挽回的錯誤,最后只能重啟計算機才行。
而對于硬件的操作是非常復雜的,參數眾多,出問題的幾率相當大,必須謹慎的進行操作,對開發人員來說是個艱巨的任務,還會增加負擔,同時開發人員在這方面也不被信任,所以操作系統內核直接屏蔽開發人員對硬件操作的可能,都不讓你碰到這些CPU指令集。
那么是如何解決上面的問題呢?
針對上面的需求,硬件設備商直接提供硬件級別的支持,做法就是對CPU指令集設置了權限,不同級別權限能使用的CPU指令集 是有限的,以Intel CPU為例,Inter把CPU指令集操作的權限由高到低劃為4級:
- ring 0
- ring 1
- ring 2
- ring 3
Linux系統僅采用ring0和ring3這2個權限。用戶態的程序工作在3,內核態的程序處于0。
- ring0權限最高,可以使用所有CPU指令集,有對硬件的所有操作權限
- ring3權限最低,僅能使用常規CPU指令集,不能使用操作硬件資源的CPU指令集。代碼沒有對硬件的直接控制權限,也不能直接訪問地址的內存,程序是通過調用系統接口(System Call APIs)來達到訪問硬件和內存
三、內核態與用戶態的空間
1.內核空間和用戶空間的劃分
在內存資源上的使用,操作系統對用戶態與內核態也做了限制,每個進程創建都會分配「虛擬空間地址」,以Linux32位操作系統為例,它的尋址空間范圍是4G
(2的32次方),而操作系統會把虛擬控制地址劃分為兩部分,一部分為內核空間,另一部分為用戶空間,高位的 1G(從虛擬地址 0xC0000000 到 0xFFFFFFFF)由內核使用,而低位的 3G(從虛擬地址 0x00000000 到 0xBFFFFFFF)由各個進程使用。
- 用戶態:只能操作
0-3G
范圍的低位虛擬空間地址 - 內核態:
0-4G
范圍的虛擬空間地址都可以操作,尤其是對3-4G
范圍的高位虛擬空間地址必須由內核態去操作 - 補充:
3G-4G
部分大家是共享的(指所有進程的內核態邏輯地址是共享同一塊內存地址),是內核態的地址空間,這里存放著整個內核的代碼和所有的內核模塊,以及內核所維護的數據。每個進程的4G
虛擬空間地址中高位1G
都是一樣的,即內核空間。只有剩余的3G
才歸進程自己使用,換句話說就是, 高位1G
的內核空間是被所有進程共享的!
最后做個小結,我們通過指令集權限區分用戶態和內核態,還限制了內存資源的使用,操作系統為用戶態與內核態劃分了兩塊內存空間,給它們對應的指令集使用
2.內核空間、用戶空間與物理內存的映射關系
每一個進程都有自己的進程地址空間,該進程地址空間由內核空間和用戶空間組成:
- 用戶所寫的代碼和數據位于用戶空間,通過用戶級頁表與物理內存之間建立映射關系。
- 內核空間存儲的實際上是操作系統代碼和數據,通過內核級頁表與物理內存之間建立映射關系。
內核級頁表是一個全局的頁表,它用來維護操作系統的代碼與進程之間的關系。因此,在每個進程的進程地址空間中,用戶空間是屬于當前進程的,每個進程看到的代碼和數據是完全不同的,但內核空間所存放的都是操作系統的代碼和數據,所有進程看到的都是一樣的內容。
需要注意的是,雖然每個進程都能夠看到操作系統,但并不意味著每個進程都能夠隨時對其進行訪問。
四、內核態和用戶態切換為何提供系統調用?
相信大家都聽過這樣的話「用戶態和內核態切換的開銷大」,但是它的開銷大在那里呢?簡單點來說有下面幾點:
- 保留用戶態現場(上下文、寄存器、用戶棧等)
- 復制用戶態參數,用戶棧切到內核棧,進入內核態
- 額外的檢查(因為內核代碼對用戶不信任)
- 執行內核態代碼
- 復制內核態代碼執行結果,回到用戶態
- 恢復用戶態現場(上下文、寄存器、用戶棧等)
實際上操作系統會比上述的更復雜,這里只是個大概,我們可以發現一次切換經歷了用戶態 —>內核態 —>用戶態。
用戶態要主動切換到內核態要有統一的入口,它們就是內核提供的系統調用接口,下面是Linux整體架構圖:
我們可以看出來通過系統調用將Linux整個體系分為內核態和用戶態,而內核提供了一組通用的訪問接口,它們使應用程序能訪問到內核的資源,如CPU、內存、I/O,這些接口就叫系統調用。
-
庫函數就是屏蔽這些復雜的底層實現細節,減輕程序員的負擔,從而更加關注上層的邏輯實現,它對系統調用進行封裝,提供簡單的基本接口給程序員。
-
Shell顧名思義,就是外殼的意思,就好像把內核包裹起來的外殼,它是一種特殊的應用程序,俗稱命令行。Shell也是可編程的,它有標準的Shell語法,符合其語法的文本叫Shell腳本,很多人都會用Shell腳本實現一些常用的功能,可以提高工作效率。
五、內核態和用戶態切換
如何理解進程切換?
- 在當前進程的進程地址空間中的內核空間,找到操作系統的代碼和數據。
- 執行操作系統的代碼,將當前進程的代碼和數據剝離下來,并換上另一個進程的代碼和數據。
注意: 當你訪問用戶空間時你必須處于用戶態,當你訪問內核空間時你必須處于內核態。
1.用戶態切換到內核態的幾種方式
- 系統調用(本質是內中斷):用戶態進程主動切換到內核態的方式,用戶態進程通過系統調用向操作系統申請資源完成工作,例如 fork( )就是一個創建新進程的系統調用,系統調用的機制核心使用了操作系統為用戶特別開放的一個中斷來實現,如Linux的 int80h 中斷,也可以稱為軟中斷
- 進程的時間片到了,導致進程切換(軟中斷):屬于時鐘中斷(Timer Interrupt)?的一種實現方式。
- 異常(內中斷):當CPU在執行用戶態的進程時,發生了一些沒有預知的異常,這時當前運行進程會切換到處理此異常的內核相關進程中,也就是切換到了內核態,如缺頁異常
- 外圍設備的中斷(硬中斷):當CPU在執行用戶態的進程時,外圍設備完成用戶請求的操作后,會向CPU發出相應的中斷信號,這時CPU會暫停執行下一條即將要執行的指令,轉到與中斷信號對應的處理程序去執行,也就是切換到了內核態。如硬盤讀寫操作完成,系統會切換到硬盤讀寫的中斷處理程序中執行后邊的操作等。
2.內核態切換到用戶態的幾種方式
- 系統調用返回時。
- 進程切換完畢。
- 異常、中斷、陷阱等處理完畢。
其中,由用戶態切換為內核態我們稱之為陷入內核。每當我們需要陷入內核的時,本質上是因為我們需要執行操作系統的代碼,比如系統調用函數是由操作系統實現的,我們要進行系統調用就必須先由用戶態切換為內核態。
特別鳴謝:
操作系統之內核態與用戶態
什么是用戶態和內核態?
Linux進程信號