Linux相关概念和易错知识点(51)(mmap文件映射、共享内存原理、malloc的原理)

目录

1. mmap

(1)文件映射

我们访问文件都是调用系统调read、write,这本质上是和文件缓冲区进行IO,之后系统自动刷新缓冲区到磁盘中。我们可以认为拿到文件缓冲区的访问权,我们就能访问文件。 除此之外,我们还可以将文件缓冲区映射到进程地址空间中,像动态库那样,这样的话我们就可以通过直接访问进程地址空间来操作文件缓冲区,进而不使用系统调用来操作文件。多个进程都可以这么做,就像一个动态库实例可以被映射到多个进程那样,这就是共享内存的原理因此,我们可以说共享内存本质上还是看到了同一块文件缓冲区。

mmap就是负责把内核中的文件缓冲区,直接映射到进程地址空间中,我们也可以用此函数实现共享内存。

(2)mmap映射后的文件缓冲区

当我们利用mmap将文件缓冲区映射到进程中后,我们就可以像操作内存一样,通过指针直接读写这块地址空间,而这些操作会自动同步到对应的文件缓冲区,最终由内核刷新到磁盘。 这就直接越过了系统调用,因为调用read、write实际上是间接和缓冲区打交道的,而mmap映射后我们可以直接进行访问。

对于多个进程而言,如果多个进程同时映射同一个文件缓冲区,它们看到的就是同一块区域,即共享内存 ,因此可见共享内存根本不是什么特殊的内存区域,就是多个进程共享了同一块文件缓冲区而已。相比命名管道而言,共享内存的优势在于随机访问,mmap后拿到的是一整个内存块。而命名管道虽然是内存级文件,但命名管道不支持随机访问,更适合流式通信。

因此我们可以总结mmap的核心用途:高效读写文件,实现进程间通信(共享内存)

(3)mmap参数含义

下面是mmap的声明

