Linux编程:mmap内存映射的使用(内存共享的一种),mmap-实现大文件拷贝的终极武器?

前言

接着上一篇讲了Linux内存共享share memory的使用,这里趁热打铁
介绍ipc通信的另外一种方法,内存映射。其实这也算共享内存的一种,只不过内存映射是将文件的某一段映射到内存,实现不同进程的数据通信。
上一篇博客传送门:https://blog.csdn.net/haohaohaihuai/article/details/106862138

基本原理

普通的读写文件

进程进行普通文件读写open后,调用read/write系统调用后会进入内核,内核开始读写文件,假设内核是在读文件,内核先把文件读取到内核缓冲区,然后把内核缓冲区的数据拷贝到用户缓冲区,实际上整个过程拷贝了两次数据,即先从文件->内核缓冲区,再从内核缓冲区->用户页缓冲区
在这里插入图片描述

内存映射

用mmap将fd进行映射后磁盘 -> 用户页缓存,只有一次拷贝。减少一次拷贝可以大大增加性能。这种技术也叫做“zero_copy”。
在这里插入图片描述
mmap把硬盘空间某个文件的连续一段映射为一段连续内存。“文件”这个概念可以是设备,可以是某个驱动设备的文件,也可以是磁盘文件。内存映射是为了让文件的读写更快更有效率,当然也可作为IPC通信的一种方式。
在这里插入图片描述

函数定义及参数

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

start:起始地址,一般默认为NULL即可
length:映射长度,映射文件时一般设为文件长度
prot:内存区域权限,读、写、执行等
flags:做映射时的其他辅助标记
fd 文件描述符,非文件映射时设为-1
offset:偏移,映射文件时一般设为0
prot解释:
PROT_EXEC页内容可以被执行。
PROT_READ页内容可能被读取。
PROT_WRITE页内容可以被写入。
PROT_NONE页内容可能无法访问。
那么PROT_WRITE | PROT_WRITE就表示可以可读写
flags解释:

  • MAP_SHARED :与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
  • MAP_PRIVATE :建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
  • MAP_DENYWRITE :这个标志被忽略。
  • MAP_EXECUTABLE :同上
  • MAP_NORESERVE :不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
  • MAP_LOCKED :锁定映射区的页面,从而防止页面被交换出内存。
  • MAP_GROWSDOWN :用于堆栈,告诉内核VM系统,映射区可以向下扩展。
  • MAP_ANONYMOUS :匿名映射,映射区不与任何文件关联。
  • MAP_ANON :MAP_ANONYMOUS的别称,不再被使用。
  • MAP_FILE :兼容标志,被忽略。
  • MAP_32BIT :将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。
  • MAP_FIXED :使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。此选项非常危险(在本身),因为它会强制删除先前存在的映射,因此使多线程进程很容易破坏自己的进程 地址空间。
  • MAP_POPULATE :为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
  • MAP_NONBLOCK :仅和MAP_POPULATE一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。

flag组合有些是互斥的,比如MAP_SHARED和MAP_PRIVATE,有些是可以混着使用的,比如
MAP_SHARED|MAP_LOCKED,MAP_SHARED|MAP_ANONYMOUS
MAP_SHARED ,MAP_PRIVATE ,MAP_ANONYMOUS 组合以后大致可以分为以下几种模式:

  • 私有文件映射
    多个进程使用同样的物理内存页进行初始化,但是各个进程对内存文件的修改不会共享,也不会反应到物理文件中。
  • 私有匿名映射
    mmap会创建一个新的映射,各个进程不共享,这种使用主要用于分配内存(malloc分配大内存会调用mmap)。
  • 共享文件映射
    多个进程通过虚拟内存技术共享同样的物理内存空间,对内存文件 的修改会反应到实际物理文件中,也是进程间通信(IPC)的一种机制。
  • 共享匿名映射
    这种机制在进行fork的时候不会采用写时复制,父子进程完全共享同样的物理内存页,这也就实现了父子进程通信(IPC)。

mmap返回0为成功,失败为MAP_FAILED,错误码在errno中:
EACCES:访问出错
EAGAIN:文件已被锁定,或者太多的内存已被锁定
EBADF:fd不是有效的文件描述词
EINVAL:一个或者多个参数无效
ENFILE:已达到系统对打开文件的限制
ENODEV:指定文件所在的文件系统不支持内存映射
ENOMEM:内存不足,或者进程已超出最大内存映射数量
EPERM:权限不足,操作不允许
ETXTBSY:已写的方式打开文件,同时指定MAP_DENYWRITE标志
SIGSEGV:试图向只读区写入
SIGBUS:试图访问不属于进程的内存区

使用示例

1.MAP_SHARED模式

