mmap主要用来做内存映射的,可以将虚拟内存和磁盘上的文件直接映射。正常来说我们在写文件读文件的时候是需要使用系统调用api来进行,比如说read/write,这两个系统调用读写文件的方式是需要进行两次拷贝的,从用户空间拷贝到内核空间,然后从内核空间再拷贝到磁盘,而mmap将文件的地址直接映射到虚拟内存,这样,我们直接往这个地址读/写内容,可以像操作malloc申请出来的空间地址一样,写到这个地址,内容就直接在文件中了,减少了一次拷贝,提高了效率。这样一个公共的内存区域,也可以用来进程间通信。linux的共享内存就是这样的。Android的binder也是这一个原理,所以在这里记录一下mmap的基础使用,以及各个参数的含义,对学习进程间通信以及Android的binder有一定的帮助。
只要带系统,基本上都会将实际的物理内存映射成虚拟内存(这个玩意儿叫MMU),一是方便管理,二是安全,这也是为什么我们理论上32位程序都可以访问到4G的地址空间的原因,如果都是物理地址,那每个程序对应的地址就是确定的,个人理解,有用就看看
我们调用write函数时,由于用户空间和系统空间是隔离的,另外就是我们程序中的地址都是虚拟地址,没有办法直接将内容写到文件中的,而是先写到内核缓冲区,然后再由内核写到文件中,进行了两次拷贝。
使用mmap进行映射后,我们会得到一个和磁盘上某一个文件的地址相同的地址,当我们往这个地址写文件的时候,内容将直接写到文件中,这里和上面相比减少了一次拷贝
#include
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
参数说明:
addr: 入参,如果这个地址为null那么内核将自己为你指定一个地址,如果不为null,将使用这个地址作为映射区的起始地址
length: 映射区的大小(<=文件的大小)
prot: 访问属性,一般用PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags:这个参数是确定映射的更新是否对映射相同区域的其他进程可见,以及是否对基础文件进行更新
MAP_SHARED: 共享此映射,映射的更新对映射相同区域的其他进程可见
MAP_PRIVATE: 创建写时专用拷贝映射,映射的更新对映射的其他进程不可见,相同的文件,并且不会传递到基 础文件。
我们一般用MAP_SHARED,这两个权限是限制内存的,而不限制文件
fd: 被映射的文件句柄
offset: 默认为0,表示映射文件全部。偏移未知,需要时4K的整数倍。
返回值:成功:被映射的首地址 失败:MAP_FAILED (void *)-1
int munmap(void *addr, size_t length);
参数说明:
addr: 被映射的首地址
length: 映射的长度
返回值: 0:成功 -1:失败
#include
#include
#include
#include
#include
#include
int main(int argc, const char *argv[])
{
char *p = NULL;
int fd = -1;
// 打开文件
fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
if (-1 == fd)
{
printf("文件打开失败...\n");
return -1;
}
// 因为我们文件不能是一个0大小的文件,所以我们需要修改文件的大小
// 有两种方式:leek,write,或者ftruncate都可以
/*
// 改变一个文件的读写指针
lseek(fd, 1023, SEEK_END);
// 写入一个结束符\0
write(fd, "\0", 1);
*/
// 我们还是用这种,比较方便,直接就修改了,和上面效果一样
ftruncate(fd, 1024);
// 创建一个内存映射,让内和指定一个映射地址,大小为1024,可读可写,共享,映射到这个fd上
p = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED)
{
printf("mmap failed\n");
close(fd);
return -1;
}
// 拿到地址之后我们就可以像操作普通地址一样写数据,读数据了,例如memcpy,strcpy等等
memcpy(p, "hello world", sizeof("hello world"));
// 读数据
printf("p = %s\n",p);
// 最后释放这个映射
if (munmap(p, 1024) == -1)
{
printf("munmap failed\n");
close(fd);
return -1;
}
close(fd);
return 0;
}
gcc mmap.c 进行编译
得到可执行文件a.out
./a.out 可以得到执行结果
p = hello world
然后看当前文件夹下会出现一个temp的文件
我们直接用cat命令进行输出:
我们会发现其实是和程序输出的一样的,到这里,基本使用就结束了。
能使用创建出来的新文件进行映射吗?
答案:能,但是需要修改文件的大小,如果不修改则会出现总线错误,程序如下:
#include
#include
#include
#include
#include
#include
int main(int argc, const char *argv[])
{
char *p = NULL;
int fd = -1;
// 打开文件
fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
if (-1 == fd)
{
printf("文件打开失败...\n");
return -1;
}
// 因为我们文件不能是一个0大小的文件,所以我们需要修改文件的大小
// 有两种方式:leek,write,或者ftruncate都可以
/*
// 改变一个文件的读写指针
lseek(fd, 1023, SEEK_END);
// 写入一个结束符\0
write(fd, "\0", 1);
*/
// 我们还是用这种,比较方便,直接就修改了,和上面效果一样
// TODO ftruncate(fd, 1024); // 主要修改了这行,我们不进行文件大小调整,那么文件大小就是0
// 创建一个内存映射,让内和指定一个映射地址,大小为1024,可读可写,共享,映射到这个fd上
p = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED)
{
printf("mmap failed\n");
close(fd);
return -1;
}
// 拿到地址之后我们就可以像操作普通地址一样写数据,读数据了,例如memcpy,strcpy等等
memcpy(p, "hello world", sizeof("hello world"));
// 读数据
printf("p = %s\n",p);
// 最后释放这个映射
if (munmap(p, 1024) == -1)
{
printf("munmap failed\n");
close(fd);
return -1;
}
close(fd);
return 0;
}
和基础使用例子一样,只是注释了修改文件大小的逻辑ftruncate(fd, 1024),这样新创建的文件大小就是0,
我们编译运行,如下图:Bus error
所以结论就是:创建映射区的文件大小为0,而指定的大小非零的时候会出现总线错误
创建映射区的文件大小为0,实际指定映射区的大小为0
得到的结果:无效的参数
如果打开文件时flag为O_RDONLY,mmap时PROT参数为PROT_READ|PROT_WRITE会怎样?
得到的结果:无效的参数
如果打开文件时flag为O_RDONLY(新文件不行,需要一个有文件大小的文件),mmap时PROT参数为PROT_READ会怎样?
得到的结果:在写数据的时候段错误
如果打开文件时flag为O_WRONLY(新文件不行,需要一个有文件大小的文件),mmap时PROT参数为PROT_WRITE会怎样?
得到的结果:没有权限,mmap在创建的时候需要读权限,mmap的读写权限应该小于等于文件的打开权限,文件至少必须要有读权限。(前提是MAP_SHARED 模式下)
文件描述符fd,在mmap创建映射区完成即可关闭,后续访问文件,用地址访问。
如果offset是1000会怎么样?
得到的结果:无效的参数,必须是4K的整数倍(这个跟MMU有关,MMU映射的最小单位就是4K)
对mmap越界操作会怎样?
得到的结果:段错误,mmap映射以页为单位,就是说得到的空间的大小是4096的倍数,举个例子就是你申请了10个字节,但系统会给你申请4096,因为不够一页(4k),如果你申请4097,那么会给你申请两个页,所以才会发现你申请10个空间却能写如20个或者4096以下的字节数也不会崩溃的原因。
对mmap++是否还能munmap成功
得到的结果:不能,无效的参数,首地址变了,munmap必须释放申请的地址
#include
#include
#include
#include
#include
#include
#include
#include
#include
// 全局变量 var
int var = 100;
int main(int argc, const char *argv[])
{
int *p;
pid_t pid;
int ret = 0;
int fd;
// 打开一个文件
fd = open("temp", O_RDWR|O_TRUNC, 0644);
if (fd < 0)
{
perror("open error");
exit(1);
}
// truncate文件大小
ftruncate(fd, 4);
// 创建映射区
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
// 关闭fd,mmap创建成功后就可以关闭了,因为直接使用地址了,不需要fd了
close(fd);
// fork一个进程
pid = fork();
if (pid == 0) // 子进程
{
*p = 2000;
var = 1000;
printf("child *p = %d, var = %d\n", *p, var);
}else{ // 父进程
sleep(1); // 休眠一秒,让子进程先执行
printf("parent *p = %d, var = %d\n", *p, var);
wait(NULL); // 回收子进程
// 释放共享内存
if (munmap(p, 4) == -1)
{
perror("munmap error");
exit(1);
}
}
return 0;
}
结果:
结果发现p指向的地址的内容改掉了,而var没有被改掉(对于父子进程共享的东西是读共享,写复制)
写进程,循环写这个结构体大小的数据到共享内存
#include
#include
#include
#include
#include
#include
#include
#include
#include
struct student{
int id;
char name[256];
int age;
};
int main(int argc, const char *argv[])
{
int fd;
struct student stu = {0, "zhangsan", 18};
struct student *p;
fd = open("temp", O_RDWR|O_CREAT|O_TRUNC, 0644);
if (fd < 0)
{
perror("open error");
exit(1);
}
ftruncate(fd, sizeof(stu));
p = mmap(NULL, sizeof(stu), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
close(fd);
while (1)
{
// 循环写
memcpy(p, &stu, sizeof(stu));
stu.id++;
sleep(3);
}
if (-1 == munmap(p, sizeof(stu)))
{
perror("munmap error");
exit(1);
}
return 0;
}
读进程,循环从共享内存中读
#include
#include
#include
#include
#include
#include
#include
#include
#include
struct student{
int id;
char name[256];
int age;
};
int main(int argc, const char *argv[])
{
int fd;
struct student stu = {0, "zhangsan", 18};
struct student *p;
fd = open("temp", O_RDONLY, 0644);
if (fd < 0)
{
perror("open error");
exit(1);
}
p = mmap(NULL, sizeof(stu), PROT_READ, MAP_SHARED, fd, 0);
if (p == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
close(fd);
while (1)
{
// 循环读
printf("stu.id = %d, stu.name = %s, stu.age = %d\n", p->id, p->name, p->age);
sleep(3);
}
if (-1 == munmap(p, sizeof(stu)))
{
perror("munmap error");
exit(1);
}
return 0;
}
一个读端一个写端执行结果如下:
一个写端多个读端执行结果如下:
多个写端一个读端:
前面我们每次使用共享内存时,都会创建一个文件,这样会造成垃圾文件,接下来我们使用unlink把创建的文件删除掉,创建完就删除这个文件:unlink(文件名)
#include
#include
#include
#include
#include
#include
#include
#include
#include
// 全局变量 var
int var = 100;
int main(int argc, const char *argv[])
{
int *p;
pid_t pid;
int ret = 0;
int fd;
// 打开一个文件
fd = open("temp", O_RDWR|O_TRUNC, 0644);
if (fd < 0)
{
perror("open error");
exit(1);
}
// TODO 添加了这句删除文件
ret = unlink("temp");
if (ret == -1)
{
perror("unlink error");
exit(1);
}
// truncate文件大小
ftruncate(fd, 4);
// 创建映射区
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if (p == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
// 关闭fd,mmap创建成功后就可以关闭了,因为直接使用地址了,不需要fd了
close(fd);
// fork一个进程
pid = fork();
if (pid == 0) // 子进程
{
*p = 2000;
var = 1000;
printf("child *p = %d, var = %d\n", *p, var);
}else{ // 父进程
sleep(1); // 休眠一秒,让子进程先执行
printf("parent *p = %d, var = %d\n", *p, var);
wait(NULL); // 回收子进程
// 释放共享内存
if (munmap(p, 4) == -1)
{
perror("munmap error");
exit(1);
}
}
return 0;
}
这样执行完成之后,那个临时文件就没了
又要open,又要unlink的好麻烦,有没有更方便的方法。答案是有的。可以直接使用匿名映射来代替,其实linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区,同样需要借助标志位flags来指定。
使用MAP_ANONYMOUS(或MAP_ANON),如:
int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
需要注意的是,MAP_ANONYMOUS和MAP_ANON这两个宏是linux操作系统中特有的,类UNIX系统中无该宏定义,可以使用如下两步来完成匿名映射区的建立
fd = open("/dev/zero", O_RDWR);
p = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, fd, 0);
linux匿名映射的例子如下:只能用于有血缘关系的进程间通信
#include
#include
#include
#include
#include
#include
#include
#include
#include
// 全局变量 var
int var = 100;
int main(int argc, const char *argv[])
{
int *p;
pid_t pid;
int ret = 0;
// 创建映射区-----TODO 匿名映射,大小随便指定,权限随便指定,fd用-1
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
if (p == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
// fork一个进程
pid = fork();
if (pid == 0) // 子进程
{
*p = 2000;
var = 1000;
printf("child *p = %d, var = %d\n", *p, var);
}else{ // 父进程
sleep(1); // 休眠一秒,让子进程先执行
printf("parent *p = %d, var = %d\n", *p, var);
wait(NULL); // 回收子进程
// 释放共享内存
if (munmap(p, 4) == -1)
{
perror("munmap error");
exit(1);
}
}
return 0;
}
类unix的例子
#include
#include
#include
#include
#include
#include
#include
#include
#include
// 全局变量 var
int var = 100;
int main(int argc, const char *argv[])
{
int *p;
pid_t pid;
int ret = 0;
int fd;
// 打开一个文件 TODO /dev/zero
fd = open("/dev/zero", O_RDWR|O_TRUNC, 0644);
if (fd < 0)
{
perror("open error");
exit(1);
}
if (ret == -1)
{
perror("unlink error");
exit(1);
}
// 创建映射区 flags 加 MAP_ANONYMOUS
p = (int *)mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, fd, 0);
if (p == MAP_FAILED)
{
perror("mmap error");
exit(1);
}
// 关闭fd,mmap创建成功后就可以关闭了,因为直接使用地址了,不需要fd了
close(fd);
// fork一个进程
pid = fork();
if (pid == 0) // 子进程
{
*p = 2000;
var = 1000;
printf("child *p = %d, var = %d\n", *p, var);
}else{ // 父进程
sleep(1); // 休眠一秒,让子进程先执行
printf("parent *p = %d, var = %d\n", *p, var);
wait(NULL); // 回收子进程
// 释放共享内存
if (munmap(p, 4) == -1)
{
perror("munmap error");
exit(1);
}
}
return 0;
}