mmap原理及使用 - lixiao-c/Daily_learning GitHub Wiki
mmap函数会将文件或者设备映射到内存的地址空间上,编程时,可使用open+mmap+memset+msync+munmap代替open+write/read+close进行文件访问,这样避免了频繁的使用系统调用。除此之外,mmap在PM编程中也是重要的访问手段,还可以用来实现共享内存、写保护的处理、userfault等。其函数原型如下:
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
- addr是程序员指定映射到内存地址,但mmap不一定会精确的映射到该地址,只是会尽量映射地址附近的位置。addr一般为NULL,此时系统会选择一块地址进行映射。
- length是映射的长度
- prot是为映射内存设置的权限
- flags可以决定该映射是否被多个进程共享,是否匿名映射等等
- fd是映射的后端文件的文件描述符
- offset是映射文件的开始偏移量
- 返回值返回映射的开始地址
下面具体解释prot和flags的用法
- PROT
prot可以为映射内存设置权限,主要有一下四种权限:
PROT_READ 内存页是可读的
PROT_WRITE 内存页是可写的
PROT_EXEC 内存是可执行的
PROT_NONE 内存是不可访问的
这几种设置可以进行OR,例如设置为PROT_READ|PROT_WRITE。
- FLAGS
FLAGS列举一下几种:
MAP_SHARED 与其他进程共享这片内存映射(映射统一文件区域),对该区域的更新其他进程也可见
MAP_PRIVATE 对该区域的更新,其他进程不可见。
MAP_ANONYMOUS 映射匿名页,不需要后端的文件(fd=-1),映射的内存初始化为0。
MAP_POPULATE 在调用mmap映射时,就将真实的数据映射到内存上(read ahead),访问时不会产生缺页。
下文将介绍几个mmap的使用情形。
[1]http://man7.org/linux/man-pages/man2/mmap.2.html [2]http://blog.decaywood.me/2017/04/10/Linux-mmap/
在默认使用mmap时,mmap并不会真的将文件的内容拷贝到内存中,而是等到读取相应内存时,触发缺页中断,才会读取文件。当使用MAP_POPULATE的标志时,mmap则会一开始就将文件内容拷贝到内存中,在读取时就不会有缺页的现象了。
对此,有研究表明使用mmap读取文件的速度并不一定比传统的read快,甚至性能还略有下降。在使用Ext4-DAX的系统中,以4K大小顺序读4G文件,最后花费的时间如下图:
造成这种情况的原因主要是频繁的用户态与内核态的切换。每次读4k,在使用默认的mmap时,几乎每一次都会缺页,造成用户态和内核态的频繁切换,如果是更细粒度的读,估计这种情况会大大缓解。
在参考文献中的论文中,提出来mmap-ahead的方式,仿照read-ahead进行预取,节省状态切换的时间,不失为一种好的解决办法。
[1]Jungsik Choi, Jiwon Kim, Hwansoo Han,"Efficient Memory Mapped File I/O for In-Memory File Systems"
PM编程一般可使用mmap,先对PM上的数据进行内存映射,然后根据映射的地址进行数据的操作。但是由于PM设备与mem都使用DIMM接口和内存总线,mmap并不真正将PM内容copy到DRAM上,只是建立映射关系(利用页管理方式)。一般的编程方式如下:
1. addr=mmap(NULL,LEN,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
2. memcpy(addr,...);
memset(addr,...);
*(addr+i)=...;
3. _mm_clflush(addr);
_mfence();
在对数据操作完成后,需要进行持久化,可以使用原有的msync()接口,但是该接口不能充分发挥PM的性能优势,所以一般使用clflush命令,该命令会将cache中的一条cache line写回PM中,mfence()是内存屏障,由于cache的刷回操作一般是乱序的,在进行其他操作之前,使用mfence保证在其之前的store都已完成。
在pm上使用mmap的代码参见 IO_PM_interface_flush.c
[1]Andy Rudoff. "Persistent Memory Programming".
使用MAP_SHARED可以在多个进程之间共享映射的内存区域,并可以借助共享内存实现进程间通信。使用mmap映射同一文件后端,可以实现进程间的内存共享。
receive.c
addr=mmap(NULL,LEN,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
while(1){
if(*(addr)=='c'){
printf("get message from another process\n");
return;
}
printf("addr value %x \n",*(addr));
}
send.c
addr=mmap(NULL,LEN,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
*(addr)='c';
具体代码可参见mmap/receive.c、 mmap/send.c。 也可以对匿名映射使用共享内存,但应该使用fork生成子进程,在父子进程之间进行共享内存通信。
(参考文章:http://blog.jcix.top/2018-10-26/mmap_tests/ )
mmap可以在一定程度上取代write/read,成为操作文件的接口,但相比之下,mmap无法像write一样改变文件大小。同时由于mmap采用了地址映射,经常会出现段错误和总线错误。在本小节中探究造成两种错误的原因。
段错误
“文件范围内, mmap范围外”,会产生SIGSEGV段错误。当我们映射的区域小于文件大小时,访问mmap范围外会触发段错误。这种情况很容易理解,因为我们访问了非法的内存区域。
此外,当mmap时,PORT只设置为PROT_READ,会对mmap区域加上写保护,当写mmap映射区域时,也会触发段错误。如果一开始没加写保护,还可以使用mprotect()修改内存保护位。
总线错误
”文件范围外,mmap范围内” 会产生SIGBUS总线错误。如前文所说,mmap不能修改文件大小,因此,当mmap映射区域大于文件范围,向文件范围外进行写操作时,会首先触发缺页,在处理缺页时,检查到写的区域超过了文件大小,抛出总线错误。
对于稀疏文件(具体可查看参考文章内容),对稀疏文件内部的文件空洞进行访问不会造成总线错误。
当使用mmap函数时,我们会设置‘port’、‘flags’等参数,其中port参数会设置mmap内存的保护信息,一般的port会设置为PROT_READ|PROT_WRITE,表示读写都可以进行,当只设置PROT_READ时,便为内存构建了写保护。此时对映射的内存区域进行写操作,便会触发经典错误:段错误(segment fault)。但是我们可以根据信号机制,对触发write protection区域进行处理。
信号是进程间通信的一种方式,又称为软中断,因此和中断类似,信号也有自己的处理函数。处理方法可以分为三类:第一种是类似中断的处理程序,对于需要处理的信号,进程可以指定处理函数,由该函数来处理。第二种方法是,忽略某个信号,对该信号不做任何处理,就象未发生过一样。第三种方法是,对该信号的处理保留系统的默认值,这种缺省操作,对大部分的信号的缺省操作是使得进程终止。
当访问写保护内存时,会发出SIGSEGV信号,即“无效的内存引用”。该信号没有默认的处理函数,所以出发后会抛出段错误,进程终止,我们可以为信号设置相应的处理函数。
sigaction函数可以改变信号的处理,即“ examine and change a signal action ”。函数原型如下:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
因此,我们需要构造一个sigaction结构体,该结构体原型如下:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
};
sa_handler和sa_sigaction都是处理函数,具体使用可以参考man sigaction,处理写保护时,需要设置sa_sigaction。利用以下代码进行信号处理注册。
//registe write pro handler
struct sigaction sa;
sa.sa_flags=SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sa.sa_sigaction=handler;
if(sigaction(SIGSEGV,&sa,NULL)==-1)
printf("error\n");
这样,当我们访问写保护地址时,便会转到handler函数处理。在代码中(write_protect&ufd/write_protect.c),简单实现了一个handler函数,打印访问的地址。
[1]sigaction. http://man7.org/linux/man-pages/man2/sigaction.2.html [2]mprotect. http://man7.org/linux/man-pages/man2/mprotect.2.html