共享内存

概念

共享内存(Shared Memory),指两个或多个进程共享一个给定的存储区。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言函数 malloc 分配的内存一样。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。可以用来做进程间通信。

image.png

System V 接口共享内存

参考文档: https://man7.org/linux/man-pages/man7/sysvipc.7.html

相关函数

1
2
3
4
5
6
7
8
9
10
11
#include <sys/shm.h>
// 返回指定 key 的共享内存片段,可以创建新的片段或者返回已存在的
int shmget(key_t key, size_t size, int shmflg);
// 将共享内存对象附加到进程的地址空间
void *shmat(int shmid, const void *_Nullable shmaddr, int shmflg);
int shmdt(const void *shmaddr);
// 将共享内存对象从进程的地址空间卸载
void *shmat(int shmid, const void *_Nullable shmaddr, int shmflg);
int shmdt(const void *shmaddr);
// 对共享内存执行各种控制操作,包括删除等。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

示例代码

写入 system v 共享内存

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: ./sv_shm_write <msg>\n");
exit(EXIT_FAILURE);
}
int shm_size = 100;
// 创建共享内存,创建后可以用 ipcs -m 查看
int shmid = shmget(IPC_PRIVATE, shm_size, IPC_CREAT | 0600);
// 或者使用一个存在的文件路径和项目ID生成key,必须先在当前目录创建一个 shmfile 文件,项目id随意
// key_t key = ftok("shmfile", 65);
// if (key == -1) {
// perror("ftok");
// return 1;
// }
// int shmid = shmget(key, shm_size, IPC_CREAT | 0600);
if(shmid < 0){
perror("shmget");
return 1;
}
// 将共享内存附加到当前进程
char* shm_addr = (char*)shmat(shmid, NULL, 0);
printf("shm addr %p\n", shm_addr);
if(atoi(shm_addr) == -1){
perror("shmat");
return -1;
}
// 向共享内存地址写入数据
printf("wirte data to shmid %d: %s\n", shmid, argv[1]);
strcpy(shm_addr, argv[1]);
// memcpy(shm_addr, argv[1], strlen(argv[1]));

// 虚拟内存解除附加
shmdt(shm_addr);

return 0;
}

写入后,通过 ipcs -m 命令可以看到共享内存的状态

image.png

读取、删除 system v 共享内存内容

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
#include <stdio.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>

int main(int argc, char* argv[]){
if (argc < 2) {
printf("Usage: ./sv_shm_read <shmid>\n");
exit(EXIT_FAILURE);
}
// 从参数获取 shmid
int shmid = atoi(argv[1]);
// 或者使用一个存在的文件路径和项目ID生成key,必须先在当前目录创建一个 shmfile 文件,项目id随意
// key_t key = ftok("shmfile", 65);
// if (key == -1) {
// perror("ftok");
// return 1;
// }
// int shmid = shmget(key, 100, 0);
// 将共享内存附加到进程的虚拟内存区
char* shm_addr = (char*)shmat(shmid, NULL, 0);
if(atoi(shm_addr) == -1) {
perror("shmat");
return 1;
}
// 读取内容,解除绑定
printf("string from shmid %d: %s\n", shmid, shm_addr);
shmdt(shm_addr);
// 删除共享内存
shmid_ds sds;
if(shmctl(shmid, IPC_RMID, &sds) != 0){
perror("shmctl");
return 1;
}
return 0;
}

POSIX 接口共享内存

相关函数

POSIX 接口提供了一组函数来操作共享内存,并使用 fd 的方式来进行管理。从 Linux 2.4 和 glibc 2.2 版本开始支持。

POSIX 共享内存有更好的兼容性,但是早期程序中 system v 共享内存使用广泛。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <sys/mman.h>
#include <sys/stat.h> /* For mode constants */
#include <fcntl.h> /* For O_* constants */
// 创建一个共享内存片段,或者返回已存在的,函数返回共享内存的 fd
int shm_open(const char *name, int oflag, mode_t mode);
// 删除共享内存片段
int shm_unlink(const char *name);

#include <unistd.h>
// 根据路径设置共享内存片段的尺寸
int truncate(const char *path, off_t length);
// 根据 fd 设置共享内存片段的尺寸
int ftruncate(int fd, off_t length);

#include <sys/mman.h>
// 将共享内存对象映射到调用进程的虚拟地址空间(不仅仅可以用于共享内存fd,也可以用于普通磁盘文件fd)
void *mmap(void addr[.length], size_t length, int prot, int flags,
int fd, off_t offset)
;

// 解除映射
int munmap(void addr[.length], size_t length);

// close / fstat / fchown / fchmod 等文件操作函数也可用于操作共享内存的 fd

示例代码

写入 posix 共享内存

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: ./posix_shm_write <msg>\n");
exit(EXIT_FAILURE);
}

// 共享内存对象的名字
const char* name = "/posix_shm";
int shm_size = 1024;

