IPC
在之前我们也有涉及到进程间通信的知识点,比如fork或exec或父进程读取子进程的退出码等,但是这种通信方式很有限,今天来学习进程间通信的其他技术——IPC(InterProcess Communication)。
IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。其中Socket和Streams支持不同主机上的两个进程IPC。
管道通信原理
管道,通常指无名管道,是UNIX系统IPC中最古老的形式
特点
- 半双工(类似于对讲机,即数据只能在一个方向上流动),具有固定的读端和写端,数据单向传递。管道中的数据读走就没有了。
- 它只能用于具有亲缘关系的进程之间的通信(父子进程或者兄弟进程之间)。
- 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的read、write等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。
原型
#include <unistd.h>
int pipe(int fd[2]); //返回值:若成功返回0,失败返回-1
当一个管道建立时,它会创建两个文件描述符:fd[0]为读而打开,fd[1]为写而打开。当我们需要关闭管道时,只需要将两个文件描述符关闭close即可。
利用管道实现进程间通信
实现父进程写入,子进程读取:
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(){
int fd[2];
pid_t pid;
char buf[128] = {'\0'};
//int pipe(int pipefd[2]);
if(pipe(fd) == -1){
printf("create pipe fail!\n");
}
pid = fork();
if(pid < 0){
printf("create child fail!\n");
}else if(pid > 0){ //父进程
printf("this is father\n");
close(fd[0]);
write(fd[1],"hello world",strlen("hello world"));
wait(NULL);
}else{ //子进程
printf("this is child\n");
close(fd[1]);
read(fd[0],buf,128);
printf("read from father:%s\n",buf);
exit(0);
}
return 0;
}
父进程在写入之前要将负责读的fd[0]关闭,子进程在读之前要将负责写的fd[1]关闭。无名管道使用比较简单,只需要分清楚fd[0]是负责读,fd[1]是负责写即可。相对来说无名管道的功能也比较单一,因此我们引入有名管道。
命名管道FIFO
FIFO也称命名管道,是一种文件类型
特点
- FIFO可以在无关的进程之间交换数据,与无名管道不同
- FIFO有路径名与之相关联,它以一种特殊设备文件形式存在于文件系统中。
原型
#include <sys/stat.h>
//返回值:成功返回0,失败返回-1
int mkfifo(const char *pathname,mode_t mode);
其中的mode参数于open参数中的mode相同。一旦创建了一个FIFO,就可以用一般的文件IO函数操作它。
当open一个FIFO时,是否设置非阻塞标志(O_NONBLOCK)的区别:
- 若没有指定O_NONBLOCK(默认),只读open要阻塞到某个其他进程为写而打开此FIFO。类似的,只写open要阻塞到某个其他进程为读而打开它。
- 若指定了O_NONBLOCK,则只读open立即返回。而只写open将出错返回-1.如果没有进程已经为读而打开该FIFO,其errno置ENXIO。
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
//int mkfifo(const char *pathname, mode_t mode);
int main(){
if((mkfifo("./file",0600) == -1) && errno == EEXIST){ //创建失败,且原因是由于文件已经存在
printf("create fifo fail!\n");
perror("why");
}
return 0;
}
运行结果:
我们可以利用open打开我们创建的FIFO:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
//int mkfifo(const char *pathname, mode_t mode);
int main(){
if((mkfifo("./file",0600) == -1) && errno != EEXIST){
printf("create fifo fail!\n");
perror("why");
}
int fd = open("./file",O_RDONLY);
printf("open success!\n");
return 0;
}
运行结果:
无法运行到printf语句,原因是当我们使用open打开FIFO时,默认使用非阻塞标志,当我们以只读的方式打开该FIFO时,它会一直阻塞,直到其他进程以写的方式打开该FIFO,因此我们需要编写一个以写的方式打开该FIFO的程序:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
//int mkfifo(const char *pathname, mode_t mode);
int main(){
int fd = open("./file",O_WRONLY);
printf("open success!\n");
首先运行read
再打开一个终端,同时运行write
此时程序成功的跑了起来。
接下去,可以通过read和write函数进行数据的交互:
读进程:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
//int mkfifo(const char *pathname, mode_t mode);
int main(){
int nread;
char readBuf[30] = {'\0'};
if((mkfifo("./file",0600) == -1) && errno != EEXIST){
printf("create fifo fail!\n");
perror("why");
}
int fd = open("./file",O_RDONLY);
printf("open success!\n");
nread = read(fd,readBuf,30);
printf("read %d byte data from fifo,context:%s\n",nread,readBuf);
close(fd);
return 0;
}
写进程:
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
//int mkfifo(const char *pathname, mode_t mode);
int main(){
char *str = "message from fifo";
int fd = open("./file",O_WRONLY);
printf("open success!\n");
write(fd,str,strlen(str));
close(fd);
return 0;
}
同时运行./read和./write运行结果:
./read
open success!
read 17 byte data from fifo,context:message from fifo
消息队列
消息队列,时消息的链表,存放在内核中。一个消息队列由一个标识符(即队列ID)来标识。
特点
- 消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
- 消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
- 消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
原型
#inlcude <sys/msg.h>
//创建或打开消息队列,成功返回队列ID,失败返回-1
int msgget(key_t key,int flag);
//添加消息:成功返回0,失败返回-1
int msgsnd(int msqid,const void *ptr,size_t size,int flag);
//读取消息:成功返回消息数据的长度,失败返回-1
int msgrcv(int msqid,void *ptr,size_t size,long type,int flag);
//控制消息队列,成功返回0,失败返回-1
int msgctl(int msqid,int cmd,struct msqid_ds *buf);
具体参数意义接键:消息队列函数(msgget、msgctl、msgsnd、msgrcv)及其范例
msgGet:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
//int msgget(key_t key, int msgflg);
//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
//int msgctl(int msqid, int cmd, struct msqid_ds *buf);
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main(){
struct msgbuf readBuf;
int msgId = msgget(0x1234,IPC_CREAT|0777); ///0777可读可写可执行
if(msgId == -1){
printf("get que fail!\n");
}
msgrcv(msgId,&readBuf,sizeof(readBuf.mtext),888,0);//msgflag=0阻塞式接收消息,没有该消息就阻塞等
待
printf("read data from que:%s\n",readBuf.mtext);
return 0;
}
msgSnd:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
//int msgget(key_t key, int msgflg);
//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
//int msgctl(int msqid, int cmd, struct msqid_ds *buf);
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main(){
struct msgbuf sendBuf = {888,"data frmo que"};
int msgId = msgget(0x1234,IPC_CREAT|0777);
if(msgId == -1){
printf("get que fail!\n");
}
msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);
return 0;
}
运行结果:
同时我们也可以让send和get同时收发数据:
msgGet.c:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
//int msgget(key_t key, int msgflg);
//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
//int msgctl(int msqid, int cmd, struct msqid_ds *buf);
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main(){
struct msgbuf readBuf;
struct msgbuf sendBuf = {988,"thank for your message"};
int msgId = msgget(0x1234,IPC_CREAT|0777);
if(msgId == -1){
printf("get que fail!\n");
}
msgrcv(msgId,&readBuf,sizeof(readBuf.mtext),888,0);//msgflag=0阻塞式接收消息,没有该消息就阻塞等
待
printf("read data from que:%s\n",readBuf.mtext);
msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);
return 0;
}
msgSnd.c:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
//int msgget(key_t key, int msgflg);
//int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
//ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
//int msgctl(int msqid, int cmd, struct msqid_ds *buf);
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[128]; /* message data */
};
int main(){
struct msgbuf sendBuf = {888,"data frmo que"};
struct msgbuf readBuf;
int msgId = msgget(0x1234,IPC_CREAT|0777);
if(msgId == -1){
printf("get que fail!\n");
}
msgsnd(msgId,&sendBuf,strlen(sendBuf.mtext),0);
msgrcv(msgId,&readBuf,sizeof(readBuf.mtext),988,0);
printf("receive data frmo get:%s\n",readBuf.mtext);
return 0;
}
运行结果:
./get
read data from que:data frmo que
./send
receive data frmo get:thank for your message
键值key生成和消息队列移除
系统建立IPC通讯(消息队列、信号量和共享内存)时必须指定一个ID值。通常情况下,该id值通过ftok函数得到
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
因此我们可以利用ftok来修改之前代码,使得代码看起来更加规范,更加高大上:
key_t key;
key = ftok(".",12);
printf("key = %x\n",key);
//在send和get代码中同时加入这段初始化定义即可,两段代码的key值要保持一致才能进行数据交互
运行结果:
./get
key = c056491
read data from que:data frmo que
./send
key = c056491
receive data frmo get:thank for your message
我们一次交互信息会用到一个key也就是创建了一个新的消息队列,很多队列我们可能使用一次之后就不会再用,留在系统中,因此我们可以考虑将其移除,此时可以用到msgctl:
msgctl
*int msgctl(int msqid, int cmd, struct msqid_ds buf);
cmd:我们此时要用的时IPC_RMID(将消息队列中的链表清除)
只需在上述两段代码的最后加入msgctl(msgId,IPC_RMID,NULL);即可完成消息队列的清除。