Linux进程通信(SystemV)
@[toc]
一、SystemV标准的进程间通信方式
在OS层面,计算机科学家专门为进程通信设计了方案,并以系统调用接口的方式给用户使用。
其中SystemV标准的方案一共有三种,分别是共享内存,消息队列以及信号量。其中共享内存以传递信息为目的,信号以实现进程同步和互斥为目的。
二、共享内存
1.原理
进程通信的条件是进程之间可以看到相同的一个内存块,而使用共享内存进行通信的时候,这个内存块是位于磁盘上的一个内存块。
以上就是共享内存的原理图,首先OS在物理内存上开辟一块空间,然后令这块空间分别挂接到进程通信的进程上(使用页表将物理内存和地址空间链接起来)。这种方式称为共享内存。
2.准备工作
在正式介绍共享内存的系统调用接口之前,我们需要明确一些事情。
1.共享内存可以同时存在多份,并且一个共享内存可以被多个进程所共享。操作系统需要对共享内存进行管理,以先描述后组织的方式。
2.共享内存一定有一个唯一标识的ID,让不同的进程能识别同一个共享内存的资源,这个ID在描述共享内存的结构体里。这个ID可以自动生成也可以由用户自己决定。
3.当某个进程不使用共享内存的时候需要去关联。
4.释放共享内存。
3.实现共享内存的函数
我们需要首先认识几个函数:
(1)shmget
用来创建共享内存的函数,可以通过man手册来查看一下它的用法:
其中第一个参数就是共享内存的ID,第二个参数就是共享内存的大小,建议是4KB的整数倍。最后一个参数是标记位,需要传递选项。
我们只关注两个选项,分别是IPC_CREAT(创建一个新的共享内存段)。
如果只传入IPC_CREAT或者只传入0,代表创建一个共享内存,如果该共享内存已经存在,则返回已经存在的共享内存。如果创建成功则返回该共享内存的系统层面的标识符。
另一个选项是IPC_EXCL,它不能够单独使用,只能通过或操作与IPC_CREAT进行连用。
IPC_CREAT|IPC_EXCL代表的是如果不存在共享内存则创立,如果已经存在则返回出错,它的意义在于如果返回成功则一定是一个新的共享内存。
(2)ftok
这个函数主要用于设定共享内存的ID的。
它的返回值为形成的ID,即最终要传到shmget的第一个参数。第一个选项为自定义路径,第二个选项为自定义项目名。其中不同的进程使用同一个路径和项目名可以找到同一块共享内存,即只要路径和项目名一样就会形成同一个id。
(3)shmctl
这个函数主要用于共享内存的控制,目前我们只用于共享内存的删除。
其中第一个参数传递的是系统层面的共享内存的标识符,即shmget的返回值。第二个参数传递的是选项,由于我们只用于删除所以传递IPC_RMID。第三个参数我们设置为空,其实它是就是共享内存的结构体指针。不过它与内核中的有差别,因为内核中的需要组织起来一定有指针。我们可以通过更改cmd的选项来找到内核的共享内存的结构体。
4.共享内存的实现
(1)comm.h
#pragma once
#include<stdio.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#include<stdlib.h>
#include<string.h>
#define PATH_NAME "./"
#define PROJ_ID 0x6666
#define SIZE 4097
(2)server.c
#include"comm.h"
int main()
{
key_t key=ftok(PATH_NAME,PROJ_ID);
if(key<0)
{
perror("ftok");
return 1;
}
int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL);
if(shmid<0)
{
perror("shmid");
return 2;
}
printf("key:%d\nshmid:%d\n",key,shmid);
}
(3)client
key_t key=ftok(PATH_NAME,PROJ_ID);
if(key<0)
{
perror("ftok");
return 1;
}
client端就不需要再创建共享内存了,因为是以同一个方式形成的key所以client端与server端的key值是一样的,此时就是将共享内存与两个进程链接了起来。
5.查看共享进程
(1)查看与删除共享进程
注意,共享进程是由操作系统创建的,它的生命周期与bash的生命周期是一致的,而与server等进程的周期无关。
我们使用ipcs选项来查看共享进程:
ipcs:查看共享内存,消息队列和信号量。
ipcs -m:共享内存
删除共享内存的指令为:
ipcrm -m shmid值
此时该共享内存就被删掉了。
我们还可以通过shmctl函数来进行删除:
sleep(10);
shmctl(shmid,IPC_RMID,NULL);
printf("key:%d\nshmid:%d\n",key,shmid);
(2)key VS shmid
key只是系统层面用来标识共享内存的唯一性的,不能用来管理共享内存。
shmid是OS给用户返回的id,用来在用户层进行共享内存的管理。
由于我们都是在用户层来进行操作,因此我们要删除共享内存使用的是shmid而不是key。
6.链接共享内存
在server代码中我们只是建立了一个server可以看到的共享内存,但是还没有对它来进行链接。下面来介绍链接和解链接共享内存的函数:
(1)shmat
用于链接共享内存。
我们可以通过man手册来进行查询:
其中shmid传入的就是shmget对共享内存的标记(它的返回值),shmaddr设为NULL,shmflg设为0即可。
如果链接成功则返回共享内存的地址(用户层使用的都是虚拟地址)。我们可以使用指针来接收这一地址。
(2)shmdt
用于解链接共享内存,它的参数是shmat得到的返回值,即共享内存的地址。
注意它只是将该进程与共享内存解链接,并没有删除共享内存,当成功时返回0,失败则返回-1。
我们可以将server.c的代码修改如下:
key_t key=ftok(PATH_NAME,PROJ_ID);
if(key<0)
{
perror("ftok");
return 1;
}
int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);//这里的0666是权限,如果不给权限的话会影响挂接
if(shmid<0)
{
perror("shmid");
return 2;
}
printf("key:%d\nshmid:%d\n",key,shmid);
char* mem=(char*)shmat(shmid,NULL,0);//链接共享内存
printf("success to connect shm!\n");
sleep(15);
//进行进程间通信的代码
shmdt(mem); //解链接共享内存
printf("success to disconnect shm!\n");
可以将client端的代码修改如下:
key_t key=ftok(PATH_NAME,PROJ_ID);
if(key<0)
{
perror("ftok");
return 1;
}
int shmid=shmget(key,SIZE,IPC_CREAT);
if(shmid<0)
{
perror("shmget");
return 1;
}
char* mem=(char*)shmat(shmid,NULL,0);
printf("success to connect!\n");
sleep(25);
//进行进程间通信的代码
shmdt(mem);
printf("success to disconnect!\n");
当我们让server和client都死循环不退出,则可以看到共享内存挂接上了两个进程:
7.共享内存进行进程通信
当使用共享内存进行进程间的通信时,其模式与访问内存的方式是一样的:
server.c的代码进程通信部分如下:
while(1)
{
sleep(1);
printf("%s\n",mem);
}
server将共享内存mem的地址的内容进行打印。
我们将client.c的代码修改如下:
char c='A';
while(c<='Z')
{
mem[c-'A']=c;
c++;
mem[c-'A']=0;
sleep(2);
}
client不断将共享内存中的数据进行修改,而server不断接收共享内存中的数据。从而完成了进程之间的通信。
通过观察我们可以发现,在共享内存进行通信的过程中,server端不会等待client写入之后进行读出,而会一直进行读操作。这也说明了共享分内存进行通信其实是不太安全的。
8.补充两个小细节
(1)共享内存申请的空间
在使用共享内存的时候尽量使用4KB的整数倍来进行空间的申请,因为共享内存在内核中申请的基本单位是页,1页的大小就是4KB。当我们申请4097的空间的时候,操作系统在OS中申请的空间其实是2*4096大小,造成了空间浪费。但是操作系统只允许我们使用4097个字节大小的空间,多申请的我们也无法使用。但是它确实是多申请了。
(2)共享内存的结构体
shmctl的最后一个参数就是一个类似内核中存储共享内存结构的结构体:
我们可以看到他的内容,第一个元素是另一个结构体,其中该结构体的第一个元素存储的就是key。这也是为什么其他进程可以通过key找到该共享内存的原因。
9.共享内存总结
1.没有调用read/write等系统调用接口,所以共享内存一旦被创建就会映射到自己进程地址空间,shmat的使用方法就如同malloc的使用方法。不需要任何系统调用接口。
2.当client没有启动或者没有进行写入,或者已经写入结束的时候,server还是直接对共享内存的内容进行读取,读取和写入两者之间并不互相干扰。所以共享内存也是进程通信的方式中最快的。
3.共享内存不提供任何同步或者互斥的机制,而是需要程序员自行保证数据的安全。
三、消息队列
消息队列的知识和共享内存的大致相同,只不过是将消息由内存存储变成由操作系统建立的队列进行存储。
我们可以通过man手册来对其一些接口来进行查询:
关于消息队列和共享内存,我们需要记住几点:
1.它们的接口都是类似的。
2.数据结构的第一个结构类型是一样的。(所有的SystemV标准的IPC资源xxxip_ds结构体的第一个成员都是ipc_perm)
3.在内核中,所有的ipc资源都是通过数组组织起来的。
四、信号量
关于信号量,我们只需要了解一些概念:
1.信号量
信号量作为SystemV资源共享内存和消息队列的区别是,后者都是以传输数据为目的的,而信号量是以通过共享资源的方式来达到实现多个进程同步和互斥为目的的。
信号量的本质是一个计数器,它用来衡量统计临界资源中资源数目的。
2.临界资源
凡是被多个执行流同时能够访问的资源称为临界资源,比如进程通信的本质就是让多个进程同时可以看到同一份资源,因此管道,消息队列,共享内存都是临界资源。
由于信号量是一个对临界资源的计数器,当临界资源增加或者减少时要对信号量进行修改,如果要修改信号量,那么首先需要看到信号量,这就说明信号量也是临界资源。
3.临界区
进程的代码有很多,用来进行进程通信部分的代码称为临界区。(注意不是创建临界资源的代码)。
4.原子性
原子性用简单的话来讲就是一件事情要么不做,要么做完,没有中间状态。
假设有一份资源父进程和子进程都可以看到,都可以修改,那么就无法确定该资源再某一时间的内容是什么,这样的资源不具有原子性。
而信号量的修改就是有原子性的。(本质是一个计数器)。
五、总结
本节主要学习了SystemV的三种通信方式,其中重点学习了共享内存。
在共享内存中我们了解了它的原理,创建,删除,挂接,解挂接的方法,以及进行通信的方法,非常的详细。
消息队列与共享内存类似,信号量是用来描述临界资源数量的,它的修改是原子性的。
- 点赞
- 收藏
- 关注作者
评论(0)