一 内存映射概述
从原理上讲,Linux系统利用已有的存储管理机制可以很自然的实现进程间的共享存储。对于一段物理存储空间,只需通过进程的虚存管理机构就可以映射到各自的3G用户地址空间中。通过这种映射,在不同进程看来“私有”的数据事实上是同一段内存单元,它们被这些不同的进程所共享。
在Linux系统实际运行时,内存中的页面要经常被换入或换出,共享存储区中的页面也不例外。一般而言,内存页面的换入/换出过程采用两种方式进行:
1. 普通页面因长时间未得到访问而被内核线程kswaps在系统空闲而得到调度时换出内存到磁盘上的页面交换区,或因为进程访问的页面不在内存引起缺页从而将曾被换出到页面交换区的页面重新换入。
2.针对某个被打开的磁盘文件在内存中的页缓冲,在内资源不足而需要增加空闲页面时,由内核线程bdflush在系统空闲而得到调度时按照LRU算法将“脏”页面写回磁盘文件以回收空闲页面(或由用户强制“刷出”页面),或者因进程所读文件某段数据不在内存而启动磁盘IO读文件数据到内存中的文件缓冲区。
方式1本身就是操作系统已实现的页调度机制,从用户角度来看,它是完全透明、不必额外关心的底层功能;而方式2依托某一种文件系统,需显式创建磁盘文件。因此该方式实现的页面交换功能对用户并不透明,需用户干涉。但从功能角度来看,它们却具有共同的本质:磁盘到物理内存之间的动态页面交换。
mmap系统调用并不是完全为了用于共享内存而设计的。它本身提供了不同于一般对普通文件的访问方式,进程可以像读写内存一样对普通文件的操作。而Posix或系统V的共享内存IPC则纯粹用于共享目的,当然mmap()实现共享内存也是其主要应用之一。
mmap系统调用使得进程之间通过映射同一个普通文件实现共享内存。普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read(),write()等操作。
二 内存映射的原理
“映射”主要是指 硬盘上文件 的位置与进程 逻辑地址空间中一块大小相同的区域之间的一一对应,如下图所示。这种对应关系纯属是逻辑上的概念,物理上是不存在的,原因是进程的逻辑地址空间本身就是不存在。在内存映射的过程中,并没有实际的数据拷贝,文件没有被载入内存,只是逻辑上被放入了内存,具体到代码,就是建立并初始化了相关的数据结构(struct address_space),这个过程有系统调用mmap()实现,所以建立内存映射的效率很高。
内存映射原理
既然建立内存映射没有进行实际的数据拷贝,那么进程又怎么能最终直接通过内存操作访问到硬盘上的文件呢?那就要看内存映射之后的几个相关的过程了。
mmap()会返回一个指针ptr,它指向进程逻辑地址空间中的一个地址,这样以后,进程无需再调用read或write对文件进行读写,而只需要通过ptr就能够操作文件。但是ptr所指向的是一个逻辑地址,要操作其中的数据,必须通过MMU将逻辑地址转换成物理地址,如上图过程2所示。
前面讲过,建立内存映射并没有实际拷贝数据,这时,MMU在地址映射表中是无法找到与ptr相对应的物理地址的,也就是MMU失败,将产生一个缺页中断,缺页中断的中断响应函数会在swap中寻找相对应的页面,如果找不到(也就是该文件从来没有被读入内存的情况),则会通过mmap()建立的映射关系,从硬盘上将文件读取到物理内存中,如上图过程3所示。
如果在拷贝数据时,发现物理内存不够用,则会通过虚拟内存机制(swap)将暂时不用的物理页面交换到硬盘上,如上图过程4所示。
在Linux系统中,设计mmap()系统调用的本意是提高文件操作的效率。通过mmap(),进程可以把一个文件的内容映射到它的虚存空间并以访问内存的方式实现文件的读写操作,这为文件读写提供了极大的方便;如果多个不同的进程通过这种方式映射同一个文件,则可以共享该文件对应的物理存储空间。 如下图所示:
注:在进程的地址空间中,栈的下方是内存映射段,内核直接将文件的内容映射到内存,通过虚存管理机构将进程中映射的地址转换为内存中的物理地址。
这恰恰完成了上述共享存储机制中的两个关键要素:底层的页面换入换出(磁盘文件->内存)功能以及物理存储(内存)到进程空间的映射,并最终实现了一种进程间通信的手段。这种共享存储的功能虽然是mmap()系统调用的“副产品”,但无论从功能角度还是从实现原理角度来看,它都具备共享存储的特征。
使用mmap对于设备文件,最大的优点就是用户空间可以直接访问设备内存;普通文件被映射到进程地址空间后,进程进程访问文件的速度也变快,不必再调read(),write()等系统调用,可以用memcpy,strcpy等操作写文件,写完后用msync()同步一下。mmap()的这种能力用于显示适配器一类的设备,屏幕帧的像素不再需要从一个用户空间到内核空间的复制过程。
从代码层面上看,从硬盘上将文件读入内存,都要经过文件系统进行数据拷贝,并且数据拷贝操作是由文件系统和硬件驱动实现的,理论上来说,拷贝数据的效率是一样的。但是通过内存映射的方法访问硬盘上的文件,效率要比read和write系统调用高,这是为什么呢?原因是read()是系统调用,其中进行了数据拷贝,它首先将文件内容从硬盘拷贝到内核空间的一个缓冲区,如下图过程1,然后再将这些数据拷贝到用户空间,如下图中过程2,在这个过程中,实际上完成了 两次数据拷贝 ;而mmap()也是系统调用,如前所述,mmap()中没有进行数据拷贝,真正的数据拷贝是在缺页中断处理时进行的,由于mmap()将文件直接映射到用户空间,所以中断处理函数根据这个映射关系,直接将文件从硬盘拷贝到用户空间,只进行了 一次数据拷贝 。因此,内存映射的效率要比read/write效率高。
read系统调用原理
小结:使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。
三、代码实现
1、mmap系统调用
#include <sys/mman.h>
void* mmap ( void * addr , size_t len , int
prot , int flags ,int fd , off_t offset );
内存映射函数mmap, 负责把文件内容映射到进程的虚拟内存空间, 通过对这段内存的读取和修改,来实现对文件的读取和修改,而不需要再调用read,write等操作。
参数含义:
addr: 指定映射的起始地址,
通常设为NULL, 由系统指定。
length: 映射到内存的文件长度,即可用访问的数据量
prot: 映射区的保护方式,它是下列常值的按位OR结果
PROT_EXEC: 映射区可被执行
PROT_READ: 映射区可被读取
PROT_WRITE: 映射区可被写入
PROT_NONE 映射区不可访问.
flags: 映射区的特性, 可以是: MAP_SHARED、MAP_PRIVATE、MAP_FIXED
MAP_SHARED:对此区域所做的修改内容奖写入文件内;允许其他映射该文件的进程共享,意思是:n个mmap.out程序在运行,这n个进程的“虚拟内存区域”的物理空间空间都相同。
MAP_PRIVATE:对此区域所做的修改不会更改原来的文件内容,对映射区的写入操作会产生一个映射区的复制(copy-on-write);意思是:n个mmap.out程序在运行,但是虚拟内存区域的物理地址会被内核另外分配。
fd: 由open返回的文件描述符, 代表要映射的文件。
offset: 以文件开始处的偏移量, 必须是分页大小的整数倍, 通常为0, 表示从文件头开始映射。
返回值:返回成功----函数的返回值为最后文件映射到进程空间的地址,进程可直接操作起始地址为该值的有效地址,该内存区域与可以通过一个打开的文件描述符访问的文件内容相关联。如果失败返回MAP_FAILED(-1),错误原因存于errno 中。
2、munmap 函数
int munmap(void *addr, size_t length);
用于取消参数addr所指的映射内存起始地址,参数length则是欲取消的内存大小。当进程结束或利用exec相关函数来执行其他程序时,映射内存会自动解除,但关闭对应的文件描述符时不会解除映射。
3、msync函数
int msync(const void *start, size_t length, int flags);
把在该内存段的某个部分的或整段中的修改写回到被映射的文件中(或者从被映射文件中读出)。
参数含义:
start 修改部分的起始地址,length为长度。
flags则有三个:
MS_ASYNC : 采用异步写方式,请Kernel快将资料写入,发出回写请求后立即返回。
MS_SYNC : 采用同步写范式,在msync结束返回前,将资料写入。
MS_INVALIDATE:通知使用该共享区域的进程,数据已经修改,在共享内容更改之后,使得文件的其他映射失效,从而使得共享该文件的其他进程去重新获取最新值。
实例:
#include <unistd.h>
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdlib.h>
typedef struct {
int integer;
char string[24];
}RECORD;
#define NRECORDS (100)
int main(int argc,char**argv)
{
RECORD record,*mapped;
int i,f;
FILE *fp;
//打开初始化文件
fp = fopen("record.dat","w+");
for (i=0;i<NRECORDS;i++)
{
record.integer = i;
sprintf(record.string,"RECORD-%d",i);
fwrite(&record,sizeof(record),1,fp);
}
fclose(fp);
//把第43条记录中的整数值由43修改为143,并把它写入第43条记录中的字符串
fp = fopen("record.dat","r+");
fseek(fp,43*sizeof(record),SEEK_SET);
fread(&record,sizeof(record),1,fp);
record.integer = 143;
sprintf(record.string,"RECORD-%d",record.integer);
fseek(fp,43*sizeof(record),SEEK_SET);
fwrite(&record,sizeof(record),1,fp);
fclose(fp);
//把这些记录映射到内存中,然后访问第43条记录,把它的整数值修改为243
f = open("record.dat",O_RDWR);
mapped = (RECORD*)mmap(0,NRECORDS*sizeof(record),PROT_READ|PROT_WRITE,MAP_SHARED,f,0);
mapped[43].integer = 243;
sprintf(mapped[43].string,"RECORD-%d",mapped[43].integer);
msync((void*)mapped,NRECORDS*sizeof(record),MS_ASYNC);
munmap((void*)mapped,NRECORDS*sizeof(record));
close(f);
exit(0);
return 0;
}
#include <unistd.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
int main(int argc,char**argv)
{
int fd; //文件描述符
char* mapped, *p;
int flength = 1024;
void *start_addr = 0;
if (argc < 2)
{
printf("argc less than 2\n");
exit(-1);
}
fd = open(argv[1],O_RDWR|O_CREAT,S_IRUSR|S_IWUSR);
flength = lseek(fd,1,SEEK_END);
write(fd,"\0",1); //在文件末尾添加一个空字符
lseek(fd,0,SEEK_SET);
mapped = mmap(start_addr,flength,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
printf("%s\n",mapped);
while((p=strstr(mapped,"test")))
{
memcpy(p,"map",3);
p+=3;
}
munmap((void*)mapped,flength);
close(fd);
return 0;
}
代码参见: https://github.com/ZhangzheBJUT/linux/tree/master/IPC
四 小结
在实际程序中大量运用了mmap,用到的正是mmap的这种“像访问普通内存一样对文件进行访问”的功能。实践证明,当要对一个文件频繁的进行访问,并且指针来回移动时,调用mmap比用常规的方法快很多。
参考:
http://blog.csdn.net/yinjiabin/article/details/7575653
Linux中mmap系统调用原理分析与实现