内存映射(mmap)
概述
内存映射(mmap)是一种将文件或设备映射到进程地址空间的机制。通过内存映射,进程可以像访问普通内存一样访问文件,也可以用于进程间通信。mmap提供了高效的文件I/O和进程间通信方式。
通信原理
基本概念
内存映射的特点:
- 文件映射:将文件映射到进程地址空间
- 共享映射:多个进程可以共享同一映射区域
- 零拷贝:直接内存访问,减少数据拷贝
- 虚拟内存:利用虚拟内存机制,按需加载
- 同步机制:可以自动同步到文件
实现机制
文件映射
创建映射:
- 使用
mmap()将文件映射到进程地址空间 - 返回映射区域的虚拟地址
- 可以指定映射大小、权限、标志等
- 使用
访问数据:
- 进程通过指针直接访问映射区域
- 访问时触发页错误,内核按需加载文件内容
- 修改数据时,根据标志决定是否写回文件
同步:
msync():手动同步映射区域到文件munmap():取消映射,自动同步
数据流向:
进程A ──┐ ├──> [共享映射区域] <──> [文件/设备] 进程B ──┤ 进程C ──┘
匿名映射(进程间通信)
创建匿名映射:
- 使用
mmap()创建匿名映射(不关联文件) - 使用MAP_SHARED标志,多个进程可以共享
- 通过fork()或显式共享实现进程间通信
- 使用
共享机制:
- 父子进程通过fork()共享映射区域
- 任意进程可以通过文件描述符共享(需要特殊处理)
API说明
mmap()
#include<sys/mman.h>void*mmap(void*addr,size_tlength,intprot,intflags,intfd,off_toffset);- 功能:创建内存映射
- 参数:
addr:建议的映射地址(通常为NULL,让系统选择)length:映射长度(字节)prot:保护标志(PROT_READ可读,PROT_WRITE可写,PROT_EXEC可执行)flags:映射标志(MAP_SHARED共享,MAP_PRIVATE私有,MAP_ANONYMOUS匿名等)fd:文件描述符(匿名映射时为-1)offset:文件偏移量(通常为0)
- 返回值:成功返回映射地址,失败返回MAP_FAILED
munmap()
intmunmap(void*addr,size_tlength);- 功能:取消内存映射
- 参数:
addr:映射地址length:映射长度
- 返回值:成功返回0,失败返回-1
msync()
intmsync(void*addr,size_tlength,intflags);- 功能:同步映射区域到文件
- 参数:
addr:映射地址length:同步长度flags:同步标志(MS_SYNC同步写,MS_ASYNC异步写,MS_INVALIDATE使缓存无效)
- 返回值:成功返回0,失败返回-1
示例代码
示例: 使用 mmap 读写文件
#include<stdio.h>#include<sys/mman.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>#include<string.h>intmain(){constchar*path="demo.txt";constchar*msg="hello mmap\n";intfd=open(path,O_RDWR|O_CREAT,0666);if(fd<0){perror("open");return1;}// 确保文件大小 >= 要写入的长度size_tlen=strlen(msg);if(ftruncate(fd,len)==-1){perror("ftruncate");close(fd);return1;}// 建立映射: 读写, 共享写回文件void*addr=mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);if(addr==MAP_FAILED){perror("mmap");close(fd);return1;}// 写入数据memcpy(addr,msg,len);// 同步到文件if(msync(addr,len,MS_SYNC)==-1){perror("msync");}// 读取验证write(STDOUT_FILENO,addr,len);// 清理munmap(addr,len);close(fd);return0;}示例: 多线程共享 mmap 区域并安全更新
#include<stdio.h>#include<stdlib.h>#include<pthread.h>#include<sys/mman.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>#include<string.h>#defineTHREADS4#definePER_THREAD100000structshared{pthread_mutex_tlock;intcounter;};void*worker(void*arg){structshared*sh=(structshared*)arg;for(inti=0;i<PER_THREAD;i++){pthread_mutex_lock(&sh->lock);sh->counter++;pthread_mutex_unlock(&sh->lock);}returnNULL;}intmain(){constchar*path="shared.dat";intfd=open(path,O_RDWR|O_CREAT,0666);if(fd<0){perror("open");return1;}// 准备文件长度if(ftruncate(fd,sizeof(structshared))==-1){perror("ftruncate");close(fd);return1;}// 建立映射structshared*sh=mmap(NULL,sizeof(structshared),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);if(sh==MAP_FAILED){perror("mmap");close(fd);return1;}// 初始化锁和计数器pthread_mutexattr_tattr;pthread_mutexattr_init(&attr);pthread_mutexattr_setpshared(&attr,PTHREAD_PROCESS_PRIVATE);// 同一进程线程间共享pthread_mutex_init(&sh->lock,&attr);sh->counter=0;// 创建线程pthread_tth[THREADS];for(inti=0;i<THREADS;i++){pthread_create(&th[i],NULL,worker,sh);}for(inti=0;i<THREADS;i++){pthread_join(th[i],NULL);}printf("Final counter = %d (expect %d)\n",sh->counter,THREADS*PER_THREAD);// 清理pthread_mutex_destroy(&sh->lock);munmap(sh,sizeof(structshared));close(fd);return0;}说明:
- 使用 MAP_SHARED 将文件映射为共享区域
- 通过 pthread_mutex_t 保证多线程更新的原子性
- 如果要在多进程间共享, 将
PTHREAD_PROCESS_PRIVATE改为PTHREAD_PROCESS_SHARED, 并确保映射为 MAP_SHARED 且 mutex 存放于共享内存中
示例: 多进程(父子进程)共享 mmap 区域并安全更新
#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/mman.h>#include<sys/stat.h>#include<sys/wait.h>#include<fcntl.h>#include<pthread.h>#include<string.h>#include<errno.h>#definePROCESSES4#definePER_PROCESS100000// 共享数据结构, 包含互斥锁和计数器structshared{pthread_mutex_tlock;// 进程间共享的互斥锁intcounter;// 共享计数器intinitialized;// 初始化标志, 确保只初始化一次};intmain(){constchar*path="shared_mmap.dat";intfd;structshared*sh;pid_tpids[PROCESSES];inti;// 创建或打开共享文件fd=open(path,O_RDWR|O_CREAT,0666);if(fd<0){perror("open");return1;}// 调整文件大小以容纳共享数据结构if(ftruncate(fd,sizeof(structshared))==-1){perror("ftruncate");close(fd);return1;}// 建立共享内存映射// MAP_SHARED 确保多个进程共享同一块物理内存sh=(structshared*)mmap(NULL,sizeof(structshared),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);if(sh==MAP_FAILED){perror("mmap");close(fd);return1;}// 父进程负责初始化共享数据结构if(sh->initialized==0){// 初始化进程间共享的互斥锁属性pthread_mutexattr_tattr;pthread_mutexattr_init(&attr);// 关键: 设置为进程间共享, 这样多个进程可以使用同一个互斥锁pthread_mutexattr_setpshared(&attr,PTHREAD_PROCESS_SHARED);// 初始化互斥锁if(pthread_mutex_init(&sh->lock,&attr)!=0){perror("pthread_mutex_init");munmap(sh,sizeof(structshared));close(fd);return1;}pthread_mutexattr_destroy(&attr);// 初始化计数器sh->counter=0;// 设置初始化标志, 防止子进程重复初始化sh->initialized=1;printf("Parent: Initialized shared memory\n");}// 创建多个子进程for(i=0;i<PROCESSES;i++){pids[i]=fork();if(pids[i]<0){perror("fork");// 清理已创建的子进程for(intj=0;j<i;j++){kill(pids[j],SIGTERM);}munmap(sh,sizeof(structshared));close(fd);return1;}elseif(pids[i]==0){// 子进程: 执行计数器递增操作printf("Child %d (PID %d): Starting increments\n",i,getpid());for(intj=0;j<PER_PROCESS;j++){// 加锁保护临界区pthread_mutex_lock(&sh->lock);sh->counter++;pthread_mutex_unlock(&sh->lock);}printf("Child %d (PID %d): Completed increments\n",i,getpid());// 子进程退出, munmap 会自动清理映射exit(0);}}// 父进程等待所有子进程完成printf("Parent: Waiting for all children to complete...\n");for(i=0;i<PROCESSES;i++){intstatus;waitpid(pids[i],&status,0);if(WIFEXITED(status)){printf("Parent: Child %d (PID %d) exited with status %d\n",i,pids[i],WEXITSTATUS(status));}}// 验证最终结果printf("Parent: Final counter = %d (expected %d)\n",sh->counter,PROCESSES*PER_PROCESS);if(sh->counter==PROCESSES*PER_PROCESS){printf("Parent: Success! Counter is correct.\n");}else{printf("Parent: Error! Counter mismatch (race condition detected).\n");}// 清理资源pthread_mutex_destroy(&sh->lock);munmap(sh,sizeof(structshared));close(fd);// 可选: 删除共享文件// unlink(path);return0;}说明:
- 使用
MAP_SHARED标志创建共享内存映射, 多个进程可以访问同一块物理内存 - 使用
PTHREAD_PROCESS_SHARED属性的互斥锁, 确保多个进程可以正确同步 - 父进程负责初始化共享数据结构(互斥锁和计数器)
- 多个子进程并发更新共享计数器, 通过互斥锁保证原子性
- 所有进程通过
fork()共享父进程的映射区域, 无需额外文件操作
示例: 多个独立进程通过文件共享 mmap 区域
// writer.c: 写入进程#include<stdio.h>#include<stdlib.h>#include<sys/mman.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>#include<pthread.h>#include<string.h>structshared{pthread_mutex_tlock;charmessage[256];intready;};intmain(){constchar*path="shared_mmap.dat";intfd=open(path,O_RDWR|O_CREAT,0666);if(fd<0){perror("open");return1;}if(ftruncate(fd,sizeof(structshared))==-1){perror("ftruncate");close(fd);return1;}structshared*sh=(structshared*)mmap(NULL,sizeof(structshared),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);if(sh==MAP_FAILED){perror("mmap");close(fd);return1;}// 初始化互斥锁(仅第一次)if(sh->ready==0){pthread_mutexattr_tattr;pthread_mutexattr_init(&attr);pthread_mutexattr_setpshared(&attr,PTHREAD_PROCESS_SHARED);pthread_mutex_init(&sh->lock,&attr);pthread_mutexattr_destroy(&attr);sh->ready=1;}// 写入数据pthread_mutex_lock(&sh->lock);snprintf(sh->message,sizeof(sh->message),"Hello from writer (PID %d)",getpid());printf("Writer: Wrote message\n");pthread_mutex_unlock(&sh->lock);sleep(2);// 等待读取进程读取munmap(sh,sizeof(structshared));close(fd);return0;}// reader.c: 读取进程#include<stdio.h>#include<stdlib.h>#include<sys/mman.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>#include<pthread.h>structshared{pthread_mutex_tlock;charmessage[256];intready;};intmain(){constchar*path="shared_mmap.dat";intfd=open(path,O_RDWR,0666);if(fd<0){perror("open");return1;}structshared*sh=(structshared*)mmap(NULL,sizeof(structshared),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);if(sh==MAP_FAILED){perror("mmap");close(fd);return1;}// 等待写入进程初始化while(sh->ready==0){usleep(100000);// 100ms}// 读取数据pthread_mutex_lock(&sh->lock);printf("Reader: Read message: %s\n",sh->message);pthread_mutex_unlock(&sh->lock);munmap(sh,sizeof(structshared));close(fd);return0;}说明:
- 两个独立的程序通过同一个文件共享 mmap 映射区域
- 写入进程负责初始化互斥锁和数据结构
- 读取进程等待初始化完成后读取数据
- 两个进程可以独立编译和运行, 通过文件系统协调
性能评价
优点
- 高效:零拷贝,直接内存访问
- 虚拟内存:利用虚拟内存机制,按需加载
- 大文件处理:适合处理大文件,无需一次性加载
- 进程间通信:可以用于进程间共享数据
- 灵活性:可以映射文件或创建匿名映射
缺点
- 需要同步:多进程访问需要同步机制
- 复杂性:API相对复杂,需要理解虚拟内存
- 系统限制:受虚拟内存大小限制
- 文件依赖:文件映射依赖文件系统
性能特点
- 延迟:低(直接内存访问)
- 吞吐量:高(零拷贝,适合大文件)
- CPU占用:低(利用虚拟内存机制)
- 内存占用:按需加载,节省内存
适用场景
- ✅ 大文件处理
- ✅ 需要高效文件I/O的场景
- ✅ 进程间共享数据
- ✅ 需要零拷贝的场景
- ✅ 数据库、缓存等应用
- ❌ 小文件简单读写(普通I/O更简单)
- ❌ 不需要共享的场景
注意事项
- 同步机制:多进程访问共享映射时,需要同步机制(如信号量)
- 页大小对齐:注意系统页大小,某些操作需要页对齐
- 文件大小:映射大小不能超过文件大小(除非扩展文件)
- 权限设置:注意prot和flags的权限设置
- 错误处理:注意处理MAP_FAILED返回值
- 资源清理:使用完毕后调用munmap()取消映射
- 同步时机:根据需要使用msync()同步数据到文件
- 匿名映射:匿名映射用于进程间通信时,需要特殊机制共享
扩展阅读
man 2 mmapman 2 munmapman 2 msyncman 7 shm_overview