文章目录
- 一.简介
- 二.管道
- 2.1 匿名管道
- 2.2 命名管道
- 三. 消息队列
一、简介
Linux进程通信机制:
- 管道
- 信号量
- 消息队列
- 共享内存
- socket通信
二、管道
管道其实质是由内核管理的一个缓冲区
形象地认为管道的两端连接着两个进程:
- 一个进程进行信息输出,将数据写入管道;
- 另一个进程进行信息输入,从管道中读取信息。
管道分为:
- 匿名管道:只能用于有亲缘关系的进程间通信,进程退出后管道会被销毁。
- 命名管道:命名管道与进程的联系较弱,相当于一个读写内存的接口,进程退出后,命名管道依然存在。
2.1 匿名管道
匿名管道的使用流程如下:
①在进程中创建匿名管道,pipe函数;
②关闭进程中不使用的管道端口,close函数;
③在待通信的进程中分别对管道的读、写端口进行操作,read/write函数;
④关闭管道,close函数。
2.1.1 pipe函数
功能:创建匿名管道
#include <unistd.h>
int pipe(int pipefd[2]);
【案例】使用pipe()实现父子进程间通信,父进程作为读端,子进程作为写端。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(){
int tempFd[2];//定义文件描述符数组
int tempRet=pipe(tempFd);//创建管道
if(tempRet == -1){
perror("pipe");
exit(1);
}
pid_t tempPid=fork();
if(tempPid > 0){//父进程—读
close(tempFd[1]);//关闭写端
char tempBuf[64]={0};
tempRet = read(tempFd[0], tempBuf, sizeof(tempBuf));//读数据
close(tempFd[0]);
write(STDOUT_FILENO, tempBuf, tempRet);//将读到的数据写到标准输出
wait(NULL);
} else if(tempPid == 0){//子进程—写
close(tempFd[0]);//关闭读端
char *tempStr="hello,pipe\n";
write(tempFd[1], tempStr, strlen(tempStr)+1);//写数据
close(tempFd[1]);
}//of if
return 0;
}//of main
分析如下:
pipe()创建管道后读端的文件描述符为fd[0],写端的文件描述符为fd[1];
调用fork后父子进程共享文件描述符,文件描述符与管道的关系如图所示:
父进程进行读操作,子进程进行写操作;使用close()函数关闭父进程的写端与子进程的读端。
运行结果如下:
2.1.2 dup2函数
重定向函数dup2:代码如下
#include <unistd.h>
int dup2(int oldfd, int newfd);
其功能是将参数oldfd的文件描述符复制给newfd
【案例】使用管道实现兄弟进程间通信,兄弟进程实现命令“ls | wc –l”的功能。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(){
int tempFd[2];
int tempRet = pipe(tempFd);
if(tempRet == -1){
perror("pipe err");
exit(1);
}//of if
int i;
pid_t tempPid, tempWpid;
for(i=0; i<2; i++){//2个子
if((tempPid = fork()) == 0){
break;
}//of if
}//of if
if(2 == i){//父进程,回收子进程
close(tempFd[0]); //关闭读
close(tempFd[1]); //关闭写
tempWpid = wait(NULL);
printf("wait child 1 success,pid=%d\n", tempWpid );
tempPid = wait(NULL);
printf("wait child 2 success,pid=%d\n", tempPid);
} else if(0 == i){//子进程1—写
close(tempFd[0]);
dup2(tempFd[1], STDOUT_FILENO);//定向到标准输出
execlp("ls", "ls", NULL);
} else if(1 == i){//子进程2—读
close(tempFd[1]);
dup2(tempFd[0], STDIN_FILENO);
execlp("wc", "wc", "-l", NULL);
}//of if
return 0;
}//of main
兄弟进程间通信,进程文件描述符与管道的关系如图所示:
运行结果如下:
2.1.3 popen/pclose函数
功能:
popen函数的功能是:
调用pipe()函数创建管道;
调用fork()函数创建子进程;
之后在子进程中通过execve()函数调用shell命令执行相应功能。
pclose函数的功能:
关闭popen()打开的I/O流;
通过调用wait()函数等待子进程命令执行结束;
返回shell的终止状态,防止产生僵尸进程。
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
【案例】使用popen与pclose函数实现管道通信。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
FILE *tempRfp,*tempWfp;
char temBuf[100];
tempRfp = popen("ls","r"); //读取命令执行结果
tempWfp = popen("wc -l","w"); //将管道中的数据传递给进程
while(fgets(temBuf, sizeof(temBuf), tempRfp)!=NULL){
fputs(temBuf, tempWfp);
}//of while
pclose(tempRfp);
pclose(tempWfp);
return 0;
}//of main
运行结果如下:
2.2 命名管道
功能:创建命名管道(FIFO文件),命名管道与系统中的一个路径名关联,以文件的形式存在于文件系统中,通过FIFO的路径名访问FIFO文件,实现进程间通信。
#include <sys/type.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
【案例】使用FIFO实现没有亲缘关系进程间的通信。没有亲缘关系的进程间通信,需要两段程序来实现:
- fifo_write.c实现FIFO的写操作;
- fifo_read.c实现FIFO的读操作。
fifo_write.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int paraArgc,char *paraArgv[]){
if(paraArgc < 2){ //判断是否传入文件名
printf("./a.out fifoname\n");
exit(1);
}//of if
int tempRet = access(paraArgv[1], F_OK); //判断fifo文件是否存在
if(tempRet == -1){ //若fifo不存在就创建fifo
int tempFIFO = mkfifo(paraArgv[1], 0664);
if(tempFIFO == -1){ //判断文件是否创建成功
perror("mkfifo");
exit(1);
} else{
printf("fifo creat success!\n");
}//of if
}//of if
int tempFd = open(paraArgv[1], O_WRONLY); //读写方式打开
while(1){ //循环写入数据
char *tempStrp="hello,world!";
write(tempFd, tempStrp, strlen(tempStrp)+1);
sleep(1);
}//of while
close(tempFd);
return 0;
}//of main
fifo_read.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int paraArgc,char *paraArgv[]){
if(paraArgc < 2){ //判断是否传入文件名
printf("./a.out fifoname\n");
exit(1);
}//of if
int tempRet = access(paraArgv[1], F_OK); //判断fifo文件是否存在
if(tempRet == -1){ //若fifo不存在就创建fifo
int tempFIFO = mkfifo(paraArgv[1], 0664);
if(tempFIFO == -1){ //判断文件是否创建成功
perror("mkfifo");
exit(1);
} else{
printf("fifo creat success!\n");
}//of if
}//of if
int tempFd = open(paraArgv[1], O_RDONLY); //只读方式打开
if(tempFd == -1){
perror("open");
exit(1);
}//of if
while(1){ //不断读取fifo中的数据并打印
char temBuf[1024]={0};
read(tempFd, temBuf, sizeof(temBuf));
printf("buf=%s\n", temBuf);
}//of while
close(tempFd); //关闭文件
return 0;
}//of main
运行结果如下:
FIFO文件和普通文件的区别:
- FIFO文件是对内存进行操作;
- 普通文件是存储在硬盘;
- 对内存的的读取会比硬盘的读写要快很多;
- 两个进程通过普通文件通信当然也是可以的。
三 消息队列
消息队列的本质是一个存放消息的链表,该链表由内核来维护。一个消息队列由一个标识符(即队列key)来标识。
消息队列的通信机制传递的数据具有某种结构,而不是简单的字节流;
向消息队列中写数据,实际上是向这个数据结构中插入一个新结点;
从消息队列中读数据,实际上是从这个数据结构中删除一个结点;
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法;
消息队列具有和管道一样的不足,每个数据块的最大长度是有上限的,系统上全体队列的最大总长度也是有上限的。
使用消息队列实现进程间通信的步骤如下:
- (1)创建消息队列;
- (2)发送消息到消息队列;
- (3)从消息队列中读取数据;
- (4)删除消息队列。
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);//接受信息,msgtyp需要指明接受的数据type
key_t ftok(const char *pathname, int proj_id);//为队列随机附加key,pathename为路径,id号可随意(1-255)
int msgctl(int msqid, int cmd, struct msqid_ds *buf);//对指定消息队列进行控制
3.1 用户消息缓冲区
无论发送进程还是接收进程,都需要在进程空间中用消息缓冲区来暂存消息。该消息缓冲区的结构定义如下:
struct msgbuf{
long int msgtype; //消息类型
anytype data; //要发送的数据,可以为任意类型
};
3.2 msgget函数
功能:创建一个消息队列或获取一个已经存在的消息队列。
#include <sys/msg.h>
int msgget(key_t key, int msgflg); //创建消息队列,返回值为该队列号
3.3 msgsnd函数
功能:向指定消息队列发送一个消息;如果msgflg = 0,调用函数的进程会被挂起,直到消息写入消息队列为止。
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);//发送消息
3.4 msgrcv函数
功能:从消息队列中读取消息,被读取的消息会从消息列表中移除。
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);//接受信息,msgtyp需要指明接受的数据type
3.5 msgctl函数
功能:对指定消息队列进行控制。
#include <sys/msg.h>
#include <sys/ipc.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);//对指定消息队列进行控制
//内核为每个消息队列维护一个msqid_ds结构,用于消息队列的管理
struct msqid_ds {
struct ipc_perm msg_perm; //所有者和权限标识
time_t msg_stime; //最后一次发送消息的时间
time_t msg_rtime; //最后一次接收消息的时间
time_t msg_ctime; //最后改变的时间
unsigned long __msg_cbytes; //队列中当前数据字节数
msgqnum_t msg_qnum; //队列中当前消息数
msglen_t msg_qbytes; //队列中允许的最大字节数
pid_t msg_lspid; //最后发送消息的进程pid
pid_t msg_lrpid; //最后接收消息的进程pid
};
【案例】使用消息队列实现不同进程间的通信。
- msgsend.c:消息发送端
- msgrcv.c:消息接收端
msgrcv.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#define MAX_TEXT 512
struct my_msg_st{
long int my_msg_type;
char anytext[MAX_TEXT];
};
int main(){
int tempIdx = 1;
int tempMsgid;
struct my_msg_st tempData;
long int tempMsgToRcv = 0;
//rcv msg
tempMsgid = msgget((key_t)1000, 0664 | IPC_CREAT);//获取消息队列
if (tempMsgid == -1){
perror("msgget err");
exit(-1);
}//of if
while (tempIdx < 5){
//接收消息
if (msgrcv(tempMsgid, (void*)&tempData, BUFSIZ, tempMsgToRcv, 0) == -1){
perror("msgrcv err");
exit(-1);
}//of if
//打印消息
printf("msg type:%ld\n", tempData.my_msg_type);
printf("msg content is:%s", tempData.anytext);
tempIdx ++;
}//of while
//删除消息队列
if (msgctl(tempMsgid, IPC_RMID, 0) == -1){
perror("msgctl err");
exit(-1);
}//of if
exit(0);
}//of main
msgsend.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <string.h>
#define MAX_TEXT 512
//消息结构体
struct my_msg_st{
long int my_msg_type; //消息类型
char anytext[MAX_TEXT]; //消息数据
};
int main() {
int tempIdx = 1;
int tempMsgid;
struct my_msg_st tempData;
char tempBuf[BUFSIZ]; //设置缓存变量
tempMsgid = msgget((key_t)1000, 0664 | IPC_CREAT);//创建消息队列
if (tempMsgid == -1){
perror("msgget err");
exit(-1);
}//of if
while (tempIdx < 5){ //发送消息
printf("enter some text:");
fgets(tempBuf, BUFSIZ, stdin);
tempData.my_msg_type = rand() % 3 + 1; //随机获取消息类型
strcpy(tempData.anytext, tempBuf);
//发送消息
if (msgsnd(tempMsgid, (void*)&tempData, sizeof(tempData), 0) == -1){
perror("msgsnd err");
exit(-1);
}//of if
tempIdx ++;
}//of while
return 0;
}//of main
BUFSIZ:Linux系统定义在stdio.h的宏,表示默认缓冲区大小;
3.6 键值和标识符
键值(ID):ID是msgget函数的返回值,一个非负整数,属于进程间通信的内部名,用来确保使用同一个消息队列。内部名即在进程内部使用,是消息队列在进程级别的唯一标识,这样的标识方法是不能支持进程间通信的。
标识符(key): key是实现进程与消息队列关联的关键,属于进程间通信的外部名,是消息队列在内存级别的唯一标识。当多个进程,针对同一个key调用msgget函数,这些进程得到的ID其实是标识了同一个进程间通信的结构。多个进程间就可以通过这个进程间通信的结构进行通信。