IPC之Posix内存映射文件详解

来源:互联网 发布:公务员网络培训学院 编辑:程序博客网 时间:2024/06/06 13:05
1.什么是内存映射文件

  内存映射文件,就是把磁盘上的物理文件映射至进程地址空间中,使用内存映射文件的特性是,所有的I/O都是在内核掩盖下完成,我们只需编写存取内存映射区中各个值的代码,也就是不需要调用read/write/lseek。

如图示:

  


2.内存映射文件与read文件时物理内存占用区别
  需要了解物理内存和进程空间地址的映射关系。
  可以通过 top -p $pid 查看进程使用的虚拟内存与物理内存
  VIRT:虚拟内存 KB
  RES:物理内存 KB
  也可以通过cat /proc/$pid/status 查看进程使用的虚拟内存与物理内存
  VmHWM:虚拟内存 KB
  VmRSS:物理内存 KB

  调用read函数时,会将文件内容读取至物理内存中,并映射至进程空间地址。
  调用内存映射文件,并不需要马上将文件内容拷贝至物理内存,由缺页中断处理。
  假设用read函数读取了一个32MB的文件,会发现进程的虚拟内存和物理内存大小马上增加了32MB,
  而用内存映射文件,进程的物理内存并不会马上增加32MB。

3.什么是缺页中断
  进程线性地址空间里的页面不必常驻内存,在执行一条指令时,如果发现他要访问的页没有在内存中,那么停止该指令的执行,并产生一个页不存在异常,对应的故障处理程序可通过从外存加载该页到内存的方法来排除故障,之后,原先引起的异常的指令就可以继续执行,而不再产生异常。

4.内存映射文件与read/write文件的效率对比(建议抱着怀疑的心态阅读此段)
  当调用read函数时,先要经过系统调用,把文件内容拷贝至内核的高速缓存区(参考free命令的cache),然后再把数据从高速缓存区拷贝至物理内存,实际是两次数据拷贝。
  当调用write函数时,先把物理内存的数据拷贝至内核的高速缓存区(参考free命令的buffer),然后再把数据从高速缓存区拷贝至磁盘的物理文件,也是两次数据拷贝。

  内存映射文件可以减少这种不必要的数据拷贝。它由mmap()将文件直接映射到进程空间地址,mmap()并没有进行数据拷贝,真正的数据拷贝是在缺页中断处理时进行的,由于mmap()将文件直接映射到进程空间地址,所以中断处理函数根据这个映射关系,直接将文件从硬盘拷贝到进程空间地址,所以只进行了一次数据拷贝,比read进行两次数据拷贝要少了一次,同理write也一样,因此,内存映射的效率要比read/write效率高(这里考虑的是磁盘与进程交互数据的效率,实际上如果文件很小而内存够大,且文件内容基本上不更改,那么应该用read将文件加载至内存而不是用mmap)。

5.内存映射文件的用途

a) 文件很大,内存没办法一次性装下,但是又需要频繁读写的文件,比如索引文件。

b) 配合同步(比如信号量),为无亲缘关系的进程提供共享的内存映射。

c) 父子进程之间共享内存区的方法之一是。父进程在调用fork前先指定MAP_SHARD调用mmap。Posix.1保证父进程中的内存映射关系存留到子进程。而且父进程所作的修改子进程能看到,反过来也一样。


6.内存映射文件需要注意的细节

/* mmap函数把一个文件或者一个Posix共享内存区对象映射至调用进程的地址空间 */

MMAP(2)                    Linux Programmer's Manual                   MMAP(2)NAME       mmap, munmap - map or unmap files or devices into memorySYNOPSIS       #include <sys/mman.h>       void *mmap(void *addr, size_t length, int prot, int flags,                  int fd, off_t offset);       See NOTES for information on feature test macro requirements.

返回:若成功则为被映射区的起始地址,若出错则为MAP_FAILED


其中addr可以指定描述符fd应被映射到进程内空间的起始地址,它通常被指定为一个空指针,这样告诉内核自己去选择起始地址,无论哪种情况下,该函数的返回值都是描述符fd所映射到内存区的其实地址。这里需要注意的是,文件需要初始化长度,否则对内存操作时会产生SIGBUS信息(硬件错误)。

  

length是映射到调用进程地址空间中字节数,它从被映射文件开头offset个字节出开始算。offset通常设置为0。


内存映射区的保护由port参数指定,通常设置为PROT_READ | PROT_WRITE(可读与可写):

PORT_READ    -> 可读

PORT_WRITE  -> 可写

PORT_EXEC    -> 可执行

PORT_NONE   -> 数据不可访问


flags用于设置内存映射区的数据被修改时,是否改变其底层支撑对象(这里的对象是文件),MAP_SHAREDMAP_PRIVATE必须指定一个

