文章目录
前言
内存映射:高效的数据访问与共享的基石
在现代计算体系结构中,内存映射是一项核心技术和编程策略,它极大地提升了数据访问的效率,促进了进程间的高效通信。本文旨在深入浅出地介绍内存映射的基本概念、工作原理、应用场景以及其实现机制,帮助读者理解这一强大工具的精髓。
一、内存映射基础
内存映射(Memory Mapping)是一种将文件或匿名内存区域直接映射到进程虚拟地址空间的技术。这意味着文件内容不再是通过传统的I/O操作逐块读取到缓冲区,而是直接映射为进程地址空间中的一部分,使得对文件的访问就如同访问普通内存一样快速和直接。
二、工作原理
1.地址空间布局:
每个进程都有自己的虚拟地址空间,由操作系统管理。内存映射过程首先会在进程的虚拟地址空间中预留一块区域。
2.映射过程:
通过系统调用(如Unix/Linux下的mmap())指定文件的起始位置、映射长度、访问权限等参数,操作系统会建立文件物理地址与进程虚拟地址空间中预留区域的映射关系。这意味着,当进程试图访问这个虚拟地址时,CPU硬件和操作系统会透明地将请求转换为对相应文件物理块的访问。
3.缓存机制:
为了进一步提升效率,大多数操作系统采用了页缓存(Page Cache)机制。即使是对磁盘文件的映射,操作系统也会尽可能地缓存经常访问的页面到物理内存中,从而减少实际的磁盘I/O操作。
三、主要优点
1高性能:
内存映射减少了数据复制次数,特别是对于大文件操作,避免了传统I/O的多次读写开销,显著提高了数据读写速度。
2.灵活的内存管理:
映射区域可以按需增长或缩小,操作系统负责管理物理内存与虚拟地址空间的对应关系,简化了内存管理的复杂度。
3.方便的共享:
通过设置映射为共享(如MAP_SHARED标志),多个进程可以映射到相同的物理内存区域,实现高效的数据共享,这对于多进程协作和通信至关重要。
4.零拷贝:
在某些情况下,内存映射还能支持零拷贝操作,直接在内核空间中传递数据,避免了用户态到内核态再到用户态的多次数据复制,进一步提升效率。
四、应用场景
1.大文件处理:
如数据库管理系统、图像处理软件,通过内存映射直接操作大型文件,实现快速访问。
2.动态库加载:
许多操作系统使用内存映射技术加载动态链接库到进程空间,加速程序启动。
3.进程间通信:
通过共享内存段进行高速数据交换,特别是在需要频繁交换大量数据的场景下。
4.内存映射日志:
在日志记录系统中,利用内存映射可以实现几乎实时的日志写入,同时保持低延迟。
五、代码实现
1.函数介绍
#include <sys/mman.h>
// 创建内存映射区
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数说明
addr:指向希望映射区域开始的地址,通常设为NULL让系统自动选择合适的地址。
length:映射区域的长度,即映射的字节数。
prot:指定映射区域的访问权限,可以是PROT_EXEC(可执行)、PROT_READ(可读)、PROT_WRITE(可写)、PROT_NONE(不可访问)的组合。
flags:映射标志,常用的是:
MAP_SHARED:映射区域可以被多个进程共享,对映射区域的修改会反映到文件或其他共享映射中。
MAP_PRIVATE:创建一个映射副本,对映射区域的修改不会影响原文件。
fd:当映射文件时,此参数为文件描述符;如果是匿名映射(不关联文件),则通常设为-1。
offset:映射文件时,映射开始的偏移量,必须是页大小的整数倍。
返回值
成功时返回映射区域的首地址,失败时返回MAP_FAILED(通常为(void *) -1)并设置errno。
2.有血缘关系的进程间通信示例 - 使用内存映射
#include <sys/mman.h>
#include <fcntl.h>
int main()
{
// 1. 打开一个磁盘文件
int fd = open("./english.txt", O_RDWR);
// 2. 创建内存映射区
void* ptr = mmap(NULL, 4000, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
// 3. 创建子进程
pid_t pid = fork();
if(pid > 0)
{
// 父进程, 写数据
const char* pt = "写入的内容";
memcpy(ptr, pt, strlen(pt)+1);
}
else if(pid == 0)
{
// 子进程, 读数据
usleep(1); // 内存映射区不阻塞, 为了让子进程读出数据
printf("从映射区读出的数据: %s\n", (char*)ptr);
}
// 释放内存映射区
munmap(ptr, 4000);
return 0;
}
3.无血缘关系的进程间通信示例 - 使用内存映射
对于没有血缘关系的进程间通信,需要在每个进程中分别创建内存映射区,但是这些进程的内存映射区必须要关联相同的磁盘文件,这样才能实现进程间的数据同步。
写进程的代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
int main()
{
// 1. 打开一个磁盘文件
int fd = open("./english.txt", O_RDWR);
// 2. 创建内存映射区
void* ptr = mmap(NULL, 4000, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
const char* pt = "写入的内容";
memcpy(ptr, pt, strlen(pt)+1);
// 释放内存映射区
munmap(ptr, 4000);
return 0;
}
读进程的代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/mman.h>
#include <fcntl.h>
int main()
{
// 1. 打开一个磁盘文件
int fd = open("./english.txt", O_RDWR);
// 2. 创建内存映射区
void* ptr = mmap(NULL, 4000, PROT_READ|PROT_WRITE,
MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED)
{
perror("mmap");
exit(0);
}
// 读内存映射区
printf("从映射区读出的数据: %s\n", (char*)ptr);
// 释放内存映射区
munmap(ptr, 4000);
return 0;
}
4.文件拷贝 - 使用内存映射
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
int main()
{
// 1. 打开一个操盘文件english.txt得到文件描述符
int fd = open("./english.txt", O_RDWR);
// 计算文件大小
int size = lseek(fd, 0, SEEK_END);
// 2. 创建内存映射区和english.txt进行关联, 得到映射区起始地址
void* ptrA = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
if(ptrA == MAP_FAILED)
{
perror("mmap");
exit(0);
}
// 3. 创建一个新文件, 存储拷贝的数据
int fd1 = open("./copy.txt", O_RDWR|O_CREAT, 0664);
// 拓展这个新文件
ftruncate(fd1, size);
// 4. 创建一个映射区和新文件进行关联, 得到映射区的起始地址second
void* ptrB = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd1, 0);
if(ptrB == MAP_FAILED)
{
perror("mmap----");
exit(0);
}
// 5. 使用memcpy拷贝映射区数据
// 这两个指针指向两块内存, 都是内存映射区
// 指针指向有效的内存, 拷贝的是内存中的数据
memcpy(ptrB, ptrA, size);
// 6. 释放内存映射区
munmap(ptrA, size);
munmap(ptrB, size);
close(fd);
close(fd1);
return 0;
}
使用内存映射区拷贝文件思路:
1.打开被拷贝文件,得到文件描述符 fd1,并计算出这个文件的大小 size
2.创建内存映射区A并且和被拷贝文件关联,也就是和fd1关联起来,得到映射区地址 ptrA
3.创建新文件,得到文件描述符 fd2,用于存储被拷贝的数据,并且将这个文件大小拓展为 size
4.创建内存映射区B并且和新创建的文件关联,也就是和fd2关联起来,得到映射区地址 ptrB
5.进程地址空间之间的数据拷贝,memcpy(ptrB, ptrA,size),数据自动同步到新建文件中
6.关闭内存映射区
注:本文为学习大丙爱编程笔记。