mmap 基本使用


文章目录

  • mmap 基本使用
  • 简介
  • 小知识
  • 1. 正常系统调用写文件流程图
  • 2. mmap内存映射写文件流程图
  • 3. mmap函数说明
  • 3.1 头文件
  • 3.2 创建内存映射mmap
  • 3.3 释放内存映射
  • 4. 基础使用
  • 5. mmap的使用注意事项
  • 6. mmap父子进程间通信
  • 7. mmap无血缘关系的进程间通信
  • 8.匿名映射


简介

mmap主要用来做内存映射的,可以将虚拟内存和磁盘上的文件直接映射。正常来说我们在写文件读文件的时候是需要使用系统调用api来进行,比如说read/write,这两个系统调用读写文件的方式是需要进行两次拷贝的,从用户空间拷贝到内核空间,然后从内核空间再拷贝到磁盘,而mmap将文件的地址直接映射到虚拟内存,这样,我们直接往这个地址读/写内容,可以像操作malloc申请出来的空间地址一样,写到这个地址,内容就直接在文件中了,减少了一次拷贝,提高了效率。这样一个公共的内存区域,也可以用来进程间通信。linux的共享内存就是这样的。Android的binder也是这一个原理,所以在这里记录一下mmap的基础使用,以及各个参数的含义,对学习进程间通信以及Android的binder有一定的帮助。

小知识

只要带系统,基本上都会将实际的物理内存映射成虚拟内存(这个玩意儿叫MMU),一是方便管理,二是安全,这也是为什么我们理论上32位程序都可以访问到4G的地址空间的原因,如果都是物理地址,那每个程序对应的地址就是确定的,个人理解,有用就看看

1. 正常系统调用写文件流程图

Android mmap 存储文件后如何读取 安卓打开mmap文件_内存映射

我们调用write函数时,由于用户空间和系统空间是隔离的,另外就是我们程序中的地址都是虚拟地址,没有办法直接将内容写到文件中的,而是先写到内核缓冲区,然后再由内核写到文件中,进行了两次拷贝。

2. mmap内存映射写文件流程图

Android mmap 存储文件后如何读取 安卓打开mmap文件_内存映射_02

使用mmap进行映射后,我们会得到一个和磁盘上某一个文件的地址相同的地址,当我们往这个地址写文件的时候,内容将直接写到文件中,这里和上面相比减少了一次拷贝

3. mmap函数说明

3.1 头文件
#include <sys/mman.h>
3.2 创建内存映射mmap
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
3.3 释放内存映射
int munmap(void *addr, size_t length);

参数说明:
       addr: 被映射的首地址
       length: 映射的长度
返回值: 0:成功  -1:失败

4. 基础使用

#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>

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的文件

Android mmap 存储文件后如何读取 安卓打开mmap文件_linux_03

我们直接用cat命令进行输出:

Android mmap 存储文件后如何读取 安卓打开mmap文件_#include_04

我们会发现其实是和程序输出的一样的,到这里,基本使用就结束了。

5. mmap的使用注意事项

  1. 能使用创建出来的新文件进行映射吗?
    答案:能,但是需要修改文件的大小,如果不修改则会出现总线错误,程序如下:
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>

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

Android mmap 存储文件后如何读取 安卓打开mmap文件_文件大小_05

Android mmap 存储文件后如何读取 安卓打开mmap文件_文件大小_06

所以结论就是:创建映射区的文件大小为0,而指定的大小非零的时候会出现总线错误

  1. 创建映射区的文件大小为0,实际指定映射区的大小为0
    得到的结果:无效的参数
  2. 如果打开文件时flag为O_RDONLY,mmap时PROT参数为PROT_READ|PROT_WRITE会怎样?
    得到的结果:无效的参数
  3. 如果打开文件时flag为O_RDONLY(新文件不行,需要一个有文件大小的文件),mmap时PROT参数为PROT_READ会怎样?
    得到的结果:在写数据的时候段错误
  4. 如果打开文件时flag为O_WRONLY(新文件不行,需要一个有文件大小的文件),mmap时PROT参数为PROT_WRITE会怎样?
    得到的结果:没有权限,mmap在创建的时候需要读权限,mmap的读写权限应该小于等于文件的打开权限,文件至少必须要有读权限。(前提是MAP_SHARED 模式下)
  5. 文件描述符fd,在mmap创建映射区完成即可关闭,后续访问文件,用地址访问。
  6. 如果offset是1000会怎么样?
    得到的结果:无效的参数,必须是4K的整数倍(这个跟MMU有关,MMU映射的最小单位就是4K)
  7. 对mmap越界操作会怎样?
    得到的结果:段错误,mmap映射以页为单位,就是说得到的空间的大小是4096的倍数,举个例子就是你申请了10个字节,但系统会给你申请4096,因为不够一页(4k),如果你申请4097,那么会给你申请两个页,所以才会发现你申请10个空间却能写如20个或者4096以下的字节数也不会崩溃的原因。
  8. 对mmap++是否还能munmap成功
    得到的结果:不能,无效的参数,首地址变了,munmap必须释放申请的地址

6. mmap父子进程间通信

#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>


// 全局变量 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;
}

结果:

Android mmap 存储文件后如何读取 安卓打开mmap文件_linux_07

结果发现p指向的地址的内容改掉了,而var没有被改掉(对于父子进程共享的东西是读共享,写复制)

7. mmap无血缘关系的进程间通信

写进程,循环写这个结构体大小的数据到共享内存

#include <stdio.h>
 #include <sys/mman.h>
 #include <fcntl.h>
 #include <unistd.h>
 #include <sys/types.h>
 #include <string.h>
 #include <stdlib.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 
 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 <stdio.h>
 #include <sys/mman.h>
 #include <fcntl.h>
 #include <unistd.h>
 #include <sys/types.h>
 #include <string.h>
 #include <stdlib.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 
 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;
 }

一个读端一个写端执行结果如下:

Android mmap 存储文件后如何读取 安卓打开mmap文件_内存映射_08

一个写端多个读端执行结果如下:

Android mmap 存储文件后如何读取 安卓打开mmap文件_#include_09

多个写端一个读端:

Android mmap 存储文件后如何读取 安卓打开mmap文件_内存映射_10

8.匿名映射

前面我们每次使用共享内存时,都会创建一个文件,这样会造成垃圾文件,接下来我们使用unlink把创建的文件删除掉,创建完就删除这个文件:unlink(文件名)

#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>


// 全局变量 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 <stdio.h>
 #include <sys/mman.h>
 #include <fcntl.h>
 #include <unistd.h>
 #include <sys/types.h>
 #include <string.h>
 #include <stdlib.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 
 
 // 全局变量 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 <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>


// 全局变量 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;
}