MAP_SHARED  -> 变动是共享的

MAP_PRIVATE  -> 变动是私自的

MAP_FIXED        -> 准确的解析addr参数


举例:当flags设定为MAP_SHARED时,在内存中对文件的修改会同步到物理文件中,可通过less查看

flags设定为MAP_PRIVATE时,在内存中对文件的修改不会同步到物理文件中,可通过less查看


mmap成功返回后,fd参数可以关闭。该操作对由于mmap建立的映射关系没有影响。


/* 从某个进程空间删除一个映射关系 */

MMAP(2)                    Linux Programmer's Manual                   MMAP(2)NAME       mmap, munmap - map or unmap files or devices into memorySYNOPSIS       #include <sys/mman.h>       int munmap(void *addr, size_t length);       See NOTES for information on feature test macro requirements.
返回:若成功则为0,若出错则为-1


其中addr参数是由mmap返回的地址,len是映射区的大小。再次访问这些地址将导致向调用进程产生一个SIGSEGV信号。



/* 同步内存映射区与硬盘上的文件内容 */
MSYNC(2)                   Linux Programmer's Manual                  MSYNC(2)NAME       msync - synchronize a file with a memory mapSYNOPSIS       #include <sys/mman.h>       int msync(void *addr, size_t length, int flags);
返回:若成功则为0,如出错则为-1


内核的虚拟内存算法保持内存映射文件(在硬盘上)与内存映射区(在内存中)的同步,前提是它是一个MAP_SHARD内存区。就是说,如果我们修改了处于内存映射到某个文件的内存区中的某个位置的内容,那么内核将在稍后某个时刻相应地更新文件。然而有时候我们希望确信硬盘上的文件内容与内存映射区中的内容一致,于是调用msync来执行这种同步。


其中addrlength通常指代内存中的整个内存映射区,不过也可以指定该内存区的一个子集。

flags的说明:

MS_ASYNC            -> 执行异步写

MS_SYNC               -> 执行同步写

MS_INVALLDATE   -> 使高速缓存的数据失效


MS_ASYNCMS_SYNC必须指定一个,但是不能同时指定,它们的差别是,一旦写操作已由内核排队,MS_ASYNC即返回,而MS_SYNC则要等到写操作完成后才返回。 如果还指定了MS_INVALLDATE,那么与文件不一致的内存副本都会失效,后续引用从文件中获取数据。


需要注意内存页对齐:

命令getconf PAGE_SIZE可以查看当前系统的内存页大小,假设为4096字节。

假设物理文件是5000字节,对这个文件进行内存映射后,在内存中能访问的地址范围是0-8191(2个内存页,页对齐),但是如果对5000-8191地址的内容进行修改,并不会同步至物理文件,一旦访问8192地址或者以上的地址,就会引发SIGSEGV信号(无效内存引用)。


不是所有文件都能进行内存映射,例如,试图把一个访问终端或套接字的描述符映射到内存将导致mmap返回一个错误。


7.简单的内存映射文件例子

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>#include <sys/mman.h>#include <errno.h>#include <unistd.h>#define MAXSIZE 1024*1024*16   /* 文件大小,建议设置成内存页的整数倍 */#define FILEPATH "./mmap.test" /* 文件路径 */int main(int argc, char const *argv[]){    /* 创建映射文件,并初始化长度 */    int fd = open(FILEPATH, O_CREAT | O_TRUNC | O_RDWR, 0777);    if (fd == -1) {        perror("open failed:");        exit(1);    }    printf("open %s success\n", FILEPATH);    char* buff = malloc(MAXSIZE);    bzero(buff, MAXSIZE);    if (write(fd, buff, MAXSIZE) != MAXSIZE) {        printf("init file failed\n");        exit(1);    }    free(buff);    /* 建立映射关系 */    char *ptr = (char*)mmap(NULL, MAXSIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);    if (ptr == MAP_FAILED) {        perror("mmap failed:");        exit(1);    }    printf("mmap %s success\n", FILEPATH);    /* 关闭文件 */    close(fd);    sleep(30);    printf("start write mmap file\n");    /* 写入内容  */    char *content = "hello world";    strncpy(ptr, content, strlen(content));    /* 强制同步到磁盘 */    if (msync(ptr, MAXSIZE, MS_SYNC) == -1) {        perror("msync failed:");    }    /* 休眠30s,通过less观察文件 */    printf("write mmap success\n");    sleep(30);    /* 关闭映射关系 */    if (munmap(ptr, MAXSIZE) == -1) {        perror("munmap failed:");    }    return 0;}

编译运行:



原文出自:http://blog.csdn.net/daiyudong2020/article/details/50493522

参考:《unix网络编程》·卷2


End;


0 0