0%

linux中的内存映射

Linux 内存映射(mmap):将文件直接映射到内存的高效 IO 技术

内存映射(Memory Mapping)是 Linux 内核提供的一种高效文件访问机制,通过将磁盘文件的部分或全部内容映射到进程的地址空间,使进程可以像访问内存一样读写文件,无需通过传统的 read()/write() 系统调用。这种技术在高性能 IO 场景(如数据库、大文件处理)中被广泛应用。

内存映射的基本原理

核心思想

内存映射通过 mmap() 系统调用建立进程地址空间的一块区域磁盘文件的关联:

  • 内核会在进程的虚拟地址空间中分配一段连续的地址范围(称为 “映射区”);
  • 这段地址不直接对应物理内存,而是与目标文件的磁盘区块建立映射关系;
  • 当进程访问映射区时,内核会通过页表将访问转换为对文件的读写操作(按需加载数据到物理内存,即 “页缓存” 机制)。

与传统 IO 的区别

传统文件访问(read()/write())需要经过 “用户态缓冲区 ↔ 内核页缓存 ↔ 磁盘” 的两次数据拷贝,而内存映射简化了流程:

操作方式 数据路径 优势场景
传统 IO 用户缓冲区 ↔ 内核页缓存 ↔ 磁盘 小文件、随机读写、需精确控制 IO
内存映射(mmap) 进程地址空间(映射区) ↔ 内核页缓存 ↔ 磁盘 大文件、顺序读写、频繁访问

核心优势:减少用户态与内核态之间的数据拷贝,提升大文件访问效率。

mmap () 系统调用详解

函数原型

1
2
3
#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
参数说明:
  • addr:指定映射区的起始地址(通常设为 NULL,由内核自动分配);

  • length:映射的文件长度(字节数,必须 ≥ 0);

  • prot:映射区的保护权限(内存访问权限):

  • PROT_READ:可读;

    • PROT_WRITE:可写;
  • PROT_EXEC:可执行;

    • PROT_NONE:无权限。
  • flags:映射类型和行为(关键参数):

    • MAP_SHARED:映射区的修改会同步到文件(进程间共享修改);
    • MAP_PRIVATE:映射区的修改是私有的(Copy-on-Write,不影响原文件);
    • MAP_ANONYMOUS:匿名映射(无关联文件,用于进程间共享内存)。
  • fd:待映射的文件描述符(需先通过 open() 打开);

  • offset:文件映射的起始偏移量(必须是页大小的整数倍,通常为 0)。

返回值:

  • 成功:返回映射区的起始地址(void *);
  • 失败:返回 MAP_FAILED(void *)-1),并设置 errno

配套函数:munmap ()

用于解除内存映射,释放映射区:

1
int munmap(void *addr, size_t length);
  • addrmmap() 返回的映射区起始地址;
  • length:映射区的长度(需与 mmap() 一致);
  • 返回值:0 表示成功,-1 表示失败。

内存映射的关键特性

两种映射模式(flags 参数)

(1)MAP_SHARED(共享映射)
  • 进程对映射区的修改会同步到磁盘文件,且其他映射该文件的进程可见;
  • 适用于需要持久化修改或多进程协作的场景(如共享日志文件)。
(2)MAP_PRIVATE(私有映射)
  • 进程对映射区的修改不会同步到原文件,也不会被其他进程看到;
  • 内核采用 “写时复制(Copy-on-Write)” 机制:仅当进程修改映射区时,才复制物理页,避免影响其他进程;
  • 适用于临时读写文件(如读取配置文件并临时修改)。

匿名映射(MAP_ANONYMOUS

  • 无需关联文件(fd 设为 -1,offset 忽略),映射区的内容在进程退出后消失;

  • 常用于进程间共享内存(结合 fork() 或线程),或需要大块内存临时存储数据的场景;

  • 示例:

    1
    2
    // 分配 4KB 匿名共享内存(可被子进程继承)
    void *addr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);

页缓存与延迟写入

内存映射依赖内核的页缓存(Page Cache) 机制:

  • 首次访问映射区时,内核会将文件内容从磁盘加载到物理内存(页缓存);

  • 进程修改映射区时,内核先更新页缓存,再通过 “延迟写回” 机制异步将脏页(修改过的页)写入磁盘(可通过 msync() 强制同步);

  • msync()函数:手动同步映射区与文件,确保修改持久化:

    1
    2
    int msync(void *addr, size_t length, int flags);
    // flags:MS_SYNC(同步等待写完成)、MS_ASYNC(异步写)、MS_INVALIDATE(丢弃缓存,重新从文件加载)

使用示例:通过 mmap 读写文件

读取文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int main() {
int fd = open("test.txt", O_RDONLY); // 打开文件
struct stat st;
fstat(fd, &st); // 获取文件大小

// 映射文件(只读,共享模式)
char *addr = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) {
perror("mmap failed");
return 1;
}

// 像访问内存一样读取文件内容
printf("File content: %s\n", addr);

// 解除映射并关闭文件
munmap(addr, st.st_size);
close(fd);
return 0;
}

写入文件内容(共享映射)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>

int main() {
int fd = open("test.txt", O_RDWR | O_CREAT, 0644); // 读写方式打开
ftruncate(fd, 1024); // 确保文件大小足够(如创建新文件)

// 映射文件(可读写,共享模式)
char *addr = mmap(NULL, 1024, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) {
perror("mmap failed");
return 1;
}

// 写入数据(直接修改内存)
strcpy(addr, "Hello, mmap!");

// 强制同步到磁盘
msync(addr, 1024, MS_SYNC);

munmap(addr, 1024);
close(fd);
return 0;
}

注意事项与限制

  1. 文件大小与映射长度
    • length 不能超过文件大小(除非文件可扩展,且以 O_RDWR 打开);
    • 映射超过文件大小的部分写入时,可能导致文件扩展(取决于文件系统支持)。
  2. 对齐要求
    • offset 必须是系统页大小(通常 4KB)的整数倍,否则 mmap() 失败(EINVAL)。
  3. 文件描述符关闭
    • 映射建立后,可关闭 fd(映射仍有效),但建议保持打开直至映射解除(便于错误排查)。
  4. 信号安全
    • mmap()munmap() 不是异步信号安全的,避免在信号处理函数中调用。
  5. 性能陷阱
    • 小文件使用 mmap() 可能因页缓存 overhead 导致性能不如传统 IO;
    • 频繁修改小区域时,MAP_PRIVATE 的写时复制可能带来额外开销。

应用场景

内存映射适用于以下场景:

  • 大文件处理:如数据库表文件、日志文件,避免频繁 read()/write() 拷贝;
  • 进程间共享内存:通过 MAP_SHARED 或匿名映射实现高效通信(比管道、消息队列快);
  • 动态加载代码:如操作系统加载可执行文件到内存执行(映射二进制文件并设置 PROT_EXEC);
  • 零拷贝 IO:结合网络编程(如 sendfile()),实现文件数据从页缓存直接发送到网络,避免用户态拷贝

欢迎关注我的其它发布渠道