Linux 記憶體管理:記憶體對映

發表於2015-09-24

之前講了那麼多記憶體的東西,那麼都離不開記憶體對映,不論虛擬地址到實體地址,還是使用者空間地址到核心空間。關於對映使用者空間最常用的是mmap來對映裝置的io空間,直接訪問,來提高io效率。核心的有ioremap對映裝置io地址空間以供核心訪問,kmap對映申請的高階記憶體,

還有DMA ,dma主要用的多的是網路卡驅動裡ring buffer機制.

下面就說說mmap:

函式原型:void* mmap ( void * start , size_t len , int prot , int flags , int fd , off_t offset )

引數說明:

  • start:對映區的開始地址,設定為0時表示由系統決定對映區的起始地址。
  • length:對映區的長度。//長度單位是 以位元組為單位,不足一記憶體頁按一記憶體頁處理
  • prot:期望的記憶體保護標誌,不能與檔案的開啟模式衝突。是以下的某個值,可以通過or運算合理地組合在一起
  • PROT_EXEC //頁內容可以被執行
  • PROT_READ //頁內容可以被讀取
  • PROT_WRITE //頁可以被寫入
  • PROT_NONE //頁不可訪問
  • flags:指定對映物件的型別,對映選項和對映頁是否可以共享。它的值可以是一個或者多個以下位的組合體
  • MAP_FIXED //使用指定的對映起始地址,如果由start和len引數指定的記憶體區重疊於現存的對映空間,重疊部分將會被丟棄。如果指定的起始地址不可用,操作將會失敗。並且起始地址必須落在頁的邊界上。
  • MAP_SHARED //與其它所有對映這個物件的程式共享對映空間。對共享區的寫入,相當於輸出到檔案。直到msync()或者munmap()被呼叫,檔案實際上不會被更新。
  • MAP_PRIVATE //建立一個寫入時拷貝的私有對映。記憶體區域的寫入不會影響到原檔案。這個標誌和以上標誌是互斥的,只能使用其中一個。
  • MAP_DENYWRITE //這個標誌被忽略。
  • MAP_EXECUTABLE //同上
  • MAP_NORESERVE //不要為這個對映保留交換空間。當交換空間被保留,對對映區修改的可能會得到保證。當交換空間不被保留,同時記憶體不足,對對映區的修改會引起段違例訊號。
  • MAP_LOCKED //鎖定對映區的頁面,從而防止頁面被交換出記憶體。
  • MAP_GROWSDOWN //用於堆疊,告訴核心VM系統,對映區可以向下擴充套件。
  • MAP_ANONYMOUS //匿名對映,對映區不與任何檔案關聯。
  • MAP_ANON //MAP_ANONYMOUS的別稱,不再被使用。
  • MAP_FILE //相容標誌,被忽略。
  • MAP_32BIT //將對映區放在程式地址空間的低2GB,MAP_FIXED指定時會被忽略。當前這個標誌只在x86-64平臺上得到支援。
  • MAP_POPULATE //為檔案對映通過預讀的方式準備好頁表。隨後對對映區的訪問不會被頁違例阻塞。
  • MAP_NONBLOCK //僅和MAP_POPULATE一起使用時才有意義。不執行預讀,只為已存在於記憶體中的頁面建立頁表入口。
  • MAP_HUGETLB (since Linux 2.6.32)
  • Allocate the mapping using “huge pages.” See the kernel source file Documentation/vm/hugetlbpage.txt for further information.
  • fd:有效的檔案描述詞。一般是由open()函式返回,其值也可以設定為-1,此時需要指定flags引數中的MAP_ANON,表明進行的是匿名對映。
  • off_t offset:被對映物件內容的起點。檔案對映的偏移量,通常設定為0,代表從檔案最前方開始對應,offset必須是分頁大小的整數倍

返回值:

成功執行時,mmap()返回被對映區的指標,失敗時,mmap()返回MAP_FAILED[其值為(void *)-1],

上邊只是對mmap的基本引數做了說明,我們知道使用者空間都是檔案訪問,即file_operations 中有函式指標mmap,那麼呼叫的mmap 的時候一般需要傳遞fd。

在include/linux/fs.h:

通過上面的結構我們知道mmap系統呼叫最後呼叫檔案操作指標函式mmap.
那麼需要看一下mmap系統呼叫的實現:mm/mmap.c:

