目录
9、进程间通信(IPC)
1、进程间通信概述
进程间通信有如下一些目的:
-
数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。
-
共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
-
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
-
资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
-
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
2、现在linux使用的进程间通信方式
- 管道(pipe)和命名管道(FIFO)
- 信号(signal)
- 消息队列
- 共享内存
- 信号量
- 套接字(socket)
3、消息队列
- 消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法
- 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
- 消息队列也有管道一样的不足,就是每个数据块的最大长度是有上限的,系统上全体队列的最大总长度也有一个上限
与管道的区别
- 避免命名管道的同步和阻塞问题
- 接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能顺序地接收。
3-1、msgget函数
- 作用:用来创建和访问一个消息队列,返回消息队列的标识码
int msgget(key_t key, int msgflg);
- 参数:
- key: 某个消息队列的名字
- msgflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
- 返回值:
- 成功将返回一个非负整数,即该消息队列的标识码;
- 失败,则返回-1
3-2、msgsnd函数
- 作用:把一条消息添加到消息队列里去
int msgsnd(int msgid,const void *msgp,size_t msgsz,int msgflg);
-
参数:
- msgid:由msgget函数返回的消息队列标识码
- msgp:是一个指针,指针指向准备发送的消息
- msgsz:是msgp指向的消息长度,这个长度不能保存消息类型的那个“long int”长整型计算在内
- msgflg:控制着当前消息队列满或到达系统上限时将要发生的事情
-
返回值:
- 成功:返回0
- 失败 :返回-1
-
msgflg=IPC_NOWAIT表示队列满不等待,返回EAGAIN错误。
-
消息结构在两方面受到制约。首先,它必须小于系统规定的上限值;其次,它必须以一个“long int”长整数开始,接收者函数将利用这个长整数确定消息的类型
-
最好把消息结构定义为下面这个样子:
struct msgbuf{
long mtype;
char mtext[1];
}
3-3、msgrcv函数
- 作用:是从一个消息队列里检索消息
int msgrcv(int msgid,void *msgp,size_t msgsz,long msgtype,int msgflg);
-
参数:
-
msgid:由msgget函数返回的消息队列标识码
-
msgp:是一个指针,指针指向准备接收的消息
-
msgsz:是msgp指向的消息长度,这个长度不能保存消息类型的那个“long int”长整型计算在内
-
msgtype:它可以实现接收优先级的简单形式
-
msgflg:控制着队列中没有相应类型的消息可供接收时将要发生的事
-
-
返回值:
- 成功:返回实际放到接收缓冲区里去的字符个数
- 失败:返回 -1
参数 | 描述 |
---|---|
msgtype=0 | 返回队列第一条信息 |
msgtype>0 | 返回队列第一条类型等于msgtype的消息 |
msgtype<0 | 返回队列第一条类型小于等于msgtype绝对值的消息 |
msgflg=IPC_NOWAIT | 队列没有可读消息不等待,返回ENOMSG错误。 |
msgflg=MSG_NOERROR | 消息大小超过msgsz时被截断 |
msgtype>0且msgflg=MSC_EXCEPT | 接收类型不等于msgtype的第一条消息。 |
3-4、msgctl函数
- 作用:消息队列的控制函数,与共享内存的控制函数很相似
int msgctl(int msqid,int cmd,strcut msqid_ds *buf);
- 参数:
- msqid:由msgget函数返回的消息队列标识码
- cmd:是将要采取的动作,(有三个可取值)
命令 | 说明 |
---|---|
IPC_STAT | 把msqid_ds结构中的数据设置为消息队列的当前关联值 |
IPC_SET | 在进程有足够权限的前提下,把消息队列的当前关联值设置为msqid_ds数据结构中给出的值 |
IPC_RMID | 删除消息队列 |
- 返回值:
- 成功:返回0
- 失败:返回-1
3-5、消息队列代码示例
//recv.cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct my_msg_st{
long my_msg_type;
char some_text[BUFSIZ];
};
int main(){
struct my_msg_st some_data;
int msgid = msgget((key_t)1234,0666 | IPC_CREAT);
if(msgid == -1){
fprintf(stderr,"msgget failed with error:%d\n",errno);
exit(1);
}
while(1){
if(msgrcv(msgid,(void *)&some_data,BUFSIZ,0,0) == -1){
fprintf(stderr,"msgrecv faild,error:%d\n",errno);
exit(1);
}
printf("recv:%ld %s\n",some_data.my_msg_type,some_data.some_text);
if(strncmp(some_data.some_text,"end",3) == 0){
break;
}
}
if(msgctl(msgid,IPC_RMID,0) == -1){
fprintf(stderr,"msgctl failed\n");
exit(1);
}
exit(0);
return 0;
}
//send.cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <iostream>
struct my_msg_st{
long my_msg_type;
char some_text[BUFSIZ];
};
int main(){
struct my_msg_st some_data;
int msgid = msgget((key_t)1234,0666 | IPC_CREAT);
if(msgid == -1){
fprintf(stderr,"msgget failed with error:%d\n",errno);
exit(1);
}
some_data.my_msg_type = 1;//必须要赋值并且>0
while(1){
printf("please input some text:\n");
scanf("%s",some_data.some_text);
if(msgsnd(msgid,(void *)&some_data,BUFSIZ,0) == -1){
fprintf(stderr,"msgsnd faild,error:%d\n",errno);
exit(1);
}
if(strncmp(some_data.some_text,"end",3) == 0){
break;
}
}
if(msgctl(msgid,IPC_RMID,0) == -1){
fprintf(stderr,"msgctl failed\n");
exit(1);
}
exit(0);
return 0;
}
4、共享内存
- 共享内存允许两个不相关的进程去访问同一部分逻辑内存
- 如果需要在两个运行中的进程之间传输数据,共享内存将是一种效率极高的解决方案
共享内存概述
- 共享内存是由IPC为一个进程创建的一个特殊的地址范围,它将出现在进程的地址空间中。
- 其他进程可以把同一段共享内存段“连接到”它们自己的地址空间里去。
- 所有进程都可以访问共享内存地址,就好像它们是有malloc分配的一样
- 如果一个进程向这段共享内存写了数据,所做的改动会立刻被有权访问同一段共享内存的其他进程看到
4-1、shmget函数
- 作用:用来创建共享内存
int shmget(key_t key,size_t size,int shmflg);
- 参数:
- key:共享内存段的名字
- size:需要共享的内存量
- shmflg:由9个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的。
- 返回值:
- 成功:返回一个非负整数,即该段共享内存的标识码
- 失败:返回 -1
4-2、shmat函数
- 作用:共享内存段刚被创建的时候,任何进程还都不能访问它,为了建立对这个共享内存段的访问渠道,必须由我们来把它连接到某个进程的地址空间,shmat函数就是用来完成这项工作的。
void *shmat(int shm_id,const void *shm_addr,int shmflg);
-
参数:
- shm_id:shmget返回的共享内存标识
- shm_addr:把共享内存连接到当前进程去的时候准备放置它的那个地址
- shmflg:是一组按位OR(或)在一起的标志。它的两个可能取值是SHM_RND和SHM_RDONLY
-
返回值:
- 成功:返回一个指针,指针指向共享内存的第一个字节
- 失败:返回 -1
参数 | 描述 |
---|---|
shmaddr为0 | 内核自动选择一个地址 |
shmaddr不为0且shmflg无SHM_RND标记 | 以shmaddr为连接地址。 |
shmaddr不为0且shmflg设置了SHM_RND标记 | 连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA) |
shmflg=SHM_RDONLY | 表示连接操作用来只读共享内存 |
- 在fork() 后,子进程继承已连接的共享内存
- 在exec后,已连接的共享内存会自动脱离(detach)
- 在结束进程后,已连接的共享内存会自动脱离(detach)
4-3、shmdt函数
- 作用:把共享内存与当前进程脱离开
int shmdt(const void *shm_addr);
- 参数:
- shm_addr:由shmat返回的地址指针
- 返回值:
- 成功:返回 0
- 失败:返回 -1
脱离共享内存并不等于删除它,只是当前进程不能再继续访问它而已
4-4、shmctl函数
- 作用:共享内存的控制函数
int shmctl(int shm_id,int conmand,struct shmid_ds *buf);
-
参数:
- shm_id:由shmget返回的共享内存标识码
- command:将要采取的动作(有三个可取值)
- buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
-
返回值:
- 成功:返回 0
- 失败:返回 -1
command:将要采取的动作(有三个可取值),分别如下:
命令 | 说明 |
---|---|
IPC_STAT | 把shmid_ds结构中的数据设置为共享内存的当前关联值 |
IPC_SET | 在进程有足够权限的前提下,把共享内存的当前关联值设置为shmid_ds数据结构中给出的值 |
IPC_RMID | 删除共享内存段 |
4-5、共享内存示例
//shminfo.h
typedef struct shmbuf_st{
int bexit;
int rdwr;
char name[64];
int id;
}shmbuf_t;
//shmread.cpp
#include <stdio.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <stdlib.h>
#include <sys/shm.h>
#include "shminfo.h"
int main(){
//1.创建或获取
//int shmget(key_t key,size_t size,int shmflg);
int shmid = shmget((key_t)1000,0,0);
if(shmid == -1){
perror("shmget");
exit(1);
}
//2.连接
//void *shmat(int shmid, const void *shmaddr, int shmflg);
void *shmaddr = shmat(shmid,NULL,0);
if(shmaddr == (void *) -1){
perror("shmat");
exit(1);
}
//3.正常对共享内存操作
shmbuf_t *pbuf = (shmbuf_t *)shmaddr;
while(1){
if(pbuf->rdwr == 0){//判断可读
printf("id = %d,name = %s \n",pbuf->id,pbuf->name);
if(pbuf->bexit){
break;
}
pbuf->rdwr = 1;//重新设置为可写
}
}
//4.断开连接
//int shmdt(const void *shm_addr);
if(shmdt(shmaddr) == -1){
perror("shmdt");
}
//5.删除共享内存
//int shmctl(int shmid, int cmd, struct shmid_ds *buf);
//shmctl(shmid, IPC_RMID, NULL);
return 0;
}
//shmwrite.cpp
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/ipc.h>
#include <stdlib.h>
#include <sys/shm.h>
#include "shminfo.h"
int main(){
//1.创建或获取共享内存
//int shmget(key_t key,size_t size,int shmflg);
int shmid = shmget((key_t)1000,0,0);
if(shmid != -1){
printf("has exit shared memory!\n");
shmctl(shmid,IPC_RMID,NULL);
}
shmid = shmget((key_t)1000,sizeof(shmbuf_t),IPC_CREAT|0666);{
if(shmid == -1){
perror("shmget");
exit(1);
}
}
//2.连接到共享内存
//void *shmat(int shmid,const void *shmaddr,int shmflg);
void *shmaddr = shmat(shmid,NULL,0);
if(shmaddr == (void *)-1){
perror("shmat");
exit(1);
}
//3.正常对共享内存操作
shmbuf_t *pbuf = (shmbuf_t *)shmaddr;
pbuf->rdwr = 1;
pbuf->bexit = 0;
while(1){
if(pbuf->rdwr == 1){
printf("Please input id:");
scanf("%d",&pbuf->id);
printf("Please input name:");
scanf("%s",pbuf->name);
printf("exit?");
scanf("%d",&pbuf->bexit);
pbuf->rdwr = 0;//设置为可读
if(pbuf->bexit){
sleep(1);
break;
}
}
}
//4.断开连接
shmdt(shmaddr);
//5.删除共享内存
//int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
5、信号量(PV操作)
- Dijkstra提出的“信号量”概念是共发程序设计领域的一项重大进步
- 信号量是一种变量,它只能取正整数值,对这些正整数只能进行两种操作:等待和信号
- 用两种记号来表示信号量的这两种操作:
- P(semaphore variable) 代表等待
- V(semaphore variable) 代表信号
信号量的分类
- 最简单的信号量是一个只能取“0”和“1”值的变量,也就是人们常说的“二进制信号量”
- 可以取多种正整数值的信号量叫做“通用信号量”
PV操作的定义
假设我们有一个信号量变量sv,则pv操作的定义如下:
- P(sv):如果sv的值大于零,就给它减去1;如果sv的值等于零,就挂起该进程的执行
- V(sv):如果有其他进程因等待sv变量而被挂起,就让它恢复执行;如果没有进程因等待sv变量而被挂起,就给它加1
PV操作工作情况
- 两个进程共享着sv信号量变量。如果其中之一执行了P(sv)操作,就等于它得到了信号量,也就能够进入关键代码部分了。
- 第二个进程将无法进入关键代码,因为当它尝试执行P(sv)操作的时候,它会被挂起等待一个进程离开关键代码并执行V(sv)操作释放这个信号量
5-1、semget函数
- 作用:创建一个新的信号量或者取得一个现有信号量的键字
int semget(key_t key,int num_sems,int sem_flag);
- 参数:
- key:是一个整数值,不相关的进程将通过这个值去访问同一个信号量
- num_sems:需要使用的信号量个数,它几乎总是取值为1
- sem_flags:是一组标志,其作用与open函数的各种标志很相似,它低端的九个位是该信号量的权限,其作用相当于文件的访问权限,可以与键值IPC_CREATE做按位的OR操作以创建一个新的信号量
- 返回值:
- 成功:返回一个正数值,它就是其他信号量函数要用到的那个标识码
- 失败:返回 -1
5-2、semop函数
- 作用:改变信号量的键值
int semop(int sem_id,struct sembuf *sem_ops,size_t num_sem_ops);
struct sembuf{
short sem_num; //信号量的编号,如果你的工作不需要使用一组信号量,这个值一般就取为0。
short sem_op; //信号量一次PV操作时加减的数值,一般-1(P)和+1(V)
short sem_flg; //通常被设置为SEM_UNDO.她将使操作系统跟踪当前进程对该信号量的修改情况
};
- 参数:
- sem_id:是该信号量的标识码,也就是semget函数的返回值
- sem_ops:是个指向一个结构数值的指针
- num_sem_ops:指定要执行的操作个数,即sem_ops数组中元素的个数。
5-3、semctl函数
- 作用:允许我们直接控制信号量的信息
int semctl(int sem_id,int sem_num,int command,...);
- 参数:
- sem_id:由semget函数返回的一个信号量标识码
- sem_num:信号量的编号,如果在工作中需要使用到成组的信号量,就要用到这个编号;它一般取值为0,表示这是第一个也是唯一的信号量
- command:将要采取的操作动作
- 如果还有第四个参数,那它将是一个“union semun”复合结构
| command | 描述 |
| -------- | -------- |
| SETVAL | 用来把信号量初始化为一个已知的值,这个值在semun结构里是以val成员的面目传递的。 |
| IPC_RMID | 删除一个已经没有人继续使用的信号量标识码|
- 如果还有第四个参数,那它将是一个“union semun”复合结构
5-4、信号量示例
semget的调用者可以给其key参数传递一个特殊的键值IPC_PRICATE(其值为0),这样无理该信号量是否已经存在,semget都将创建一个新的信号量。使用该键值创建的信号量并非像它的名字声称的那样是进程私有的。其他进程,尤其是子进程,也有方法来访问这个信号量。所以semget的man手册的BUGS部分上说,使用名字IPC_PRIVATE有些误导(历史原因),应该称为IPC_NEW。比如下面的代码清单就在父、子进程间使用一个IPC_PRIVATE信号量来同步。
#include <sys/sem.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
union semun{
int val;
struct semid_ds *buf;
unsigned short int *array;
struct seminfo *_buf;
};
//op为-1时执行P操作,op为1时执行V操作
void pv(int sem_id,int op)
{
struct sembuf sem_b;
sem_b.sem_num = 0;
sem_b.sem_op = op;
sem_b.sem_flg = SEM_UNDO;
//int semop(int sem_id,struct sembuf *sem_ops,size_t num_sem_ops);
semop(sem_id,&sem_b,1);
}
int main(int argc,char *argv[])
{
//int semget(key_t key,int num_sems,int sem_flag);
int sem_id = semget(IPC_PRIVATE,1,0666);
union semun sem_un;
sem_un.val = 1;
//int semctl(int sem_id,int sem_num,int command,...);
semctl(sem_id,0,SETVAL,sem_un);
pid_t id = fork();
if(id < 0){
return 1;
}else if(id == 0){
printf("child try to get binary sem\n");
//在父、子进程间共享IPC_PRIVATE信号量的关键就在于二者都可以操作该信号量的标识符sem_id
pv(sem_id,-1);
printf("child get the sem and would release it after 5 second\n");
sleep(5);
pv(sem_id,1);
exit(0);
}else{
printf("parent try to get binary sem\n");
pv(sem_id,-1);
printf("parent get the sem and would release it after 5 second\n");
sleep(5);
pv(sem_id,1);
}
waitpid(id,NULL,0);
semctl(sem_id,0,IPC_RMID,sem_un);
return 0;
}