mmap詳解

  記憶體映射,簡而言之就是將內核空間的一段記憶體區域映射到用戶空間。映射成功後,用戶對這段記憶體區域的修改可以直接反映到內核空間,相反,內核空間對這段區域的修改也直接反映用戶空間。那麼對於內核空間與用戶空間兩者之間需要大量數據傳輸等操作的話效率是非常高的。當然,也可以將內核空間的一段記憶體區域同時映射到多個進程,這樣還可以做到進程間的共享記憶體通信。

系統調用mmap()就是用來做到上面說的記憶體映射。最長見的操作就是文件(在Linux下設備也被看做文件)的操作,可以將某文件映射至記憶體(進程空間),如此可以把對文件的操作轉為對記憶體的操作,以此避免更多的lseek()與read()、write()操作,這點對於大文件或者頻繁訪問的文件而言尤其受益。

概述

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

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

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

通常使用mmap()的三種情況:提高I/O效率、匿名記憶體映射、共享記憶體進程通信。

用戶空間mmap()函數void *mmap(void *start, size_t length, int prot, int flags,int fd, off_t offset),下面就其參數解釋如下:

start:用戶進程中要映射的用戶空間的起始地址,通常為NULL(由內核來指定)

length:要映射的記憶體區域的大小

prot:期望的記憶體保護標誌

flags:指定映射對象的類型

fd:文件描述符(由open函數返回)

offset:設置在內核空間中已經分配好的的記憶體區域中的偏移,例如文件的偏移量,大小為PAGE_SIZE的整數倍

返回值:mmap()返回被映射區的指針,該指針就是需要映射的內核空間在用戶空間的虛擬地址

CPU體系架構-image/os_mm/mmap_ker2virt.png

記憶體映射的應用

X Window服務器

眾多記憶體數據庫如MongoDB操作數據,就是把文件磁盤內容映射到記憶體中進行處理,為什麼會提高效率? 很多人不解. 下面就深入分析記憶體文件映射.

通過malloc來分配大記憶體其實調用的是mmap,可見在malloc(10)的時候調用的是brk, malloc(10 * 1024 * 1024)調用的是mmap;

mmap()用於共享記憶體的兩種方式

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

view plainprint?

fd=open(name,flag,mode);

if(fd<0)

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

通過mmap()做到共享記憶體的通信方式有許多特點和要注意的地方。

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

示例1-驅動+應用

首先在驅動程序分配一頁大小的記憶體,然後用戶進程通過mmap()將用戶空間中大小也為一頁的記憶體映射到內核空間這頁記憶體上。映射完成後,驅動程序往這段記憶體寫10個字節數據,用戶進程將這些數據顯示出來。

view plainprint?

#include<linux/miscdevice.h>

#include<linux/delay.h>

#include<linux/kernel.h>

#include<linux/module.h>

#include<linux/init.h>

#include<linux/mm.h>

#include<linux/fs.h>

#include<linux/types.h>

#include<linux/delay.h>

#include<linux/moduleparam.h>

#include<linux/slab.h>

#include<linux/errno.h>

#include<linux/ioctl.h>

#include<linux/cdev.h>

#include<linux/string.h>

#include<linux/list.h>

#include<linux/pci.h>

#include<linux/gpio.h>

#defineDEVICE_NAME”mymap”

staticunsignedchararray[10]={0,1,2,3,4,5,6,7,8,9};

staticunsignedchar*buffer;

staticintmy_open(structinode*inode,structfile*file)

{

return0;

}

staticintmy_map(structfile*filp,structvm_area_struct*vma)

{

unsignedlongpage;

unsignedchari;

unsignedlongstart=(unsignedlong)vma->vm_start;

//unsignedlongend=(unsignedlong)vma->vm_end;

unsignedlongsize=(unsignedlong)(vma->vm_end-vma->vm_start);

//得到物理地址

page=virt_to_phys(buffer);

//將用戶空間的一個vma虛擬記憶體區映射到以page開始的一段連續物理頁面上

if(remap_pfn_range(vma,start,page>>PAGE_SHIFT,size,PAGE_SHARED))//第三個參數是頁幀號,由物理地址右移PAGE_SHIFT得到

return-1;

//往該記憶體寫10字節數據

for(i=0;i<10;i++)

buffer[i]=array[i];

return0;

}