在說之前,需要補充一下知識,第一使用者空間的記憶體佈局,和結構體struct vm_area_struct

其實之前文章已經說過這個佈局。
我們看include/linux/mm_types.h:

Struct vm_area_struct用紅黑樹來管理。不是和vmalloc裡一些結構很相似?但是別搞混了.
核心中每一個這樣的物件都表示使用者程式地址空間的一段區域。
當linux 執行一個應用程式時,系統呼叫exec通過load_elf_binary函式把elf載入到使用者虛擬空間。前面我們已經說了棧和堆。Text不用多解釋。

那麼基本流程就是:
1. 使用者呼叫mmap系統呼叫
2. 核心在使用者空間mmap區域分配一個空閒的vm_area_struct物件。
3. 然後修改頁目錄表項把物件的地址和裝置的記憶體對應起來

那麼在使用者空間,mmap系統呼叫函式原型為:
Void *mmap(void *start,size_t length,int prot ,int flags,int fd, off_t offset);

它能夠起作用的前提是開啟的裝置檔案的驅動裡實現了mmap。

看看mmap系統呼叫核心實現,
1.找到fd對應的struct file;
2 do_mmap_pgoff完成對映的工作。

細說do_mmap_pgoff函式
(1) 呼叫get_unmapped_area獲得未使用的vm_area_struct
(2) 後續是mmap_region
(3) 呼叫到驅動file->mmap的具體實現
(4) 具體驅動層mmap的實現

在具體實現驅動層的mmap前,linux核心已經實現了頁表對映的介面api供我們使用。

Remap_pfn_range (memory.c)也有其他延伸介面

Mmap是可以忽略fd引數的:MAP_ANONYMOUS建立匿名對映。此時會忽略引數fd,不涉及檔案,而且對映區域無法和其他程式共享
引數fd:要對映到記憶體中的檔案描述符。如果使用匿名記憶體對映時,即flags中設定了MAP_ANONYMOUS,fd設為-1。有
些系統不支援匿名記憶體對映,則可以使用fopen開啟/dev/zero檔案,然後對該檔案進行對映,可以同樣達到匿名記憶體對映的效果。
MAP_HUGETLB是核心2.6.32引入的一個mmap flags, 用於使用huge pages分配共享記憶體.

使用大頁面的好處是在大記憶體的管理上減少CPU的開銷。Linux對大頁面記憶體的引入對減少TLB的失效效果不錯,特別是記憶體大而密集型的程式,比如說在資料庫中的使用

顯然正常的mmap呼叫流程會走人第一個if語句獲取file指標.

接著呼叫了:

獲取互斥鎖,呼叫do_mmap_pgoff

首先呼叫get_unmapped_area在使用者記憶體空間map區裡分配一個空閒區。 然後呼叫mmap_region具體的對映.
在mmap_region中:

這個函式有兩個關鍵的地方,第一就是申請了vma並初始化,然後呼叫 file->f_op->mmap(file, vma);
這樣整個流程就清晰了,驅動開發人員只需要關注裝置驅動裡file操作中mmap實現就可以了。

關於可執行檔案的對映我們可以參考幾個圖:

那麼對應每個程式都有個一個mm_struct:

在mm_struct中有
struct vm_area_struct * mmap; /* list of VMAs */

它儲存了程式所有對映的區域,之前我們提到過每個vma(即結構vm_area_struct都代表使用者空間的一個對映)。那麼它在這裡連線起來。

 

我們在mmap_region中看到這樣一行程式碼:

vma_link(mm, vma, prev, rb_link, rb_parent); 即把申請的vma加入管理中.

這裡需要說明庫檔案的map和裝置驅動的對映不太一樣,前者不要求實體地址連續,但是後者要求,因為裝置io空間預設是連續的.

對於任何一個普通檔案,對於的file *中的mmap操作是什麼呢?

這個跟fs有關係:
.mmap=generic_file_mmap // filemap.c

我們也可以通過proc來檢視:
#cat /proc/pid/maps

而檢視靜態的bin可以通過nm和objdump,Nm檢視bin的符號,objdump可以檢視elf資訊,也可以通過file 和readelf檢視

這裡就說說mmap支援的功能:

1. mmap共享記憶體:

(1)使用普通檔案提供的記憶體對映:
適用於任何程式之間。此時,需要開啟或建立一個檔案,然後再呼叫mmap()

