引言
首先我們要解釋一個概念——進程(Process)。簡單來說,一個可執行程序就是一個進程,前面我們使用C語言編譯生成的程序,運行后就是一個進程。進程最顯著的特點就是擁有獨立的地址空間。
嚴格來說,程序是存儲在磁盤上的一個文件,是指令和數據的集合,是一個靜態的概念;進程是程序加載到內存運行后一些列的活動,是一個動態的概念。
前面我們在講解地址空間時,一直說“程序的地址空間”,這其實是不嚴謹的,應該說“進程的地址空間”。一個進程對應一個地址空間,而一個程序可能會創建多個進程。
內核模式和用戶模式
內核空間存放的是操作系統內核代碼和數據,是被所有程序共享的,在程序中修改內核空間中的數據不僅會影響操作系統本身的穩定性,還會影響其他程序,這是非常危險的行為,所以操作系統禁止用戶程序直接訪問內核空間。
要想訪問內核空間,必須借助操作系統提供的 API 函數,執行內核提供的代碼,讓內核自己來訪問,這樣才能保證內核空間的數據不會被隨意修改,才能保證操作系統本身和其他程序的穩定性。
用戶程序調用系統 API 函數稱為系統調用(System Call);發生系統調用時會暫停用戶程序,轉而執行內核代碼(內核也是程序),訪問內核空間,這稱為內核模式(Kernel Mode)。
用戶空間保存的是應用程序的代碼和數據,是程序私有的,其他程序一般無法訪問。當執行應用程序自己的代碼時,稱為用戶模式(User Mode)。
計算機會經常在內核模式和用戶模式之間切換:
當運行在用戶模式的應用程序需要輸入輸出、申請內存等比較底層的操作時,就必須調用操作系統提供的 API 函數,從而進入內核模式;
操作完成后,繼續執行應用程序的代碼,就又回到了用戶模式。
總結:用戶模式就是執行應用程序代碼,訪問用戶空間;內核模式就是執行內核代碼,訪問內核空間(當然也有權限訪問用戶空間)。
為什么要區分兩種模式
我們知道,內核最主要的任務是管理硬件,包括顯示器、鍵盤、鼠標、內存、硬盤等,並且內核也提供了接口(也就是函數),供上層程序使用。當程序要進行輸入輸出、分配內存、響應鼠標等與硬件有關的操作時,必須要使用內核提供的接口。但是用戶程序是非常不安全的,內核對用戶程序也是充分不信任的,當程序調用內核接口時,內核要做各種校驗,以防止出錯。
從 Intel 80386 開始,出於安全性和穩定性的考慮,CPU 可以運行在 ring0 ~ ring3 四個不同的權限級別,也對數據提供相應的四個保護級別。不過 Linux 和 Windows 只利用了其中的兩個運行級別:
一個是內核模式,對應 ring0 級,操作系統的核心部分和設備驅動都運行在該模式下。
另一個是用戶模式,對應 ring3 級,操作系統的用戶接口部分(例如 Windows API)以及所有的用戶程序都運行在該級別。
為什么內核和用戶程序要共用地址空間
既然內核也是一個應用程序,為何不讓它擁有獨立的4GB地址空間,而是要和用戶程序共享、占用有限的內存呢?
讓內核擁有完全獨立的地址空間,就是讓內核處於一個獨立的進程中,這樣每次進行系統調用都需要切換進程。切換進程的消耗是巨大的,不僅需要寄存器進棧出棧,還會使CPU中的數據緩存失效、MMU中的頁表緩存失效,這將導致內存的訪問在一段時間內相當低效。
而讓內核和用戶程序共享地址空間,發生系統調用時進行的是模式切換,模式切換僅僅需要寄存器進棧出棧,不會導致緩存失效;現代CPU也都提供了快速進出內核模式的指令,與進程切換比起來,效率大大提高了。