staticstructfile_operationsdev_fops={

.owner=THIS_MODULE,

.open=my_open,

.mmap=my_map,

};

staticstructmiscdevicemisc={

.minor=MISC_DYNAMIC_MINOR,

.name=DEVICE_NAME,

.fops=&dev_fops,

};

staticint__initdev_init(void)

{

intret;

//註冊混雜設備

ret=misc_register(&misc);

//記憶體分配

buffer=(unsignedchar*)kmalloc(PAGE_SIZE,GFP_KERNEL);

//將該段記憶體設置為保留

SetPageReserved(virt_to_page(buffer));

returnret;

}

staticvoid__exitdev_exit(void)

{

//註銷設備

misc_deregister(&misc);

//清除保留

ClearPageReserved(virt_to_page(buffer));

//釋放記憶體

kfree(buffer);

}

module_init(dev_init);

module_exit(dev_exit);

MODULE_LICENSE(“GPL”);

MODULE_AUTHOR(“[email protected]”);

應用程序:

view plainprint?

#include<unistd.h>

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

#include<fcntl.h>

#include<linux/fb.h>

#include<sys/mman.h>

#include<sys/ioctl.h>

#definePAGE_SIZE4096

intmain(intargc,char*argv[])

{

intfd;

inti;

unsignedchar*p_map;

//打開設備

fd=open(“/dev/mymap”,O_RDWR);

if(fd<0)

{

printf(“openfail\n”);

exit(1);

}

//記憶體映射

p_map=(unsignedchar*)mmap(0,PAGE_SIZE,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

if(p_map==MAP_FAILED)

{

printf(“mmapfail\n”);

gotohere;

}

//列印映射後的記憶體中的前10個字節內容

for(i=0;i<10;i++)

printf(“%d\n”,p_map[i]);

here:

munmap(p_map,PAGE_SIZE);

return0;

}

示例2-進程間共享記憶體

UNIX訪問文件的傳統方法是用open打開它們, 如果有多個進程訪問同一個文件, 則每一個進程在自己的地址空間都包含有該文件的副本,這不必要地浪費了存儲空間。 下圖說明了兩個進程同時讀一個文件的同一頁的情形。 系統要將該頁從磁盤讀到高速緩沖區中, 每個進程再執行一個存儲器內的復制操作將數據從高速緩沖區讀到自己的地址空間。

CPU體系架構-image/os_mm/mmap_open.png

現在考慮另一種處理方法共享存儲映射: 進程A和進程B都將該頁映射到自己的地址空間, 當進程A第一次訪問該頁中的數據時, 它生成一個缺頁中斷. 內核此時讀入這一頁到記憶體並更新頁表使之指向它.以後, 當進程B訪問同一頁面而出現缺頁中斷時, 該頁已經在記憶體, 內核只需要將進程B的頁表登記項指向次頁即可. 如下圖所示:

CPU體系架構-image/os_mm/mmap_open.png

下面就是進程A和B共享記憶體的示例。兩個程序映射同一個文件到自己的地址空間, 進程A先運行, 每隔兩秒讀取映射區域, 看是否發生變化。進程B後運行, 它修改映射區域, 然後退出, 此時進程A能夠觀察到存儲映射區的變化。

進程A的代碼:

view plainprint?

#include<sys/mman.h>

#include<sys/stat.h>

#include<fcntl.h>

#include<stdio.h>

#include<stdlib.h>

#include<unistd.h>

#include<error.h>

#defineBUF_SIZE100

intmain(intargc,char**argv)

{

intfd,nread,i;

structstatsb;

char*mapped,buf[BUF_SIZE];

for(i=0;i<BUF_SIZE;i++){

buf[i]=’#’;

}

/*打開文件*/

if((fd=open(argv[1],O_RDWR))<0){

perror(“open”);

}

/*獲取文件的屬性*/

if((fstat(fd,&sb))==-1){

perror(“fstat”);

}

/*將文件映射至進程的地址空間*/

if((mapped=(char*)mmap(NULL,sb.st_size,PROT_READ|

PROT_WRITE,MAP_SHARED,fd,0))==(void*)-1){

perror(“mmap”);

}

/*文件已在記憶體,關閉文件也可以操縱記憶體*/

close(fd);

/*每隔兩秒查看存儲映射區是否被修改*/

while(1){

printf(“%s\n”,mapped);

sleep(2);

}

return0;

}

進程B的代碼:

view plainprint?

#include<sys/mman.h>

#include<sys/stat.h>

#include<fcntl.h>

#include<stdio.h>

#include<stdlib.h>

#include<unistd.h>

#include<error.h>

#defineBUF_SIZE100

intmain(intargc,char**argv)

{

intfd,nread,i;

structstatsb;

char*mapped,buf[BUF_SIZE];

for(i=0;i<BUF_SIZE;i++){

buf[i]=’#’;

}

/*打開文件*/

if((fd=open(argv[1],O_RDWR))<0){

perror(“open”);

}

/*獲取文件的屬性*/

if((fstat(fd,&sb))==-1){

perror(“fstat”);

}

/*私有文件映射將無法修改文件*/

if((mapped=(char*)mmap(NULL,sb.st_size,PROT_READ|

PROT_WRITE,MAP_PRIVATE,fd,0))==(void*)-1){

perror(“mmap”);

}

/*映射完後,關閉文件也可以操縱記憶體*/

close(fd);

/*修改一個字符*/

mapped[20]=’9′;

return0;

}

示例3-匿名映射做到父子進程通信

view plainprint?

#include<sys/mman.h>

#include<stdio.h>

#include<stdlib.h>

#include<unistd.h>

#defineBUF_SIZE100

intmain(intargc,char**argv)

{

char*p_map;

/*匿名映射,創建一塊記憶體供父子進程通信*/

p_map=(char*)mmap(NULL,BUF_SIZE,PROT_READ|PROT_WRITE,

MAP_SHARED|MAP_ANONYMOUS,-1,0);

if(fork()==0){

sleep(1);

printf(“childgotamessage:%s\n”,p_map);

sprintf(p_map,”%s”,”hi,dad,thisisson”);

munmap(p_map,BUF_SIZE);//實際上,進程終止時,會自動解除映射。

exit(0);

}

sprintf(p_map,”%s”,”hi,thisisfather”);

sleep(2);

printf(“parentgotamessage:%s\n”,p_map);

return0;

}

mmap進行記憶體映射的原理

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

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

假如vm_area_struct描述的是一個文件映射的虛存空間,成員vm_file便指向被映射的文件的file結構,vm_pgoff是該虛存空間起始地址在vm_file文件裡面的文件偏移,單位為物理頁面。mmap系統調用所完成的工作就是準備這樣一段虛存空間,並建立vm_area_struct結構體,將其傳給具體的設備驅動程序.

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

建立文件映射的第二步就是建立虛擬地址和具體的物理地址之間的映射,這是通過修改進程頁表來做到的。mmap方法是file_opeartions結構的成員:int (*mmap)(struct file *,struct vm_area_struct *);

linux有2個方法建立頁表:

使用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)。

使用nopage VMA方法每次建立一個頁表項。struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);

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

當實際訪問新映射的頁面時的操作(由缺頁中斷完成)page cache及swap cache中頁面的區分:一個被訪問文件的物理頁面都駐留在page cache或swap cache中,一個頁面的所有信息由struct page來描述。struct page中有一個域為指針mapping ,它指向一個struct address_space類型結構。page cache或swap cache中的所有頁面就是根據address_space結構以及一個偏移量來區分的。

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

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

對於共享記憶體映射情況,缺頁異常處理程序首先在swap cache中尋找目標頁(符合address_space以及偏移量的物理頁),如果找到,則直接返回地址;如果沒有找到,則判斷該頁是否在交換區 (swap area),如果在,則執行一個換入操作;如果上述兩種情況都不滿足,處理程序將分配新的物理頁面,並把它插入到page cache中。進程最終將更新進程頁表。 註:對於映射普通文件情況(非共享映射),缺頁異常處理程序首先會在page cache中根據address_space以及數據偏移量尋找相應的頁面。如果沒有找到,則說明文件數據還沒有讀入記憶體,處理程序會從磁盤讀入相應的頁面,並返回相應地址,同時,進程頁表也會更新.

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