#include <sys/mman.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(int argc, char **argv)
{
    
    
    int fd;
    char *map_ptr;
    struct stat buff = {
    
    0};
    //打开文件
    fd = open(argv[1], O_RDWR);
    if(fd<0)
    {
    
    
        printf("open erro: %s\n", strerror(errno));
        close(fd);
        return -1;
    }
    //获取文件状态
    if (fstat(fd, &buff) < 0)
    {
    
    
        printf("file state error:%s\n", strerror(errno));
        close(fd);
        return -1;
    }
    //允许读写
    //允许其它进程访问此内存区域
    printf("mmap file size: %ld\n", buff.st_size);
    map_ptr = mmap(NULL, buff.st_size+10, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
    if(map_ptr == MAP_FAILED)
    {
    
    
        printf("mmap erro: %s\n", strerror(errno));
        close(fd);
        return -1;
    }
    //使用映射区域
    //读
    printf("mmap content: %s\n", map_ptr);
    //写
    memcpy(map_ptr, "LLL",3);
    
    munmap(map_ptr, buff.st_size);
    close(fd);
    
    return 0;
}

在工程目录下新建一个文件mmap_demo.txt,写入字符“菠萝菠萝蜜~”。保存为UTF-8编码格式文件。
在这里插入图片描述
编译文件:gcc -o mmapwrite mmapwrite.c
这里采用的utf-8编码,中文字符一般是3个字节的长度,本示例中用LLL替换前面3个字节。
执行程序后,MAP_SHARED文件共享方式,内容会更新到文件。
在这里插入图片描述

2.MAP_PRIVATE模式

上述示例映射的代码稍作修改,建立私有文件映射

    //允许读写
    //允许其它进程访问此内存区域
    printf("mmap file size: %ld\n", buff.st_size);
    map_ptr = mmap(NULL, buff.st_size+10, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
    if(map_ptr == MAP_FAILED)
    {
    
    
        printf("mmap erro: %s\n", strerror(errno));
        close(fd);
        return -1;
    }
    //使用映射区域
    printf("mmap content1: %s\n", map_ptr);
    memcpy(map_ptr, "LLL",3);
    printf("mmap content2: %s\n", map_ptr);

在这里插入图片描述
使用MAP_PRIVATE后,不允许其它进程访问此内存区域。可以看到读进程并没有读到“LLL”字符内容,只读处了原有文件内容,只有mmwrite进程可以读取到更改后的内容,且更改的内容不会同步到文件。

3.mmap大文件拷贝示例

#include <sys/mman.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int main(int argc, char **argv)
{
    
    
    int fd,fd1;
    char *rmap_ptr,*wmap_ptr;
    struct stat buff = {
    
    0};
    //打开文件
    fd = open(argv[1], O_RDWR);
    if(fd<0)
    {
    
    
        printf("open src erro: %s\n", strerror(errno));
        close(fd);
        return -1;
    }
    //获取文件状态
    if (fstat(fd, &buff) < 0)
    {
    
    
        printf("file state error:%s\n", strerror(errno));
        close(fd);
        return -1;
    }
	//判断目标文件是否存在, 不存在则创建目标文件
    fd1 = open("./file_copy", O_RDWR | O_CREAT | O_EXCL, 0777);
	if(fd1<0)
    {
    
    
        printf("open dest file erro: %s\n", strerror(errno));
        close(fd);
        return -1;
    }
	//设置文件大小
	ftruncate(fd1, buff.st_size);

    //读文件映射,使用MAP_PRIVATE
    printf("mmap file size: %ld\n", buff.st_size);
    rmap_ptr = mmap(NULL, buff.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
    if(rmap_ptr == MAP_FAILED)
    {
    
    
        printf("rmmap erro: %s\n", strerror(errno));
    	close(fd1);
        close(fd);
        return -1;
    }

    //写文件映射,必须用MAP_SHARED,否则内容将不能更新到文件
    wmap_ptr = mmap(NULL, buff.st_size, PROT_WRITE, MAP_SHARED, fd1, 0);
    if(wmap_ptr == MAP_FAILED)
    {
    
    
        printf("wmmap erro: %s\n", strerror(errno));
    	close(fd1);
        close(fd);
        return -1;
    }

    //拷贝文件内容
	memcpy(wmap_ptr,rmap_ptr,buff.st_size);

    close(fd1);
	close(fd);

   //解除映射
    munmap(wmap_ptr, buff.st_size);
    munmap(rmap_ptr, buff.st_size);

    return 0;
}

需要拷贝的文件wps-office_11.1.0.8392_amd64.deb大小为213113086字节。
执行程序:./mmwrite wps-office_11.1.0.8392_amd64.deb
即可得到拷贝后的目标文件file_copy。拷贝速度非常之快。
在这里插入图片描述

使用总结:
1.本示例文件mmap_demo.txt大小为19byte,映射了buff.st_size+10字节。但是mmap()必须以PAGE_SIZE为单位进行映射,物理内存页单位是4096字节,所以实际映射了一个页4096字节。
此时

  • 访问 0~19字节可返回文件操作内容,更改内容更新到文件。
  • 访问20~4096字节返回是0,写内容不会更新到文件。
  • 访问超过4096字节,会造成程序SIGSECV错误。

2.mmap可用于实现高效的大规模数据读写,需要用磁盘空间换内存空间得时候,mmap就功效巨大了。当使用MAP_SHARED的时候可用做IPC通信。

3.mmap之后,并没有在将文件内容加载到物理页上,只上在虚拟内存中分配了地址空间。当进程在访问这段地址时,通过查找页表,发现虚拟内存对应的页没有在物理内存中缓存,则产生"缺页",由内核的缺页异常处理程序处理,将文件对应内容,以页为单位(4096)加载到物理内存。

作者:费码程序猿
欢迎技术交流:QQ:255895056
转载请注明出处,如有不当欢迎指正

猜你喜欢

转载自blog.csdn.net/haohaohaihuai/article/details/107039410