文章目录
systemV 标准
systemV标准是大佬定制的一套标准,该标准可以在操作系统层面上,完成进程之间的通信;
systemV标准给用户提供了系统调用接口,只要我们使用它所提供的系统调用就可以完成进程间的通信;
对于systemV标准提供了三种方式:
- 消息队列
- 共享内存
- 信号量;
其中消息队列和共享内存都是来完成进程通信的,而信号量主要是保证同步和互斥的;
共享内存通信原理
共享内存的通信原理:
在物理内存开辟一份共享内存的空间,然后把这份空间映射到需要进行相互通信的虚拟地址空间中;
这样使得不同的进程看到了同一份资源,这样就可以通过共享内存进行通信了;
至于是如何在内存开辟一共享内存空间?并且如何建立共享内存空间映射到进程的虚拟地址空间中的?
这都是systemV标准提供给我们共享内存操作IPC完成的;
共享内存的理解
- 请问:操作系统会不会同时存在多个进程,使用不同的共享内存进行通信呢?
那肯定会的,共享内存在操作系统中是存在多份的,只要你有办法创建出共享内存就可以(当然,我们可以通过操作系统提供的系统调用创建,这个是肯定搞得);
那既然在操作系统可以存在多份共享内存,操作系统毫无疑问需要进行对共享内存的管理;那对于操作系统来说,毫无疑问管理的方式只有一个逻辑,先描述,再组织;先用一个结构体描述这个共享内存的信息,再用一些数据结构,如链表进行对该共享内存结构体进行管理;
- 既然共享内存那么多,那你怎么保证两个或多个进程之间通信,能够看到同一份共享内存呢?
毫无疑问,在我们匿名管道中,为了让通信之间的进程看到同一份管道资源,使用策略是子进程继承父进程的信息;在我们的命名管道里,为了让通信之间的进程看到同一份命名管道资源,使用的策略是路径+文件名;
那么在我们共享内存中,巍峨让通信之间的进程看到同一份共享内存资源,使用的策略是该共享内存的唯一id号即可;
我们发现,无论那种通信方式,都是为了让通信之间的进程,看到同一份资源,并且这份资源是唯一的;
那么共享内存的id号,到底在哪儿呢?毫无疑问肯定在描述共享内存的结构体中,这就和标识一个进程id一样,该进程的id也是在描述进程的结构体中;
共享内存的创建–shmget函数
- 创建共享内存函数
shmget
第二个参数为什么是最好是4KB的整数倍?
原因就是共享内存申请内存的基本单位是4KB,也就是一个页;
这个函数的返回值是:
创建成功,OS返回给用户一个id值,用户通过该id来管理共享内存(去关联,关联,删除等),出错久返回-1;
这个函数的第一个参数 key:设置key为共享内存的唯一ID号;
为了达到不同之间的进程通信,就需要不同进程看到同一份共享内存,看到同一份共享内存的前提是:需要知道该共享内存的唯一ID,这个key值,理论上是可以自己随意设置的,只要保证该id号是唯一的;
但是自己设置,总有些不好,所以操作系统给我们提供了一个系统调用ftok
,来帮我们设置共享内存key值;
这个key值是什么不重要,它只要宝子该共享内存是唯一的,和其他共享内存id号不一样就可以;
所以在我们调用shmget函数之前,必须调用ftok来帮我们生成一个key,该key就是给shmget函数第一个参数使用了,表示表示该共享内存的唯一标识,这个key在内核中,会在共享内存的结构体,设置给共享内存的ID;
那我们如何保证不同进程看到同一份共享内存呢?我们就只需要保证不同进程看到的是同一个key 值的共享内存即可;那么又如何保证不同进程是看到同一个key值的共享内存呢?只需要在不同的进程都是用ftok函数和相同的路径名,和项目ID
,就可以生成相同的key,这样就可以保证不同进程看到是同一个key值的共享内存了;
我们火速来创建一个共享内存来看看:
来挖掘共享内存得特性:
#include <stdio.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<sys/shm.h>
int main(){
key_t key = ftok("./",0x1002); //这里两个参数都是随意给的
//创建共享内存
int shmid = shmget(key,4096,IPC_CREAT | IPC_EXCL);
if(shmid < 0){
perror("shmget:");
return 2;
}
//创建成功,我们打印看看key和shmid值
printf("key = %u,shmid = %d",key,shmid);
return 0;
}
当我第一次运行 注意:第一次运行我在强调这个词的时候,我们发现:key 是一个很大的数,但是这个数是什么不重要,它就是表示共享内存的ID而已;
我们看到 shmid = 0; 这个shmid就是shmget函数调用成功返回的值;
当我们第二次,第三次之后再次执行该进程:我们发现一个现象啊;就是创建共享内存失败了,原因就是我们的
shmget的第三个参数是IPC_CREAT | IPC_EXCL
,它表示内存存在共享内存就会创建失败,不存在就会创建一个;
所以这里失败了,就反向推出共享内存还是存在内存中的;
但是我们思考了一个问题:明明我执行完了该进程,为什么该共享内存还在呢?这里我们就知道,共享内存的声明周期肯定不是跟随进程的;
查看共享内存和释放共享内存方式
那既然共享内存还是纯在的话?我们可以通过一个命令查看共享内存ipcs -m
它表示可以查看内存中存在的IPC资源, -m
选项表示查看共享内存的资源;
我们发现即使进程退出,共享内存还是存在;那共享内存的声明周期是随谁的呢?
systemV的IPC资源都是随内核的。并不是随进程的。那么共享内存也就是随内核的。
那么我们如何释放该共享内存呢?
- 程序员手动释放,通过系统调用接口
shmctl
- 命令方式释放
ipcrm -m shmid
,该shmid是共享内存的shmget返回的id号;- 重启操作系统
比如我们通过命令释放共享内存:
我们还可以使用通过一个系统调用接口:shmctl
;
这个是一个共享内存的控制函数,但是我们不用来控制共享内存其他属性,我们只用来释放共享内存
具体操作,如何释放共享内存呢?
起始只要在程序中:调用shmctl(shmid,IPC_RMID,NULL)
;
#include <stdio.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<sys/shm.h>
int main(){
key_t key = ftok("./",0x1002); //这里两个参数都是随意给得
//创建共享内存
int shmid = shmget(key,4096,IPC_CREAT | IPC_EXCL);
if(shmid < 0){
perror("shmget:");
return 2;
}
printf("key = %u\n,shmid = %d\n",key,shmid);
sleep(2); //没释放之前
shmctl(shmid,IPC_RMID,NULL); //释放共享内存
sleep(2);//释放之后
return 0;
}
我们再开多一个终端,监控上面代码的可执行程序:观察共享内存的变化:
监控脚本命令:while:;do ipcs -m;sleep 1;echo"##########";done
;
我们可以看到一个变化:共享内存从有变无的过程,表示该系统调用确实释放了共享内存;
挂在共享内存–shmat函数
当我们通过shmget函数创建在物理空间创建好了共享内存时候,接下来我们就需要把该共享内存挂在到需要进行通信的进程虚拟地址中了;
在OS中给我们提供了一个系统调用接口shmat
函数:
该函数:
第一个参数就是shmget函数返回给用户层的id号;
第二个参数就是要把该共享内存挂在到虚拟地址哪个地方,通常我们都设置为NULL,不需要手动设置为具体数值地址,因为我们也不知道该虚拟地址的共享内存地址在哪个地方呀;
第三个参数我们设置为0就可以;
这个函数我们关注的是返回值:
成功返回共享内存挂在到进程的虚拟地址的起始位置;
失败返回(void*)-1
;
该函数的返回值类似malloc的返回值一样,只不过malloc返回的地址是在进程虚拟地址的堆空间段,而shmat返回的地址在进程虚拟地址的共享内存段;
去挂在共享内存–shmdt函数
当我们不想再让共享内存和进程进行关联时候,我们就可以去掉共享内存和进程的关联;
在OS中提供一个系统调用接口:shmdt
;
该函数的一个参数是 shmat函数返回共享内存关联进程的虚拟地址;
返回值,成功就返回0,失败就-1;
而且我们必须理解:这个shmdt是去掉共享内存和进程虚拟地址的关联,并不是释放共享内存;
共享内存使用的基本框架逻辑代码
#include <stdio.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<sys/shm.h>
#include<unistd.h>
int main(){
key_t key = ftok("./",0x1002); //这里两个参数都是随意给得
//创建共享内存
int shmid = shmget(key,4096,IPC_CREAT | IPC_EXCL);
if(shmid < 0){
perror("shmget:");
return 2;
}
//创建完共享内存后,我们就需要挂在共享内存
char* mem = (char*) shmat(shmid,NULL,0);
printf("attaches shared_memory success!\n");
//进行通信的逻辑代码区域
//.....
//.....
//当我们该进程不使用该共享内存时候,我们需要去掉该共享内存的关联
shmdt(mem);
printf("deatach shared_memory success!\n");
shmctl(shmid,IPC_RMID,NULL); //释放共享内存
return 0;
}
上面的代码就是共享内存使用的框架逻辑,我们完成了五个步骤:
1. 创建共享内存;
2. 挂接共享内存与进程之间的关联;
3. 完成通信逻辑代码;
4. 去挂接共享内存与进程之间的关联;
5. 释放共享内存;
共享内存完成进程之间通信
有了上面的基本逻辑框架,我们来通过共享内存完成一份代码:
一个进程server读取共享内存的数据;
一个进程client 向共享内存写入数据;
对于server.c的代码:
#include <stdio.h>
#include<sys/ipc.h>
#include<sys/types.h>
#include<sys/shm.h>
#include<unistd.h>
int main(){
key_t key = ftok("./",0x1002); //这里两个参数都是随意给得
if(key <0){
perror("ftok:");
return 1;
}
//创建共享内存
int shmid = shmget(key,4096,IPC_CREAT | IPC_EXCL|0666);
if(shmid < 0){
perror("shmget:");
return 2;
}
//创建完共享内存后,我们就需要挂在共享内存
char* mem = (char*) shmat(shmid,NULL,0);
printf("attaches shared_memory success!\n");
//进行通信的逻辑代码区域
//.....
//.....
while(1){
sleep(1);
printf("client sent to server:%s\n",mem);
}
//当我们该进程不使用该共享内存时候,我们需要去掉该共享内存的关联
shmdt(mem);
printf("deatach shared_memory success!\n");
shmctl(shmid,IPC_RMID,NULL); //释放共享内存
return 0;
}
对于client.c代码:
#include <stdio.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<unistd.h>
#include<sys/shm.h>
int main(){
//client生成的共享内存key值,是和server生成的key是一样的
//因为我们使用 ftok传入的参数也是一样的
key_t key = ftok("./",0x1002);
if(key <0){
perror("ftok");
return 1;
}
//这里的共享内存不需要在创建了,只需要使用server端创建的共享内存即可
//我们的key是和server相同的,所以就可以保证client也是看到server的共享内存
//由于不需要client创建共享内存,所以我们第三个参数传入的是IPC_CREAT
//表示,有和key相关的共享内存,那么就会返回该共享内存给用户,不会再创建
int shmid = shmget(key,4096,IPC_CREAT);
if(shmid <0){
perror("shmget:");
return 1;
}
//有了共享内存我们就需要挂在该共享内存到client进程的虚拟地址
char* mem = (char*)shmat(shmid,NULL,0);
//挂在共享内存到client进程成功后,这就是client的业务逻辑代码区域
//我们给共享内存每隔一秒发送字母
char c = 'A';
while(c <='Z'){
mem[c-'A'] = c; //往共享内存中写入字母
c++;
mem[c-'A']='\0';
sleep(2);//睡两秒是为了观察server端口是否接收到client发送的数据
}
//当业务逻辑处理完,不再使用共享内存,我们可以去掉该共享内存的关联
shmdt(mem);
return 0;
}
当我们启动了服务端server进程,再启动client进程;此时就可以通信了;
共享内存通信的特点
- 共享内存通信是所有进程间通信最快的方式;原因就是:一旦共享内存建立和进程地址的关联后,该进程就直接可以使用该共享内存了;并不像管道通信那样,还需要调用read wirte函数;
- 我们的共享内存是没有提同步互斥机制的,这个需要我们程序员自己来管理这部分的资源;再我们上面的测试代码进程通信也可以看出,当server进程先运行后,并不会等待client发数据过来再读取内容,server一旦启动就会一直读取共享内存;
- 共享内存的生命周期随内核;
浅谈systemV标准的IPC数据结构
我们不是说操作系统为了管理共享内存,一定会管理共享内存的数据结构吗?那肯定是的呀;
于此同时操作系统为了管理消息队列,信号量,也会管理他们的数据结构!
我们可以通过
man shmctl
man msctl
man semclt
查看共享内存,消息队列,信号量的用户层数据结构(内核层的数据结构和他类似);
共享内存的用户层的结构体
消息队列的结构体:
信号量的结构体
我们惊奇的发现:systemV 标准的数据结构尽管他们的数据结构不一样,但是很类似;
并且第一个成员的数据类型都是struct _ipc_perm
,该类型的第一个成员就是_key,也就是用来标识该信号量。共享内存,消息队列的唯一性的一个关键字,所以说,我们上面的共享内存哪个ftok生成的key值就是设置到给共享内存的数据结构 struct shmid_ds
的第一个成员 struct ipc_perm shm_perm 变量的第一个成员 key_t __key
这里的;
最关键的是:明明三个IPC数据结构都是不一样的,第一个成员都是一样的?操作系统这么设计的目的是什么?
就是为了管理systemV IPC的资源?如何管理呢?
你可以理解,在OS中,有一个数组,struct ipc_perm* arrary[64]
,它存放的就是各个systemV标准的IPC数据结构;
有同学说,明明systemV标准的IPC数据结构类型为 struct semid_ds`` struct shmid_ds ``struct msqid_ds
,都是不一样的,为什么能够存储在 struct ipc_perm
的数组中?
你们别忘记了struct semid_ds
struct shmid_ds
struct msqid_ds
的第一个成员都是 struct ipc_perm
类型啊;
只要我们systemV标准的地址强制类型转换为struct ipc_perm*
就可以存入它数组了;
那假如我要访问systemV标准的IPCstruct semid_ds
struct shmid_ds
struct msqid_ds
其他数据成员呢?那不就是不行了?
错了错了,肯定可以的,我们只要取出struct ipc_perm* arrary[64]
的元素,它的元素就是systemV标准的IPC的数据结构地址,我们强制类型转化,就可以访问到各自的数据结构了;
这就是C语言的切片特性啊,通过一个公共的数据,存放不一样的数据结构;
这个和C++中父类的指针可以指向子类的对象是否很相似,这也是C++中的切片行为;
不知道同学们记不记得,当我们在申请共享内存时候,为什么共享内存shmid,是从零一直往上增长的?
原因就是struct ipc_perm* arrary[64]
的数组下标咯,就那么简单;