// 创建共享内存对象。O_CREAT 标志表示如果对象不存在则创建,O_RDWR 表示可读写。
int fd = shm_open(name, O_CREAT | O_RDWR, 0666);
if(fd == -1){
perror("shm_open");
return 1;
}

// 设置共享内存对象的大小
if(ftruncate(fd, shm_size) == -1){
perror("ftruncate");
return 1;
}

// 映射共享内存对象到进程的地址空间
char* ptr = (char*)mmap(0, shm_size, PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED){
perror("mmap");
return -1;
}

// 写入数据到共享内存
printf("write data to posix_shm: %s\n", argv[1]);
sprintf(ptr, "%s", argv[1]);

// 解除映射
munmap(ptr, shm_size);
close(fd);

return 0;
}

image.png

执行写入程序后,可以在共享内存文件中看到写入的内容,需要注意的是

  • 只能按 ‘/name’ 路径指定共享内存名,不能指定其它子路径
  • 此文件的内容并不是存储在磁盘上,而是在内存中,除非做了额外的持久化逻辑,系统重启后会被清空。

读取、删除 posix 共享内存

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

int main() {
// 共享内存对象的名字
const char* name = "/posix_shm";
int shm_size = 1024;

// 打开共享内存对象
int fd = shm_open(name, O_RDONLY, 0666);
if(fd == -1){
perror("shm_open");
return 1;
}

// 映射共享内存对象到进程的地址空间
char* ptr = (char*)mmap(0, shm_size, PROT_READ, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED){
perror("mmap");
return -1;
}

// 读取共享内存的内容
printf("string from posix_shm: %s\n", ptr);

// 解除映射
munmap(ptr, shm_size);
close(fd);

// 删除共享内存对象
if(shm_unlink(name) == -1){
perror("shm_unlink");
return 1;
}

return 0;
}

文件内存映射 mmap

在 posix 共享内存的部分,其实已经有使用过 mmap 函数来将共享内存文件附加到进程的虚拟内存空间了,网上很多文章也是将 mmap 作为一种共享内存的手段来描述。这里为什么还要单独把它拿出来说呢?

其实 mmap 并不一定只能映射共享内存和匿名的内存片段,它提供将文件映射到进程的虚拟内存空间,并自动完成内存页和磁盘数据的同步操作。posix 共享内存也是提供一个 fd 并支持的文件相关操作,所以可以使用 mmap 将共享内存映射到进程虚拟内存空间,但不代表 mmap 仅能用于映射虚拟内存,它也可以基于一个普通磁盘文件实现 IPC。

概念

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

image.png

相关函数

1
2
3
4
5
6
#include <sys/mman.h>
// 文件-内存映射
void *mmap(void addr[.length], size_t length, int prot, int flags,
int fd, off_t offset)
;

// 取消-文件内存映射
int munmap(void addr[.length], size_t length);

示例代码

下面是基于 mmap 使用普通文件内存映射实现 ipc 的示例代码。

使用 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
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

int main(int argc, char* argv[]) {
if (argc < 2) {
printf("Usage: ./mmap_write <msg>\n");
exit(EXIT_FAILURE);
}

// 文件的路径
const char* filepath = "./mmapfile";
int shm_size = 1024;

// 打开文件,如果不存在则创建
int fd = open(filepath, O_CREAT | O_RDWR, 0666);
if(fd == -1){
perror("open");
return 1;
}

// 设置文件的大小
if(ftruncate(fd, shm_size) == -1){
perror("ftruncate");
return 1;
}

// 映射文件到进程的地址空间
char* ptr = (char*)mmap(0, shm_size, PROT_WRITE, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED){
perror("mmap");
return -1;
}

// 循环写入数据到映射的内存
int i = 0;
while (i < 100)
{
printf("write data to mmapfile: %s-%d\n", argv[1], i);
sprintf(ptr, "%s-%d", argv[1], i);
i++;
sleep(1);
}

// 解除映射
munmap(ptr, shm_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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

int main() {
// 文件的路径
const char* filepath = "./mmapfile";
int shm_size = 1024;

// 打开文件
int fd = open(filepath, O_RDONLY, 0666);
if(fd == -1){
perror("open");
return 1;
}

// 映射文件到进程的地址空间
char* ptr = (char*)mmap(0, shm_size, PROT_READ, MAP_SHARED, fd, 0);
if(ptr == MAP_FAILED){
perror("mmap");
return -1;
}
// 循环写入数据到映射的内存
int i = 0;
while (i < 100)
{
// 读取映射的内存的内容
printf("string from mmapfile: %s\n", ptr);
i++;
sleep(1);
}


// 解除映射
munmap(ptr, shm_size);
close(fd);

return 0;
}

常规文件操作需要从磁盘到页缓存再到用户内存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户内存的一次数据拷贝过程。mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。

image.png

参考文档:

☞ 参与评论