写在前面
写一下内存映射/虚拟内存方面的内容总结, 主要参考了Linux/Unix系统编程手册.
内存映射可以用于进程间通信(IPC)和其他很多方面.
内存映射
mmap() 系统调用, 在调用进程的虚拟地址空间中创建一个新的内存映射.
分类
- 文件映射: 将一个文件的一部分直接映射到调用进程的虚拟内存中. 一旦一个文件被映射之后, 就可以通过在相应的内存区域中操作字节来访问文件内容了.
映射的分页会在需要的时候从文件中加载(也被称为
基于文件的映射
或者内存映射文件
). - 匿名映射: 没有对应的文件, 这种映射的分页会被初始化为0.
发生文件映射共享的情况
- 两个进程映射了一个文件的同一个区域, 此时这两个进程会共享物理内存的相同分页
- 通过fork()创建的子进程会继承其父进程映射的副本, 并且这些映射所引用的物理内存分页与父进程中相应映射所引用的分页相同.
私有映射和共享映射
- 私有映射:
在映射内容上发生的变更对其他进程不可见, 对于文件映射来说, 变更将不会在底层文件上进行.
-
共享映射:
在映射内容上发生的变更对所有共享在同一个映射的其他进程都可见. 对文件来说, 变更会发生在底层的文件上.
变更的可见性 | 文件映射 | 匿名映射 |
---|---|---|
私有映射 | 根据文件内容初始化内存 | 内存分配 |
共享映射 | 内存映射I/O, 进程间共享内存(IPC) | 进程间共享内存(IPC) |
查看malloc 源码(musl 实现)发现 malloc 用到了两个系统调用, 即 brk 和 mmap, 这里 mmap 主要采用私有匿名映射实现.
创建内存映射: mmap
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
// 成功: 返回映射的起始地址, 失败返回MAP_FAILED
- addr: 映射被放置的虚拟地址, 若为NULL, 则内核会为映射选择一个合适的地址. 如果为非NULL值, 内核会在选择将映射放置在何处时将这个参数值作为一个提示信息来处理.
- length: 映射的字节数(如果不足一个分页大小, 向上提升为一个分页大小的下一个倍数)
- prot: 位掩码, 指定了施加于映射上的保护信息.
- flags: 设置私有或者共享
- fd和offset, 都是针对文件映射而言, 匿名映射会忽略.
创建私有文件映射
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
char *addr;
int fd;
struct stat sb;
fd = open("aa", O_RDONLY);
assert(fd != -1);
assert(fstat(fd, &sb) != -1);
addr = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
assert(addr != MAP_FAILED);
assert(write(STDOUT_FILENO, addr, sb.st_size) == sb.st_size);
return 0;
}
存一个文本文件aa
, 内容随意, 上述程序通过mmap
系统调用实现了一个简易的cat
命令.
这里有个小坑, 就是如果内存映射的文件为源码文件, 则会 core-dump, 这个情况我不太理解.
解除映射区域munmap
#include <sys/mman.h>
int munmap(void *addr, size_t length); // 0: success, -1: error
- addr: 待解除映射的地址范围的起始地址, 必须与一个分页边界对齐
- length: 非负整数, 指定了待解除映射区域的大小(字节数)
同时也可以解除部分映射, 或者跨越多个区域解除.
文件映射
- 获取文件的一个描述符, 通过open调用返回
- 将文件描述符作为fd参数传入mmap
执行之后, mmap会将打开的文件的内容映射到调用进程的地址空间中, 一旦mmap被调用之后, 就能够关闭文件描述符了, 而不会对映射产生任何影响.
mmap还能映射真实和虚拟设备的内容, 如磁盘和
/dev/mem
.
私有文件映射
用途:
- 允许多个执行同一个程序或使用同一个共享库的进程共享同样的(只读)文本段, 它是从底层可执行文件或库文件的相应部分映射而来的.
- 映射一个可执行文件或共享库的初始化数据段. 这种映射会被处理成私有, 而使得对映射数据段内容的变更不会发生在底层文件上.
共享文件映射
例子:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#define MEM_SIZE 10
int main(int argc, char *argv[]) {
char *addr;
int fd;
fd = open("s.txt", O_RDWR);
addr = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
assert(addr != MAP_FAILED);
close(fd);
printf("string is %.*s\n", MEM_SIZE, addr);
memset(addr, 0, MEM_SIZE);
/* char *str = "hello"; */
char *str = "world";
strncpy(addr, str, MEM_SIZE - 1);
msync(addr, MEM_SIZE, MS_SYNC);
printf("copied \"%s\" to shared memory\n", str);
return 0;
}
首先通过dd
命令与/dev/zero
设备文件 创建一个全空的文件:
/dev/zero
: 一个虚拟设备文件, 从中读取数据时总是返回0, 写入到这个设备中的数据总会被丢弃
dd if=/dev/zero of=s.txt bs=1 count=1024
然后执行一次(解注释21行, 注释22)程序, 再注释21行, 解注释22执行, 会得到第一次写入的内容: “hello”.
同步映射区域: msync()
虽然内核会自动将发生在MAP_SHARED映射内容上的变更写入底层文件中, 但在默认情况下, 内核不保证这种同步操作会在何时发生. msync() 系统调用的作用就是显式控制应用程序何时完成共享映射与映射文件之间的同步.
并且允许一个应用程序确保在可写入映射上发生的更新会对在该文件上执行read()的其他进程可见.
#include <sys/mman.h>
int msync(void *addr, size_t length, int flags); // return 0: success, -1: error
flags可以取:
- MS_SYNC: 执行同步文件写入, 设置此flag会阻塞直到内存区域中所有被修改过的分页被写入到底盘为止.
- MS_ASYNC: 异步文件写入, 内存区域中被修改过的分页会在后面某一个时刻被写入磁盘并立即对在响应文件区域中执行read()的其他进程可见.
匿名映射
创建匿名映射的两种方式:
- MAP_ANONYMOUS标志
- 设备文件
/dev/zero
重新映射: mremap()
Linux特定的调用, 不可移植
#include <sys/mman.h>
void* mremap(void *old_addr, size_t old_size, size_t new_size, int flags, ...);
// 返回重映射区域的起始地址, 或者MAP_FAILED(失败)
共享内存
共享内存是最高效的IPC机制, 因为它不涉及进程之间的任何数据传输, 这种高效率带来的问题是, 必须用其他辅助手段同步进程对共享内存的访问, 否则会出现竞态条件.
下面谈到的共享内存均指POSIX共享内存, 而不是System V共享内存.
共享内存能够让无关进程共享一个映射区域, 而无需创建一个相应的映射文件.
使用POSIX共享内存前的准备
- 使用
shm_open()
打开一个与指定的名字对应的对象, 该调用与open()类似, 会创建一个新共享对象或者打开一个既有对象. 返回结果: 引用该对象的文件描述符fd. - fd传入mmap()调用, 并在flags参数中指定MAP_SHARED, 这会将共享内存对象映射到进程的虚拟地址空间.
创建共享内存对象