Linux C++ 内存映射文件及其应用
- 一. 原理
- 二. 应用场景
- 1. 实现多进程通信
- 2. 实现内存持久化
- 3.实现读写大文件
- 三. 问答
- 参考链接
一. 原理
内存映射文件,是从一个文件到一块内存的映射。
内存映射文件与虚拟内存有些类似,通过内存映射文件可以将文件与内存中一个虚拟地址块关联在一起。从而在应用(包括多个进程)可以直接对内存执行读取和写入操作,从而读写文件。
优势
- 效率高:
- 数据映射到内存进行操作(由OS负责在恰当时机将数据同步到磁盘)故效率高
- 直接将文件从硬盘拷贝到用户空间(没有拷贝到内核空间),只进行了一次数据拷贝
- 易用性:可以直接对内存执行读取和写入操作,从而读写文件,不必再对文件执行I/O操作。
- 按需映射:只将文件的部分内容映射到内存,在处理大数据量的文件时能起到相当重要的作用。
- 多映射共享:同一个文件可被多个进程映射到各自内存,各进程看到的内容一致,即该技术自身做到了“同步”。
二. 应用场景
1. 实现多进程通信
a.cpp
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>
#include <sys/stat.h>
#include <fcntl.h>
const int SIZE = 1 << 20; // 1M
const char* szFileName = "./testshm.txt";
int main()
{
int fd = open(szFileName, O_RDWR | O_CREAT);
ftruncate(fd, SIZE);
void *ptr = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED)
{
perror("mmap");
return -1;
}
memset(ptr, ' ', sizeof(char) * SIZE);
strcpy((char *)ptr, "hello, I am process a.");
munmap(ptr, SIZE);
return 0;
}
b.cpp
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>
#include <sys/stat.h>
#include <fcntl.h>
const char* szFileName = "./testshm.txt";
int main()
{
int fd = open(szFileName, O_RDWR);
int size = lseek(fd, 0, SEEK_END);
void *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
printf("read data: %s\n", ptr);
munmap(ptr, size);
return 0;
}
编译执行
[root@QingYun ~]# g++ a.cpp -o a
[root@QingYun ~]# g++ b.cpp -o b
[root@QingYun ~]# ./a
[root@QingYun ~]# ./b
read data: hello, I am process a.
[root@QingYun ~]#
2. 实现内存持久化
共享内存持久化的实现中有重要的两点:
- 共享内存映射的虚拟地址不是从0开始,而是从一个大地址开始(128M)
- 共享内存映射的地址是连续的,并且指定了MAP_FIXED参数
基于以上两点,可以实现 - 可以增量的扩展共享内存文件
- 映射出来连续的地址空间,从而实现可以通过记录号查找记录数据
示例代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
const int CNT = 8; //
const int SIZE = 1 << 20; // 1M
const int BASE = 1 << 27; // 基址=128M,注意,如果不指定一个较大的地址作为起始地址,会导致mmap映射失败!!
const char *szData = "hello, mmap.\n"; //
int main(int argc, char **argv) {
// 1. 打开共享内存
int fd = shm_open("testshm.dat", O_CREAT | O_RDWR, 0777);
if (fd < 0) {
printf("error open\n");
return 0;
}
// 2.增量映射共享内存
for (int i = 0; i < CNT; i++) {
// 2.1 扩增共享内存文件
ftruncate(fd, (i + 1) * SIZE);
// 2.2 映射扩增共享内存文件
void *lpShm = mmap((void *)(BASE + i * SIZE), SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, fd, SIZE * i);
if (lpShm == MAP_FAILED) {
printf("mmap error %p, errno=%d\n", lpShm, errno);
return -1;
}
else {
memset(lpShm, '0', SIZE * sizeof(char));
memcpy(lpShm, szData, sizeof(char) * strlen(szData));
printf("mmap success: i=%d, lpShm=%p\n", i, lpShm);
}
sleep(1);
}
szData = "hello, 12345678\n";
// 注意:只有映射地址连续,才能使用(BASE + SIZE*2 + 1000)的基址变址寻址操作,否则程序会崩溃!!!!!
memcpy((void*)(BASE + SIZE*2 + 1000), szData, sizeof(char) * strlen(szData));
// munmap(lpData, sizeof(char) * CNT * SIZE);
close(fd);
}
3.实现读写大文件
c.cpp
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>
#include <sys/stat.h>
#include <fcntl.h>
const int SIZE = 1 << 20; // 1M
const char* szFileName = "./testshm.txt";
int main()
{
int fd = open(szFileName, O_RDWR | O_CREAT);
ftruncate(fd, SIZE);
void *ptr = mmap(NULL, 0, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED)
{
perror("mmap");
return -1;
}
memset(ptr, 0, sizeof(char) * SIZE);
// 此处修改成写到 ptr + 4096
strcpy((char *)ptr + 4096, "hello, I am process a.");
munmap(ptr, SIZE);
return 0;
}
d.cpp
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <wait.h>
#include <sys/stat.h>
#include <fcntl.h>
const char *szFileName = "./testshm.txt";
int main()
{
int fd = open(szFileName, O_RDWR);
int size = lseek(fd, 0, SEEK_END);
// 此处修改成,指定从4096位置开始,读取16字节
void *ptr = mmap(NULL, 16, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 4096);
if (ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
char buf[1024] = {0};
snprintf(buf, 16, "%s", ptr);
printf("read data: %s\n", buf);
munmap(ptr, 16);
return 0;
}
三. 问答
1.如果对mmap的返回值(ptr)做++操作(ptr++),munmap是否能成功?
void* ptr=mmap(…); ptr++; 可以对齐进行++操作 munmap(ptr,len); //错误,要保持地址
2.如果open时O_RDONLY,mmap时prot参数指定PORT_READ | PORT_WRITE会怎样?
错误,返回MAP_FAILED open()函数中的权限建议和prot参数的权限保持一致。
3.如果文件偏移量为1000会怎样?
偏移量必须是页大小(通常为4k)的整数倍,返回MAP_FAILED
4.mmap什么情况下会调用失败?
第二个参数:length=0,报错:Invalid argument
第三个参数:prot指定的权限比如:PROT_WRITE,在第五个参数fd中不满足(open()使用O_RDONLY),报错:Permission denied
第六个参数:设置文件的偏移量不是页大小的整数倍,报错:Invalid argument
5.可以open的时候O_CREAT一个新文件来创建映射区吗?
可以的,但是创建文件的大小如果为0的话,肯定不行
可以对新的文件进行扩展
- lseek()
- truncate()
6.mmap后关闭文件描述符,对mmap映射有没有影响?
映射区还是存在,创建映射区的fd被关闭,没有任何影响。
7.对ptr越界操作会怎样?
void* ptr=mmap(NULL,100,…);
越界操作操作的时非法内存 -> 段错误
参考链接
- 操作系统 | 内存文件映射 —— 文件到内存的映射
- 共享内存之——mmap内存映射
- Linux的mmap 源码分析
- Linux C/C++内存映射