Skip to content

存储映射区

存储映射区可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作。

它的本质是将一个磁盘文件与存储空间中的一个缓冲区相映射。从缓冲区中取数据,就相当于读文件中的相应字节;将数据写入缓冲区,则会将数据写入文件。

使用存储映射这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。

mmap函数

函数作用

建立存储映射区

函数原型

cpp
#include <sys/mman.h>

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

函数返回值

  • 成功:返回创建的映射区首地址;
  • 失败:MAP_FAILED

函数参数

  • addr:指定映射的起始地址,通常设为NULL,由系统指定
  • length:映射到内存的文件长度
  • prot:映射区的保护方式,最常用的:
    • 读:PROT_READ
    • 写:PROT_WRITE
    • 读写:PROT_READ | PROT_WRITE
  • flags:映射区的特性,可以是
    • MAP_SHARED:写入映射区的数据会写回文件,且允许其他映射该文件的进程共享。
    • MAP_PRIVATE:对映射区的写入操作会产生一个映射区的复制(copy-on-write),对此区域所做的修改不会写回原文件。
  • fd:由open返回的文件描述符,代表要映射的文件。
  • offset:以文件开始处的偏移量,必须是4k的整数倍,通常为0,表示从文件头开始映射。

使用mmap函数建立匿名映射:

cpp
mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

mmap使用总结

  • 第一个参数 写成NULL
  • 第二个参数 要映射的文件大小 > 0
  • 第三个参数:PROT_READPROT_WRITE
  • 第四个参数:MAP_SHARED 或者 MAP_PRIVATE
  • 第五个参数:打开的文件对应的文件描述符
  • 第六个参数:4k的整数倍

munmap函数

函数作用

释放由mmap函数建立的存储映射区

函数原型

cpp
#include <sys/mman.h>

int munmap(void* addr, size_t length);

函数返回值

  • 成功:返回0
  • 失败:返回-1,设置errno值

函数参数

  • addr:调用mmap函数成功返回的映射区首地址
  • length:映射区大小(mmap函数的第二个参数)

注意事项

  • 创建映射区的过程中,隐含着一次对映射文件的读操作,将文件内容读取到映射区
  • MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
  • 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭。
  • 特别注意,当映射文件大小为0时,不能创建映射区。所以,用于映射的文件必须要有实际大小;mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
  • munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
  • 文件偏移量必须为0或者4K的整数倍
  • mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。

相关思考题

  • 可以open的时候O_CREAT一个新文件来创建映射区吗?
  • 如果open时O_RDONLY,mmap时PROT参数指定PROT_READ|PROT_WRITE会怎样?
  • mmap映射完成之后,文件描述符关闭,对mmap映射有没有影响?
  • 如果文件偏移量为1000会怎样?
  • 对mem越界操作会怎样?
  • 如果mem++,munmap可否成功?
  • mmap什么情况下会调用失败?
  • 如果不检测mmap的返回值,会怎样?

练习

对文件的读写操作

先试用 shell 命令创建file1,然后向file1中写入hello五个字符;然后编写程序对 file1 文件建立 5 个字节的内存映射区,并读取映射区的内容,之后将其改为world五个字符。程序运行结束使用 cat 命令验证。

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

void argsCheck(int argc, const char *filename);

int main(int argc, char *argv[]) {
 argsCheck(argc, argv[1]);

 int fd = open(argv[1], O_RDWR);
 char *p = (char *) mmap(NULL, 5, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

 puts("内存映射区的内容为:");
 for (int i = 0; i < 5; ++i) {
     printf("%c", *(p + i));
 }
 puts("");

 for (int i = 0; i < 5; ++i) {
     sprintf(p, "world");
 }

 munmap(p, 5);
 close(fd);

 return 0;
}

void argsCheck(int argc, const char *filename) {
 if (argc != 2) {
     printf("args error.\n");
     _exit(-1);
 }
 struct stat fileStat;
 int ret = stat(filename, &fileStat);
 if (ret == -1) {
     perror("stat");
     _exit(-1);
 }
 if (fileStat.st_size == 0) {
     printf("If the mapping file size is 0, the mapping area cannot be created.\n");
     _exit(-1);
 }
}

父子进程间通信

图解说明

思路

  • 调用mmap函数创建存储映射区,返回映射区首地址ptr
  • 调用fork函数创建子进程,子进程也拥有了映射区首地址
  • 父子进程可以通过映射区首地址指针ptr完成通信
  • 调用munmap函数释放存储映射区
cpp
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
 int fd = open("file1", O_RDWR);
 if (fd == -1) {
     perror("open");
     return -1;
 }
 void *p = mmap(NULL, 4096, PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0);
 if (fork() == 0) {
     printf("%s\n", (char *) p);
 } else {
     sprintf(p, "Son, can you see my message?");
     wait(NULL);
     munmap(p, 4096);
     close(fd);
 }

 return 0;
}

程序输出:

shell
$ ./mmap1 
Son, can you see my message?

没有血缘关系的进程间通信

思路

  • 两个进程都打开相同的文件,然后调用mmap函数建立存储映射区,这样两个进程共享同一个存储映射区。

mmap2_.c

cpp
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
 int fd = open("file2", O_RDWR);
 if (fd == -1) {
     perror("open");
     return -1;
 }
 char *p = mmap(NULL, 4096, PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0);
 printf("%s", p);
 munmap(p, 4096);
 close(fd);

 return 0;
}

mmap2.c

cpp
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
 int fd = open("file2", O_RDWR);
 if (fd == -1) {
     perror("open");
     return -1;
 }
 char *p = mmap(NULL, 4096, PROT_WRITE | PROT_READ, MAP_SHARED, fd, 0);

 sprintf(p, "Hello process, can you see my message?\n");
 munmap(p, 4096);
 close(fd);

 execl("mmap2_", "./mmap2_", NULL);

 return 0;
}

程序输出:

shell
$ gcc mmap2_.c -o mmap2_
$ gcc mmap2.c -o mmap2
$ ./mmap2
Hello process, can you see my message?