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