于此止?头像
关注
【Linux 篇】数字世界的内存桥梁 —— mmap 内存映射深度实战解析封面图

【Linux 篇】数字世界的内存桥梁 —— mmap 内存映射深度实战解析

于此止?:个人主页

本文选自专栏: 《Linux篇

❄️个人专栏传送门: 《QT百日筑基篇》 《算法篇》 

C++与STL剖析篇》 《数据结构篇》 《Python基础篇》《嵌入式砺剑录

⭐️纵有狂风拔地起,我亦乘风破万里⭐️

mmap

前言:本文细致拆解 Linux 下 mmap 内存映射机制的原理、分类、使用流程与落地场景。核心内容包含:1. mmap 相较于常规 read/write 文件读写的底层差异与性能优势;2. 内存映射两大类型:文件映射、匿名映射适用场景区分;3. mmap 函数参数详解、映射区域权限设置、munmap 释放流程;4、malloc的实现。


目录/索引

初始mmap

一、核心定义

mmap() 是 Linux/Unix 系统调用,全称 memory map(内存映射),把文件 / 设备空间直接映射到进程虚拟地址空间,进程可以像读写内存指针一样读写文件,无需传统 read/write 的内核缓冲区中转。也就说命令该操作的效率的高。

二、主要作用

1. 高效读写文件(最常用)

传统文件读写流程: 应用缓冲区 → 内核页缓存 → 磁盘(两次数据拷贝:用户态 ↔ 内核态)

mmap 映射后: 磁盘文件直接映射到进程虚拟内存省去一次内核缓冲区拷贝

  • 读文件:直接访问内存地址,系统缺页异常时内核加载磁盘数据
  • 写文件:修改内存数据,内核定时自动刷回磁盘,也可手动调用 msync() 强制落盘 优势:大文件读写性能大幅提升。

2. 进程间通信(IPC:共享内存)

创建一块匿名映射 / 映射同一个文件,多个进程绑定同一片虚拟内存:

  • 一个进程修改内存内容,其他进程立刻可见也就是我们之前共享内存的概念。
  • 速度远快于管道、消息队列(无内核缓冲区复制) 适用场景:大数据、视频流、缓存数据多进程共享。

3. 加载可执行程序、动态库

操作系统运行程序时:

  1. 用 mmap 将可执行文件、.so 动态库映射进进程地址空间
  2. 不会一次性全部载入内存,采用按需分页加载
  3. 节省物理内存,多个进程共用同一份库文件物理内存(共享文本段)

4. 硬件设备内存映射(嵌入式常用)

驱动将硬件寄存器、外设物理内存通过 mmap 暴露给用户态: 用户程序无需调用 ioctl,直接指针读写硬件寄存器,嵌入式开发高频使用。

5. 匿名内存分配

创建无文件关联的匿名映射,替代 malloc 申请大块内存:

  • 适合超大内存申请
  • 可用于父子进程间内存共享(fork 后共享映射区)

三、关键优缺点

优点

  1. 数据拷贝少,IO 效率更高;

  2. 内存访问方式操作文件,编码简洁;

  3. 多进程共享内存,IPC 性能顶尖;

  4. 懒加载,节省物理内存占用。

缺点

  1. 映射长度必须按内存页(4KB)对齐,小文件会有内存浪费;这时候我们可以调用ftruncate来进行文件大小的扩容。

  2. 进程崩溃时,未刷盘的数据容易丢失;

  3. 内存越界会污染映射文件,风险更高;

  4. 小文件读写场景优势不明显。

四、基础使用流程

  1. open() 打开文件

  2. mmap() 建立映射,返回内存首地址

  3. 指针直接读写这片内存

  4. munmap() 解除映射

  5. close() 关闭文件

五、典型应用场景

  • 数据库(Redis、MySQL):日志、数据文件高速读写;

  • 音视频播放器:超大媒体文件流式读取;

  • 多进程服务器:缓存数据共享;

  • 嵌入式:操控外设寄存器;

  • 系统底层:程序加载器、动态链接器。

主要就是我们的文件打开后获得文件描述符,然后通过mmap将这个文件映射到我们的进程地址空间中, 然后我们后续的操作直接在堆栈之间进行操作即可。

详解mmap的接口

我们关于mmap的接口这里我们查寻一下我们对应的手册。

man mmap

