Linux記憶體管理之mmap詳解

  • 2019 年 10 月 6 日
  • 筆記

作者:freeboy1015

來源:http://lib.csdn.net/article/linux/62126

一. mmap系統調用

1. mmap系統調用

mmap將一個文件或者其它對象映射進記憶體。文件被映射到多個頁上,如果文件的大小不是所有頁的大小之和,最後一個頁不被使用的空間將會清零。munmap執行相反的操作,刪除特定地址區域的對象映射。

當使用mmap映射文件到進程後,就可以直接操作這段虛擬地址進行文件的讀寫等操作,不必再調用read,write等系統調用.但需注意,直接對該段記憶體寫時不會寫入超過當前文件大小的內容.

採用共享記憶體通訊的一個顯而易見的好處是效率高,因為進程可以直接讀寫記憶體,而不需要任何數據的拷貝。對於像管道和消息隊列等通訊方式,則需要在內核和用戶空間進行四次的數據拷貝,而共享記憶體則只拷貝兩次數據:一次從輸入文件到共享記憶體區,另一次從共享記憶體區到輸出文件。實際上,進程之間在共享記憶體時,並不總是讀寫少量數據後就解除映射,有新的通訊時,再重新建立共享記憶體區域。而是保持共享區域,直到通訊完畢為止,這樣,數據內容一直保存在共享記憶體中,並沒有寫迴文件。共享記憶體中的內容往往是在解除映射時才寫迴文件的。因此,採用共享記憶體的通訊方式效率是非常高的。

基於文件的映射,在mmap和munmap執行過程的任何時刻,被映射文件的st_atime可能被更新。如果st_atime欄位在前述的情況下沒有得到更新,首次對映射區的第一個頁索引時會更新該欄位的值。用PROT_WRITE 和 MAP_SHARED標誌建立起來的文件映射,其st_ctime 和 st_mtime在對映射區寫入之後,但在msync()通過MS_SYNC 和 MS_ASYNC兩個標誌調用之前會被更新。

用法:

#include

void *mmap(void *start, size_t length, int prot, int flags,

int fd, off_t offset);

int munmap(void *start, size_t length);

返回說明:

成功執行時,mmap()返回被映射區的指針,munmap()返回0。失敗時,mmap()返回MAP_FAILED[其值為(void *)-1],munmap返回-1。errno被設為以下的某個值

EACCES:訪問出錯

EAGAIN:文件已被鎖定,或者太多的記憶體已被鎖定

EBADF:fd不是有效的文件描述詞

EINVAL:一個或者多個參數無效

ENFILE:已達到系統對打開文件的限制

ENODEV:指定文件所在的文件系統不支援記憶體映射

ENOMEM:記憶體不足,或者進程已超出最大記憶體映射數量

EPERM:權能不足,操作不允許

ETXTBSY:已寫的方式打開文件,同時指定MAP_DENYWRITE標誌

SIGSEGV:試著向只讀區寫入

SIGBUS:試著訪問不屬於進程的記憶體區

參數:

start:映射區的開始地址。

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一起使用時才有意義。不執行預讀,只為已存在於記憶體中的頁面建立頁表入口。

fd:有效的文件描述詞。如果MAP_ANONYMOUS被設定,為了兼容問題,其值應為-1。

offset:被映射對象內容的起點。

2. 系統調用munmap()

#include

int munmap( void * addr, size_t len ) 該調用在進程地址空間中解除一個映射關係,addr是調用mmap()時返回的地址,len是映射區的大小。當映射關係解除後,對原來映射地址的訪問將導致段錯誤發生。

3. 系統調用msync()

#include

int msync ( void * addr , size_t len, int flags) 一般說來,進程在映射空間的對共享內容的改變並不直接寫回到磁碟文件中,往往在調用munmap()後才執行該操作。可以通過調用msync()實現磁碟上文件內容與共享記憶體區的內容一致。

二. 系統調用mmap()用於共享記憶體的兩種方式

(1)使用普通文件提供的記憶體映射:適用於任何進程之間;此時,需要打開或創建一個文件,然後再調用mmap();典型調用程式碼如下:

fd=open(name, flag, mode);

if(fd<0)

ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0);

通過mmap()實現共享記憶體的通訊方式有許多特點和要注意的地方

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

三. mmap進行記憶體映射的原理

mmap系統調用的最終目的是將,設備或文件映射到用戶進程的虛擬地址空間,實現用戶進程對文件的直接讀寫,這個任務可以分為以下三步:

1.在用戶虛擬地址空間中尋找空閑的滿足要求的一段連續的虛擬地址空間,為映射做準備(由內核mmap系統調用完成)

每個進程擁有3G位元組的用戶虛存空間。但是,這並不意味著用戶進程在這3G的範圍內可以任意使用,因為虛存空間最終得映射到某個物理存儲空間(記憶體或磁碟空間),才真正可以使用。

那麼,內核怎樣管理每個進程3G的虛存空間呢?概括地說,用戶進程經過編譯、鏈接後形成的映象文件有一個程式碼段和數據段(包括data段和bss段),其中程式碼段在下,數據段在上。數據段中包括了所有靜態分配的數據空間,即全局變數和所有申明為static的局部變數,這些空間是進程所必需的基本要求,這些空間是在建立一個進程的運行映像時就分配好的。除此之外,堆棧使用的空間也屬於基本要求,所以也是在建立進程時就分配好的,如圖3.1所示:

圖3.1 進程虛擬空間的劃分

在內核中,這樣每個區域用一個結構struct vm_area_struct 來表示.它描述的是一段連續的、具有相同訪問屬性的虛存空間,該虛存空間的大小為物理記憶體頁面的整數倍。可以使用 cat /proc//maps來查看一個進程的記憶體使用情況,pid是進程號.其中顯示的每一行對應進程的一個vm_area_struct結構.

