引用:前面的系列文章介绍管道,本文介绍另外一种比较高效的进程间通讯方式——内存映射。
一、内存映射概述
内存映射(Memory-mapped I/O)使得一个磁盘文件与存储空间中的一个缓冲区相映射,相当于将磁盘文件的数据映射到内存中,用户通过修改内存就能修改磁盘文件。
于是当从缓冲区中取数据,就相当于读文件中的相应字节。以此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不使用 read 和 write 函数的情况下,使用地址(指针)完成 I/O 操作(通过内存操作函数完成I/O操作)。
内存映射也是进程间通讯的一种方式,而且效率比较高,因为它相当于直接对内存进行操作。其原理是把磁盘文件中的数据映射到内存当中,映射之后返回映射地址,在程序中就可以直接操作这块内存,操作过程中会把数据同步到磁盘文件中,这样可以实现进程间通讯。
二、内存映射 API
mmap 函数
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
功能:一个文件或者其它对象映射进内存中
参数:
addr : 指定映射的起始地址, 通常设为NULL, 由系统指定。
【补充】如果 addr 为 NULL,则内核会自行挑选一个页对齐的地址;
如果 addr 不为 NULL ,则内核只是将其作为一个提示。
length:映射到内存的文件长度,这个值不能为 0(即文件大小 > 0);建议直接使用文件的长度。
【补充】获取文件长度可通过 stat()、lseek() 等函数
prot: 映射区的保护方式(【注意】要操作映射内存,必须要有读的权限):
a) 读:PROT_READ
b) 写:PROT_WRITE
c) 读写:PROT_READ | PROT_WRITE
flags: 映射区的特性, 可以是
a) MAP_SHARED : 写入映射区的数据会复制回文件, 即映射区的数据会自动和磁盘文件同步;
且允许其他映射该文件的进程共享,所以进程间通信,必须要设置这个选项。
b) MAP_PRIVATE : 对映射区的写入操作会产生一个映射区的复制(copy - on - write),
对此区域所做的修改不会写回原文件,即映射区的数据会自动和磁盘文件不同步。
fd:由 open() 返回的文件描述符, 代表要映射的文件。注意点如下:
a) 文件的大小不能为 0;
b) open() 指定的权限不能和 prot 参数冲突(即映射区的权限 <= 文件打开的权限):
prot: PROT_READ open:只读/读写
prot: PROT_READ | PROT_WRITE open:读写
offset:以文件开始处的偏移量, 必须是4k的整数倍;
一般不用,所以通常为0, 表示从文件头开始映射(4k是页大小)
返回值:
成功:返回创建的映射区首地址
失败:MAP_FAILED宏
munmap 函数
#include <sys/mman.h>
int munmap(void *addr, size_t length);
功能:释放内存映射区
参数:
addr:使用 mmap 函数创建的映射区的首地址
length:映射区的大小,即要释放的内存的大小,要和mmap函数中的length参数的值一样。
返回值:
成功:0
失败:-1
API 使用注意事项
-
创建映射区的过程中,隐含着一次对映射文件的读操作。 -
当 MAP_SHARED 时,要求:映射区的权限 <= 文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为 mmap 中的权限是对内存的限制。 -
映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。 -
特别注意: 当映射文件大小为0时,不能创建映射区。所以用于映射的文件必须要有实际大小; mmap 函数使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。 -
munmap 函数传入的地址一定是 mmap 的返回地址;所以对于mmap 函数的返回值,建议不要对该指针进行 ++ 操作。如果确实需要这样做,需要保存 ++ 前的地址,这样在释放空间的时候,传入 ++ 前的地址才是正确释放空间。 -
文件偏移量必须为 4K 的整数倍,如果不是 4k 的整数倍,则函数调用出错,返回MAP_FAILED。 -
mmap 函数创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。
三、内存映射使用场景
内存映射实现进程间通信
(1)有关系的进程间通信
内存映射实现父子进程间通信
- 准备一个大小不是 0 的磁盘文件
- 还没有子进程的时候,通过唯一的父进程,先创建内存映射区
- 有了内存映射区以后,创建子进程
- 父子进程共享创建的内存映射区
- 【注意】内存映射区通信,是非阻塞。
参考示例:创建一个 test.txt 文件,并保证该文件大小大于 0。
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <wait.h>
int main()
{
// 1.打开一个文件
int fd = open("test.txt", O_RDWR);// 打开一个文件
int len = lseek(fd, 0, SEEK_END);//获取文件大小
// 2.创建内存映射区
void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED)
{
perror("mmap error");
exit(-1);
}
close(fd); //关闭文件
// 创建子进程
pid_t pid = fork();
if (pid == 0) //子进程
{
sleep(1); //保证父进程先执行
// 读数据
printf("%s\n", (char*)ptr);
}
else if (pid > 0) //父进程
{
// 写数据
strcpy((char*)ptr, "i am u father!!");
// 回收子进程资源
wait(NULL);
}
// 释放内存映射区
int ret = munmap(ptr, len);
if (ret == -1)
{
perror("munmap error");
exit(-1);
}
// 关闭文件
close(fd);
return 0;
}
运行结果:
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
i am u father!!
yxm@192:~$
(2)没有关系的进程间通信
内存映射实现不同进程间通讯
1. 准备一个大小不是 0 的磁盘文件
2. 进程 1 通过磁盘文件创建内存映射区,得到一个操作这块内存的指针
3. 进程 2 通过磁盘文件创建内存映射区,得到一个操作这块内存的指针。【注意】进程 1 与进程 2 是通过同一磁盘文件创建内存映射区的。
4. 使用内存映射区通信
5. 【注意】内存映射区通信,是非阻塞。
参考示例:创建一个 test.txt 文件,并保证该文件大小大于 0。
// write.c
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <wait.h>
int main(void)
{
int fd = -1;
int ret = -1;
pid_t pid = -1;
void *addr = NULL;
// 1 以读写的方式打开一个文件
fd = open("test.txt", O_RDWR);
if(-1 == fd)
{
perror("open");
return 1;
}
int len = lseek(fd, 0, SEEK_END);//获取文件大小
// 2 将文件映射到内存
addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED)
{
perror("mmap");
return 1;
}
printf("文件存储映射ok.....\n");
// 3 关闭文件
close(fd);
// 4 写入到存储映射区
memcpy(addr, "1234567890", 10);
// 5断开存储映射
munmap(addr, 1024);
return 0;
}
// read.c
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <wait.h>
int main(void)
{
int fd = -1;
int ret = -1;
pid_t pid = -1;
void *addr = NULL;
// 1 以读写的方式打开一个文件
fd = open("test.txt", O_RDWR);
if(-1 == fd)
{
perror("open");
return 1;
}
int len = lseek(fd, 0, SEEK_END);//获取文件大小
// 2 将文件映射到内存
addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED)
{
perror("mmap");
return 1;
}
printf("文件存储映射ok.....\n");
// 3 关闭文件
close(fd);
// 4 读存储映射区数据
printf("addr:%s\n", (char*)addr);
// 5断开存储映射
munmap(addr, 1024);
return 0;
}
运行结果:
yxm@192:~$ gcc write.c -o write
yxm@192:~$ gcc read.c -o read
yxm@192:~$ ./write
文件存储映射ok.....
yxm@192:~$ ./read
文件存储映射ok.....
addr:1234567890
yxm@192:~$
匿名映射实现父子进程通信
通过使用我们发现,使用内存映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个大小不为 0 的文件才能实现。
通常为了建立映射区要 open 一个 temp 文件,创建好了再 unlink、close 掉,比较麻烦。其实 Linux 系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区,这样可以直接使用匿名映射来代替前面提到的内存映射,【注意】匿名映射只能用于具有血缘关系的进程间通讯 。
匿名映射同样需要借助标志位参数 flags 来指定,使用 MAP_ANONYMOUS 或 MAP_ANON(MAP_ANON 已经被废弃) 特性即可实现。
int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
- 4 是随意举例,该位置表示映射区大小,可依实际需要填写。
- MAP_ANONYMOUS 和 MAP_ANON 这两个宏是Linux操作系统特有的宏。
程序示例:
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
// 创建匿名内存映射区
int len = 4096;
void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
if (ptr == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
// 创建子进程
pid_t pid = fork();
if (pid > 0) //父进程
{
// 写数据
strcpy((char*)ptr, "hello mike!!");
// 回收
wait(NULL);
}
else if (pid == 0)//子进程
{
sleep(1); //保证父进程先执行
// 读数据
printf("%s\n", (char*)ptr);
}
// 释放内存映射区
int ret = munmap(ptr, len);
if (ret == -1)
{
perror("munmap error");
exit(-1);
}
return 0;
}
运行结果:
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
hello, world
内存映射的方式操作文件
共享内存除了可以实现进程间通讯外,还可以实现文件操作。不过,很少有人使用内存映射的方式操作文件,此处只简单举例说明:
// 使用内存映射实现文件拷贝的功能
/*
思路:
1.对原始的文件进行内存映射
2.创建一个新文件(拓展该文件)
3.把新文件的数据映射到内存中
4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存中
5.释放资源
*/
#include <stdio.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
int main()
{
// 1.对原始的文件进行内存映射
int fd = open("english.txt", O_RDWR);
if(fd == -1)
{
perror("open");
exit(0);
}
// 获取原始文件的大小
int len = lseek(fd, 0, SEEK_END);
// 2.创建一个新文件(拓展该文件)
int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0664);
if(fd1 == -1)
{
perror("open");
exit(0);
}
// 对新创建的文件进行拓展
truncate("cpy.txt", len);
write(fd1, " ", 1);
// 3.分别做内存映射
void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
void * ptr1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
if(ptr1 == MAP_FAILED)
{
perror("mmap");
exit(0);
}
// 内存拷贝
memcpy(ptr1, ptr, len);
// 释放资源
munmap(ptr1, len);
munmap(ptr, len);
close(fd1);
close(fd);
return 0;
}
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
yxm@192:~$ ls -l
total 332
-rw-rw-r-- 1 yxm yxm 129772 Sep 6 02:45 cpy.txt
-rw-rw-r-- 1 yxm yxm 129772 Sep 6 02:44 english.txt
.....
-rwxrwxr-x 1 yxm yxm 9016 Sep 6 02:45 test
-rw-rw-r-- 1 yxm yxm 1546 Sep 6 02:44 test.c
四、内存映射注意事项
-
如果 open 时模式为 O_RDONLY,mmap 时 prot 参数指定 PROT_READ | PROT_WRITE 会怎样? 此时调用 mmap() 函数会出错:返回MAP_FAILED。 如果想要函数调用正常,open() 函数中的权限需要和 prot 参数的权限保持一致,权限不能和 prot 参数冲突。 -
mmap 什么情况下会调用失败?
- 第二个参数:length = 0
- 第三个参数:prot 参数只指定了写权限
- 第五个参数:fd 参数,open 指定的权限和 prot 参数有冲突。
-
可以open 的时候 O_CREAT 一个新文件来创建映射区吗? 可以的,但是创建的文件的大小如果为0的话,肯定不行,此时可以使用 lseek() truncate() 函数对新的文件进行扩展。 -
mmap 后关闭文件描述符,对 mmap 映射有没有影响? int fd = open(“XXX”); mmap(,fd,0); close(fd); 映射区还存在,创建映射区的fd被关闭,没有任何影响。 -
如果文件偏移量为 1000 会怎样? 偏移量必须是 4K 的整数倍,如果不是,则会报错并返回 MAP_FAILED
|