函数原型:

#include <sys/mman.h>

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

我们所包含的头文件:#include <sys/mman.h>

看这个参数之前我们可以类比一下我们的共享内存的参数。

这里我们拿这个进行类比

void *shmat(int shmid, const void *shmaddr, int shmflg);

我们的第二个参数

所以这里的第一个参数和他是一样的, 我们指定映射到我们进程地址空间的哪个地方。

参数的介绍:

addr: 我们将文件映射到虚拟地址空间的哪个地方。
length:表示我们这个文件映射的长度, 一般是4096一个页的大小。

prot:输入型参数,指定我们映射区域的内存保护属性

PROT_READ:映射区域可读。

PROT_WRITE:映射区域可写。

PROT_EXEC:映射区域可执行。

PROT_NONE : 页面可能无法访问

这里我们要注意!注意!!注意!!!

我们要将我们对应的文件读取到我们的虚拟空间的页缓存中,我们这里必须要求读权限,但是我们这里如果不写的情况下我们是可以不使用我们的写权限的。

open 打开模式mmap prot是否成功
O_RDONLYPROT_READ✅成功
O_RDONLYPROT_READ|PROT_WRITE❌失败,fd 没有写权限
O_WRONLYPROT_READ|PROT_WRITE❌失败,fd 没有读权限
O_RDWRPROT_READ✅成功
O_RDWRPROT_READ|PROT_WRITE✅成功(最常用)

flags:我们对应的标记位,输出型参数。

这里的flags有许多种的参数。

MAP_PRIVATE:创建一个私有映射,对映射区的修改不会反应到底层的文件中。

MAP_SHARED:创建一个共享映射,对映射区域的修改会反应底层的文件中(前提是文件是以写方式打开的,并且文件系统支持这样的操作)。

标志修改内存会写到磁盘用于 IPC 进程通信
MAP_PRIVATE❌不会写回文件❌不可以
MAP_SHARED✅内存改动同步磁盘✅可以

其他选项(如MAP_ANONYMOUS、MAPANONYMOUS等)可能会存在于某些系统中,用于创建不与文件关联的匿名映射。

fd:要映射文件的文件描述符。

offset:文件中的起始偏移量,及映射区域开始的位置。offset和lenth一起定义了映射区域在文件中的位置和大小。

返回值:

成功返回指向这个空间的虚拟地址, 失败返回-1 ((void*)-1)

使用mmap

在使用之前我们要先了解两个有用的系统调用的接口。

ftruncate

这个参数就是将我们的文件截到给定的长度。

man ftruncate

函数原型:

SYNOPSIS
       #include <unistd.h>
       #include <sys/types.h>

       int truncate(const char *path, off_t length);
       int ftruncate(int fd, off_t length);

参数的介绍:

fd:我们要扩大文件的文件描述符。

lenth: 要将这个文件扩大到多少。我们是以4096进行内存的划分的一般扩展到我们对应的4096。

返回值:

成功返回0 , 失败返回-1 , 错误码被设置。

fstat

获取我们当前文件的大小。

man fstat

函数原型:

SYNOPSIS
       #include <sys/types.h>
       #include <sys/stat.h>
       #include <unistd.h>

       int stat(const char *pathname, struct stat *statbuf);
       int fstat(int fd, struct stat *statbuf);

参数的介绍

fd: 获取文件的文件描述符。

statbuf:我们的输出型参数。取出一个结构体。里面有size会返回对应文件的大小。

返回值:

成功返回0,失败返回-1.错误码被设置。

我们是用mmap映射到进程地址空间

我们的commit文件:

#pragma once
#include<cstdio>
#include<iostream>
#include<sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define  LEN_MAX 4096
#include "commit.hpp"

int main()
{
    //先打开文件
    int fd = open("test.txt", O_RDWR| O_CREAT | O_TRUNC,0666);
    ftruncate(fd,LEN_MAX); //使用他的目的是我们是以4kb为单位进行开辟空间的,避免空间被浪费。
    void* address = mmap(nullptr,LEN_MAX, PROT_WRITE|PROT_READ , MAP_SHARED,fd, 0);
    if(address == (void*)-1)
    {
        perror("mmap fail!");
        exit(1);
    }
    
    return 0;
}

我们这个文件进行写操作,我们这里要写入A-Z的字母吧。