c 复制代码
void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);
  • 返回值 :成功则返回映射区域的虚拟地址起始地址 ;失败则返回MAP_FAILED(本质就是把-1强转成void*
  • 参数详解
    1. addr:期望的映射起始地址。一般传nullptr,让内核自动选择最合适的地址;如果传具体地址,内核会尽量满足,但不保证
    2. length:要映射到地址空间的字节数。注意:系统实际分配的大小一定是页面大小的整数倍(通常是4KB) 。比如你传3500字节,内核会给你映射4096字节(1页),因为操作系统管理内存的最小单位是页。但注意我们实际上能访问的还是3500字节,申请多少访问多少,只不过物理层面开辟的空间会对齐。要理解这个,我们须知道对齐操作是为了提高硬件效率设计出来的,操作系统要是处处根据其硬件实际处理来调节访问权限,那就乱套了!也就是说进程地址空间和物理内存都会浪费一定空间。
    3. prot:映射区域的访问权限,用宏定义按位或组合:
      • PROT_READ:可读
      • PROT_WRITE:可写
      • PROT_EXEC:可执行
    4. (重点,后续会讲) flags:映射类型,最常用的两个:
      • MAP_PRIVATE | MAP_ANONYMOUS:私有匿名映射。对映射区域的修改不会写回底层文件,也不会被其他进程看到(写时复制机制)
      • MAP_SHARED:共享映射。对映射区域的修改会写回底层文件,并且对所有映射该文件的进程可见
    5. fd:要映射的文件描述符。如果是匿名映射(不需要文件),传-1
    6. offset:从文件的哪个位置开始映射。比如文件有3000字节,offset=1000就表示从第1000字节开始映射,此时length传2000就够了

(4)munmap

映射的内存使用完后,必须手动释放,否则会造成内存泄漏:

c 复制代码
int munmap(void* addr, size_t length);
  • addr:就是mmap返回的起始地址,就是需要回收的地址
  • length:和mmap中传入的length保持一致

(5)mmap的使用

a.写文件

mmap写文件有一个非常重要的前提:必须先保证文件的大小足够容纳你要映射的区域 。因为mmap只负责映射文件缓冲区,它不会自动帮你扩容文件。

所以写文件的步骤是:

  1. open打开文件(如果不存在就创建)
  2. ftruncate调整文件到目标大小(空文件必须做这一步!)
  3. mmap映射文件
  4. 直接通过指针操作内存,完成写入
  5. munmap取消映射
  6. close关闭文件
c 复制代码
// 示例:用mmap写入"Hello mmap"到文件
int fd = open("test.txt", O_RDWR | O_CREAT, 0644);
ftruncate(fd, 10); // 把文件调整到10字节大小,默认填充0
char* buf = (char*)mmap(nullptr, 10, PROT_WRITE, MAP_SHARED, fd, 0); // 共享映射
strcpy(buf, "Hello mmap"); // 直接写内存就等于写文件
munmap(buf, 10);
close(fd);

注意:ftruncate调整后的文件,未写入的部分默认是0,用文本编辑器打开可能会显示成^@乱码。

b.读文件

读文件相对简单,不需要提前扩容,只需要先获取文件的实际大小即可:

  1. open打开文件
  2. fstat获取文件属性,得到文件大小st_size
  3. mmap映射整个文件(或部分)
  4. 直接通过指针读取内存
  5. munmap取消映射
  6. close关闭文件
c 复制代码
// 示例:用mmap读取文件内容
int fd = open("test.txt", O_RDONLY);
struct stat st;
fstat(fd, &st);
char* buf = (char*)mmap(nullptr, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
printf("%s\n", buf); // 直接读内存就等于读文件
munmap(buf, st.st_size);
close(fd);

对于大文件,我们可以通过length + offset的组合,分段映射文件的不同部分,实现大文件的分片映射。同时注意,不管读文件还是写文件,mmap映射的区域不要超过文件缓冲区的大小,因为这样会出现越界访问。

2.malloc的原理

(1)共享映射和私有映射

这是mmap最重要的参数,理解了这个参数我们可以进一步理解内存开辟的本质,特别是私有映射。

a.共享映射(MAP_SHARED)

对映射区域的修改会实时写回磁盘 ,并且对所有映射该文件的进程可见。这就是实现共享内存的基础。因此我们可以实现进程间通信(IPC),多个进程同时映射同一个磁盘文件,就可以通过这块共享内存直接交换数据

b.私有映射(MAP_PRIVATE)和匿名映射(MAP_ANONYMOUS)

私有映射,私有两字即对映射区域的修改不会写回磁盘 ,也不会被其他进程看到。内核采用写时拷贝(Copy-On-Write) 机制,当你修改私有映射的内存时,内核会复制一份新的内存给你,后续的修改都在这个副本上进行。也就类似于进程保持数据独立性的方法一样,映射后保持只读,只要修改触发中断,根据访问地址确定是否为越界访问,如果不是则进行写时拷贝。 因此我们可以理解为mmap映射了一个内存级文件,其实就是在进程地址空间中映射出来一块可用的内存,并且这个内存只能该进程使用(相比而言,共享映射强调通信)。

只读文件的读取不需要写回磁盘的场景,用私有映射更安全。

匿名映射是一种特殊的映射,它不需要关联任何磁盘文件,直接由内核分配一块物理内存映射到进程地址空间。

下面是私有匿名映射的使用:

c 复制代码
void* buf = mmap(nullptr, 1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

问题是这有什么用?这不就是malloc加载动态库的原理吗?

malloc会直接调用mmap分配私有匿名映射内存。以及进程加载动态库,就是把动态库文件映射到自己的地址空间,用的就是私有映射。

或者父子进程间的共享内存(父进程先创建匿名共享映射,再fork子进程,子进程会继承这个映射)都是使用mmap的这个选项。mmap默认映射的空间是在堆栈之间的,我们也可自己指定。

(2)mmap的底层原理

mmap的底层实现,核心依赖内核中的两个数据结构:

  1. struct mm_struct:每个进程都有一个,描述整个进程的地址空间
  2. struct vm_area_struct:描述地址空间中的一个连续区域(比如代码段、数据段、堆、栈、mmap映射区)

vm_area_struct中几个关键字段:

  • vm_start:映射区域的起始虚拟地址
  • vm_end:映射区域的结束虚拟地址
  • vm_mm:指向所属进程的mm_struct
  • vm_file:指向映射的文件对象(如果是文件映射);如果是匿名映射,这个字段为NULL

当我们调用mmap时,内核会做两件事:

  1. 在进程的地址空间中,找到一块合适的空闲区域,创建一个新的vm_area_struct
  2. 如果是文件映射,就把这个vm_area_struct和对应的文件对象关联起来。如果像malloc那样只是为了开辟内存,则为NULL

注意:此时内核并没有立即分配物理内存,只有当进程第一次访问这个虚拟地址时,才会触发缺页异常,内核才会真正分配物理页 ,并建立虚拟地址到物理地址的映射。这就是mmap延迟分配机制。

相关推荐
Rabitebla2 小时前
C++ 入门基础:从 C 到 C++ 的第一步
c语言·开发语言·c++
IT摆渡者2 小时前
JUMPSERVER堡垒机部署
linux·运维·网络·经验分享·笔记
学习永无止境@2 小时前
Sobel边缘检测的MATLAB实现
图像处理·opencv·算法·计算机视觉·fpga开发
人工小情绪2 小时前
Linux下离线安装timm
linux·运维·服务器
Trouvaille ~2 小时前
【MySQL篇】表的操作:数据的容器
linux·数据库·mysql·oracle·xshell·ddl·表的操作
c++逐梦人2 小时前
记忆化搜索(DFS)
算法·深度优先
阿Y加油吧2 小时前
二分查找进阶:搜索二维矩阵 & 查找元素首尾位置 深度解析
线性代数·算法·矩阵
SEO-狼术2 小时前
Visualize Org Charts and Decision Trees in WinForms
算法·决策树·机器学习
爱学习的小囧2 小时前
vSphere 9.0 API 实操教程 —— 轻松检索 vGPU 与 DirectPath 配置文件
linux·运维·服务器·网络·数据库·esxi·vmware