Linux下对mmap封装使用

时间:2025-01-29 17:14:12

Linux下对mmap封装使用

  • 1、mmap简介
  • 2、Linux下mmap使用介绍
    • 2.1、mmap函数
    • 2.2、munmap函数
  • 3、对mmap进行封装
  • 4、对封装类MEM_MAP进行测试
  • 5、mmap原理
  • 6、源代码下载

1、mmap简介

mmap即memory map,是一种内存映射文件的技术。mmap可以将一个文件或者其它对象映射到进程的地址空间,进而实现磁盘地址和进程虚拟地址的一一对应关系。通过使用mmap技术,我们可以让不同进程通过映射到同一个普通文件的方式实现共享内存,普通文件被映射到进程地址空间当中之后,进程可以向访问普通内存一样对文件高效地进行一系列操作。

2、Linux下mmap使用介绍

2.1、mmap函数

mmap() 必须以 PAGE_SIZE(默认为4KB) 为单位进行映射,而内存也只能以页为单位进行映射,若要映射非 PAGE_SIZE 整数倍的地址范围,要先进行内存对齐,强行以 PAGE_SIZE 的倍数大小进行映射。函数声明如下:

#include <sys/>
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);

参数说明:

  • addr: 映射区的开始地址,设置为0时表示由系统决定映射区的起始地址
  • length: 映射区的长度,长度单位是以字节为单位,不足一内存页按一内存页处理
  • port: 映射区域的权限,不能与文件的打开模式冲突,可以通过or运算合理地组合在一起
  • flags: 映射的标志位,可以通过or运算合理地组合在一起
  • fd: 有效的文件描述词,一般是由open()函数返回
  • offset: 文件偏移量

参数 port 的取值如下:

PORT_EXEC:映射区具有可执行权限
PROT_READ:映射区具有可读权限
PROT_WRITE:映射区具有可写权限
PROT_NONE:映射区不可被访问

参数 flags 的取值如下:

MAP_SHARED:对映射区域的写入操作直接反映到文件当中
MAP_FIXED:若在start上无法创建映射则失败(如果没有此标记会自动创建)
MAP_PRIVATE:对映射区域的写入操作只反映到缓冲区当中不会写入到真正的文件
MAP_ANONYMOUS:匿名映射将虚拟地址映射到物理内存而不是文件(忽略fd)
MAP_DENYWRITE:拒绝其它文件的写入操作
MAP_LOCKED:锁定映射区域保证其不被置换

返回值:成功执行时,mmap() 返回被映射区的指针。失败时,mmap() 返回 MAP_FAILED(其值为(void *)-1),并且 errno 被设为以下的某个值

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

2.2、munmap函数

munmap() 声明如下:

#include <sys/>
int munmap(void* start, size_t length);

参数说明:

  • start: 映射区的指针,即mmap()的返回值
  • length: 映射区的长度

返回值:成功执行时,munmap() 返回0。失败时,munmap() 返回-1。

3、对mmap进行封装

下面给出 MEM_MAP 类的设计:

#ifndef __MEM_MAP_H
#define __MEM_MAP_H

#include <iostream>
#include <string>
#include <sys/>
#include <>
#include <>
#include <>

class Noncopyable
{
public:
    Noncopyable() = default;

    ~Noncopyable() = default;

    Noncopyable(const Noncopyable&) = delete;

    Noncopyable& operator=(const Noncopyable&) = delete;
};

#define MAX_PATH_LEN    200

class MEM_MAP : private Noncopyable
{
public:
    MEM_MAP();

    ~MEM_MAP();

    //取消映射
    void unmap();

    //取消映射并删除对应文件
    void remove();

    //映射指定的文件
    int map(const char* filename,
        size_t length,
        int flags = O_RDWR | O_CREAT,
        mode_t mode = 0644,
        int prot = PROT_READ | PROT_WRITE,
        int share = MAP_SHARED,
        void* addr = NULL,
        off_t offset = 0);

public:
    //将数据立即刷到文件里(MS_ASYNC、MS_SYNC、MS_INVALIDATE)
    int sync(size_t len, int flags = MS_SYNC);
    int sync(int flags = MS_SYNC);

public:
    //返回基地址
    void* addr() const;
    
    //返回映射大小
    size_t size() const;

    //返回文件描述符
    int fd() const;

    //返回文件名
    const char* filename() const;

private:
    //mmap基地址
    void* m_addr;
    //mmap大小
    size_t m_length;
    //mmap文件描述符
    int m_fd;
    //mmap文件名
    char m_filename[MAX_PATH_LEN];
};

