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

张开发
2026/4/9 7:32:26 15 分钟阅读

分享文章

Linux相关概念和易错知识点(51)(mmap文件映射、共享内存原理、malloc的原理)
目录1. mmap1文件映射2mmap映射后的文件缓冲区3mmap参数含义4munmap5mmap的使用a.写文件b.读文件2.malloc的原理1共享映射和私有映射a.共享映射MAP_SHAREDb.私有映射MAP_PRIVATE和匿名映射MAP_ANONYMOUS2mmap的底层原理1. mmap1文件映射我们访问文件都是调用系统调read、write这本质上是和文件缓冲区进行IO之后系统自动刷新缓冲区到磁盘中。我们可以认为拿到文件缓冲区的访问权我们就能访问文件。除此之外我们还可以将文件缓冲区映射到进程地址空间中像动态库那样这样的话我们就可以通过直接访问进程地址空间来操作文件缓冲区进而不使用系统调用来操作文件。多个进程都可以这么做就像一个动态库实例可以被映射到多个进程那样这就是共享内存的原理因此我们可以说共享内存本质上还是看到了同一块文件缓冲区。mmap就是负责把内核中的文件缓冲区直接映射到进程地址空间中我们也可以用此函数实现共享内存。2mmap映射后的文件缓冲区当我们利用mmap将文件缓冲区映射到进程中后我们就可以像操作内存一样通过指针直接读写这块地址空间而这些操作会自动同步到对应的文件缓冲区最终由内核刷新到磁盘。这就直接越过了系统调用因为调用read、write实际上是间接和缓冲区打交道的而mmap映射后我们可以直接进行访问。对于多个进程而言如果多个进程同时映射同一个文件缓冲区它们看到的就是同一块区域即共享内存因此可见共享内存根本不是什么特殊的内存区域就是多个进程共享了同一块文件缓冲区而已。相比命名管道而言共享内存的优势在于随机访问mmap后拿到的是一整个内存块。而命名管道虽然是内存级文件但命名管道不支持随机访问更适合流式通信。因此我们可以总结mmap的核心用途高效读写文件实现进程间通信共享内存3mmap参数含义下面是mmap的声明void*mmap(void*addr,size_tlength,intprot,intflags,intfd,off_toffset);返回值成功则返回映射区域的虚拟地址起始地址失败则返回MAP_FAILED本质就是把-1强转成void*参数详解addr期望的映射起始地址。一般传nullptr让内核自动选择最合适的地址如果传具体地址内核会尽量满足但不保证length要映射到地址空间的字节数。注意系统实际分配的大小一定是页面大小的整数倍通常是4KB。比如你传3500字节内核会给你映射4096字节1页因为操作系统管理内存的最小单位是页。但注意我们实际上能访问的还是3500字节申请多少访问多少只不过物理层面开辟的空间会对齐。要理解这个我们须知道对齐操作是为了提高硬件效率设计出来的操作系统要是处处根据其硬件实际处理来调节访问权限那就乱套了也就是说进程地址空间和物理内存都会浪费一定空间。prot映射区域的访问权限用宏定义按位或组合PROT_READ可读PROT_WRITE可写PROT_EXEC可执行重点后续会讲flags映射类型最常用的两个MAP_PRIVATE | MAP_ANONYMOUS私有匿名映射。对映射区域的修改不会写回底层文件也不会被其他进程看到写时复制机制MAP_SHARED共享映射。对映射区域的修改会写回底层文件并且对所有映射该文件的进程可见fd要映射的文件描述符。如果是匿名映射不需要文件传-1offset从文件的哪个位置开始映射。比如文件有3000字节offset1000就表示从第1000字节开始映射此时length传2000就够了4munmap映射的内存使用完后必须手动释放否则会造成内存泄漏intmunmap(void*addr,size_tlength);addr就是mmap返回的起始地址就是需要回收的地址length和mmap中传入的length保持一致5mmap的使用a.写文件用mmap写文件有一个非常重要的前提必须先保证文件的大小足够容纳你要映射的区域。因为mmap只负责映射文件缓冲区它不会自动帮你扩容文件。所以写文件的步骤是open打开文件如果不存在就创建用ftruncate调整文件到目标大小空文件必须做这一步mmap映射文件直接通过指针操作内存完成写入munmap取消映射close关闭文件// 示例用mmap写入Hello mmap到文件intfdopen(test.txt,O_RDWR|O_CREAT,0644);ftruncate(fd,10);// 把文件调整到10字节大小默认填充0char*buf(char*)mmap(nullptr,10,PROT_WRITE,MAP_SHARED,fd,0);// 共享映射strcpy(buf,Hello mmap);// 直接写内存就等于写文件munmap(buf,10);close(fd);注意ftruncate调整后的文件未写入的部分默认是0用文本编辑器打开可能会显示成^乱码。b.读文件读文件相对简单不需要提前扩容只需要先获取文件的实际大小即可open打开文件用fstat获取文件属性得到文件大小st_sizemmap映射整个文件或部分直接通过指针读取内存munmap取消映射close关闭文件// 示例用mmap读取文件内容intfdopen(test.txt,O_RDONLY);structstatst;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映射了一个内存级文件其实就是在进程地址空间中映射出来一块可用的内存并且这个内存只能该进程使用相比而言共享映射强调通信。只读文件的读取不需要写回磁盘的场景用私有映射更安全。匿名映射是一种特殊的映射它不需要关联任何磁盘文件直接由内核分配一块物理内存映射到进程地址空间。下面是私有匿名映射的使用void*bufmmap(nullptr,1024,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS,-1,0);问题是这有什么用这不就是malloc、加载动态库的原理吗malloc会直接调用mmap分配私有匿名映射内存。以及进程加载动态库就是把动态库文件映射到自己的地址空间用的就是私有映射。或者父子进程间的共享内存父进程先创建匿名共享映射再fork子进程子进程会继承这个映射都是使用mmap的这个选项。mmap默认映射的空间是在堆栈之间的我们也可自己指定。2mmap的底层原理mmap的底层实现核心依赖内核中的两个数据结构struct mm_struct每个进程都有一个描述整个进程的地址空间struct vm_area_struct描述地址空间中的一个连续区域比如代码段、数据段、堆、栈、mmap映射区vm_area_struct中几个关键字段vm_start映射区域的起始虚拟地址vm_end映射区域的结束虚拟地址vm_mm指向所属进程的mm_structvm_file指向映射的文件对象如果是文件映射如果是匿名映射这个字段为NULL当我们调用mmap时内核会做两件事在进程的地址空间中找到一块合适的空闲区域创建一个新的vm_area_struct如果是文件映射就把这个vm_area_struct和对应的文件对象关联起来。如果像malloc那样只是为了开辟内存则为NULL注意此时内核并没有立即分配物理内存只有当进程第一次访问这个虚拟地址时才会触发缺页异常内核才会真正分配物理页并建立虚拟地址到物理地址的映射。这就是mmap的延迟分配机制。

更多文章