1. 什么是存储映射IO
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间(内存)中的一个缓冲区相映射。这样的话,当从缓冲区中取数据,就相当于读文件中的相应的字节,而将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不使用read和write函数的情况下,使用地址(指针)完成I/O操作,当然也可以使用内存操作函数strcpy,memcpy等。
使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。
2. mmap函数介绍
2.1 mmap函数创建映射区
- 包含头文件
#include <sys/mman.h>
- 函数原型
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
void *mmap64(void *addr, size_t length, int prot, int flags, int fd, off64_t offset);
int munmap(void *addr, size_t length);
- 函数功能 mmap() creates a new mapping in the virtual address space of the calling process. The starting address for the new mapping is specified in addr. The length argument specifies the length of the mapping.
- 函数参数
- addr:建立映射区的首地址,由Linux内核指定,使用时直接传递NULL 。
- length:想要创建的映射区的大小 。
- prot:映射区权限
- PROT_READ:可读
- PROT_WRITE:可写(PROT_READ | PROT_WRITE 读写)
- PROT_EXEC:可执行
- PROT_NONE:不可用
- flags:标志位参数,常用于设定更新物理区域、设置共享、创建匿名映射区
- MAP_SHARED:共享的,会将映射区所做的操作反映到物理设备(磁盘)上。也就是说,对内存的修改会影响到源文件。
- MAP_PRIVATE:私有的,映射区所做的修改不会反映到物理设备(磁盘)。
- fd:用来建立映射区的文件描述符,映射区是从文件映射来的,所以创建映射区肯定要打开一个文件。
- offset:偏移量,映射文件的偏移(4k的整数倍)。
- 函数返回值
- On success, mmap() returns a pointer to the mapped area. 成功返回创建的映射区首地址。
- On error, the value MAP_FAILED (that is, (void *) -1) is returned, and errno is set appropriately. 失败返回一个宏MAP_FAILED,并设置errno。
2.2 munmap函数释放映射区
- 包含头文件
#include <sys/mman.h>
- 函数原型
int munmap(void *addr, size_t length);
- 函数功能 释放mmap创建的映射区(可以联想malloc/free, new/delete等内存管理函数对)。
- 函数参数
- addr:mmap的返回值(由mmap返回的映射区首地址)
- length:mmap创建的映射区的长度
- 函数返回值
- On success, munmap() returns 0.
- On failure returns -1, and errno is set (probably to EINVAL).
3. mmap函数用法示例及注意事项
代码语言:javascript复制/************************************************************
>File Name : mmap_test.c
>Author : Mindtechnist
>Company : Mindtechnist
>Create Time: 2022年05月22日 星期日 15时52分45秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
int main(int argc, char* argv[])
{
int fd = open("mem.txt", O_RDWR);
/*int fd = open("mem.txt", O_RDWR | O_CREAT | O_TRUNC, 0644); 创建文件并截断
ftruncate(fd, 8); 新创建文件必须进行扩展,大小不能为0*/
char* mem = mmap(NULL, 8, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
/*char* mem = mmap(NULL, 8, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);*/
if(mem == MAP_FAILED)
{
perror("mmap err");
return -1;
}
/*我们操作的是内存,但是这种操作会被同步到文件*/
strcpy(mem, "hello"); /*写入字符串,所以mmap()返回char*类型*/
int mret = munmap(mem, 8);
if(mret < 0)
{
perror("munmap err");
}
close(fd);
return 0;
}
mmap函数使用时的注意事项:
- 使用mmap()函数创建缓冲区返回的地址mem是多少,munmap()释放的时候传入的参数mem就是多少,不能更改,也就是说,要把mmap()返回的地址原模原样传进munmap()才能正常释放。(munmap传入的地址一定是mmap的返回地址,不能对地址指针进行 等位移操作。)
- mem.txt文件的大小对映射区操作也是有影响的,如果我们设置的映射缓冲区为len,而实际上的mem.txt文件大小大于len,假如说我们写入映射区的内容大于len,只要不超过文件大小,也是可以写进去的。如果写入的内容大于文件的长度,那么超出文件长度的部分就被截断。
- 文件偏移量必须为4K的整数倍。mmap()函数中,最后一个参数偏移量必须是4K的整数倍,这是操作系统的限制,文件最小门槛就是8个512,也就是4096。如果一个文件本身大小没有达到4K,那么它占用的空间大小也是4096。比如下面mem.txt实际上只有10字节大小,但是占据了8个512,这就证明文件最小占据4K空间。
- 映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭。这是因为,只要使用mmap()函数映射完缓冲区,那么这个映射区和文件之间的通道已经建立,即使提前关闭文件描述符,也不影响对映射区和文件的操作。
- 如果文件mem.txt的为0,也就是空文件,那么会报错“总线错误(核心已转储)”,所以映射时使用的文件大小不能是0(当映射文件大小为0时,不能创建映射区,用于映射的文件必须要有实际大小。mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的)。
- 创建映射区的过程中,隐含着一次对映射文件的读操作,所以open打开文件时,必须要有读权限。
- 当MAP_SHARED时,要求:映射区的权限应小于等于文件打开的权限(出于对映射区的保护,因为映射区的操作会影响文件)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
- mmap创建映射区时出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。
4. 使用mmap实现父子进程通信
父子等有血缘关系的进程之间也可以通过mmap()函数建立的映射区来完成通信,但相应的要在创建映射区的时候指定对应的标志位参数flags:MAP_PRIVATE(私有映射)表示父子进程各自独占映射区;MAP_SHARED(共享映射)表示父子进程共享映射区。
使用mmap来实现父子进程间通信的原理是,fork子进程是对读进程的复制,所以子进程也会复制父进程mmap得到的映射区地址等信息。所以使用mmap实现父子进程间通信,应该先使用父进程mmap映射文件,然后再fork创建子进程。
代码语言:javascript复制/************************************************************
>File Name : mmap_ipc.c
>Author : Mindtechnist
>Company : Mindtechnist
>Create Time: 2022年05月22日 星期日 18时32分24秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
int main(int argc, char* argv[])
{
int fd = open("mem.txt", O_RDWR);
int* mem = mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(mem == MAP_FAILED)
{
perror("mmao err");
return -1;
}
close(fd);
pid_t pid = fork();
if(pid == 0)
{
*mem = 666; /*写入整形数字,所以mmap()返回int*类型*/
printf("child mem: %dn", *mem);
sleep(3); /*等待父进程修改*/
printf("child mem: %dn", *mem);
}
if(pid > 0)
{
sleep(1); /*给子进程时间去修改映射区*/
printf("parent mem: %dn", *mem);
*mem = 777;
printf("parent mem: %dn", *mem);
wait(NULL); /*回收子进程*/
}
munmap(mem, 4);
return 0;
}
如果我们将MAP_SHARED改成MAP_PRIVATE,那么父进程改的是父进程的,子进程改的是子进程的,它们都是改的自己的映射区。所以,要想通信,必须用MAP_SHARED。
5. 匿名映射
通过上面的分析我们可以体会到,使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易且速度更快。但不足之处在于,每次创建映射区一定要依赖一个文件才能实现。每次为了创建映射区都要open一个临时文件,创建好了再通过unlink或close删除关闭掉,比较麻烦。 通过使用匿名映射可以解决这个问题,其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区,匿名映射的实现需要借助标志位参数flags来指定,使用MAP_ANONYMOUS (MAP_ANON)。
使用MAP_ANONYMOUS (MAP_ANON)
代码语言:javascript复制/************************************************************
>File Name : mmap_ipc2.c
>Author : Mindtechnist
>Company : Mindtechnist
>Create Time: 2022年05月22日 星期日 18时32分24秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
int main(int argc, char* argv[])
{
int* mem = mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);
if(mem == MAP_FAILED)
{
perror("mmap err");
return -1;
}
pid_t pid = fork();
if(pid == 0)
{
*mem = 666;
printf("child: %dn", *mem);
sleep(3);
printf("child: %dn", *mem);
}
if(pid > 0)
{
sleep(1);
printf("parent: %dn", *mem);
*mem = 777;
printf("parent: %dn", *mem);
wait(NULL);
}
munmap(mem, 4);
return 0;
}
需注意的是,MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏,在类Unix系统中如果没有该宏定义,可使用如下两步来完成匿名映射区的建立。
- fd = open("/dev/zero", O_RDWR);
- p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);
/dev/zero 文件号称聚宝盆,可以随意进行映射,没有大小限制;还有一个文件叫做 /dev/null 号称无底洞,任何数据都可以放入,无大小限制,但是放入的数据都消失了,不会占用磁盘空间,一般会把没用的错误信息重定向到该文件中。
6. 无血缘关系进程间通信
6.1 无血缘关系进程通信的原理
我们知道,父子进程间通信的原理是fork子进程的时候,子进程会复制得到父进程mmap映射得到的映射区首地址,所以父子进程都可以通过这个地址对映射区操作,从而实现通信。那么没有血缘关系的进程之间是如何实现通信的呢?虽然无血缘关系的两个进程都是在自己进程内mmap得到的映射区,是两个不同的内存块(父子进程通过fork复制得到映射区地址),但是这两个内存是通过通过同一个文件映射得到的。只要选择了MAP_SHARED参数,两个进程对自己的内存映射区的操作都会反映到同一个文件中,所以这个文件充当了桥梁的作用,两个进程也就实现了数据交换。
6.2 无血缘关系进程通信实例
一个进程写映射区
代码语言:javascript复制/************************************************************
>File Name : w_mmap.c
>Author : Mindtechnist
>Company : Mindtechnist
>Create Time: 2022年05月22日 星期日 20时42分52秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
typedef struct _str
{
int num;
char string[20];
}Str;
int main(int argc, char* argv[])
{
if(argc < 2)
{
printf("not found mapfilen");
return -1;
}
int fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0644); /*OTRUNC文件清0*/
int st_len = sizeof(Str);
ftruncate(fd, st_len); /*扩展文件和结构体一样大*/
Str* str = mmap(NULL, st_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); /*往映射区写入的内容是结构体,mmap()的返回值是一个缓冲区地址,所以要根据写入内容的类型来决定*/
if(str == MAP_FAILED)
{
perror("mmap err");
return -1;
}
int count = 1;
while(1)
{
str->num = count;
sprintf(str->string, "string-d", count );
sleep(1); /*每隔一秒修改一次映射区内容*/
}
munmap(str, st_len);
close(fd);
return 0;
}
一个进程读映射区
代码语言:javascript复制/************************************************************
>File Name : r_mmap.c
>Author : Mindtechnist
>Company : Mindtechnist
>Create Time: 2022年05月22日 星期日 20时42分47秒
************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>
typedef struct _str
{
int num;
char string[20];
}Str;
int main(int argc, char* argv[])
{
if(argc < 2)
{
printf("not found mapfilen");
return -1;
}
int fd = open(argv[1], O_RDWR | O_CREAT, 0644); /*O_TRUNC这里不能再阶段了,否则core dump*/
int st_len = sizeof(Str);
Str* str = mmap(NULL, st_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(str == MAP_FAILED)
{
perror("mmap err");
return -1;
}
while(1)
{
printf("num = d, string = %sn", str->num, str->string);
sleep(1);
}
munmap(str, st_len);
close(fd);
return 0;
}
同样先执行写进程,再执行读进程
同样可以多个进程去读,但是和FIFO不同的是,使用FIFO实现进程通信的时候,当多个进程去读的时候,被读进程1读走的数据就不会再被进程2读取了,因为FIFO是磁盘上的缓冲区。而mmap就可以被多个进程一块读,读到的数据都一样,因为mmap映射区是内存上的,不论哪个进程都可以读取数据。