前一篇博客介紹了操作系統中進程和線程的概念,下面接着介紹操作系統內核關於進程隔離的基本內容。
進程隔離是操作系統內核對於資源管理和安全增強的特性,其最終的目的是對於操作系統內核能夠更好的控制程序對資源的申請和使用,並且控制此程序可訪問資源的范圍並限定此程序異常之后能夠影響的范圍。 現有的小型嵌入式系統內核比如UC/OS 2, LittleKernel如果沒有而外的庫的幫助(例如LK的uthread庫),都是不支持進程隔離的,其所有的任務運行在一個大的對每個任務都可見的內存空間之上,這么做的優點是其可以運行在沒有MMU的處理器之上比如ARM的Cortex M處理器甚至是51單片機之上,但是其缺點是每個被操作系統調度的任務是沒有機制隔離其資源的,而系統的安全性由於無法將單個任務崩潰造成的影響控制在此任務之內,所以整個系統的安全性和穩定性都非常的低。
1. 進程隔離的硬件要求
進程隔離對硬件有一些基本的要求,其中最主要的硬件是MMU (Memory Management Unit 內存管理單元),有時候ARM Cortex M之上的MPU也能達到類似的功能,但是其功能很弱,無法做到地之間的翻譯,而只能在物理內存地址之上划定線性的范圍。
下面重點介紹一下MMU的功能, 對於X86處理器來說其32位兼容模式運行還會有分段式內存訪問的模式(其也可以達到隔離內存和翻譯的作用),本文不會介紹,本文主要介紹內存分頁訪問的相關內容。
MMU的作用簡單用一句話概括就是將線性地址翻譯為物理地址, 對於理解操作系統內核來說我們並不需要了解太多MMU的細節,但是如果我們要實現一個內核,這部分知識是根據處理器體系結構的不同而不同的。
2. 為什么需要線性地址(虛擬地址) 到 物理地址的翻譯
我們先看如果沒有線性地址的概念只有物理地址會出現的問題:
a. 整個操作系統能夠訪問的地址空間和實際插入的物理內存大小相關。
b. 每個進程能夠訪問的地址空間是從物理內存空間上的一部分。
c. 編譯生成的程序需要事先知道運行在物理地址空間的范圍,才能夠生成相應的執行代碼。
而對於上面的三個問題是一般的操作系統都無法忍受的,對於通用的操作系統來說,其能訪問的內存空間是根據地址線的范圍來決定的,例如32位系統就是2^32, 而對於64位操作系統是2^48 不會因為實際插入的物理內存大小的變化而變化, 而同樣的為操作系統編譯生成的可執行程序需要具有通用性,而不能針對不同的硬件都重新生成可執行程序來適配不同的物理內存大小和范圍。
引入了線性地址到物理地址的翻譯就能夠解決以上的問題, 例如對於32位處理器上運行的Linux操作系統,其每個進程的內存空間都如下圖所示
從上圖我們可以看到對於其上運行的每一個進程,它的內存線性地址空間地址都是從0 -- > 4GB的范圍,其中Linux內核的地址空間為 3-->4GB的線性地址高位空間(同一個內核的地址空間映射在每個進程的3-->4GB的內存范圍內), 而對於0 -- > 3GB來說為每個進程自己的用戶態地址空間范圍。 所以每個不同的應用程序在運行時其進程的0 -- > 3GB為它們的可訪問地址范圍,雖然它們的代碼邏輯完全不同,但是內存的可訪問范圍卻完全相同。
3. 分頁內存訪問的操作系統如何做到進程的線性地址空間隔離
操作系統內核上電之后會初始化MMU並將自己映射到3-- > 4GB的線性地址空間, 而當我們通過系統調用如fork創建進程時,其會在進程描述結構體內集成內存虛擬地址空間的結構體,其內容包含的是當前進程的地址空間頁表,當操作系統進行任務切換時會改寫CPU的頁表基地址寄存器為當前被運行進程的頁表基地址,從而達成切換地址空間范圍到相應的進程內存范圍的目的。
4. 地址翻譯的過程
MMU將一個線性地址翻譯為一個物理地址的過程如下圖所示
對於32位處理器,其按照10, 10, 12的規則,頭10位線性地址對應頁目錄的index, 通過此10位的值在頁目錄中查詢到一個頁表項的地址,然后再根據中間10位定位實際對應的頁的物理地址, 最后根據最后12位4KB的偏移范圍在一個物理頁中尋址出實際需要訪問的內存位置。
所以一個線性地址分為3部分,頭10位是頁目錄中的索引值,中間10位是頁表中的索引值,最后12位是頁內便宜地址。
而不同的線性地址可以映射到相同的物理地址,此處可以極大的節省實際的內存開銷,例如同一個共享庫如glibc.so其實例在物理內存中只存在一份,而被不同的進程映射到了自己的線性地址空間中,它們共享的訪問glibc的代碼段,而其數據段則每個進程有不同的glibc數據段副本。