Linux的内存映射之mmap

共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据: 一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

1.函数介绍
1.1 mmap
在Linux中内存映射需要用到mmap函数,其函数原型如下:

void * mmap (void *addr,size_t len,int prot,int flags,int fd,off_t offset);

返回值:成功返回映射到的内存区的起始地址;失败返回-1。

其参数介绍如下:

  • addr:要映射的起始地址,通常指定为NULL,让内核自动选择;
  • len:映射到进程地址空间的字节数;
  • prot:映射区保护方式,其可选择项如下:
    PROT_NONE:无权限;
    PROT_READ:读权限;
    PROT_WRITE:写权限;
    PROT_EXEC:执行权限;
  • flags:描述了映射的类型,其可选择项如下:
    MAP_FIXED:开启这个选项,则 addr 参数指定的地址是作为必须而不是建议;
    MAP_SHARED:多个进程对同一个文件的映射是共享的,一个进程对映射的内存做了修改,另一个进程也会看到这种变化;
    MAP_ANONYMOUS:匿名映射,映射区不与任何文件关联;
    MAP_PRIVATE:多个进程对同一个文件的映射不是共享的,一个进程对映射的内存做了修改,另一个进程并不会看到这种变化,也不会真的写到文件中去;
    MAP_NORESERVE:不要为这个映射保留交换空间,当交换空间被保留,对映射区修改的可能会得到保证;当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号;
    MAP_LOCKED:锁定映射区的页面,从而防止页面被交换出内存;
    MAP_GROWSDOWN :用于堆栈,告诉内核VM系统,映射区可以向下扩展;
  • fd:文件描述符;
  • offset:从文件头开始的偏移量,必须是页大小的整数倍(在32位体系统结构上通常是4K);

1.2 msync
Linux系统中将进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中需要用到msync函数,其函数原型如下:

int  msync(void *addr,size_t len,int flags)

返回值:成功返回0,失败返回-1。

其参数介绍如下:

  • addr:内存映射的起始地址,即mmap函数返回值;
  • len:映射到进程地址空间的字节数,跟mmap的len参数一致;
  • flags:设置标志位,其可选择项如下:
    MS_ASYNC:不管映射区是否更新,直接冲洗返回;
    MS_SYNC:如果映射区更新了,则冲洗返回,如果映射区没有更新,则等待,知道更新完毕,就冲洗返回;
    MS_INVALIDATE:丢弃映射区中和原文件相同的部分;

1.3 munmap
Linux中mmap函数用来建立映射关系,munmap函数则是用来解除映射关系,其函数原型如下:

int munmap( void * addr, size_t len );

返回值:成功返回0,失败返回-1。

其参数介绍如下:

  • addr:内存映射的起始地址,即mmap函数返回值;
  • len:映射到进程地址空间的字节数,跟mmap的len参数一致;

2 内存映射的应用
2.1 共享映射访问或写改文件
源码如下:

#include <error.h> 
#include <fcntl.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <sys/mman.h>  
#include <sys/stat.h>  
  
int main(int argc, char **argv)  
{  
    /* 打开文件 */  
    int fd;
    if ((fd = open(argv[1], O_RDWR)) < 0) {  
        perror("open");  
    }  
  
    /* 获取文件的属性 */  
    struct stat sb; 
    if ((fstat(fd, &sb)) == -1) {  
        perror("fstat");  
    }  
  
    /* 将文件映射至进程的地址空间 */
    char *mapped;  
    if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ |   
                    PROT_WRITE, MAP_SHARED, fd, 0)) == (void *)-1) {  
        perror("mmap");  
    }  
  
    /* 映射完后, 关闭文件也可以操纵内存 */  
    close(fd);  
  
    printf("%s", mapped);  
  
    /* 修改一个字符,同步到磁盘文件 */  
    mapped[1] = '9';  
    if ((msync((void *)mapped, sb.st_size, MS_SYNC)) == -1) {  
        perror("msync");  
    }  
  
    /* 释放存储映射区 */  
    if ((munmap((void *)mapped, sb.st_size)) == -1) {  
        perror("munmap");  
    }  
  
    return 0;  
}  

2.2 共享映射实现进程通讯
进程A源码:

#include <error.h>  
#include <fcntl.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <sys/mman.h>  
#include <sys/stat.h>  
  
  
int main(int argc, char **argv)  
{      
    /* 打开文件 */  
    int fd;
    if ((fd = open(argv[1], O_RDWR)) < 0) {  
        perror("open");  
    }  
  
    /* 获取文件的属性 */  
    struct stat sb;  
    if ((fstat(fd, &sb)) == -1) {  
        perror("fstat");  
    }  
  
    /* 将文件映射至进程的地址空间 */ 
    char *mapped;   
    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);  
    }  
  
    return 0;  
}  

进程B源码:

#include <error.h>  
#include <fcntl.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>
#include <sys/mman.h>  
#include <sys/stat.h>   

int main(int argc, char **argv)  
{  
    /* 打开文件 */  
    int fd; 
    if ((fd = open(argv[1], O_RDWR)) < 0) {  
        perror("open");  
    }  
  
    /* 获取文件的属性 */  
    struct stat sb;  
    if ((fstat(fd, &sb)) == -1) {  
        perror("fstat");  
    }  
  
    /* 私有文件映射将无法修改文件 */  
    char *mapped;  
    if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ |   
                    PROT_WRITE, MAP_SHARED, fd, 0)) == (void *)-1) {  
        perror("mmap");  
    }  
  
    /* 映射完后, 关闭文件也可以操纵内存 */  
    close(fd);  
  
    /* 修改一个字符 */  
    mapped[1] = '9';  
    
    return 0;  
}  

先运行进程A,每隔2秒打印内存映射的文件数据内容;启动进程B,通过内存映射同一个文件并修改其数据内容,观察进程A打印的数据信息是否发送变化。

2.3 匿名映射实现父子进程通信
函数源码如下:

#include <sys/mman.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
  
#define BUF_SIZE 100  
  
int main(int argc, char** argv)  
{  
    /* 匿名映射,创建一块内存供父子进程通信 */  
    char    *p_map = (char *)mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE,  
            MAP_SHARED | MAP_ANONYMOUS, -1, 0);  
  
    if(fork() == 0) {  
        sleep(1);  
        printf("child got a message: %s\n", p_map);  
        sprintf(p_map, "%s", "hi, dad, this is son");  
        munmap(p_map, BUF_SIZE); //实际上,进程终止时,会自动解除映射。  
        exit(0);  
    }  
  
    sprintf(p_map, "%s", "hi, this is father");  
    sleep(2);  
    printf("parent got a message: %s\n", p_map);  
  
    return 0;  
}  

版权声明:本文为u010304442原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。