内存映射

创建内存映射区

如果要实现进程间通信,可以通过函数创建一块内存映射区,和管道不同的是管道对应的内存空间在内核中,而内存映射区对应的内存空间在进程的用户区(用于加载动态库的那个区域),也就是说进程间通信使用的内存映射区不是一块,而是在每个进程内部都有一块。
由于每个进程的地址空间是独立的,各个进程之间也不能直接访问对方的内存映射区,需要通信的进程需要将各自的内存映射区和同一个磁盘文件进行映射,这样进程之间就可以通过磁盘文件这个桥梁完成数据的交互。

内存映射区
磁盘文件数据可以完全加载到进程的内存映射区,也可以部分加载到进程的内存映射区。当进程 A 中的内存映射区数据被修改了,数据会被自动同步到磁盘文件,同时和磁盘文件建立映射关系的其他进程内存映射区中的数据也会和磁盘文件进行数据的实时同步,这个同步机制保证了各个进程之间的数据共享。

使用内存映射区既可以用于有血缘关系的进程间通信,也可以用于没有血缘关系的进程间通信。
内存映射区的函数原型如下:

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
29
30
31
32
33
34
35
36
37
38
39
#include <sys/mman.h>
// 创建内存映射区
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
/*
参数:
addr:
从动态库加载区的什么位置开始创建内存映射区,一般指定为 NULL,委托内核分配
length:
创建的内存映射区的大小(单位:字节),实际上这个大小是按照 4k 的整数倍分配的
prot: 对内存映射区的操作权限
PROT_READ: 读内存映射区
PROT_WRITE: 写内存映射区
对映射区有读写权限: PROT_READ | PROT_WRITE
flags:
MAP_SHARED: 多个进程可以共享数据,进行映射区数据同步
MAP_PRIVATE: 映射区数据是私有的,不能同步给其他进程
fd:
文件描述符,对应一个打开的磁盘文件,内存映射区通过这个文件描述符和磁盘文件建立关联
offset:
磁盘文件的偏移量,文件从偏移到的位置开始进行数据映射,使用这个参数需要注意两个问题:
偏移量必须是 4k 的整数倍,写 0 代表不偏移
这个参数必须是大于 0
返回值:
成功:返回一个内存映射区的起始地址
失败: MAP_FAILED (that is, (void *) -1)
*/

// 释放内存映射区
int munmap(void *addr, size_t length);
/*
参数:
addr:
mmap() 的返回值,创建的内存映射区的起始地址
length:
和 mmap() 第二个参数相同
返回值:
成功返回 0
失败返回 -1
*/

进程间通信

操作内存映射区和操作管道是不一样的,得到内存映射区之后是直接对内存地址进行操作,管道是通过文件描述符读写队列中的数据。管道的读写是阻塞的,内存映射区的读写是非阻塞的。内存映射区创建成功之后,得到了映射区内存的起始地址,使用相关的内存操作函数读写数据就可以。

有血缘关系

由于创建子进程会发生虚拟地址空间的复制,所以在父进程中创建的内存映射区也会被复制到子进程中。在子进程里就可以直接使用这块内存映射区,对于有血缘关系的进程,进程间通信是非常简单的,示例代码如下:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
/*
1.先创建内存映射区得到一个起始地址,假设使用 ptr 指针保存这个地址。
2.通过 fork() 创建子进程 -> 子进程中也就有一个内存映射区,子进程中也有一个 ptr 指针指向这个地址。
3.父进程往自己的内存映射区写数据,数据同步到了磁盘文件中,磁盘文件数据又同步到子进程的映射区中;
子进程从自己的映射区往外读数据,这个数据就是父进程写的。
*/
#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 error");
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;
}

没有血缘关系

对于没有血缘关系的进程间通信,需要在每个进程中分别创建内存映射区,但是这些进程的内存映射区必须关联相同的磁盘文件,这样才能实现进程间的数据同步。

进程 A 的示例代码:

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 <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 error");
exit(0);
}

const char* pt = "父进程,往内存映射区写数据...";
memcpy(ptr, pt, strlen(pt) + 1);

// 释放内存映射区
munmap(ptr, 4000);

return 0;
}

进程 B 的示例代码:

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 <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 error");
exit(0);
}

// 读内存映射区
printf("子进程,从内存映射区读出的数据: %s\n", (char*)ptr);

// 释放内存映射区
munmap(ptr, 4000);

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/*
使用内存映射区拷贝文件思路:
1.打开被拷贝文件,得到文件描述符 fd1,计算出这个文件的大小 size
2.创建内存映射区 A 并且和被拷贝文件关联(就是和 fd1 关联起来),得到映射区地址 ptrA
3.创建新文件,得到文件描述符 fd2,用于存储被拷贝的数据,将这个文件大小拓展为 size
4.创建内存映射区 B 并且和新创建的文件关联(就是和 fd2 关联起来),得到映射区地址 ptrB
5.进程地址空间之间的数据拷贝,memcpy(ptrB, ptrA, size),数据自动同步到新建文件中
6.关闭内存映射区
*/
#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 error");
exit(0);
}

// 3.创建一个新文件,存储拷贝的数据
int fd2 = open("./copy.txt", O_RDWR | O_CREAT, 0664);
// 拓展这个新文件
ftruncate(fd2, size);

// 4.创建一个映射区和新文件进行关联,得到映射区的起始地址 second
void* ptrB = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd2, 0);
if (ptrB == MAP_FAILED)
{
perror("2th mmap error");
exit(0);
}

// 5.使用memcpy拷贝映射区数据
// 这两个指针指向两块内存,都是内存映射区
// 指针指向有效的内存,拷贝的是内存中的数据
memcpy(ptrB, ptrA, size); // dest, source, size

// 6.释放内存映射区
munmap(ptrA, size);
munmap(ptrB, size);
close(fd);
close(fd2);

return 0;
}

参考资料

https://subingwen.cn/linux/mmap


内存映射
https://lcf163.github.io/2021/08/08/内存映射/
作者
乘风的小站
发布于
2021年8月8日
许可协议