Linux 进程间通信——内存映射与共享内存

本文最后更新于:2021年4月4日 下午

概览:Linux内存映射、共享内存。

内存映射

内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。

可以做进程间通信,而且效率较高。

通信方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
使用内存映射实现进程间通信:
1.有关系的进程(父子进程)
- 还没有子进程的时候
- 通过唯一的父进程,先创建内存映射区
- 有了内存映射区以后,创建子进程
- 父子进程共享创建的内存映射区

2.没有关系的进程间通信
- 准备一个大小不是0的磁盘文件
- 进程1 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 进程2 通过磁盘文件创建内存映射区
- 得到一个操作这块内存的指针
- 使用内存映射区通信

注意:内存映射区通信,是非阻塞。

注意:内存映射区域通信,是非阻塞的

函数

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
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
/*
- 功能:将一个文件或者设备的数据映射到内存中
- 参数:
- void *addr: 指定NULL时, 由内核指定分配
- length : 要映射的数据的长度,这个值不能为0。建议使用文件的长度。
获取文件的长度:stat lseek(fd,0,SEEK_END)
- prot : 对申请的内存映射区的操作权限
-PROT_EXEC :可执行的权限
-PROT_READ :读权限
-PROT_WRITE :写权限
-PROT_NONE :没有权限
要操作映射内存,必须要有读的权限。
PROT_READ、PROT_READ|PROT_WRITE
- flags :
- MAP_SHARED : 映射区的数据会自动和磁盘文件进行同步,进程间通信,必须要设置这个选项
- MAP_PRIVATE :不同步,内存映射区的数据改变了,对原来的文件不会修改,会重新创建一个新的文件。(copy on write)
- fd: 需要映射的那个文件的文件描述符
- 通过open得到,open的是一个磁盘文件
- 注意:文件的大小不能为0,open指定的权限不能和prot参数有冲突。
prot: PROT_READ open:只读/读写
prot: PROT_READ | PROT_WRITE open:读写
- offset:偏移量,一般不用。必须指定的是4k的整数倍,0表示不便宜。
- 返回值:返回创建的内存的首地址
失败返回MAP_FAILED,即是(void *) -1
*/

int munmap(void *addr, size_t length);

/*
- 功能:释放内存映射
- 参数:
- addr : 要释放的内存的首地址
- length : 要释放的内存的大小,要和mmap函数中的length参数的值一样。
*/

父子进程间通过内存映射沟通

思路:父进程先创建内存映射区域,然后创建子进程,之后父子进程就共享同一块内存映射区域。

子进程能通过文件描述符的方式写文件吗???可以

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

int main(){

//打开文件,创建内存映射区域
int fd = open("test.txt",O_RDWR);
if(fd <= 0){
perror("open");
exit(0);
}
int len = lseek(fd,0,SEEK_END);
void * addr = mmap(NULL,len,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);

if(addr == MAP_FAILED){
perror("mmap");
exit(0);
}

pid_t pid = fork();
if(pid > 0){
//父进程
wait(NULL);//回收子进程

//读取并输出子进程的结果
char buf[128];
strcpy(buf,(char *)addr);
printf("read data: %s \n",buf);



}else if(pid == 0){
//子进程
//向内存映射区域写入内容
char *str1 = "你好啊!";
strcpy((char *)addr,str1);
sleep(2);
strcpy((char *)addr + strlen(str1),"今天的风甚是喧嚣!");

}else{
perror("fork");
exit(0);
}

//关闭内存映射区域
munmap(addr,len);

return 0;
}
  • 父进程、子进程读取方式
    • 能否用fd来读写:可以
    • 读写之后,内容还在不在? 修改都保存在了内容之中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//当采用向文件写入的时候
if(pid > 0){
//父进程
wait(NULL);//回收子进程

//内容多的时候应当采用循环读入的方式
//读取并输出子进程的结果
char buf[128];
strcpy(buf,(char *)addr);
printf("read data: %s \n",buf);
}else if(pid == 0){
//子进程
//子进程使用fd写入文件
char *str1 = "你好啊!";
write(fd,str1,strlen(str1));

}
  • 子进程向文件写入,默认就写到了文件的末尾,而父进程读取的话,就会将整个文件内容都读入。

