《Linux C编程实战》笔记:mmap

其实这书里根本没有mmap相关内容,只是也是Linux相关内容,一并归入Linux宇宙介绍了。

mmap() 会在进程的虚拟地址空间中创建一段 内存区域(VMA),并且这段区域可以:

  • 映射一个文件

  • 映射匿名内存(当作 malloc 替代)

  • 映射共享内存(跨进程通信)

  • 映射设备文件

  • 使用 copy-on-write 技术实现 fork 高效复制

再简单讲:

mmap = 给你一片虚拟地址,背后可以连到文件、内存或其他资源。

mmap 函数原型

cpp 复制代码
#include <sys/mman.h>
void *mmap (void *__addr, size_t __len, int __prot,
		   int __flags, int __fd, __off_t __offset)

直观理解:在文件 FD 中从偏移量 OFFSET 开始,映射长度为 LEN 字节的地址

返回值为实际选择的映射地址,或者在出错时为 MAP_FAILED(此时会设置 `errno')。成功的 `mmap' 调用会释放受影响区域的任何先前映射。

addr ------ 建议映射地址(一般写 NULL)

  • NULL 让内核自动选择一个合适的地址

  • 也可以指定地址 请求 映射到某一处(不建议)

length ------ 映射大小

必须是 页对齐 (4KB 的整数倍)。

如果不是,内核会自动向上取整。

prot ------ 页权限

意义
PROT_READ 可读
PROT_WRITE 可写
PROT_EXEC 可执行
PROT_NONE 不可访问

protmmap / mprotect 中是 int 类型的掩码(可以 | 组合):

cpp 复制代码
mmap(NULL, size, PROT_READ | PROT_WRITE, ... );
  • PROT_WRITE 不等于能写文件

    • 要把映射写回到磁盘,必须使用 MAP_SHAREDMAP_PRIVATE 写入只影响私有副本(COW),不修改文件。

    • 而如果 fd 是只读打开(O_RDONLY),内核可能拒绝 PROT_WRITE 映射(权限检查)。

  • PROT_EXEC 受安全策略影响

    • 即使你传了 PROT_EXEC,系统安全模块(SELinux、PaX、grsecurity、W^X)或 CPU 上的 NX bit 也可能阻止实际执行。

    • JIT 引擎常用"写时移除 EXEC,写完后设回 EXEC"的策略。

  • PROT_WRITE 常与 PROT_READ 一起用

    • 虽然位是独立的,但一些系统不允许 PROT_WRITE 而没有 PROT_READ;在可移植代码里建议 PROT_READ|PROT_WRITE
  • mprotect 可改变权限

    • mprotect(addr, len, PROT_* ) 用于修改映射权限;改变为更宽松权限可能因为进程权限/文件权限而失败(EACCES 等)。

flags ------ 控制映射行为

① 共享/私有映射(必须二选一)

flag 意义
MAP_SHARED 写会同步到文件,多个进程共享
MAP_PRIVATE Copy-on-write,写不会改变文件

这是最重要的 flag,决定行为完全不同。

② 匿名映射(不映射文件)

MAP_ANONYMOUS或者MAP_ANON(这两宏是一样的)

此时 fd 必须为 -1offset 必须为 0

③ 其它常用 flags

flag 意义
MAP_FIXED 强制使用 addr(危险,会覆盖已有映射)
MAP_POPULATE 提前分配页框,避免 page fault
MAP_LOCKED 将页锁在内存,不允许换出
MAP_HUGETLB 使用大页(Huge Page)

fd ------ 文件描述符

如果是文件映射:fd就用open打开的文件描述符

如果是匿名映射:fd直接赋值-1

offset ------ 文件偏移

必须按页对齐。

例如 offset = 4096、8192 等。

什么是匿名映射?

mmap() 中,映射分两类:

1. 文件映射(File-backed mapping)

  • 使用 真实文件 做后端存储

  • 调用方式一般类似:

cpp 复制代码
int fd = open("data.bin", O_RDWR);
void* p = mmap(nullptr, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

特点:

  • 修改内存 → 写到文件

  • 文件内容被映射进内存

  • 文件大小必须足够大,否则会 SIGBUS(访问超界)

2. 匿名映射(Anonymous Mapping)

匿名映射 没有对应的文件 ,内核从 swap 分配私有内存页

创建方式:

cpp 复制代码
void* p = mmap(nullptr, size,
               PROT_READ | PROT_WRITE,
               MAP_PRIVATE | MAP_ANONYMOUS,
               -1, 0);

特点:

  • 没有文件后端,fd 必须为 -1

  • 内容初始全是 0

  • 修改不会写回任何地方

  • 对进程来说就像是 malloc() 分配的一大块内存

匿名映射 = malloc 的更底层版本

实际上

  • malloc 小块:用 brk 扩展堆

  • malloc 大块:用 mmap 匿名映射

应用场景:

  • 操作大块连续内存(大 buffer)

  • 实现共享内存(如果加 MAP_SHARED)

munmap

cpp 复制代码
int munmap(void *addr, size_t length);

取消映射

必须与 size 对应,否则会错误或部分解除。

mprotect

cpp 复制代码
int mprotect(void *addr, size_t len, int prot);

修改映射区域权限。可以把区域改为只读、可写、不可执行等。

msync

cpp 复制代码
int msync (void *__addr, size_t __len, int __flags);

刷新到磁盘。用于 MAP_SHARED

常量 意义
MS_SYNC 同步写
MS_ASYNC 异步写
MS_INVALIDATE 丢弃缓存重新读磁盘

madvise

cpp 复制代码
int madvise (void *__addr, size_t __len, int __advice)

优化内存访问模式

常见:

常量 意义
MADV_RANDOM 随机访问
MADV_SEQUENTIAL 顺序访问
MADV_WILLNEED 内核提前预读
MADV_DONTNEED 这段不用了,可以回收页

示例代码1

通过mmap的方式追加写入文件

cpp 复制代码
#include<sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include<unistd.h>
#include <string.h>
int main() {
	//创建并打开一个文件
	int fd = open("test.txt", O_RDWR | O_CREAT|O_APPEND, S_IRUSR | S_IWUSR | S_IXUSR);
    //计算当前文件大小
	int fsize = lseek(fd, 0, SEEK_END);
	const char* msg = "hello mmap\n";
	size_t len = strlen(msg);
	size_t total_len = fsize + len;
	// 关键步骤:先扩展文件大小!!
	ftruncate(fd, total_len);
	//通过mmap的方式读入内存,注意用了MAP_SHARED标志,可以写入文件
	char* addr = static_cast<char*>(mmap(NULL, total_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));
	//把这句话写入文件末尾
	strcpy(addr+ fsize, msg);
	//同步写
	msync(addr, total_len, MS_SYNC);
	//取消映射
	munmap(addr, total_len);
	close(fd);
}

mmap 的底层原理(进程虚拟内存视角)

cpp 复制代码
+----------------------+  <-- high address
|   stack              |
+----------------------+
|   shared libraries   |
+----------------------+
|   heap (malloc)      |
+----------------------+
|   mmap region        | <--- mmap 返回的地址
+----------------------+
|   bss / data         |
+----------------------+
|   text               |
+----------------------+  <-- low address

每一次 mmap 会创建一个 VMA(Virtual Memory Area)。

当你第一次访问对应页面时:

  1. 发生 page fault

  2. 内核为该页分配物理页

  3. 如果映射的是文件,加载该页内容

  4. 返回用户态继续执行

示例代码2:匿名共享内存

匿名映射没有路径,没有文件,所以不同进程 不能通过独立的 mmap 调用做到共享

所以这个共享只能在fork层面的父子进程实现

子进程继承了整个虚拟地址空间:

cpp 复制代码
父   addr ----+
               | 指向同一物理页
子   addr ----+

因此父子进程:

  • 访问的是同一个缓冲区

  • 读写互相可见

  • 不需要管 fd

cpp 复制代码
#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main() {
	const int SIZE = 4096;

	char* addr = (char*)mmap(
		NULL,
		SIZE,
		PROT_READ | PROT_WRITE,
		MAP_SHARED | MAP_ANONYMOUS,
		-1,
		0
	);

	if (addr == MAP_FAILED) {
		perror("mmap");
		return 1;
	}

	pid_t pid = fork();

	if (pid == 0) {
		// 子进程
		sleep(1);  // 等父进程写
		printf("child sees: %s\n", addr);
		strcpy(addr, "Hello from child!");
	}
	else {
		// 父进程
		strcpy(addr, "Hello from parent!");
		sleep(2);  // 等子进程写回
		printf("parent sees: %s\n", addr);
	}
    //父子进程都要调用
	munmap(addr, SIZE);
	return 0;
}
cpp 复制代码
child sees: Hello from parent!
parent sees: Hello from child!

证明父子进程共享同一片物理内存。

注意,如果子进程使用exec系列函数,转到其他进程,子进程会完全丢弃当前进程的整个虚拟地址空间,并用新程序的 ELF 文件重新创建新的地址空间。这时候mmap创建的匿名内存就不能用了

还有一点,父子进程都可以(也应该)调用 munmap

munmap(addr, SIZE) 的作用是:

  • 解除当前进程虚拟地址 → 物理页 的映射

  • 但并不会影响:

    • 其他进程的映射

    • 共享物理页的存在

也就是说:

  • 父进程 munmap:只影响父进程自己

  • 子进程 munmap:只影响子进程自己

  • 物理共享页什么时候释放?

    最后一个持有映射的进程也 munmap 后,物理页才真正被释放

所以,父子进程都 munmap 是完全正常和正确的

有点类似C++的共享指针的设计

这个例子用sleep实现同步,其实不太合理,可以使用信号量的方式

cpp 复制代码
sem_t *sem = mmap(NULL, sizeof(sem_t),
                  PROT_READ | PROT_WRITE,
                  MAP_SHARED | MAP_ANONYMOUS,
                  -1, 0);
sem_init(sem, 1, 0); // 第二个参数 1 表示可跨进程共享
相关推荐
小尧嵌入式2 小时前
深入理解C/C++指针
java·c语言·开发语言·c++·qt·音视频
呆萌小新@渊洁2 小时前
Linux离线环境安装ffmpeg
linux·ffmpeg·php
梦想的旅途22 小时前
Hook技术与内存注入在企业微信外部群数据获取中的技术与风险分析
linux·运维·服务器
ULTRA??2 小时前
字符串处理小写字母转换大写字母
c++·python·rust
fish_xk2 小时前
c++的字符串string
开发语言·c++
DeltaTime2 小时前
一 图形学概述, 线性代数
c++·图形渲染
robator2 小时前
ubuntu 22.04 升级nvidia显卡驱动、cuda 和cudnn
linux·服务器·ubuntu
肖恭伟2 小时前
Pycharm历史community版本下载
linux·ubuntu·pycharm·下载·community
牛奶咖啡132 小时前
Linux中搭建Samba服务并实现共享目录的配置及其不同策略授权访问操作实践教程
linux·samba服务的安装部署·samba共享目录和权限的配置·特殊场景共享目录授权·smb的匿名用户访问共享目录·smb的指定用户访问共享目录·强制清除smb缓存