轉載位址:http://www.poluoluo.com/server/201107/138420.html
在研究dahdi驅動的時候,見到了一些get_user,put_user的函數,不知道其來由,故而搜尋了這篇文章,前面對linux記憶體的架構描述不是很清晰,描述的有一點亂,如果沒有剛性需求,建議不用怎麼關注,倒不如直接看那幾個圖檔。對我非常有用的地方就是幾個函數的介紹,介紹的比較詳細,對應用有需求的可以着重看一個這幾個函數。
Linux 記憶體
在 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 集合,跨平台為您管理這個複雜的任務。