典型呼叫程式碼如下:
fd=open(name, flag, mode); if(fd<0) …
ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0);
通過mmap()實現共享記憶體的通訊方式有許多特點和要注意的地方,可以參看UNIX網路程式設計第二卷。

(2)使用特殊檔案提供匿名記憶體對映:
適用於具有親緣關係的程式之間。由於父子程式特殊的親緣關係,在父程式中先呼叫mmap(),然後呼叫 fork()。那麼在呼叫fork()之後,子程式繼承父程式匿名對映後的地址空間,同樣也繼承mmap()返回的地址,這樣,父子程式就可以通過對映區 域進行通訊了。注意,這裡不是一般的繼承關係。一般來說,子程式單獨維護從父程式繼承下來的一些變數。而mmap()返回的地址,卻由父子程式共同維護。 對於具有親緣關係的程式實現共享記憶體最好的方式應該是採用匿名記憶體對映的方式。此時,不必指定具體的檔案,只要設定相應的標誌即可。

2. 提高檔案訪問效率

3. 對映裝置

實現對映裝置的函式mmap的時候,需要用到remap_pfn_range
remap_pfn_range不能對映常規記憶體,只存取保留頁和在實體記憶體頂之上的實體地址。因為保留頁和在物理
記憶體頂之上的實體地址記憶體管理系統的各個子模組管理不到。640 KB 和 1MB 是保留頁可能對映,裝置I/O
記憶體也可以對映。如果想把kmalloc()申請的記憶體對映到使用者空間,則可以通過mem_map_reserve()把相應
的記憶體設定為保留後就可以。

remap_pfn_range常用於裝置記憶體對映,而nopage()常用於RAM對映
呼叫mmap()時就決定了對映大小,不能再增加。換句話說,對映不能改變檔案的大小。反過來,由檔案被對映部分,而不是由檔案大小來決定程式可訪問記憶體空間範圍(對映時,指定offset最好是記憶體頁面大小的整數倍)。

通常使用mmap()的三種情況.提高I/O效率、匿名記憶體對映、共享記憶體程式通訊。

在kernel裡,通常有3種申請記憶體的方式:vmalloc, kmalloc, alloc_pages。kmalloc與alloc_pages類似,均是申請連續的地址空間。而vmalloc則可以申請一段不連續的實體地址空間,並將其對映到連續的線性地址上。每次vmalloc之後,核心會建立一個vm_struct,用以對映分配到的不連續的記憶體區域。vm_struct類似vma,但是又不是一回事。vma是將實體記憶體對映到程式的虛擬地址空間。而vm_struct是將實體記憶體對映到核心的線性地址空間。  既然vmalloc拿到的不是連續的實體記憶體,那麼將這些記憶體對映到vma時,就不能直接利用remap_pfn_range()了。此時可以採用兩種方法,一種是實現vm_operations_struct的fault()方法,用以在缺頁時再對映需要的頁。此方法操作起來較為麻煩。另一種方法是直接使用remap_vmalloc_range()函式。該函式的原型為:

int remap_vmalloc_range(struct vm_area_struct *vma, void *addr,

unsigned long pgoff)

其中引數vma是mmap使用呼叫傳下來的,addr即為vmalloc()所分配記憶體的起始地址。而pgoff則為mmap()系統呼叫裡的偏移引數,可以通過vma->vm_pgoff獲得。該函式成功執行後,返回值為0。如果返回值為負數,則說明出錯了。通常是由於所傳的引數不正確。

需要注意的是,需要對映到使用者空間的記憶體段,不能直接利用vmalloc()分配,而應該使用vmalloc_user()函式。該函式除了分配記憶體之外,還會將相應的vm_struct結構標記為VM_USERMAP。否則,remap_vmalloc_range將返回錯誤。

下面附上自己裝置對映的測試程式碼(由於是測試只對映核心記憶體,用了兩種方式一種是kmalloc 一種是vmalloc,而對映裝置的時候直接傳遞裝置io地址)

使用者空間程式:

核心模組程式碼:

Makfile:
obj-m:=hello.o

編譯:
make -C /usr/src/linux M=pwd modules // /usr/src/linux是核心路徑或者核心標頭檔案路徑
安裝 insmod hello.ko // 還需要自己查詢裝置號來建立裝置檔案.

相關文章