所谓内存映射指的是 让一个磁盘文件与内存中的一个缓冲区相映射,进程访问这块内存时,就等同于访问文件对应映射部分,不必再调用 read / write 。
我们可以使用mmap函数来建立内存和文件某一部分的映射关系。
目录
一、共享内存映射的创建 / 释放
1、创建共享内存映射:mmap
2、释放共享内存映射:munmap
二、mmap使用及其注意事项
1、mmap使用
(1) 打开文件
(2) mmap建立映射
(3) 通过映射向文件写入内容
(4) 完整代码
2、mmap使用注意事项
(1) 映射区只要建立成功,文件可以立即关闭
(2) 映射的文件大小必须大于0,否则会报总线错误
(3) 文件偏移量必须为0或者4K的整数倍
(4) 映射空间大小可以大于文件大小,但要注意不要访问区外部分
(5) 设置的映射空间大小 ≠ 实际分配的映射空间大小
一、共享内存映射的创建 / 释放
1、创建共享内存映射:mmap
mmap 函数的作用是创建共享内存映射。mmap函数的参数较多,几乎每一个参数都有注意事项,下面介绍的重点是mmap函数的参数。
(1) addr
用户可以手动指定要映射的内存地址,一般设置为NULL,让OS自动选择合适的内存地址,如果最后映射建立成功,mmap会返回内存中映射区的首地址。
(2) length
为内存中映射地址空间分配的字节数(length > 0)。这里分为了两种情况:
- length > 文件映射部分大小
- length < 文件大小
当 length < 文件映射部分大小 时,文件有一部分无法映射到内存。
当 length > 文件映射部分大小 时,有一部分无法映射到文件,这就意味着,即便向这部分内存写入内容,也不会反馈给文件。
因此,一般建议设置的映射空间大小直接和文件大小保持一致。文件大小的计算可以使用lseek函数。
// 起始偏移量为0,将文件指针移动到末尾(SEEK_END)
// 返回的结果就是 文件指针相对于起始位置的字节数
int size = lseek(fd, 0, SEEK_END);
(3) prot
指定内存映射空间的访问权限。其实就是要以何种形式来访问这块映射空间,如可读、可写、可执行等,可选值如下:
可选值 | 含义 |
PROT_READ | 可读 |
PROT_WRITE | 可写 |
PROT_EXEC | 可执行 |
PROT_NONE | 不可访问 |
(4) flags
指定内存映射空间的映射方式。可以是共享,代表其他进程可以看到;可以是私有,代表其他进程看不到;也可以是匿名,一般用于有血缘关系之间的进程。可选值如下:
可选值 | 含义 |
MAP_SHARED | 共享 |
MAP_PRIVATE | 私有 |
MAP_ANONYMOUS | 匿名 |
注意: MAP_SHARED 和 MAP_PRIVATE必选其一
(5) fd
指定要映射的文件。当一个文件被成功打开的时候,就会有一个文件描述符来唯一的指向该文件。如果是匿名映射,填 -1
问:为什么建立内存映射需要打开文件?
答:在创建映射区的过程中,隐藏着一次对映射文件的读操作,目的是将部分文件内容读取到映射区。(读取多少,取决于偏移量的设置)
(6) offset
表示映射文件的偏移量。设置为0表示从头开始映射。当你向映射区写入内容的时候,其实是向文件的起始映射位置开始写入,而不是从文件起始位置开始写入。
映射部分:向映射区写入的内容,会映射到文件的对应部分
剩余部分:内存是以页为单位进行分配的,一页大小是4K,映射区的大小如果小于4K,剩下没有映射的就是“剩余部分”,访问该部分的时候,不会有任何问题,因为这块内存还在一页的范围里。但是你向该部分写入的内容,不会映射到文件,因为在文件里不存在对应的映射区域。
区外部分:该部分是内存没有分配给你的,访问这部分会报总线错误。
(6) 返回值
映射创建成功,返回创建映射区的首地址,失败返回 MAP_FAILED(((void *) -1)),设置errno值
2、释放共享内存映射:munmap
释放共享内存映射的时候,需要提供映射区的首地址,以及你手动分配的大小,注意这里要填的不是实际分配的大小,而是 mmap 第二个参数填入的值。
二、mmap使用及其注意事项
1、mmap使用
(1) 打开文件
映射文件必须以读写 (O_RDWR) 的形式打开,同时该文件的大小必须要大于0,否则会出现
int fd = open("./log.txt", O_RDWR);
if(fd < 0)
{
perror("open");
return -1;
}
(2) mmap建立映射
针对mmap 的各个参数,设置如下:
- 第一个参数设为NULL,交由OS来分配内存
- 第二个参数设为文件大小
- 第三个参数设为可写 PROT_WRITE
- 第四个参数设为共享 MAP_SHARED
- 第五个参数填入上面得到的文件描述符
- 第六个参数设为0,代表从文件起始位置开始映射
int len = lseek(fd, 0, SEEK_END);
void* addr = mmap(NULL, len, PROT_WRITE, MAP_SHARED, fd, 0);
if(addr == MAP_FAILED)
{
perror("mmap");
return -1;
}
(3) 通过映射向文件写入内容
我们向内存映射空间中写入一个字符串,然后看一下文件中是否有对应的字符串
const char* str = "hello, world";
memcpy(addr, str, strlen(str));
(4) 完整代码
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(){
int fd = open("./log.txt", O_RDWR);
if(fd < 0)
{
perror("open");
return -1;
}
int len = lseek(fd, 0, SEEK_END);
void* addr = mmap(NULL, len, PROT_WRITE, MAP_SHARED, fd, 0);
if(addr == MAP_FAILED)
{
perror("mmap");
return -1;
}
const char* str = "hello, world";
memcpy(addr, str, strlen(str));
return 0;
}
2、mmap使用注意事项
(1) 映射区只要建立成功,文件可以立即关闭
在创建映射区的过程中,隐藏着一次对映射文件的读操作,目的是将部分文件内容读取到映射区。映射建立成功也就意味着读写完毕,此时只需要向内存映射中写入即可。
(2) 映射的文件大小必须大于0,否则会报总线错误
如果映射文件的大小为0,那就说明我们无论怎么向映射里写入内容,都不会反馈到文件中,那就失去了映射的意义。
(3) 文件偏移量必须为0或者4K的整数倍
(4) 映射空间大小可以大于文件大小,但要注意不要访问区外部分
映射空间的大小可以大于文件大小,只是有一部分空间无法映射到文件,向这部分空间写入的内容不会反馈给文件。
(5) 设置的映射空间大小 ≠ 实际分配的映射空间大小
实际在分配内存的时候,是以页为单位进行分配的,每页大小是4K,实际大小会根据文件大小来定。并不是你设置了多少,内存就给你分配多少。
如果文件大小是2000字节,即便mmap第二个参数填入的是6000,映射空间的大小就是 4K,此时需要注意,超出4K部分属于区外部分,访问区外部分会报总线错误。
如果文件大小是6000字节,大于了一页的大小,此时就会分配两页大小,映射空间大小就是8K。