无关系的两个进程通信的话,方式就是打开同一个文件。

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
//pro1
int main(){

//打开文件,创建内存映射区域
int fd = open("test.txt",O_RDWR);
if(fd <= 0){
perror("open");
exit(0);
}
int len = lseek(fd,0,SEEK_END);
void * addr = mmap(NULL,len,PORT_READ|PORT_WRITE,MAP_SHARED,fd,0);

if(addr == MAP_FAILED){
perror("mmap");
exit(0);
}

//进程1等待进程2发送消息
//?如何写?阻塞吗?
//可以等待进程2发送完消息之后,进程1再开启,再读

//关闭内存映射区域
munmap(addr,len);

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
//pro2
int main(){

//打开文件,创建内存映射区域
int fd = open("test.txt",O_RDWR);
if(fd <= 0){
perror("open");
exit(0);
}
int len = lseek(fd,0,SEEK_END);
void * addr = mmap(NULL,len,PORT_READ|PORT_WRITE,MAP_SHARED,fd,0);

if(addr == MAP_FAILED){
perror("mmap");
exit(0);
}

//进程2发送消息
//?发送消息的方式?

//关闭内存映射区域
munmap(addr,len);

return 0;
}

使用内存映射做文件拷贝

一般不用做内存拷贝,文件太大, 内存可能放不下。

方法:建立一个新文件,并且扩展大小,之后将源文件和新文件都映射到内存之中,再使用内存拷贝memcpy的方式来完成文件拷贝,最后释放资源即可。

内存映射注意事项

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
1.如果对mmap的返回值(ptr)做++操作(ptr++), munmap是否能够成功?
void * ptr = mmap(...);
ptr++; 可以对其进行++操作
munmap(ptr, len); // 错误,要保存地址

2.如果open时O_RDONLY, mmap时prot参数指定PROT_READ | PROT_WRITE会怎样?
错误,返回MAP_FAILED
open()函数中的权限建议和prot参数的权限保持一致。

3.如果文件偏移量为1000会怎样?
偏移量必须是4K的整数倍,返回MAP_FAILED

4.mmap什么情况下会调用失败?
- 第二个参数:length = 0
- 第三个参数:prot
- 只指定了写权限
- prot PROT_READ | PROT_WRITE
5个参数fd 通过open函数时指定的 O_RDONLY / O_WRONLY

5.可以open的时候O_CREAT一个新文件来创建映射区吗?
- 可以的,但是创建的文件的大小如果为0的话,肯定不行
- 可以对新的文件进行扩展
- lseek()
- truncate()

6.mmap后关闭文件描述符,对mmap映射有没有影响?
int fd = open("XXX");
mmap(,,,,fd,0);
close(fd);
映射区还存在,创建映射区的fd被关闭,没有任何影响。

7.对ptr越界操作会怎样?
越界操作操作的是非法的内存 -> 段错误

匿名映射

不需要文件实体的一种内存映射。只能用于父子关系之间的映射。

需要一个特殊的设置,MAP_SAHRED | MAP_ANONYMOUS,同时,文件描述符传递-1,offset必须从0开始。

1
void * addr = mmap(NULL,4096,MAP_READ|MAP_WRITE,MAP_SHARED | MAP_ANONYMOUS,-1,0);

共享内存

共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。

与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC 技术的速度更快

共享内存使用步骤

  • 调用 shmget() 创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。

  • 使用 shmat() 来附上共享内存段,即使该段成为调用进程的虚拟内存的一部分。

  • 此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由 shmat() 调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。

  • 调用 shmdt() 来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。

  • 调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之后内存段才会销毁。只有一个进程需要执行这一步。

共享内存操作函数

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
58
59
60
61
62
63
64
65
66
67
68
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
/*
- 功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。
新创建的内存段中的数据都会被初始化为0
- 参数:
- key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。
一般使用16进制表示,非0值
- size: 共享内存的大小
- shmflg: 属性
- 访问权限
- 附加属性:创建/判断共享内存是不是存在
- 创建:IPC_CREAT
- 判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用
IPC_CREAT | IPC_EXCL | 0664
- 返回值:
失败:-1 并设置错误号
成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。
*/

void *shmat(int shmid, const void *shmaddr, int shmflg);
/*
- 功能:和当前的进程进行关联
- 参数:
- shmid : 共享内存的标识(ID),由shmget返回值获取
- shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定
- shmflg : 对共享内存的操作
- 读 : SHM_RDONLY, 必须要有读权限
- 读写: 0
- 返回值:
成功:返回共享内存的首(起始)地址。 失败(void *) -1
*/

int shmdt(const void *shmaddr);
/*
- 功能:解除当前进程和共享内存的关联
- 参数:
shmaddr:共享内存的首地址
- 返回值:成功 0, 失败 -1
*/

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/*
- 功能:对共享内存进行操作。删除共享内存,共享内存要删除才会消失,创建共享内存的进行被销毁了对共享内存是没有任何影响。
- 参数:
- shmid: 共享内存的ID
- cmd : 要做的操作
- IPC_STAT : 获取共享内存的当前的状态
- IPC_SET : 设置共享内存的状态
- IPC_RMID: 标记共享内存被销毁
- buf:需要设置或者获取的共享内存的属性信息
- IPC_STAT : buf存储数据
- IPC_SET : buf中需要初始化数据,设置到内核中
- IPC_RMID : 没有用,NULL
*/

key_t ftok(const char *pathname, int proj_id);
/*
- 功能:根据指定的路径名,和int值,生成一个共享内存的key
- 参数:
- pathname:指定一个存在的路径
/home/nowcoder/Linux/a.txt
/
- proj_id: int类型的值,但是这系统调用只会使用其中的1个字节
范围 : 0-255 一般指定一个字符 '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
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

int main(){

//1.创建一个共享内存
int shmid = shmget(100,4096,IPC_CREAT|0664);
printf("shmid : %d\n",shmid);

//2.和当前进程相关联
void *ptr = shmat(shmid,NULL,0);

//3.写数据
char *str = "hello world";
memcpy(ptr,str,strlen(str)+1);

printf("按任意键继续\n");
getchar();//阻塞进程,以便运行另一个进程,否则开辟完的这块内存又会被关闭

//4.解除关联
shmdt(ptr);

//5.删除共享内存
shmctl(shmid,IPC_RMID,NULL);

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

int main(){

//获取一个共享内存
int shmid = shmget(100,0,IPC_CREAT);
printf("shmid : %d\n",shmid);

//2.然后关联
void *ptr = shmat(shmid,NULL,0);

//3.读取数据
printf("recv: %s\n",(char*)ptr);

printf("按任意键继续\n");
getchar();

//4.解除关联
shmdt(ptr);

//5.删除共享内存
shmctl(shmid,IPC_RMID,NULL);

return 0;
}

其他问题

问题1:操作系统如何知道一块共享内存被多少个进程关联?

  • 共享内存维护了一个结构体struct shmid_ds 这个结构体中有一个成员 shm_nattch
  • shm_nattach 记录了关联的进程个数

linux中有一些命令可以来查看相关信息

1
2
3
4
5
6
7
8
9
10
11
12
13
ipcs 用法
ipcs -a // 打印当前系统中所有的进程间通信方式的信息
ipcs -m // 打印出使用共享内存进行进程间通信的信息,常用
ipcs -q // 打印出使用消息队列进行进程间通信的信息
ipcs -s // 打印出使用信号进行进程间通信的信息

ipcrm 用法
ipcrm -M shmkey // 移除用shmkey创建的共享内存段
ipcrm -m shmid // 移除用shmid标识的共享内存段
ipcrm -Q msgkey // 移除用msqkey创建的消息队列
ipcrm -q msqid // 移除用msqid标识的消息队列
ipcrm -S semkey // 移除用semkey创建的信号
ipcrm -s semid // 移除用semid标识的信号

例如执行上面的写入程序

1
2
3
4
5
6
7
8
9
10
11
aliyun@ali-colourso:~/learn/02sig$ ipcs -m

------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000064 32768 aliyun 664 4096 1
aliyun@ali-colourso:~/learn/02sig$ ipcrm -m 32768 //删除
aliyun@ali-colourso:~/learn/02sig$ ipcs -m

------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x00000000 32768 aliyun 664 4096 1 dest

key值其实就是100的十六进制形式,删除之后key变成了0,表示被标记删除,但是当前程序还没执行完。

问题2:可不可以对共享内存进行多次删除 shmctl

  • 可以的,因为shmctl 标记删除共享内存,不是直接删除

什么时候真正删除呢?

  • 当和共享内存关联的进程数为0的时候,就真正被删除
  • 当共享内存的key为0的时候,表示共享内存被标记删除了
  • 如果一个进程和共享内存取消关联,那么这个进程就不能继续操作这个共享内存。也不能再次进行关联。

共享内存和内存映射的区别

内存映射,需要文件,匿名映射不需要,但只能用于有亲缘关系的进程之间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
共享内存和内存映射的区别
1.共享内存可以直接创建,内存映射需要磁盘文件(匿名映射除外)
2.共享内存效果更高
3.内存
所有的进程操作的是同一块共享内存。
内存映射,每个进程在自己的虚拟地址空间中有一个独立的内存。
4.数据安全
- 进程突然退出
共享内存还存在
内存映射区消失
- 运行进程的电脑死机,宕机了
数据存在在共享内存中,没有了
内存映射区的数据 ,由于磁盘文件中的数据还在,所以内存映射区的数据还存在。

5.生命周期
- 内存映射区:进程退出,内存映射区销毁
- 共享内存:进程退出,共享内存还在,标记删除(所有的关联的进程数为0),或者关机
如果一个进程退出,会自动和共享内存进行取消关联。