#include "commit.hpp"
#define LEN_MAX 4096
int main()
{
    int fd = open("test.txt",O_RDWR | O_CREAT | O_TRUNC,0666);

    // 重点:用 LEN_MAX(4096),严禁SIZE_MAX
    ftruncate(fd, LEN_MAX);

    void* address = mmap(nullptr,LEN_MAX, PROT_WRITE|PROT_READ, MAP_SHARED,fd, 0);
    if(address == (void*)-1)
    {
        perror("mmap fail!");
        exit(1);
    }

    for(char c='A';c<='Z';c++)
    {
        ((char*)address)[c - 'A'] = c;
    }
    close(fd);
    return 0;
}

munmap

当然这里会有些安全方面的问题,我们使用完我们的mmap要进行我们mmap的回收,这里我们使用我们的munmap

man munmap

函数原型

int munmap(void *addr, size_t length);

参数介绍

addr: 我们刚开始返回的虚拟地址的起始地址。

length:我们该文件的长度。当然这里我们是配合我们对应的fstat使用的。

我们知道我们对应的长度的情况:

#include "commit.hpp"
#define LEN_MAX 4096
int main()
{
    int fd = open("test.txt",O_RDWR | O_CREAT | O_TRUNC,0666);

    // 重点:用 LEN_MAX(4096),严禁SIZE_MAX
    ftruncate(fd, LEN_MAX);

    void* address = mmap(nullptr,LEN_MAX, PROT_WRITE|PROT_READ, MAP_SHARED,fd, 0);
    if(address == (void*)-1)
    {
        perror("mmap fail!");
        exit(1);
    }

    for(char c='A';c<='Z';c++)
    {
        ((char*)address)[c - 'A'] = c;
    }
    
    munmap(address, LEN_MAX);
    close(fd);
    return 0;
}

我们使用我们的fstat

#include "commit.hpp"
#define LEN_MAX 4096
int main()
{
    int fd = open("test.txt",O_RDWR | O_CREAT | O_TRUNC,0666);

    // 重点:用 LEN_MAX(4096),严禁SIZE_MAX
    ftruncate(fd, LEN_MAX);

    void* address = mmap(nullptr,LEN_MAX, PROT_WRITE|PROT_READ, MAP_SHARED,fd, 0);
    if(address == (void*)-1)
    {
        perror("mmap fail!");
        exit(1);
    }

    for(char c='A';c<='Z';c++)
    {
        ((char*)address)[c - 'A'] = c;
    }
    
    struct stat statbuf;
    fstat(fd,&statbuf);
    munmap(address, statbuf.st_size);
    close(fd);
    return 0;
}

使用我们的mmap模拟实现我们malloc

模拟实现我们的malloc

mymalloc.hpp

#pragma once
#include<cstdio>
#include<iostream>
#include<sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define SPA_SIZE_MAX 4096
class my_malloc
{
public:
    my_malloc(int size):_spa_size(size),_addr(nullptr),_fd(-1)
    {
        _fd = open("malloc.txt", O_RDWR|O_CREAT, 0666);
        if(_fd<0)
        {
            perror("open fail");
            exit(1);
        }
        if(ftruncate(_fd, _spa_size)==-1) 
        {
            perror("fteuncate fail");
            exit(2);
        }
        _addr=mmap(nullptr,size, PROT_WRITE| PROT_READ,MAP_SHARED,_fd,0);
        if(_addr==(void*)-1)
        {
            perror("mmap fail");
            exit(3);
        }
    }
    
    void* get_addr()
    {
        return _addr;
    }

    ~my_malloc()
    {
        struct stat statbuf;
        if(fstat(_fd,&statbuf)==-1)
        {
            perror("fstat fail");
            exit(4);
        }
        if(munmap(_addr,statbuf.st_size)==-1)
        {
            perror("mummap fail");
            exit(5);
        }
    }
private:
    int _spa_size;
    void* _addr;
    int _fd;
};

mymalloc.cc

#include "mymalloc.hpp"

int main()
{
    my_malloc mc(1024);
    void* add = mc.get_addr();
    for(char c= 'A'; c<='Z';c++)
    {
        ((char*)add)[c-'A'] =c;
    }
    return 0;
}

我们运行查看

我们发现我们果然模拟实现了我们的malloc

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/buhuizhiyuci/article/details/163638785

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--