在 Linux 中,用戶內存和內核內存是獨立的,在各自的地址空間實現。地址空間是虛擬的,就是說地址是從物理內存中抽象出來的(通過一個簡短描述的過程)。由於地址空間是虛擬的,所以可以存在很多。事實上,內核本身駐留在一個地址空間中,每個進程駐留在自己的地址空間。這些地址空間由虛擬內存地址組成,允許一些帶有獨立地址空間的進程指向一個相對較小的物理地址空間(在機器的物理內存中)。不僅僅是方便,而且更安全。因為每個地址空間是獨立且隔離的,因此很安全。
但是與安全性相關聯的成本很高。因為每個進程(和內核)會有相同地址指向不同的物理內存區域,不可能立即共享內存。幸運的是,有一些解決方案。用戶進程可以通過 Portable Operating System Interface for UNIX? (POSIX) 共享的內存機制(shmem)共享內存,但有一點要說明,每個進程可能有一個指向相同物理內存區域的不同虛擬地址。
虛擬內存到物理內存的映射通過頁表完成,這是在底層軟件中實現的(見圖 1)。硬件本身提供映射,但是內核管理表及其配置。注意這裡的顯示,進程可能有一個大的地址空間,但是很少見,就是說小的地址空間的區域(頁面)通過頁表指向物理內存。這允許進程僅為隨時需要的網頁指定大的地址空間。
圖 1. 頁表提供從虛擬地址到物理地址的映射
由於缺乏為進程定義內存的能力,底層物理內存被過度使用。通過一個稱為 paging(然而,在 Linux 中通常稱為 swap)的進程,很少使用的頁面將自動移到一個速度較慢的存儲設備(比如磁盤),來容納需要被訪問的其它頁面(見圖 2 )。這一行為允許,在將很少使用的頁面遷移到磁盤來提高物理內存使用的同時,計算機中的物理內存為應用程序更容易需要的頁面提供服務。注意,一些頁面可以指向文件,在這種情況下,如果頁面是髒(dirty)的,數據將被沖洗,如果頁面是干淨的(clean),直接丟掉。
圖 2. 通過將很少使用的頁面遷移到速度慢且便宜的存儲器,交換使物理內存空間得到了更好的利用
MMU-less 架構
不是所有的處理器都有 MMU。因此,uClinux 發行版(微控制器 Linux)支持操作的一個地址空間。該架構缺乏 MMU 提供的保護,但是允許 Linux 運行另一類處理器。
選擇一個頁面來交換存儲的過程被稱為一個頁面置換算法,可以通過使用許多算法(至少是最近使用的)來實現。該進程在請求存儲位置時發生,存儲位置的頁面不在存儲器中(在存儲器管理單元 [MMU] 中無映射)。這個事件被稱為一個頁面錯誤 並被硬件(MMU)刪除,出現頁面錯誤中斷後該事件由防火牆管理。該棧的詳細說明見 圖 3。
Linux 提供一個有趣的交換實現,該實現提供許多有用的特性。Linux 交換系統允許創建和使用多個交換分區和優先權,這支持存儲設備上的交換層次結構,這些存儲設備提供不同的性能參數(例如,固態磁盤 [SSD] 上的一級交換和速度較慢的存儲設備上的較大的二級交換)。為 SSD 交換附加一個更高的優先級使其可以使用直至耗盡;直到那時,頁面才能被寫入優先級較低的交換分區。
圖 3. 地址空間和虛擬 - 物理地址映射的元素
並不是所有的頁面都適合交換。考慮到響應中斷的內核代碼或者管理頁表和交換邏輯的代碼,顯然,這些頁面決不能被換出,因此它們是固定的,或者是永久地駐留在內存中。盡管內核頁面不需要進行交換,然而用戶頁面需要,但是它們可以被固定,通過 mlock(或 mlockall)函數來鎖定頁面。這就是用戶空間內存訪問函數的目的。如果內核假設一個用戶傳遞的地址是有效的且是可訪問的,最終可能會出現內核嚴重錯誤(kernel panic)(例如,因為用戶頁面被換出,而導致內核中的頁面錯誤)。該應用程序編程接口(API)確保這些邊界情況被妥善處理。
內核 API
現在,讓我們來研究一下用戶操作用戶內存的內核 API。請注意,這涉及內核和用戶空間接口,而下一部分將研究其他的一些內存 API。用戶空間內存訪問函數在表 1 中列出。
表 1. 用戶空間內存訪問 API
函數 描述 access_ok 檢查用戶空間內存指針的有效性 get_user 從用戶空間獲取一個簡單變量 put_user 輸入一個簡單變量到用戶空間 clear_user 清除用戶空間中的一個塊,或者將其歸零。 copy_to_user 將一個數據塊從內核復制到用戶空間 copy_from_user 將一個數據塊從用戶空間復制到內核 strnlen_user 獲取內存空間中字符串緩沖區的大小 strncpy_from_user 從用戶空間復制一個字符串到內核
正如您所期望的,這些函數的實現架構是獨立的。例如在 x86 架構中,您可以使用 ./linux/arch/x86/lib/usercopy_32.c 和 usercopy_64.c 中的源代碼找到這些函數以及在 ./linux/arch/x86/include/asm/uaccess.h 中定義的字符串。
當數據移動函數的規則涉及到復制調用的類型時(簡單 VS. 聚集),這些函數的作用如圖 4 所示。
圖 4. 使用 User Space Memory Access API 進行數據移動
access_ok 函數
您可以使用 access_ok 函數在您想要訪問的用戶空間檢查指針的有效性。調用函數提供指向數據塊的開始的指針、塊大小和訪問類型(無論這個區域是用來讀還是寫的)。函數原型定義如下:
access_ok( type, addr, size );
type 參數可以被指定為 VERIFY_READ 或 VERIFY_WRITE。VERIFY_WRITE 也可以識別內存區域是否可讀以及可寫(盡管訪問仍然會生成 -EFAULT)。該函數簡單檢查地址可能是在用戶空間,而不是內核。
get_user 函數
要從用戶空間讀取一個簡單變量,可以使用 get_user 函數,該函數適用於簡單數據類型,比如,char 和 int,但是像結構體這類較大的數據類型,必須使用 copy_from_user 函數。該原型接受一個變量(存儲數據)和一個用戶空間地址來進行 Read 操作:
get_user( x, ptr );
get_user 函數將映射到兩個內部函數其中的一個。在系統內部,這個函數決定被訪問變量的大小(根據提供的變量存儲結果)並通過 __get_user_x 形成一個內部調用。成功時該函數返回 0,一般情況下,get_user 和 put_user 函數比它們的塊復制副本要快一些,如果是小類型被移動的話,應該用它們。
put_user 函數
您可以使用 put_user 函數來將一個簡單變量從內核寫入用戶空間。和 get_user 一樣,它接受一個變量(包含要寫的值)和一個用戶空間地址作為寫目標:
put_user( x, ptr );
和 get_user 一樣,put_user 函數被內部映射到 put_user_x 函數,成功時,返回 0,出現錯誤時,返回 -EFAULT。
clear_user 函數
clear_user 函數被用於將用戶空間的內存塊清零。該函數采用一個指針(用戶空間中)和一個型號進行清零,這是以字節定義的:
clear_user( ptr, n );
在內部,clear_user 函數首先檢查用戶空間指針是否可寫(通過 access_ok),然後調用內部函數(通過內聯組裝方式編碼)來執行 Clear 操作。使用帶有 repeat 前綴的字符串指令將該函數優化成一個非常緊密的循環。它將返回不可清除的字節數,如果操作成功,則返回 0。
copy_to_user 函數
copy_to_user 函數將數據塊從內核復制到用戶空間。該函數接受一個指向用戶空間緩沖區的指針、一個指向內存緩沖區的指針、以及一個以字節定義的長度。該函數在成功時,返回 0,否則返回一個非零數,指出不能發送的字節數。
copy_to_user( to, from, n );
檢查了向用戶緩沖區寫入的功能之後(通過 access_ok),內部函數 __copy_to_user 被調用,它反過來調用 __copy_from_user_inatomic(在 ./linux/arch/x86/include/asm/uaccess_XX.h 中。其中 XX 是 32 或者 64 ,具體取決於架構。)在確定了是否執行 1、2 或 4 字節復制之後,該函數調用 __copy_to_user_ll,這就是實際工作進行的地方。在損壞的硬件中(在 i486 之前,WP 位在管理模式下不可用),頁表可以隨時替換,需要將想要的頁面固定到內存,使它們在處理時不被換出。i486 之後,該過程只不過是一個優化的副本。
copy_from_user 函數
copy_from_user 函數將數據塊從用戶空間復制到內核緩沖區。它接受一個目的緩沖區(在內核空間)、一個源緩沖區(從用戶空間)和一個以字節定義的長度。和 copy_to_user 一樣,該函數在成功時,返回 0 ,否則返回一個非零數,指出不能復制的字節數。
copy_from_user( to, from, n );
該函數首先檢查從用戶空間源緩沖區讀取的能力(通過 access_ok),然後調用 __copy_from_user,最後調用 __copy_from_user_ll。從此開始,根據構架,為執行從用戶緩沖區到內核緩沖區的零拷貝(不可用字節)而進行一個調用。優化組裝函數包含管理功能。
strnlen_user 函數
strnlen_user 函數也能像 strnlen 那樣使用,但前提是緩沖區在用戶空間可用。strnlen_user 函數帶有兩個參數:用戶空間緩沖區地址和要檢查的最大長度。
strnlen_user( src, n );
strnlen_user 函數首先通過調用 access_ok 檢查用戶緩沖區是否可讀。如果是 strlen 函數被調用,max length 參數則被忽略。
strncpy_from_user 函數
strncpy_from_user 函數將一個字符串從用戶空間復制到一個內核緩沖區,給定一個用戶空間源地址和最大長度。
strncpy_from_user( dest, src, n );
由於從用戶空間復制,該函數首先使用 access_ok 檢查緩沖區是否可讀。和 copy_from_user 一樣,該函數作為一個優化組裝函數(在 ./linux/arch/x86/lib/usercopy_XX.c 中)實現。
內存映射的其他模式
上面部分探討了在內核和用戶空間之間移動數據的方法(使用內核初始化操作)。Linux 還提供一些其他的方法,用於在內核和用戶空間中移動數據。盡管這些方法未必能夠提供與用戶空間內存訪問函數相同的功能,但是它們在地址空間之間映射內存的功能是相似的。
在用戶空間,注意,由於用戶進程出現在單獨的地址空間,在它們之間移動數據必須經過某種進程間通信機制。Linux 提供各種模式(比如,消息隊列),但是最著名的是 POSIX 共享內存(shmem)。該機制允許進程創建一個內存區域,然後同一個或多個進程共享該區域。注意,每個進程可能在其各自的地址空間中映射共享內存區域到不同地址。因此需要相對的尋址偏移(offset addressing)。
mmap 函數允許一個用戶空間應用程序在虛擬地址空間中創建一個映射,該功能在某個設備驅動程序類中是常見的,允許將物理設備內存映射到進程的虛擬地址空間。在一個驅動程序中,mmap 函數通過 remap_pfn_range 內核函數實現,它提供設備內存到用戶地址空間的線性映射。
結束語
本文討論了 Linux 中的內存管理主題,然後討論了使用這些概念的用戶空間內存訪問函數。在用戶空間和內核空間之間移動數據並沒有表面上看起來那麼簡單,但是 Linux 包含一個簡單的 API 集合,跨平台為您管理這個復雜的任務。