一.进程通信的概念
为什么要进程通信?
进程通信:顾名思义,应该是两个进程间进行通信。
进程之间具有独立性,每个进程都有自己的虚拟地址空间,进程A不知道进程B的虚拟地址空间的数据内容(类似于一个人不知道另一个人脑子里在想啥)
二.进程间通信方式的分类
进程间通信方式的共同点:进程间需要“介质”—两个进程都能访问到的公共资源。
常见的通信方式:
-
文件(最简单的方法)
假如用vim打开一个test.c文件,这时候会自动产生一个以文件名结尾的.swap文件,用于保存数据。
当正常关闭时,此文件会被删除。当文件非正常关闭时(比如编辑代码时突然断网),如果此时再次通过vim打开该文件,就会提示存在.swap文件,此时你可以通过它来恢复文件:vim -r filename.c 恢复以后把.swap文件删掉,就不会再出现一堆提示了。所以该文件存在就是为了进行进程中的通信。 -
管道
1.管道定义:一个进程连接到另一个进程的数据流。ps aux | grep test
,将前一个进程(ps)的输出作为后一个进程(grep)的输入两进程间通过管道进行通信。ps aux | -l
:wc指word count,-l指行数,将ps aux进程的标准输出作为wc -l的标准输入。
2.管道分类
匿名管道和命名管道。
匿名管道
管道是在内核中的一块内存(构成了一个队列),使用一对文件描述符来操作内核中的内存。当前的文件描述符就是内存的句柄,此时读文件描述符—从队列中取数据,写文件描述符—往队列中插数据。Linux中,一切皆文件,所以可以借助管理文件的思想来管理内存。
1.匿名管道特点
- 使用完需要及时关闭文件描述符close();
- 匿名管道必须用于具有亲缘关系之间的进程(父子进程,爷孙进程,兄弟进程),两个操作不同管道的进程之间无法行通信。
- 管道提供流式服务。
- 管道的生命周期随进程,所有引用管道的进程退出,管道就释放。
- 内核会对管道进行同步和互斥。
- 管道是半双工的,数据只能向一个方向流动。需要两个进程之间双向通信时,就需要两个管道。
2.操作管道的函数
#include <unistd.h>
//函数原型
int pipe(int fd[2]);
//输出型参数 fd:文件描述符数组,其中fd[0]表示读端(读数据), fd[1]表示写端(写数据)
//返回值:返回值<0时,pipe失败,否则成功
下面我就来使用pipe函数创建一对文件描述符,通过这一对问价描述符来操作内核中的管道。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
//pipe
int fd[2];
int ret=pipe(fd);
if(ret<0)
{
perror("pipe");
return 1;
}
//write words to pipeline
char buf_write[1024]="hello pipe!";
write(fd[1],buf_write,strlen(buf_write));
char buf_read[1024]={0};
ssize_t n=read(fd[0],buf_read,sizeof(buf_read)-1);//a place for '\0'
buf_read[n]='\0';
printf("%s\n",buf_read);
//close fd
close(fd[0]);
close(fd[1]);
return 0;
}
运行后看结果:输出结果是从fd[0]中读取出来的,它对应的就是通过write写进去的管道中的数据。
熟悉了一下pipe函数的用法后,下面需要进行两个进程间的通信。
原理:当fork出来一个子进程时,会复制父进程的PCB,由于PCB中包括文件描述符表,所以文件描述符表也会被复制一份,子进程也能访问到相同的管道这个时候就可以实现一个进程往管道中写数据,一个进程从管道中读数据。(父子进程读写都没什么区别)
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
//pipe
int fd[2];
int ret=pipe(fd);
if(ret<0)
{
perror("pipe");
return 1;
}
pid_t ret1=fork();
if(ret1>0)
{
//father 读数据
char buf_write[1024]="hell0,father!\n";
write(fd[1],buf_write,strlen(buf_write));
}
else if (ret==0)
{
//child 写数据
char buf_read[1024]={0};
ssize_t n=read(fd[0],buf_read,sizeof(buf_read)-1);
buf_read[n]='\0';
printf("child read:%s\n",buf_read);
}
else{
perror("fork");
}
//close fd
close(fd[0]);
close(fd[1]);
return 0;
}
运行结果:子进程确实打印了父进程写入管道的数据。父进程会向管道中写入数据,子进程就会从管道中读数据
如果尝试父进程写,父子进程同时读?
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main()
{
//pipe
int fd[2];
int ret=pipe(fd);
if(ret<0)
{
perror("pipe");
return 1;
}
pid_t ret1=fork();
if(ret1>0)
{
//father 读数据
char buf_write[1024]="hell0,father!\n";
write(fd[1],buf_write,strlen(buf_write));
char buf_read[1024]={0};
ssize_t n=read(fd[0],buf_read,sizeof(buf_read)-1);
buf_read[n]='\0';
printf("father read:%s\n",buf_read);
}
else if (ret==0)
{
//child 写数据
char buf_read[1024]={0};
ssize_t n=read(fd[0],buf_read,sizeof(buf_read)-1);
buf_read[n]='\0';
printf("child read:%s\n",buf_read);
}
else{
perror("fork");
}
//close fd
close(fd[0]);
close(fd[1]);
return 0;
}
结果是父进程先读到数据。
假如我在写数据的下面加上一个sleep(1),即延迟父进程读数据过程。再次编译执行后,结果就是子进程先读到数据,并且好像阻塞住了。
那么关于父子进程谁先读到数据,取决于谁的read函数先执行。
管道内置的“同步互斥机制”限制了:
- 不会出现两个管道一个读一半数据的错乱情况。
- 如果管道中的数据一旦被读,就相当于出队列,这个时候管道就为空,如果管道为空,如果有多个进程尝试来读数据,都会读不到,就会在read函数处阻塞。
- 如果管道满了,就会在write函数处阻塞。
这就可以解释上面运行结果的阻塞了,先打开一个新的会话窗口,用ps aux 来查看一下当前的进程。再通过gdb attach+进程号来调试正在运行的进程,看父进程是否在read函数处阻塞。
进入调试后敲bt,查看调用栈,可以看到有两行,第一行的这个函数就是read函数,证明当前父进程就是阻塞在read函数处。
接下来再来看看在什么情况下管道会满,我现在尝试一直网管道写数据,只写不读。
int count=0;
while(1)
{
write(fd[1],"a",1);
printf("count:%d\n",count);
count++;
}
可以看到,管道最大容量是65535。此时如果像上面方法一样再用gdb attach调试当前进程,就可以证明,管道写满后,就会在write函数处阻塞。
为什么pipe要放在fork的上面?
命名管道
命令行创建语句:mkfifo filename
下面就来尝试一下使用该语句创建一个命名管道,可以看到一种新的文件类型p类型。
下面就来尝试使用该命名管道进行通信。将读数据和写数据放在两个可执行程序中,对应两个进程,一个尝试读取,另一个尝试写入。
//read
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
//操作命名管道,与文件一样
//1.先打开命名管道,只读
int fd =open("./myfifo",O_RDONLY);
if(fd<0)
{
perror("read open");
return 1;
}
//2.读数据
while(1)
{
char buf[1024]={0};
ssize_t n=read(fd,buf,sizeof(buf)-1);
if(n<0)
{
perror("read");
return 1;
}
if(n==0)//所有写端关闭,读段已经结束
{
printf("read over\n");
return 0;
}
buf[n]='\0';
printf("readbuf:%s\n",buf);
}
close(fd);
return 0;
}
//write
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main()
{
//先打开管道(文件)
int fd=open("./myfifo",O_WRONLY);
if(fd<0)
{
perror("write open");
return 1;
}
//写数据
while(1)
{
//提示用户输入一个数据
printf("enter>:");
fflush(stdout);
char buf[1024]={0};
ssize_t n=read(0,buf,sizeof(buf)-1);//0--是stdin的文件描述符
if(n<0)
{
perror("write");
return 1;
}
buf[n]='\0';
write(fd,buf,strlen(buf));
}
close(fd);
return 0;
}
两个代码编译后,如果先执行./fiforeadtest,则会阻塞在open处,因为此时该管道只有一个人按read方式打开,没有人按write方式打开,那么read就不能打开这个文件。
执行./fifowritetest后(但不输入写入内容时),会阻塞在read函数处,因为管道中为空。
只有在执行./fifowritetest且输入写入内容后,read函数才能读出内容。
那么这个时候,每写入一个字符串,就会显示该字符串到显示屏,让我想起了聊天小窗口。
匿名管道和命名管道的区别
- 匿名管道只限于具有亲缘关系之间的进程(父子进程,爷孙进程,兄弟进程),两个操作不同管道的进程之间无法行通信。而对于命名管道,任何的多个进程之间都能通信。
其余的部分两种管道都相同,要注意用mkfifo创建出来的 myfifo这个文件仅仅是管道的一个入口,管道的本体依然是内核中的一个内存。所谈到的生命周期是围绕着内核中的内存来讨论的。
来源:CSDN
作者:赵铁蛋
链接:https://blog.csdn.net/qq_42913794/article/details/103825570