进程间通信-管道&消息队列
进程间通信的目的:
(1)数据传输:一个进程需要将它的数据发送给另一个进程;
(2)资源共享:多个进程之间需要共享同样的资源;
(3)通知事件:一个进程需要向另一个进程发送消息,通知它(它们)发生了某些事(比如子进程终止时要通知父进程);
(4)进程控制:有些进程希望完全控制另一个进程的执行,能够及时知道它的状态改变。
进程间通信(IPC)分类:
(1)管道
1)匿名管道
2)命名管道
(2)System V IPC
1)System V消息队列
2)System V共享内存
3)System V信号量
(3)POSIX IPC
1)消息队列
2)共享内存
3)信号量
4)互斥量
5)条件变量
6)读写锁
本文主要介绍进程间通信的前两类,即管道和System V标准的IPC。
一.管道
我们把从一个进程连接到另一个进程的一个数据流称为一个管道。
(1)管道的几个特性:
管道只能实现单向通信,若要实现双方通信,可以建立两个管道;
只要带有血缘关系的进程都可通信(命名管道可以实现任意两个进程间的通信,不用管是否有血缘关系);
进程退出,管道释放,即管道的生命周期随进程;
管道自带互斥与同步机制(不用担心若两个进程同时向管道写数据而产生的数据的不一致等问题);
管道提供面向字节流的服务(不用将管道内的数据全部读完,想读多少字节完全由上层应用决定)。
(2)相关的几个名字解释
数据不一致:读写双方共同访问资源,写端还未写完,但读端已读,此时读到的数据可能就是错误的;
临界资源:两个毫不相干的进程看到的公共资源;
临界区:访问临界资源的代码;
互斥:任一时刻,在临界区访问临界资源的,有且仅有它一个;
同步:在互斥的访问临界资源的基础上,具有的顺序性;
原子性:要么不做,要么做完,无中间状态。
(3)管道本质
在Linux下,一切皆文件。所以在内核看来,管道也是一个文件。所以向管道读写数据时,可以调用write、read函数(具体用法在之前的基础IO中有写过)。
1. 匿名管道
只要有血缘关系(常见父子进程)的进程间都可通信。
(1)使用方式
1)函数原型
2)功能:创建一个匿名管道
3)参数:pipefd[2]是一个文件描述符数组,其中pipefd[1]表示读端,pipefd[2]表示写端
4)返回值:成功返回0,失败返回错误代码
(2)匿名管道的读写规则:
管道内无数据可读时,读端一直阻塞式等待,直至有数据为止;
管道满时,写端阻塞,直至读端读走数据;
写端写入一定数据后不再写入且关闭,读端读完管道内数据后(读完管道内数据时,会返回一个0值,表示读到结尾)通信才算结束,管道才关闭;
若写端一直在写,但读端不读且关闭,写端会被OS
发送13号SIGPIPE信号而中止。
(3)匿名管道实现
以下代码实现父子进程间的通信,子进程每隔1秒向管道写数据,父进程则一直在管道里读数据。
#include <stdio.h> #include <unistd.h> #include <string.h> int main() { int fd[2]; int ret = pipe(fd);//创建管道 if(ret < 0) { perror("pipe"); return 1; } pid_t id = fork(); if(id == 0)//child->w { close(fd[0]);//关闭读段 const char* msg = "hello ,i am child\n"; while(1) { sleep(1); write(fd[1],msg,strlen(msg));//向管道里写数据,即向文件里写数据,\0不属于字符串内容 } } else//parent->r { close(fd[1]);//关闭写段 char buf[64]; while(1) { size_t s = read(fd[0],buf,sizeof(buf)-1);//s为实际读的个数 if(s > 0)//读成功了 { buf[s] = '\0'; printf("f say: %s\n",buf); } } } return 0; }
运行结果:
2. 命名管道
匿名管道的一个限制即只能在有血缘关系的进程间实现通信,而命名管道可以让任意两个(甚至于毫无关系)的两个进程实现通信。命名管道同样是一种特殊的文件类型。
(1)使用方式
1)命令行上创建
mkfifo filename
2)程序中创建
函数原型:
参数:
pathname:路径
mode: 要创建的管道的权限
返回值:成功返回0,失败返回-1
(2)命名管道打开规则
若当前打开方式为读而打开FIFO,阻塞到有进程为写打开FIFO;
若当前打开方式为写而打开FIFO,阻塞到有进程为读打开FIFO;
(3)命名管道实现
读取文件txt内容,写入管道myfifo
//将txt文件内容写入管道 #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { mkfifo("myfifo", 0644); //得到txt文件的文件描述符 int infd = open("txt",O_RDONLY);//成功返回文件描述符,失败返回-1 if(infd == -1) { perror("open"); return; } //得到管道文件的文件描述符 int outfd = open("myfifo",O_WRONLY); if(outfd == -1) { perror("open"); return; } char buf[64]; size_t s; while((s = read(infd,buf,sizeof(buf))) > 0)//将txt文件内容读到buf { write(outfd,buf,s);//将txt读到buf中的内容写到管道中 } close(infd); close(outfd); return 0; }
读取管道,将数据写到txt1文件中
//将管道内容写入文件txt1 #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { mkfifo("myfifo",644); //得到输出文件的文件描述符 int outfd = open("txt1",O_WRONLY | O_CREAT | O_TRUNC,0644);//成功返回文件描述符,失败返回-1 //O_TRUNC表示文件若已存在,且以只写或可读可写方式打开,则将其长度截断为0字节 if(outfd == -1) { perror("open"); return; } //得到管道文件的文件描述符 int infd = open("myfifo",O_RDONLY); if(infd == -1) { perror("open"); return; } char buf[64]; size_t s; while((s = read(infd,buf,sizeof(buf))) > 0)//将管道中数据读到buf { write(outfd,buf,s);//将管道中读到buf中的内容写到txt1中 } close(infd); close(outfd); unlink("myfifo");//删除指定名字的文件 return 0; }
运行结果:
首先运行test,将文件内容写入管道,再打开一个终端运行test1,将管道内容写到新文件txt1中。
运行test时,它一直处于运行状态,不会自己结束。等把test1运行之后,test1读完管道内的数据,进程结束,管道同样结束。此时test才会终止。
对比原文件txt内容,发现代码实现了文件的拷贝功能,而且是两个毫不相干的进程。
(4)用命名管道实现两个进程间的单向通信
创建server.c用于接收信息,实现功能是创建一个命名管道并从管道内拿数据,具体代码如下:
//接收client发来的内容 #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> #include <fcntl.h> int main() { int ret = mkfifo("myfifo", 0644); if(ret < 0) { perror("mkfifo"); return 1; } int fd = open("./myfifo",O_RDONLY);//read if(fd < 0) { perror("open"); return 2; } char buf[64]; while(1) { size_t s = read(fd,buf,sizeof(buf)-1);//将管道内容读到buf中 if(s > 0) { buf[s] = '\0'; printf("server# %s\n",buf); } else if(s == 0)//读完了 { printf("client quit\n"); break; } else { perror("read"); return 3; } } close(fd); return 0; }再创建一个client.c用于发消息,实现的功能为向server.c创建的管道内写数据,具体代码如下
//向server发消息 #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <string.h> #include <fcntl.h> int main() { int fd = open("./myfifo",O_WRONLY);//write if(fd < 0) { perror("open"); return 1; } char buf[64]; while(1) { buf[0] = 0; printf("Please Enter:"); fflush(stdout); //scanf("%s",buf); size_t s = read(0,buf,sizeof(buf)-1);//从键盘读数据 if(strcmp(buf,"quit") == 0)//输入quit表示结束通信 { break; } if(s > 0) { buf[s] = 0; write(fd,buf,strlen(buf));//向管道内写数据,向文件里写不用读入'\0' } else { perror("read"); return 2; } } close(fd); unlink("myfifo"); return 0; }
特别要注意的是:运行时,必须让server.c先跑起来,因为它的程序代码创建了管道。
运行结果如下:
3. 匿名管道与命名管道的区别
(1)匿名管道由pipe函数创建并打开,在硬盘上无对应文件;
(2)命名管道由mkfifo函数创建,在硬盘上有对应文件,用open打开;
(3)唯一区别就是创建与打开的方式不同。
二. 消息队列
消息队列提供了从一个进程向另一个进程发送有类型数据块(可以标记是哪个进程发的)的方法;
每个有数据块都被认为有类型,接收者进程接收的数据块可以有不同的类型值;
每个消息的最大长度有上限,每个消息队列总字节数也有上限,系统上消息队列的总数也有上限。
上图为消息队列实现进程间通信的基本原理,进程A,B想看到同一消息队列的方法在Linux下是调用ftok函数产生两个相同的key值,这样就看到同一个消息队列。在Linux下,系统提供了几个接口函数以实现利用消息队列实现进程间通信。
1. 消息队列函数介绍
(1)msgget函数
2)函数功能:创建和访问一个消息队列
3)参数:
key:某个消息队列的名字
msgflg:它可有以下几个选择:IPC_CREAT、IPC_EXCL
若IPC_CREAT和IPC_EXCL同时设置,若消息队列已存在则出错返回,否则创建消息队列并返回。它保证在成功创建的前提下,能够获得一个全新的消息队列;
若只设置IPC_CREAT,
若消息队列已存在则返回,否则创建消息队列并返回。它能够获得一个已有的消息队列;
若只设置IPC_EXCL无意义
4)返回值:成功返回一个非负整数(消息队列的标识码),失败返回-1
(2)msgctl函数
1)函数原型:
2)函数功能:控制消息队列(可根据参数进行相关操作)
3)参数:
msqid:由msgget函数返回的消息队列标识码
cmd:将要采取的动作,有三个可取值:IPC_STAT(拷贝)、IPC_SET(设置)、IPC_RMID(删除)
buf:消息队列管理结构体,不使用的时候设置为NULL
4)返回值:成功返回0,失败返回-1
(3)msgsnd函数
1)函数原型:
2)函数功能:把一条消息添加到消息队列中
3)参数:
msqid:由msgget函数返回的消息队列标识码
msgp:一个指针,指向准备发送的消息
msgsz:msgp指向消息的长度(不含保存消息类型的长整型)
msgflg:控制当前消息队列满或到达系统上限时将要发生的事情,缺省为0,默认为阻塞方式。msgflg = IPC_NOWAIT表示队列满不等待,返回EAGAIN错误
4)返回值:成功返回0,失败返回-1
5)说明:
消息结构必须小于系统规定的上限值;
消息结构必须以一个长整型开始,接受者将利用这个长整型确定消息的类型。
消息结构的参考类型如下:
struct msgbuf { long mtype; char mtext[1]; };
(4)msgrcv函数
1)函数原型
2)函数功能:从一个消息队列接收消息
3)参数
msqid:由msgget函数返回的消息队列标识码
msgp:一个指针,指向准备发送的消息
msgsz:msgp指向消息的长度(不含保存消息类型的长整型)
msgtype:实现结束优先级的简单形式
msgflg:控制当前消息队列满或到达系统上限时将要发生的事情,缺省为0
4)返回值:成功返回实际放到缓冲区里去的字符个数,失败返回-1
5)说明:
msgtype = 0,返回消息队列的第一条信息;
msgtype > 0,返回消息队列第一条类型等于msgtype的消息;
msgtype < 0,返回消息队列第一条类型小于等于msgtype绝对值的消息,并且是满足条件的消息类型最小的消息;
msgflg = IPC_NOWAIT,队列没有可读消息不等待,返回ENOMSG错误;
msgflg = MSG_NOERROR,消息大小超过msgsz时被截断;
msgtype > 0且msgflg = MSG_EXCEPT,接收类型不等于msgtype的第一条消息
(5)ftok函数
1)函数原型:
2)函数功能:就如前文所提,要使两个进程看到相同的消息队列,在linux下是让它们产生同一个key值,这个ftork函数就是用来产生key值
3)参数
pathname:一个存在的可访问的路径或文件
proj_id:该参数的后8位与第一个参数一起确定一个key值,所以该参数是产生是否相同的key值的关键。且同一个该参数不能出现在同一个路径下
4)返回值:失败返回-1,成功返回一个key值
2. 消息队列实现两进程间双向通信
server.c
#include "comm.c" int main() { //printf("## server: ##\n"); int msgid = CreateMsgQueue();//创建一个消息队列 char buf[1024]; printf("## server: ##\n"); while(1) { //接收消息 int ret = ReceveMsg(msgid, CLIENT_TYPE, buf);//接收队列第一条type为CLIENT_TYPE的消息 if(ret == 0) printf("client# %s\n",buf); else printf("receve error\n"); //发送消息 printf("Please Enter# "); fflush(stdout); ssize_t s = read(0, buf, sizeof(buf)-1); if(s > 0) { buf[s] = 0; int ret = SendMsg(msgid, SERVER_TYPE, buf);//发送type为SERVER_TYPE的消息 if(ret == 0) printf("send done!\n"); else printf("send error\n"); } else perror("read"); } DestroyMsgQueue(msgid); return 0; }
client.c
#include "comm.c" int main() { printf("## client: ##\n"); int msgid = GetMsgQueue();//server已创建一个消息队列,所以这里只需获得已有消息队列即可 char buf[1024]; while(1) { //发送消息 printf("Please Enter# "); fflush(stdout);//printf未遇到回车换行,上句不能自己输出到屏幕 ssize_t s = read(0, buf, sizeof(buf)-1); if(s > 0) { buf[s] = 0; int ret = SendMsg(msgid, CLIENT_TYPE, buf);//发送type为CLIENT_TYPE的消息 if(ret == 0) printf("send done!\n"); else printf("send error\n"); } else perror("read"); //接收消息 int ret = ReceveMsg(msgid, SERVER_TYPE, buf);//接收队列第一条type为SERVER_TYPE的消息 if(ret == 0) printf("client# %s\n",buf); else printf("receve error\n"); } return 0; }
运行结果:
./server
./client
同样,在运行时要先运行./server,因为该程序创建了消息队列,./client只是获得了它已创建好的消息队列以实现通信。
3. ipcs & ipcrm命令
在以上用ctrl+c命令中止的两进程,还未调用销毁消息队列的代码,所以创建的IPC未销毁,在下次再运行以上代码时会出错,如下图:
解决办法就是,删除IPC资源,Linux下对应的命令就有ipcs和ipcrm两个,使用方法如下图:
可以看到,再次运行程序时,就不会有问题。当然,你也可以将以上的代码不用while(1)死循环,而是设置相应的退出条件,以实现后面能够进行调用销毁消息队列的代码,就不会存在这样的问题了。