#endif /*__MEM_MAP_H*/

以下是 MEM_MAP 类各个方法的具体实现:

#include "mem_map.h"


MEM_MAP::MEM_MAP()
  : m_addr(NULL),
    m_length(0),
    m_fd(-1)
{
    memset(m_filename, 0, sizeof(m_filename));
}

MEM_MAP::~MEM_MAP()
{
    unmap();
}

void MEM_MAP::unmap()
{
    if (m_addr != NULL)
    {
        munmap(m_addr, m_length);
    }
    
    if (m_fd != -1)
    {
        close(m_fd);
    }
    
    m_addr = NULL;
    m_length = 0;
    m_fd = -1;
    memset(m_filename, 0, sizeof(m_filename));
}

void MEM_MAP::remove()
{
    char filename[MAX_PATH_LEN];
    strncpy(filename, m_filename, MAX_PATH_LEN);

    unmap();

    if (filename[0] != '\0')
    {
         unlink(filename);
    }
}

int MEM_MAP::map(const char* filename,
                 size_t length,
                 int flags,
                 mode_t mode,
                 int prot,
                 int share,
                 void* addr,
                 off_t offset)
{
    //取消旧的映射
    unmap();

    //数据赋值
    m_length = length;
    strncpy(m_filename, filename, MAX_PATH_LEN);

    //打开文件描述符
    m_fd = open(m_filename, flags, mode);
    if (m_fd == -1)
    {
        std::cout << strerror(errno) << std::endl;
        unmap();
        return -1;
    }

    //指定文件大小
    if (truncate(m_filename, m_length) == -1)
    {
        std::cout << strerror(errno) << std::endl;
        unmap();
        return -2;
    }

    //建立映射
    m_addr = mmap(addr, length, prot, share, m_fd, offset);
    if (m_addr == MAP_FAILED)
    {
        std::cout << strerror(errno) << std::endl;
        m_addr = NULL;
        unmap();
        return -3;
    }

    return 0;
}

int MEM_MAP::sync(size_t len, int flags)
{
    return msync(m_addr, len, flags);
}

int MEM_MAP::sync(int flags)
{
    return msync(m_addr, m_length, flags);
}

void* MEM_MAP::addr() const
{
    return m_addr;
}

size_t MEM_MAP::size() const
{
    return m_length;
}

int MEM_MAP::fd() const
{
    return m_fd;
}

const char* MEM_MAP::filename() const
{
  return m_filename;
}

4、对封装类MEM_MAP进行测试

测试代码如下,运行之后会生成一个拥有315行 Hello World! 的文件,且末尾会剩余一个字节

#include "mem_map.h"


int main(int argc, char* argv[])
{
    MEM_MAP mmap;
    
    if (mmap.map("", 4 * 1024) != 0)
    {
        return -1;
    }

    char* str = (char*)mmap.addr();

    while (1)
    {
        if (strlen(str) + strlen("Hello World!\n") > mmap.size())
        {
            break;
        }
        strcat(str, "Hello World!\n");
    }

    return 0;
}

5、mmap原理

mmap实现内存映射的过程主要分为以下三个阶段:

1. 创建映射区域

过程:进程在用户空间调用 mmap() ,此时进程会在当前进程的地址空间当中寻找一段连续的空闲虚拟地址,并给这块虚拟地址分配一个 vm_area_struct 结构(会自动对各个区域进行初始化),然后将新键的虚拟结构插入到虚拟地址空间的链表或者红黑树当中。

2. 实现物理内存地址和虚拟地址的映射关系

过程:通过待映射的文件描述符指针,在文件描述符表当中找到对应的文件描述符链接到内核已经打开的文件描述符集当中的 struct_file,这个 struct_file 维护着这个被打开的文件的各项信息,通过这个文件的结构体链接到 file_operations,调用内核的mmap函数(原型为 int mmap(struct file* filp, struct vm_area_struct* vma),注意这个不是用户态的mmap),内核mmap函数通过虚拟文件系统当中的inode定位到文件的物理地址,通过 reamp_pfn_range() 建立页表即实现了文件地址和虚拟地址的映射关系。

3. 实现虚拟地址上的数据同步到磁盘文件中

过程:进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上,因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用 nopage() 把所缺的页从磁盘装入到主存中,之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址(由 flags 参数决定是否会自动回写),即完成了写入到文件的过程。

6、源代码下载

下载地址:mmap封装测试代码