【Linux应用编程】mmap内存映射

  linux操作系统采用虚拟内存管理技术,把内存空间分为用户空间和内核空间,用户空间由用户进程使用,用户进程无法直接访问内核空间,只能通过系统调用(软中断)或者硬中断间接访问。对于32位linux系统来说,系统物理内存最大寻址范围是2^32=4GB,用户空间分配的大小是3GB,地址范围是0x0——0xbfffffff;内核空间是1GB,地址范围是0xc0000000——0xffffffff。

在这里插入图片描述
  对于用户来说,直接访问到的是虚拟内存空间,这种虚拟地址的方式是现代操作系统中几乎都应用的,这样的好处是不言而喻的,不仅通过内存地址转换解决了多个进程访问内存冲突的问题,还提高系统的安全性。

  • 内存完整性,每个进程占用的虚拟内存是比实际物理内存要大(比如32位linux是3G),由于虚拟内存对进程的”欺骗”,每个进程都认为自己获取的内存是一块连续的地址,这样在编写应用程序时无需担心大块内存分配失败问题。

  • 安全性:进程访问内存时,通过通过页表来寻址,对于页表访问由操作系统管理各类访问权限,实现内存的权限控制。

  • 内存mmap,利用虚拟内存,可以将文件、设备物理地址映射到用户地址空间,用户进程可以直接访问,省去了用户空间和内核空间之间内存拷贝过程。

  • 内存swap,linux 系统引入swap分区,在可用物理内存不足时,将暂时不用的内存数据拷贝到存储介质(磁盘)上,让出内存让出给优先进程使用,进程使用完,再将原数据从磁盘加载到内存中。通过这种swap技术,linux系统可以让进程有“无限”内存可以用。


1. mmap

  mmap就是将文件映射到进程虚拟内存空间,用户程序操作这段内存空间达到读写文件的目的,而且提高访问效率。linux思想是一切接文件,因此系统的驱动设备的物理空间同样可以映射到用户内存空间。


1.1 mmap优势

  无论是普通文件还是设备文件吗,都是基于系统的虚拟文件系统接口,普通文件为了保护磁盘,避免频繁读写,还引入带缓冲页机制,通过read/write/ioctl访问文件时,都需经历“用户到内核”的内存拷贝过程,然后才将文件内容写入磁盘。如图,单个进程访问文件时,需经历2个拷贝过程。进程通信(IPC)机制,如管道、消息队列等,同样需经历4个拷贝过程才能实现两个进程间通信,共享内存除外,所以共享内存也是最快的IPC方式。
在这里插入图片描述

  通过mmap方法,将文件(包括设备文件)映射到用户进程虚拟内存空间,代替read/write/ioctl的访问方式,此时内存拷贝过程只有“用户空间到虚拟内存空间”,省去了“用户到内核”的拷贝过程,在数据量大的情况下能显著提升读写效率。因此,mmap也称为“零拷贝”(zero copy)技术。此时与IPC中的“共享内存”机制是比较类似的。

在这里插入图片描述

注:
一般涉及到大数据,非频繁读写的情况考虑使用mmap;频繁读写且是零碎内容的,对磁盘有一定损伤。


1.2 mmap原理

  mmap的实现是,系统首先先分配一段空闲的进程空间,先建立一个vm_area_struct的数据结构,表示虚拟进程空间地址,然后将新建的虚拟区结构vm_area_struct加入进程的虚拟地址区域链表或树中,内核调用内核态mmap函数把磁盘文件区域和虚拟进程空间地址映射起来。此时,只是建立了映射关系,实际物理内存空间没有内容的,需要经过MMU将“虚拟空间”和“物理空间”建立映射。进程在发起访问映射虚拟进程空间操作时,由于实际内存的物理页面还没有放置到页框中,内核会产生一个缺页中断从而进入实际页框分配过程,然后根据mmap()函数建立的vm_area_struct将磁盘文件加载到物理内存中。之后进程可以正常访问这段映射内存了。

  linux内存采用的是页式管理机制,页是内存的最小粒度,大小通常是4K字节。因此,映射的文件大小不足一页时,实际分配的虚拟进程空间也是按整数页分配的。


