mmap详谈
简述:
mmap函数将文件系统内的文件或者是Posix共享内存对象映射到调用进程的地址空间。
用途:
1.对普通文件使用mmap提供内存映射I/O,以避免系统调用(read、write、lseek)带来的性能开销。同时减少了数据在内核缓冲区和进程地址空间的拷贝次数。
2.使用特殊文件提供匿名内存映射。
3.使用shm_open以提供无亲缘关系进程间的Posix共享内存区。
接口说明:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
mmap返回成功后,fd文件描述符可以关闭且不会对内存映射产生影响。
对内存区的保护由prot参数指定:
PROT_EXEC:数据可执行
PROT_READ:数据可读
PROT_WRITE:数据可写
PROT_NONE:数据不可访问
对映射内存区的操作标志由flags指定:
MAP_SHARED:对内存区数据操作的变动是共享的。即所有映射到该内存地址的进程都能收到该数据的变动。
MAP_PRIVATE:对内存区数据的操作进行写时复制。(注:仅仅在内存中有多一份拷贝,文件并不受到影响)
mmap函数通过文件描述符定位到文件位置再到用户地址空间:
int munmap(void *addr, size_t len);
从某个进程的地址空间删除一个内存映射关系。len是映射区的大小。
如果被映射区是使用MAP_PRIVATE标志映射的,那么调用进程对它的所有变动都不会写回文件,并且被丢弃。
细节:
1.考虑不同的进程调用mmap内存映射同一个文件(前提设置了MAP_SHARED),那么某个进程对这个内存映射的修改是否会影响其他进程的内存映射。
//orgin.c用于打印内存映射区内的值 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mman.h> int main() { int fd = open("./testfile", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR); int num = 0; write(fd, &num, sizeof(int)); int *ptr = mmap(NULL, sizeof(int), PROT_WRITE, MAP_PRIVATE, fd, 0); sleep(10); printf("%d\n", *ptr); }
//mod.c用于修改内存映射区内的数据 #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mman.h> int main() { int fd = open("./testfile", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR); int num = 0; write(fd, &num, sizeof(int)); int *ptr = mmap(NULL, sizeof(int), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); printf("%d\n", ++(*ptr)); }
结论:
当不同的进程映射同一个文件的时候,内存映射区其实变为内核为各个进程维护的共享内存区。各个进程对内存映射区的修改都会写回文件,并影响其他进程在该共享内存上的值。
2.内存分配策略以页为单位(假设一页为4096个字节)。考虑内存映射空间和文件大小都为5000个字节的时候,文件映射的读写会如何。
#include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <sys/mman.h> #include <unistd.h> int main() { int fileSize = 5000; int mapSize = 5000; long pageSize; int fd = open("./testfile", O_RDWR|O_CREAT|O_TRUNC, S_IRUSR|S_IRUSR); lseek(fd, fileSize-1, SEEK_SET); write(fd, "", 1); char *ptr = mmap(NULL, mapSize, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); pageSize = sysconf(_SC_PAGESIZE); int i; for (i = 0; i < 5000; i+=pageSize) { //打印每个页的首字节内容,并置为1 printf("ptr[%d] = %d\n", i, ptr[i]); ptr[i] = 1; //打印每个页的尾字节内容,并置为1 printf("ptr[%d] = %d\n", i + pageSize - 1, ptr[i+pageSize-1]); ptr[i+pageSize-1] = 1; } return 0; }
输出结果:
ptr[0] = 0 ptr[4095] = 0 ptr[4096] = 0 ptr[8191] = 0
使用命令:od -b(以八进制形式查看内容) -A b(以十进制输出地址) testfile 查看地址对应的内容。
结果:
内存映射其实分配了2个页的大小(第一个页0~4095字节,第二个页4096~8191字节),对于在这两个页范围内的读和写操作都不会导致“Segmentation Fault”,但对在5000~8191字节范围内的写操作不会写回文件中(因为文件大小为5000字节)。对于超过8191字节(即第三个页)的读写将导致段错误。
结论:
在内存分配的页范围内的读写不会导致段错误,在文件大小范围内的写操作才会写回文件。
3.进程采用mmap操作文件内容一定比read\write快吗?
讨论这个问题前先看进程采用系统条用read/write方式读写文件过程。
数据先从文件系统复制到内核缓冲区(最小单位为一个页大小)---->复制数据到用户的进程空间;此过程中如果进程的下一次读操作的数据依然在内核缓冲区的话内核不会再从文件系统中读取,而是将数据从内核缓冲区直接复制给进程。
后续对文件的操作还需要其他的系统调用如lseek\write。进程将多次在内核态和用户态之间切换。而mmap则只需一次系统调用,其后操作都在用户态上。
由此可知:
对于小文件的读写操作,内核完全有可能直接从内核缓冲区读写数据并不见得会比mmap慢,但是对于大文件且频繁读写的操作,mmap会比read\write要快。
完。