系统调用(mmap和munmap)
mmap
内存映射类型
mmap() 系统调用用于在调用进程的虚拟地址空间中 创建内存映射,主要分为两种类型:
-
文件映射:将文件的一部分直接映射到虚拟内存中,允许通过内存访问文件内容,映射的分页会在需要时自动加载
-
匿名映射:没有对应文件,分页初始化为0,可以视为一个内容总是为0的虚拟文件映射
映射内存可以被多个进程共享,具体情况包括:
- 共享映射(
MAP_SHARED):修改内容对所有共享进程可见,直接影响底层文件 - 私有映射(
MAP_PRIVATE):修改内容对其他进程不可见,使用 写时复制 技术确保每个进程的修改独立
映射类型总结
| 映射类型 | 变更可见性 | 主要用途 |
|---|---|---|
| 私有文件映射 | 不可见 | 初始化内存区域,如文本和数据段 |
| 私有匿名映射 | 不可见 | 分配零填充内存 |
| 共享文件映射 | 可见 | 内存映射 I/O,进程间共享内存 (IPC) |
| 共享匿名映射 | 可见 | 进程间共享内存 (IPC),仅限相关进程 |
其他说明
-
通过
fork()创建的子进程会继承映射,但在执行exec()时映射会丢失 -
每次调用
mmap()创建的新映射是独立的,尤其是在匿名映射的情况下
函数原型
#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_READ:可读
- PROT_WRITE:可写
- PROT_EXEC:可执行
- PROT_NONE:不可访问
- flags:映射的选项,例如:
- MAP_SHARED:共享映射,多个进程可以访问相同的映射
- MAP_PRIVATE:私有映射,写入时复制,写入不会影响原文件
- MAP_ANONYMOUS:不与任何文件关联的匿名映射
- fd:要映射的文件描述符,使用
open系统调用获取 - offset:文件映射的起始偏移量,必须是页面大小的倍数
-
返回值:
- 成功时返回映射区域的指针,失败时返回 MAP_FAILED,并设置 errno
munmap
munmap() 系统调用用于从进程的虚拟地址空间中 删除一个映射
函数原型
#include <sys/mman.h>
int munmap(void *addr, size_t length);
-
参数说明:
- addr:待解除映射的起始地址,必须与分页边界对齐
- length:指定解除映射区域的大小,必须为非负整数,通常应为系统分页大小的倍数
-
返回值:
- 如果指定范围内不存在映射,
munmap()将无效并返回 0(表示成功)
- 如果指定范围内不存在映射,
补充说明
-
解除映射:
- 通常解除整个映射,可以将
addr设置为mmap()返回的地址,并使length与mmap()使用的值相同 - 也可以部分解除映射,可能导致映射收缩或分割
- 通常解除整个映射,可以将
-
内存锁:
- 解除映射时,内核会删除在指定范围内的所有内存锁(由
mlock()或mlockall()创建)
- 解除映射时,内核会删除在指定范围内的所有内存锁(由
-
自动解除:
- 进程终止或执行
exec()时,所有映射会自动解除
- 进程终止或执行
-
注意事项:
- 在解除共享文件映射之前,应先调用
msync()确保内容写入底层文件
- 在解除共享文件映射之前,应先调用
文件映射

这张图表示由参数offset和length决定哪些文件区域被映射到虚拟内存中
共享/私有文件映射
多个进程共享同一区域的内存映射,共享文件映射 所有的修改都是可见的,同时也会反映到底层文件,私有文件映射 的修改仅调用进程自己可见,并且不会反应到底层文件,这是使用 写时复制 的技术实现,即,当要对该内存映射做修改时,内核会复制一份相同的给进程,从而使其真正的独立出来

内存映射I/O
内存映射 I/O 是一种将文件的内容映射到进程的虚拟内存地址空间的技术,使得程序可以通过 直接访问内存来执行文件 I/O 操作,而无需使用传统的 read() 和 write() 系统调用
关键特点
-
共享文件映射:映射的内存内容源自文件,并且对映射内容的任何更改都会自动反映到文件中。这意味着可以通过简单的内存访问操作来进行文件 I/O
-
结构化数据类型:通常,程序会定义一个结构化数据类型,与磁盘文件的内容对应,以便于访问和处理映射的内存内容
举个例子,假设磁盘文件中存储的是员工信息,那我可以先定义一个员工结构体
struct Employee { int id; // 员工ID char name[50]; // 员工姓名 float salary; // 员工薪资 };一旦文件被映射到内存,程序可以直接通过访问结构体的字段来读取和修改员工信息,而不需要手动处理字节的偏移和数据格式
struct Employee *employees = mmap(NULL, fileSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); printf("Employee ID: %d\n", employees[0].id); // 直接访问第一个员工的ID
优势
-
简化应用逻辑:使用内存访问替代传统的
read()和write()调用,可以使一些应用程序的逻辑更为简洁和直观。 -
提高性能:
- 减少数据传输:传统的
read()和write()操作需要两次数据传输:一次在文件和内核缓冲区之间,另一次在内核缓冲区和用户空间之间。而使用mmap()可以省去第二次传输,用户进程可以直接访问内存中的数据。 - 共享内存:当使用
mmap()时,内核空间和用户空间共享同一个缓冲区,避免了在用户空间和内核空间之间复制数据的开销。如果多个进程在同一文件上进行 I/O,它们可以共享同一内核缓冲区,从而节省内存使用。
- 减少数据传输:传统的
边界情况
-
文件内容更大,超过映射区域
![3]()
-
文件内容小于映射区域
![4]()
同步映射区域:msync()
msync() 系统调用用于 显式控制共享内存映射与底层文件之间的同步。虽然内核会自动将 MAP_SHARED 映射内容的更改写入文件,但默认情况下并 不保证同步的时间
作用
- 数据完整性:在数据库等应用中,调用
msync()可以强制将数据写入磁盘,以确保数据完整性 - 可见性:确保在可写映射上进行的更新对执行
read()的其他进程可见
函数原型
#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);
-
参数
- addr:需要同步的内存区域的起始地址,必须分页对齐
- length:同步区域的大小,向上舍入到系统分页大小的下一个整数倍
- flags:指定同步行为,可以是以下值:
- MS_SYNC:执行同步写入,调用会阻塞直到所有修改的数据页写入磁盘
- MS_ASYNC:执行异步写入,修改的数据页将在将来的某个时间写入磁盘,立即对其他进程可见
- MS_INVALIDATE:使映射数据的缓存副本失效,确保下一次访问时从文件读取更新的内容
匿名映射
匿名映射
匿名映射是一种没有对应文件的内存映射。可以通过以下两种方式在Linux中创建匿名映射:
-
使用
MAP_ANONYMOUS:- 在
mmap()的flags中指定MAP_ANONYMOUS,同时将fd设置为 -1。这个值会被忽略,但为可移植性,建议遵循这个约定 - 需要在代码中定义
_BSD_SOURCE或_SVID_SOURCE来使用MAP_ANONYMOUS
- 在
-
使用
/dev/zero:- 打开
/dev/zero设备文件并将其文件描述符传递给mmap()。该设备始终返回0,写入的数据会被丢弃
- 打开
无论使用哪种方法,得到的映射都会被初始化为0,且 offset 参数会被忽略
匿名映射类型
-
MAP_PRIVATE 匿名映射:
- 用于分配进程私有的内存块,并将其初始化为0。
glibc的malloc()函数在分配大于MMAP_THRESHOLD(默认128 KB,可调整)的内存时使用此映射,以提高内存管理效率并减少内存碎片
-
MAP_SHARED 匿名映射:
- 允许相关进程(如父子进程)共享一块内存区域而无需对应的映射文件
- 如果在创建共享映射后调用
fork(),子进程会继承该映射,从而实现进程间的内存共享
代码示例
#ifdef USE_MAP_ANON
#define _BSD_SOURCE // 获取 USE_MAP_ANON 定义
#endif
int *addr; // 假设为int
#ifdef USE_MAP_ANON
addr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS, -1, 0);
#else
int fd = open("/dev/zero", O_RDWR);
addr = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
#endif
内存锁
修改内存保护信息
mprotect()用于动态修改虚拟内存区域的权限。这在实现某些安全策略或内存保护机制时非常有用
函数原型
int mprotect(void *addr, size_t len, int prot);
参数
addr: 要修改保护属性的内存区域的起始地址。该地址必须是页面边界对齐的len: 要保护的字节数,通常是页面大小的倍数prot: 新的保护标志,可以是以下值的组合:PROT_READ: 可读PROT_WRITE: 可写PROT_EXEC: 可执行PROT_NONE: 无权限
使用示例
void *addr = mmap(NULL, pagesize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// 修改保护属性为只读
mprotect(addr, pagesize, PROT_READ);
// 试图写入将导致段错误
strcpy(addr, "Hello, World!");
内存加锁和解锁
内存加锁就是把指定的页固定到内存中
mlock()和mlockall()用于防止特定的内存页被换出到磁盘,这在需要高性能或低延迟的场合非常重要
munlock()和munlockall()用于解锁特定区域的内存页
函数原型
mlock()
int mlock(const void *addr, size_t len);
mlockall()
int mlockall(int flags);
munlock()
int munlock(const void *addr, size_t len);
munlockall()
int munlockall(void);
参数
mlock()和munlock():addr: 要锁定/解锁的内存区域的起始地址len: 要锁定/解锁的字节数
锁定/解锁的内存区域是以页为单位,也就是只会解锁/锁定整数倍的页
mlockall() 和 munlockall():flags: 锁定的范围,通常是以下之一:MCL_CURRENT: 锁定当前进程的所有已映射内存MCL_FUTURE: 锁定当前进程未来映射的所有内存
使用示例
size_t pagesize = getpagesize();
char *buf = mmap(NULL, pagesize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
mlock(buf, pagesize); // buf 现在被锁定在物理内存中
munlock(buf, pagesize);
mincore()
mincore()用于检查某个内存区域的页面是否驻留在物理内存中。返回的vec数组中的每一位代表一个页面的状态,这在内存管理和优化方面非常有用
函数原型
int mincore(void *addr, size_t len, unsigned char *vec);
参数
addr: 要检查的内存区域的起始地址len: 要检查的字节数vec: 指向字节数组的指针,每个字节对应一个页面,指示该页面是否驻留在物理内存中
使用示例
size_t pagesize = getpagesize();
char *buf = mmap(NULL, pagesize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
unsigned char vec[1]; // 一个字节表示一个页面的状态
mincore(buf, pagesize, vec);
printf("Page is %s in physical memory\n", (vec[0] & 1) ? "resident" : "not resident");
// vec中的字节,仅最低位有效,1表示存在,0表示不存在
munmap(buf, pagesize);
补充说明
- 内存锁操作的是虚拟页
- 内存锁主要是 操作虚拟页,而由于虚拟页和物理页之间的映射关系,内存锁的效果是确保某些 虚拟页对应的物理页 不会被换出
- 内存锁的删除
- 显示的调用
munlock()或munlockall() - 在进程终止时
- 当被锁住的分页通过
munmap()被解除映射时。 - 当被锁住的分页被使用
mmap()MAP_FIXED标记的映射覆盖时
- 显示的调用
- 内存锁语义
-
继承与保留:
- 内存锁不会在通过
fork()创建的子进程中继承,也不会在exec()执行期间保留
- 内存锁不会在通过
-
共享内存锁:
- 当多个进程共享相同的分页(例如通过
MAP_SHARED映射)时,只要至少有一个进程保持对这些分页的内存锁,这些分页就会被锁定在内存中
- 当多个进程共享相同的分页(例如通过
-
锁的唯一性:
- 内存锁是独立的,不会在单个进程中叠加。如果一个进程在同一虚拟地址区域多次调用
mlock(),只有一个锁会被建立,且只需一个munlock()调用即可解除锁
- 内存锁是独立的,不会在单个进程中叠加。如果一个进程在同一虚拟地址区域多次调用
-
多重映射的影响:
- 如果同一组分页(相同的文件)在一个进程中被多次映射到不同的位置,并且分别对这些映射进行加锁,则这些分页会保持在 RAM 中,直到所有映射都被解锁
-
锁的逻辑不正确性:
- 由于内存锁的单位是分页且不能叠加,单独对 同一虚拟分页 上的不同数据结构(如指针
p1和p2指向的结构)进行mlock()和munlock()调用在逻辑上是错误的。尽管所有调用都可能成功,但最终会导致整个分页被解锁,从而影响所有指向该分页的数据结构
- 由于内存锁的单位是分页且不能叠加,单独对 同一虚拟分页 上的不同数据结构(如指针
-


posted on
浙公网安备 33010602011771号