下面是struct vm_area_struct結構體的定義:

#include

/* This struct defines a memory VMM memory area. */

struct vm_area_struct {

struct mm_struct * vm_mm; /* VM area parameters */

unsigned long vm_start;

unsigned long vm_end;

/* linked list of VM areas per task, sorted by address */

struct vm_area_struct *vm_next;

pgprot_t vm_page_prot;

unsigned long vm_flags;

/* AVL tree of VM areas per task, sorted by address */

short vm_avl_height;

struct vm_area_struct * vm_avl_left;

struct vm_area_struct * vm_avl_right;

/* For areas with an address space and backing store,

vm_area_struct *vm_next_share;

struct vm_area_struct **vm_pprev_share;

struct vm_operations_struct * vm_ops;

unsigned long vm_pgoff; /* offset in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */

struct file * vm_file;

unsigned long vm_raend;

void * vm_private_data; /* was vm_pte (shared mem) */

};

通常,進程所使用到的虛存空間不連續,且各部分虛存空間的訪問屬性也可能不同。所以一個進程的虛存空間需要多個vm_area_struct結構來描述。在vm_area_struct結構的數目較少的時候,各個vm_area_struct按照升序排序,以單鏈表的形式組織數據(通過vm_next指針指向下一個vm_area_struct結構)。但是當vm_area_struct結構的數據較多的時候,仍然採用鏈表組織的化,勢必會影響到它的搜索速度。針對這個問題,vm_area_struct還添加了vm_avl_hight(樹高)、vm_avl_left(左子節點)、vm_avl_right(右子節點)三個成員來實現AVL樹,以提高vm_area_struct的搜索速度。

  假如該vm_area_struct描述的是一個文件映射的虛存空間,成員vm_file便指向被映射的文件的file結構,vm_pgoff是該虛存空間起始地址在vm_file文件裡面的文件偏移,單位為物理頁面。

圖3.2 進程虛擬地址示意圖

因此,mmap系統調用所完成的工作就是準備這樣一段虛存空間,並建立vm_area_struct結構體,將其傳給具體的設備驅動程式.

2. 建立虛擬地址空間和文件或設備的物理地址之間的映射(設備驅動完成)

建立文件映射的第二步就是建立虛擬地址和具體的物理地址之間的映射,這是通過修改進程頁表來實現的.mmap方法是file_opeartions結構的成員:

int (*mmap)(struct file *,struct vm_area_struct *);

linux有2個方法建立頁表:

(1) 使用remap_pfn_range一次建立所有頁表.

int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot);

返回值:

成功返回 0, 失敗返回一個負的錯誤值 參數說明:

vma 用戶進程創建一個vma區域

virt_addr 重新映射應當開始的用戶虛擬地址. 這個函數建立頁表為這個虛擬地址範圍從 virt_addr 到 virt_addr_size.

pfn 頁幀號, 對應虛擬地址應當被映射的物理地址. 這個頁幀號簡單地是物理地址右移 PAGE_SHIFT 位. 對大部分使用, VMA 結構的 vm_paoff 成員正好包含你需要的值. 這個函數影響物理地址從 (pfn<

size 正在被重新映射的區的大小, 以位元組.

prot 給新 VMA 要求的"protection". 驅動可(並且應當)使用在vma->vm_page_prot 中找到的值.

(2) 使用nopage VMA方法每次建立一個頁表項.

struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);

返回值:

成功則返回一個有效映射頁,失敗返回NULL.

參數說明:

address 代表從用戶空間傳過來的用戶空間虛擬地址.

返回一個有效映射頁.

(3) 使用方面的限制:

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

3. 當實際訪問新映射的頁面時的操作(由缺頁中斷完成)

(1) page cache及swap cache中頁面的區分:一個被訪問文件的物理頁面都駐留在page cache或swap cache中,一個頁面的所有資訊由struct page來描述。struct page中有一個域為指針mapping ,它指向一個struct address_space類型結構。page cache或swap cache中的所有頁面就是根據address_space結構以及一個偏移量來區分的。

(2) 文件與 address_space結構的對應:一個具體的文件在打開後,內核會在記憶體中為之建立一個struct inode結構,其中的i_mapping域指向一個address_space結構。這樣,一個文件就對應一個address_space結構,一個 address_space與一個偏移量能夠確定一個page cache 或swap cache中的一個頁面。因此,當要定址某個數據時,很容易根據給定的文件及數據在文件內的偏移量而找到相應的頁面。

(3) 進程調用mmap()時,只是在進程空間內新增了一塊相應大小的緩衝區,並設置了相應的訪問標識,但並沒有建立進程空間到物理頁面的映射。因此,第一次訪問該空間時,會引發一個缺頁異常。

(4) 對於共享記憶體映射情況,缺頁異常處理程式首先在swap cache中尋找目標頁(符合address_space以及偏移量的物理頁),如果找到,則直接返回地址;如果沒有找到,則判斷該頁是否在交換區 (swap area),如果在,則執行一個換入操作;如果上述兩種情況都不滿足,處理程式將分配新的物理頁面,並把它插入到page cache中。進程最終將更新進程頁表。

註:對於映射普通文件情況(非共享映射),缺頁異常處理程式首先會在page cache中根據address_space以及數據偏移量尋找相應的頁面。如果沒有找到,則說明文件數據還沒有讀入記憶體,處理程式會從磁碟讀入相應的頁面,並返回相應地址,同時,進程頁表也會更新.

(5) 所有進程在映射同一個共享記憶體區域時,情況都一樣,在建立線性地址與物理地址之間的映射之後,不論進程各自的返回地址如何,實際訪問的必然是同一個共享記憶體區域對應的物理頁面。