文章目录
- 一、共享内存示意图
- 二、学习共享内存前的准备工作
- 三、共享内存函数
- 3.1创建共享内存:
- 3.2控制共享内存:
- 3.3挂接和去挂接:
一、共享内存示意图
上一篇文章中讲述的是管道的通信方式,而这里要讲的是操作系统层面专门为进程间通信设计的一个方案,而在同一主机内进行的进程间通信就是system V方案。
那么这种通信方式是谁设计的?该以怎么样的形式给用户使用?
答:这是很久以前的计算机科学家和程序员设计的。
由于操作系统不相信任何人,所以使用该通信方式时,必须通过系统提供的一系列函数接口调用。
在管道的讲解中,已经说明了一个概念:要想实现进程间通信,就必须要让不同的进程看到同一资源,而system V用来实现这个目的的方式有三种----共享内存、消息队列和信号量,本篇博客就对第一种方式进行讲解。
共享内存原理:
如图所示为两个进程在内存中的管理,具体的详细介绍在我之前的文章【关于Linux中----进程优先级、环境变量和进程地址空间】中已经解释过了,不太了解的读者可以先跳转过去看一下。
而共享内存就是先在物理内存中开辟出一段空间,然后通过某种调用将多个进程“挂接”到同一块物理内存上(经过页表映射实现),这样也就使得不同的进程看到了同一份资源。
而当进程不再使用这份资源时,就会进行去挂接,然后再释放这块内存。
二、学习共享内存前的准备工作
来理清几个问题是学习共享内存前的重中之重!
在操作系统中可能同时存在多个进程使用不同的共享内存来进行进程间通信,也就意味着物理内存中可能存在多个共享内存,那么操作系统如何管理它们呢?
答:先描述,再组织----将每一个共享内存的属性都存入一个描述共享内存的结构体中,再将所有的这些结构体以某一种数据结构(可能是顺序表、链表等等)的方式串联起来。这时,对共享内存的管理也就变成了对某一种数据结构进行增删查改等的操作了。
如何保证多个需要进行通信的进程看到的是同一份资源呢?
答:每一个共享内存一定都有一个标识自己唯一性的ID,进程通过每一个共享内存的ID就可以确定这到底是不是 要使用的资源。而这个ID就存在于上面问题中所说的结构体中,因为它是共享内存的属性之一。
三、共享内存函数
前面说过的使用共享内存的四个过程对应四个接口
3.1创建共享内存:
下面对该接口的三个参数进行解释:
key_t key表示的就是上文中说过的用来确定内存空间唯一性的ID,它需要用户自己设置。
而这个值的设置需要使用下面这个函数:
该函数的两个参数都需要用户自己根据需要设置,分别是自定义路径名和自定义项目标识符,而返回值就是内存空间的ID(创建失败就返回-1)。而这个返回值也就是key。
size_t size表示的是创建的共享内存的大小(一般建议是4KB的整数倍)。
shmflg的表示如下:
它有两部分组成。
如果单独使用IPC_CREAT或shmflg设置为0,则表示如果该共享内存已经存在就返回这个共享内存的ID,而如果不存在则创建一个共享内存。
而IPC_EXCL不单独使用,单独使用没有意义,它必须和IPC_CREAT配合使用,以IPC_EXCL|IPC_CREAT的方式出现。表示如果不存在共享内存则进行创建,而如果已经存在就返回出错。这样做的目的是使每次返回的共享内存都是新的、未使用过的。
关于上面IPC_CREAT|IPC_EXCL的使用方式在我之前的文章【关于Linux中----文件接口、描述符、重定向、系统调用和缓冲区】中已经介绍过,不太了解的读者可以先跳转过去看一下。
下面通过代码样例体会一下:
创建一个.h文件和两个.c文件。.h文件内容如下:
(这里的第二个参数的宏是随便写的,也可以是其他值)
sever.c内容如下:
Makefile内容如下:
执行结果如下:
[sny@VM-8-12-centos practice]$ make clean;make
rm -f sever slient fifo
gcc -o sever sever.c
[sny@VM-8-12-centos practice]$ ./sever
1711344665
[sny@VM-8-12-centos practice]$ ./sever
1711344665
而要想让另一个进程也看到同一个共享内存,就必须设置相同的key值,这样才能进行通信,如下:
[sny@VM-8-12-centos practice]$ cat sever.c > client.c
[sny@VM-8-12-centos practice]$ make clean;make
rm -f sever slient fifo
gcc -o sever sever.c
gcc -o client client.c
[sny@VM-8-12-centos practice]$ ./sever
1711344665
[sny@VM-8-12-centos practice]$ ./client
1711344665
接下来进行创建共享内存,对sever.c稍作改动:
执行结果如下:
[sny@VM-8-12-centos practice]$ make clean;make
rm -f sever slient fifo
gcc -o sever sever.c
[sny@VM-8-12-centos practice]$ ./sever
key:1711344665 shmid:0
[sny@VM-8-12-centos practice]$ ./sever
shmget: File exists
可以看到,第一次执行创建共享内存成功,第二次创建失败,因为共享内存已经存在,返回错误。
那么该进程结束之后,创建的共享内存释放了吗?
答案是没有,可以用ipcs -m指令查看共享内存,如下:
[sny@VM-8-12-centos practice]$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
0x66010c19 0 sny 0 4666 0
所以也就得出了一个很重要的结论:共享内存是由内核控制的,不随某一个进程的结束而释放。必须有程序员显示地调用命令或接口以及操作系统重启才能释放。
如下:
[sny@VM-8-12-centos practice]$ ipcrm -m 0
[sny@VM-8-12-centos practice]$ ipcs -m
------ Shared Memory Segments --------
key shmid owner perms bytes nattch status
可以看到上面的释放共享内存是通过指令搭配shmid实现的。
那么key和shmid有什么区别?
答:key只是一个用来标识共享内存唯一性的东西,并不能控制shm;而shmid是操作系统给用户返回的ID,用来进行用户层面上对共享内存的管理。
3.2控制共享内存:
这里的第一个参数就是上一个接口的返回值,第二个参数是具体的选项(选择以哪种方式管理共享内存),这里着重讲解其中一种方式:
顾名思义就知道这个选项是释放共享内存的,而一旦选择这个选项,就可以直接将第三个参数设置为空。
第三个参数很明显就是一个结构体指针,这个结构体就是上文中所说的管理共享内存相关属性的结构体,其为空时表示该共享内存已被释放。
举个例子,在sever.c后加上这几行代码:
接下来用这样一个指令检测系统中的共享内存:
[sny@VM-8-12-centos practice]$ while :; do ipcs -m; sleep 1; echo"#########################################"; done
结果如下:
可以看到,共享内存成功地进行了创建和释放。
3.3挂接和去挂接:
挂接:
同样的第一个参数还是创建共享内存的返回值,第二个参数是一个指针,指针指向所开辟的共享内存的起始地址(虚拟地址),第三个参数和上一个接口中的一样也是选项。
至于去挂接就很简单了,参数只有一个(注意去挂接并不是释放内存!)
直接上代码看一下:
#include "comm.h"
2 #include <unistd.h>
3 int main()
4 {
5 key_t key=ftok(PATH_NAME,PROJ_ID);
6 if(key<0)
7 {//创建key值失败
8 perror("ftok");
9 return 1;
10 }
11 int shm_id=shmget(key,SIZE,IPC_CREAT|IPC_EXCL);//创建一个全新的内存
12 if(shm_id<0)
13 {
14 perror("shmget");
15 return 2;
16 }
17 printf("key:%u shmid:%d\n",key,shm_id);
18 sleep(10);
19 char* mem=(char*)shmat(shm_id,NULL,0);
20 printf("attach successfully!\n");
21 sleep(5);
22 //在这里完成通信逻辑
23 shmdt(mem);
24 printf("deattach successfully!\n");
25 sleep(5);
26 shmctl(shm_id,IPC_RMID,NULL);
27 printf("key:0x%x shmin:%d -> delete successfully!\n",key,shm_id);
28 sleep(10);
29 return 0;
30 }
同样的,也可以用上一个接口中的检测共享内存的方式验证一下整个过程,由于内容类似,这里就不进行粘贴了。
下面编写一下client.c:
#include "comm.h"
2 #include <unistd.h>
3 int main()
4 {
5 key_t key=ftok(PATH_NAME,PROJ_ID);
6 if(key<0)
7 {//创建key值失败
8 perror("ftok");
9 return 1;
10 }
11 printf("%u\n",key);
12 //client只需要直接获取sever中的共享内存即可
13 int shm_id=shmget(key,SIZE,IPC_CREAT);
14 if(shm_id<0)
15 {
16 perror("shmget");
17 return 2;
18 }
19 char* mem=(char*)shmat(shm_id,NULL,0);
20 sleep(5);
21 printf("client attach successfully!\n");
22 shmdt(mem);
23 sleep(5);
24 return 0;
25 }
读者同样可以自己测试一下整个过程。
下面编写通信逻辑:
完整的sever.c如下:
#include "comm.h"
2 #include <unistd.h>
3 int main()
4 {
5 key_t key=ftok(PATH_NAME,PROJ_ID);
6 if(key<0)
7 {//创建key值失败
8 perror("ftok");
9 return 1;
10 }
11 int shm_id=shmget(key,SIZE,IPC_CREAT|IPC_EXCL);//创建一个全新的内存
12 if(shm_id<0)
13 {
14 perror("shmget");
15 return 2;
16 }
17 printf("key:%u shmid:%d\n",key,shm_id);
18 char* mem=(char*)shmat(shm_id,NULL,0);
19 printf("attach successfully!\n");
20 //在这里完成通信逻辑
21 while(1)
22 {
23 sleep(1);
24 printf("%s\n",mem);
25 }
26 shmdt(mem);
27 printf("deattach successfully!\n");
28 shmctl(shm_id,IPC_RMID,NULL);
29 printf("key:0x%x shmin:%d -> delete successfully!\n",key,shm_id);
30 return 0;
31 }
完整的client.c如下:
#include "comm.h"
2 #include <unistd.h>
3 int main()
4 {
5 key_t key=ftok(PATH_NAME,PROJ_ID);
6 if(key<0)
7 {//创建key值失败
8 perror("ftok");
9 return 1;
10 }
11 printf("%u\n",key);
12 //client只需要直接获取sever中的共享内存即可
13 int shm_id=shmget(key,SIZE,IPC_CREAT);
14 if(shm_id<0)
15 {
16 perror("shmget");
17 return 2;
18 }
19 char* mem=(char*)shmat(shm_id,NULL,0);
20 printf("client attach successfully!\n");
21 char c='A';
22 while(c<='Z')
23 {
24 mem[c-'A']=c;
25 c++;
26 mem[c-'A']=0;
27 sleep(2);
28 }
29 shmdt(mem);
30 printf("client dettach successfully!\n");
31 return 0;
32 }
在上面的通信代码中,并没有使用系统接口,为什么?
共享内存一旦建立好映射金自己进程的地址空间,该进程就可以直接看到该共享内存,就像malloc开辟空间一样,所以不需要系统接口。
由于结果是一个动态的过程,这里就不粘贴了,读者可以自己运行一下试试。
本篇完!来日方长,继续努力!