Linux內存管理之mmap詳解
一.?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?<sys/mman.h>
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?<sys/mman.h>
int?munmap(?void?*?addr,?size_t?len?)?該調用在進程地址空間中解除一個映射關系,addr是調用mmap()時返回的地址,len是映射區的大小。當映射關系解除后,對原來映射地址的訪問將導致段錯誤發生。
3.?系統調用msync()?
#include?<sys/mman.h>
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/<pid>/maps來查看一個進程的內存使用情況,pid是進程號.其中顯示的每一行對應進程的一個vm_area_struct結構.
下面是struct?vm_area_struct結構體的定義:
#include?<Linux/mm_types.h>
/*?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<<PAGE_SHIFT)?到?(pfn<<PAGE_SHIFT)+size.
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)?所有進程在映射同一個共享內存區域時,情況都一樣,在建立線性地址與物理地址之間的映射之后,不論進程各自的返回地址如何,實際訪問的必然是同一個共享內存區域對應的物理頁面。
掃描二維碼,添加馬哥個人微信,領取kindle大獎!