1.3 mmap应用

  mmap主要有两种常用用法

  • 存储介质(磁盘)文件映射到用户进程的虚拟地址空间,通过操作内存访问文件
  • 建立匿名映射,用于父子进程之间共享内存(通信)


linux驱动设备也是“文件”,但字符设备(LED、GPIO、串口)等一些“字节流”式的设备一般不支持mmap方法,因为字符设备和块设备的缓冲同步策略不一样。


2. mmap常用接口

2.1 创建映射

函数原型

void* mmap (caddr_t addr, size_t len, int prot, int flags, off_t offset)

映射示意图
在这里插入图片描述

  • 功能,将文件内容映射到指定进程空间
  • addr,映射到进程虚拟空间起始地址;传入NULL则有系统分配
  • len,映射到进程虚拟空间大小
  • prot,映射保护区权限,读、写、执行,一般为读、写权限
标识 函数
PROT_READ 映射区可读
PROT_WRITE 映射区可写
PROT_EXEC 映射区可执行
PROT_NONE 映射区不可访问
  • flags,映射区特性,有两种特性,共享特性和私有特性,常用宏特性如下
    MAP_SHARED,对映射区写数据同时会写入文件,且允许其他映射该文件的进程共享
    MAP_PRIVATE,对映射区的写入操作会产生一个私有映射区的复制(copy-on-write), 对该区域的修改不会写入原文
    MAP_ANONYMOUS,表示创建匿名映射,此时会忽略参数fd,不涉及文件,而且映射区只用于父子进程共享
    MAP_DENYWRITE,只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝
    MAP_LOCKED,表示锁定映射区,该区域不会被内存交互(swap)
  • fd,待映射的文件描述符,文件open时的返回值;如果是匿名映射(MAP_ANONYMOUS),fd设为-1
  • offset, 文件映射偏移地址,必须是分页大小的整数倍;一般设为为0,表示从文件开始地址映射
  • 返回值,成功返回有效的映射虚拟内存地址;失败返回负数,错误码存于error

注:
建立映射后,即使文件关闭,映射依然存在。因为映射的是磁盘的地址,不是文件本体,与文件描述符无关。同时,由于映射机制是按页映射的,可用于进程间通信的有效地址空间是容纳映射文件大小的整数页。


2.2 释放映射

函数原型

int munmap(void *addr, size_t len)
  • addr,映射到进程虚拟空间地址
  • len,映射到进程虚拟空间大小
  • 返回值,成功返回0,失败返回-1,错误码存于error

2.3 访问

  文件映射到进程空间后,用户进程即可通过访问内存的方式间接访问文件,适用于常用的内存操作函数memcpy、memset以及字符操作函数strncpy、sprintf等。关于访问范围,有几个注意的点:

  • 映射范围,实际映射的空间有可能比映射文件空间要大,大小由mmap()函数len参数决定
  • 进程访问范围,映射空间范围以页为单位,进程能访问的大小是容纳文件被映射大小的整数页
  • 有效访问范围,进程有效的访问范围是映射文件内容的实际长度
  • 扩张范围,如果文件大小扩张,并且在映射的页范围,有效访问范围也随着扩张,与最初建立的映射无关
  • 超出有效访问范围,导致总线错误或者进程崩溃

  以下图为例,映射进程空间范围是page0——page3,实际映射文件大小page0、page1及一部分page2,因此进程可访问范围也就是page0——page2。访问超出文件大小范围(page2超出文件映射部分),是无法修改文件内容的;访问page3将导致总线错误,访问非映射范围(page3之后)会导致进程崩溃(段错误)。如果,此时文件大小扩张,并且在page3范围,有效访问空间也扩张。因此,使用mmap时需注意映射空间有效访问范围。

在这里插入图片描述


2.4 同步操作

  进程在映射空间的对共享内容的修改不会实时同步写回到磁盘文件中,只有调用munmap()函数释放映射后才会执行同步操作。mmap机制提供msync()函数,用于手动同步修改内容到磁盘源文件。

int msync ( void * addr, size_t len, int flags)
  • addr,映射到进程虚拟空间地址
  • len,映射到进程虚拟空间大小
  • flags,同步特性
    MS_ASYNC,异步操作,函数调用后立即返回,不需等待同步完成
    MS_SYNC,同步操作,等待同步完成函数才返回
    MS_INVALIDATE,通知其他共享映射区域数据变动,进程映射已失效,需重新建立映射获取最新数据内容
  • 返回值,成功返回0,失败返回-1,错误码存于error

