目录
进程间通信的目的
进程通信的分类
进程通信之匿名管道
创建匿名管道
匿名管道的特点
匿名管道四种通信类型
在现实生活中,人们要进行合作,就必须进行交流,那么在进程之间,会存在交流的情景吗?答案是肯定的,进程之间肯定也会交流,我们称之为进程间通信,本期将开始进程间通信的学习。
进程间通信的目的
1.数据传输:一个进程要将它的数据传输给另一个进程。
2.资源共享:多个进程共享同样的资源。
3.通知事件:子进程退出时,会通知父进程将自己的退出信息进行回收,避免子进程成为僵尸进程。
进程通信的分类
进程通信主要有三个类别,管道通信,System V进程间通信,POSIX进程间通信。
管道通信:分为匿名管道和命名管道。
System V进程间通信:System V消息队列,System V共享内存,System V信号量。这些主要针对进程间通信。
POSIX进程间通信:消息队列,共享内存,信号量,互斥量,条件变量,读写锁。这些主要是针对线程间通信的。
进程通信之匿名管道
在谈及匿名管道之前,我们先讨论一下管道,在生活中管道也很常见,管道有两个端口,一个端口用来进物质,一个端口用来出物质。进程中的管道也是同理的,也有两个端口,一个端口用来写数据,一个端口用来读数据,通过管道实现了两个进程的通信,基于此,我们下来学习匿名管道。
在日常生活中,我们可能会见到这样的场景,比如在学习疫情期间大家上网课,比如在某讯课堂这个平台,老师在这个平台上讲课,学生在这个平台上听课,从而达到了学生和老师的通信。又比如,微信,qq等等这些app可以让任意的两个人实现通信,但是大家稍微留意一下就会发现,两个个体要实现通信,首先必须得有一个公共的平台,这一点在进程之间也是适用的,两个进程要进行通信,也必须看到一份公共的资源,我们首先以父子进程为例,怎么样让父子进程看到同一份公共的资源呢。我们通过下图回顾一下以往知识点。
我们知道使用fork在创建子进程时,父进程相关的数据结构子进程也会拷贝一份,所以对应的struct files_struct也会拷贝一份,这就会导致struct files_struct中的指针数组arr也被拷贝了一份,这就相当于,子进程也和父进程一样打开了同一份文件,这就会导致父子进程看到了同一份文件,这也就具有了两个进程通信的前提,具有了同一份公共资源。
创建匿名管道
使用pipe函数创建匿名管道,要实现管道通信,必须保证一个进程向管道中写文件,一个进程从管道中读文件。所以pipe中的参数就是对应了两个文件描述符,将同一个文件分别以读的形式和写的形式打开,返回值为0时创建成功,返回值小于0时创建失败。
分析以下代码。
#include<stdio.h>
#include<unistd.h>
int main()
{
int pid[2]={0};
if(pipe(pid)<0)
{
perror("pipe fail\n");
return 1;
}
printf("%d,%d\n",pid[0],pid[1]);
return 0;
}
运行结果如下。
我们发现,当我们在使用读和写打开同一份文件时,所分配的文件描述符为3和4,这是因为所有进程已经默认使用了0,1,2号文件描述符。直白点说,创建的这个管道其实就是之前我们讲的文件缓冲区中的操作系统中的文件内核缓冲区中的一块空间。读的内容和写的内容都是在文件的内核缓冲区进行的,不涉及底层物理文件的写入,不涉及驱动中的相关write接口和read接口的使用。
匿名管道的特点
图示如下。
管道的第一个特点:管道是单向的,一个进程从一个端口写入数据,另一个进程从另一个端口读出数据。
分析下述代码。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int pid[2]={0};
if(pipe(pid)<0)
{
perror("pipe fail\n");
return 1;
}
// printf("%d,%d\n",pid[0],pid[1]);
//子进程,子进程进行数据写入
//子进程关闭读端
pid_t id=fork();
if(id < 0)
{
perror("fork error");
return 2;
}
else if(id == 0)
{
//子进程,子进程进行文件的写入
close(pid[0]);
const char* msg="hello yjd";
while(1)
{
sleep(5);
write(pid[1],msg,strlen(msg));
}
// close(pid[1]);
exit(0);
}
else
{
//父进程,父进程进程数据读取
//父进程关闭写端
close(pid[1]);
char buff[64]={0};
while(1)
{
// sleep(1);
ssize_t size = read(pid[0],buff,sizeof(buff)-1);
if(size > 0)
{
buff[size]=0;
printf("parent get message from child: %s\n",buff);
}
else if(size == 0)
{
printf("child quit\n");
break;
}
else{
perror("read fail\n");
break;
}
// close(pid[0]);
}
}
return 0;
}
上述代码我们让子进程等待,让父进程不等待,其实就是让子进程写的慢,让父进程读的快。
运行结果如下。
因为让子进程等了5秒,所以刚开始缓冲区中没有数据,所以父进程就会等待子进程5秒,等待子进程往缓冲区中写入数据,当子进程写入数据之后,父进程立马读取了缓冲区中的数据,将数据读完之后,缓冲区又没有了数据,然后又等待子进程。且我们发现,我们写入数据时是以"hello yjd" 为单位的,但是我们发现读取时,输出的确实一大串字符串,我们称之为字节流,这便是匿名管道的另一个特点。
匿名管道的第二个特点:管道是面向字节流的。
我们上述讲述的匿名管道通信是建立在父子进程之上的,因为子进程可以继承父进程的数据结构,最终看到了同一份资源。所以两个进程之间只要是具有血缘关系的,那么两个进程就可以使用匿名管道进行通讯,这便是匿名管道的第三个特点。
匿名管道的第三个特点:匿名管道只允许具有血缘关系的进程进行通信。
匿名管道的第四个特点:在匿名管道内,子进程的写和父进程的读是原子性的,读和写的操作必须建立在另一个操作完成了的基础上,即匿名管道内部一次只允许一个操作。
匿名管道的第五个特点:匿名管道是创建了一个文件之后所对应的文件内核缓冲区中所对应的一部分空间,这个缓冲区是父子进程用来进行数据的写入和读取的,所以这块空间的生命周期是随着父子进程的生命周期的。所以管道也具有生命周期,声明周期随着父子进程的生命周期。
匿名管道四种通信类型
1.子进程写的慢,父进程读的快。
代码和运行结果上一标题已经展示。直接得出结论。
当子进程写的慢,父进程读的快时,父进程必须等待子进程的写入。
2.子进程写的快,父进程读得慢,这分为两种情况,一种是缓冲区没满,一种是缓冲区没满。
当缓冲区没满时,代码如下。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int pid[2]={0};
if(pipe(pid)<0)
{
perror("pipe fail\n");
return 1;
}
// printf("%d,%d\n",pid[0],pid[1]);
//子进程,子进程进行数据写入
//子进程关闭读端
pid_t id=fork();
if(id < 0)
{
perror("fork error");
return 2;
}
else if(id == 0)
{
//子进程,子进程进行文件的写入
close(pid[0]);
const char* msg="hello yjd";
while(1)
{
write(pid[1],msg,strlen(msg));
}
// close(pid[1]);
exit(0);
}
else
{
//父进程,父进程进程数据读取
//父进程关闭写端
close(pid[1]);
char buff[64]={0};
while(1)
{
sleep(5);
ssize_t size = read(pid[0],buff,sizeof(buff)-1);
if(size > 0)
{
buff[size]=0;
printf("parent get message from child: %s\n",buff);
}
else if(size == 0)
{
printf("child quit\n");
break;
}
else{
perror("read fail\n");
break;
}
// close(pid[0]);
}
}
return 0;
}
运行结果如下。
结论:当缓冲区没满时,父进程读的慢,但是在父进程没有读的时候,子进程仍然会往缓冲区写入数据,直到写满。
当缓冲区满了时,代码如下。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int pid[2]={0};
if(pipe(pid)<0)
{
perror("pipe fail\n");
return 1;
}
// printf("%d,%d\n",pid[0],pid[1]);
//子进程,子进程进行数据写入
//子进程关闭读端
pid_t id=fork();
if(id < 0)
{
perror("fork error");
return 2;
}
else if(id == 0)
{
//子进程,子进程进行文件的写
close(pid[0]);
const char* msg="hello yjd";
char a = 'a';
int count = 0;
while(1)
{
write(pid[1],&a,1);
count++;
printf("count:%d \n",count);
}
// close(pid[1]);
exit(0);
}
else
{
//父进程,父进程进程数据读取
//父进程关闭写端
close(pid[1]);
char buff[64]={0};
while(1)
{
// sleep(5);
// ssize_t size = read(pid[0],buff,sizeof(buff)-1);
// if(size > 0)
// {
// buff[size]=0;
// printf("parent get message from child: %s\n",buff);
// }
// else if(size == 0)
// {
// printf("child quit\n");
// break;
// }
// else
// {
// perror("read fail\n");
// break;
// }
// close(pid[0]);
}
}
return 0;
}
我们让子进程一直写入数据,一次写一个字节,在此期间,父进程不去读数据。
运行结果如下。
我们发现当子进程洗了65536个字节,也就是4KB时, 子进程不再写入,这也就意味着,管道所占缓冲区的大小为4KB。
接着让父进程进行读取,先读取63个字节的数据。
所产生的现象就是,刚开始在父进程读取时,因为读取的数据很少,我们也称读取的很慢,所以刚开始子进程并不会写入,但是当父进程读取了一定大小的数据之后,才激活了子进程的写入,此时子进程开始进行数据的写入。
结论:当缓冲区没满,父进程读的很慢,子进程会往缓冲区中写入数据。当缓冲区满了之后,父进程读的很慢,当读取的数据大小到达了一定值时,子进程才会往缓冲区中写入数据。
3.子进程关闭写端口并且退出,父进程会将缓冲区中的数据读取完之后退出。
代码如下。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int pid[2]={0};
if(pipe(pid)<0)
{
perror("pipe fail\n");
return 1;
}
// printf("%d,%d\n",pid[0],pid[1]);
//子进程,子进程进行数据写入
//子进程关闭读端
pid_t id=fork();
if(id < 0)
{
perror("fork error");
return 2;
}
else if(id == 0)
{
//子进程,子进程进行文件的写
close(pid[0]);
const char* msg="hello yjd";
char a = 'a';
int count = 0;
while(1)
{
write(pid[1],msg,strlen(msg));
break;
// count++;
// printf("count:%d \n",count);
}
close(pid[1]);
exit(0);
}
else
{
//父进程,父进程进程数据读取
//父进程关闭写端
close(pid[1]);
char buff[64]={0};
while(1)
{
ssize_t size = read(pid[0],buff,sizeof(buff)-1);
if(size > 0)
{
buff[size]=0;
printf("parent get message from child: %s\n",buff);
}
else if(size == 0)
{
printf("child quit\n");
break;
}
else
{
perror("read fail\n");
break;
}
// close(pid[0]);
}
}
return 0;
}
运行结果如下。
运行结果符合我们的预期,子进程写入字符串之后,关闭写端然后退出,父进程将缓冲区中的字符串读取之后,也会退出。
4.父进程关闭读端口并且退出,子进程也会直接退出。
代码如下。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int main()
{
int pid[2]={0};
if(pipe(pid)<0)
{
perror("pipe fail\n");
return 1;
}
// printf("%d,%d\n",pid[0],pid[1]);
//子进程,子进程进行数据写入
//子进程关闭读端
pid_t id=fork();
if(id < 0)
{
perror("fork error");
return 2;
}
else if(id == 0)
{
//子进程,子进程进行文件的写
close(pid[0]);
const char* msg="hello yjd";
char a = 'a';
int count = 0;
while(1)
{
write(pid[1],&a,1);
count++;
printf("count:%d \n",count);
}
close(pid[1]);
exit(0);
}
else
{
//父进程,父进程进程数据读取
//父进程关闭写端
close(pid[1]);
char buff[2]={0};
while(1)
{
sleep(5);
ssize_t size = read(pid[0],buff,sizeof(buff)-1);
if(size > 0)
{
buff[size]=0;
printf("parent get message from child: %s\n",buff);
}
else if(size == 0)
{
printf("child quit\n");
break;
}
else
{
perror("read fail\n");
break;
}
close(pid[0]);
}
}
return 0;
}
运行结果如下。
我们发现运行结果符合预期,当父进程关闭读端口然后退出,子进程也会随之退出,此时的子进程并不是正常推出的,而是接受到了操作系统发送的信号导致的异常退出。
以上便是本期的所有内容,本期内容到此结束^_^