3. 进程使用

3.1 磁盘文件映射

  编写一个例子实现:

  • 将磁盘一文件映射到进程空间
  • 通过内存修改文件内容
  • 同步文件
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <error.h> 

int main(int argc, char **argv)
{
	int fd = 0, fsize = 1024;
	char *mmap_mem = NULL;
	struct stat ft;
	 
	if (argc <= 1)
	{
		printf("%s: file path error\n", argv[0]);	/* argv[1] 为文件名称 */
		exit(-1);
	}

	fd = open(argv[1], O_RDWR);
	if (fd < 0)
	{
		perror ("open");
	}

	/* 获取文件属性 */
  	if ((fstat (fd, &ft)) == -1)
    {
    	perror ("fstat");
    }
  
	/* 将文件映射到进程虚拟空间 */
	mmap_mem = (char *) mmap (NULL, ft.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (mmap_mem == (void *) -1)
	{
		perror ("mmap");
		exit(-1);
	}
	
	/* 映射后, 即使关闭文件可以操作文件 */
	close (fd);

	/* 打印输出文件 */
	printf("before file:%s\n", mmap_mem); 

	/* 修改文件并立即同步 */
	mmap_mem[0] = 'h';
	if ((msync ((void *) mmap_mem, ft.st_size, MS_SYNC)) == -1)
    {
    	perror ("msync");
    }
	printf("after file:%s\n", mmap_mem); 
	
	/* 释放映射区 */
	munmap(mmap_mem, ft.st_size);
  
	return 0;
}

  手动创建一个“file_test”文件,并输入“Hello word”内容,然后保存。在Ubuntu16 64位系统下编译上述程序并执行。

在这里插入图片描述

3.2 进程共享内存通信

  编写一个例子实现:

  • 创建父、子进程
  • 映射一块虚拟空间用于父子进程间交互信息
  • 父子进程分别访问虚拟空间

#include <sys/mman.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <pthread.h>

#define MMAP_MEM_SIZE 1024

int main (int argc, char **argv)
{
	char *mmap_mem = NULL;
	int pid = 0;
	pthread_mutex_t mutex;			/* 互斥锁 */
    pthread_mutexattr_t mutexattr;  /* 互斥锁属性 */

	/* 创建互斥锁 */
	pthread_mutexattr_init(&mutexattr);        /* 初始化 mutex 属性 */
    pthread_mutexattr_setpshared(&mutexattr, PTHREAD_PROCESS_SHARED);    /* 修改属性为进程间共享 */
    pthread_mutex_init(&mutex, &mutexattr);    

	/* 创建内存匿名映射, 用于父子进程间 */
	mmap_mem = (char *) mmap (NULL, MMAP_MEM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
	if (mmap_mem == (void *) -1)
	{
		perror ("mmap");
		exit(-1);
	}
	
	pid = fork();
	if (pid == 0)
	{
		sleep(1);											/* 让父进程先执行 */
		pthread_mutex_lock(&mutex);
		printf ("child process read mem: %s\n", mmap_mem);	/* 获取父进程内容 */
		sprintf (mmap_mem, "%s", "child process content");	/* 修改映射区 */
		pthread_mutex_unlock(&mutex);
		munmap (mmap_mem, MMAP_MEM_SIZE);
		exit (0);
	}
	else if (pid > 0)
	{
		pthread_mutex_lock(&mutex);
		sprintf(mmap_mem, "%s", "parent process content");		/* 修改映射区 */
		pthread_mutex_unlock(&mutex);
		sleep(2);												/* 主动挂起父进程,让子进程访问 */
		printf ("parent process read mem: %s\n", mmap_mem); 	/* 获取子进程内容 */
	}
	else
	{
		perror("fork");
	}
	
	return 0;
}

  在Ubuntu16 64位系统下编译上述程序并执行。
在这里插入图片描述


4. 参考

【1】【Linux】Linux的虚拟内存详解(MMU、页表结构)
【2】 细话mmap

原创文章 128 获赞 147 访问量 36万+

猜你喜欢

转载自blog.csdn.net/qq_20553613/article/details/105183418
今日推荐