进程通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
进程间通信的认识
进程间通信的本质是让相互通信的进程能够看到(共享)同一份资源。
首先我们要知道,我们在打开一个文件时会形成文件描述符,而0 1 2默认是标准输入,标准输出,标准错误。所以我们打开的文件就会从最小的下标开始进行文件描述符的分配。
那么如果一个进程以不同的方式打开同一个文件多次的话,那会有几个文件描述符呢???
int main()
{
int fd = open("test.txt",O_CREAT|O_WRONLY,0666);
int fdd = open("test.txt",O_RDONLY|O_CREAT);
int fddd = open("test.txt",O_RDONLY|O_CREAT);
cout<<fd<<" "<<fdd<<" "<<fddd<<endl;
return 0;
}
在验证了之后不难看出,就这同一个文件打开三次就形成了三个文件描述符,只不过这三个文件描述符指向的是同一个文件,也就是资源共享一份,只不过区别是不同文件描述符指向同一个文件的读写位置不同。
进程间通信分类
匿名管道
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
匿名管道的原理
匿名管道其实就是父进程创建管道,也就是分别通过读写方式打开同一个文件,然后创建子进程,创建子进程的PCB,此时子进程会和父进程的资源共享,会将进程地址空间、页表都拷贝一份。而对应的文件描述符表也会进行拷贝一份(浅拷贝)。那么此时的父子进程的文件描述符表中指向的文件都是相同的。最后分别关闭对应的文件读写端就可以构成单向通信(文件描述符指向文件会采用引用计数的方式)。此时也就形成了管道。所以可以得出结论:匿名管道允许的是具有血缘关系的进程进行通信。
管道读写规则
我们通过代码的方式来理解管道读写的规则方式,采用父进程读数据,子进程写数据的方式:(红色字体是结论)
int main()
{
//建立管道
int tmp[2]={0};
int n=pipe(tmp);
if(n==-1)
{
cout<<"管道建立失败"<<endl;
exit(-1);
}
//创建子进程,父子进程指向同一个文件
int id=fork();
if(id<0)
{
cout<<"进程创建失败"<<endl;
exit(-1);
}
//child-w
if(id==0)
{
close(tmp[0]);//关闭读文件
char str[1024];int cnt=10;
while(1)
{
snprintf(str,sizeof(str)-1,"hello parent, I am chilld,mypid = %d,cnt = %-2d",getpid(),cnt--);//防止str被写穿,预留\0
write(tmp[1],str,strlen(str));//写入到文件中(文件中是不存在\0的)
if(cnt==0)break;
}
//exit(0);
//结束if语句就算子进程退出
}
//parent-r
if(id>0)
{
close(tmp[1]);//关闭写文件
char buffer[1024];
while(1)
{
int sz = read(tmp[0],buffer,sizeof(buffer)-1);//防止读入buffer的数据超出临时空间,预留\0
//正常情况sz=0的时候,是因为管道没数据,写端口阻塞
if(sz>0)
{
buffer[sz]=0;//字符串末尾加0
cout<<buffer<<" parent_id:"<<getpid()<<endl;
}
}
pid_t rid = waitpid(id,nullptr,0);//等待子进程退出
if(rid==id)
{
cout<<"wait ok"<<endl;
}
}
return 0;
}
分析代码内容:先创建管道,本质就是通过读写方式打开同一个文件两次,然后创建子进程,此时子进程就会拷贝父进程的文件描述符表,父子进程分别执行自己对应的代码。然后分别在父子进程中关闭关闭其不需要的操作文件方式,此时关闭并不会直接将文件给关闭,因为我们文件的指向是采用引用计数的方式的,所以关闭进程的文件,只会使引用计数--,只有减到0才会真正的关闭文件。
但是我们发现,我们的结果是按照顺序读写的,也就是子进程写完一份内容,父进程才会读取一份内容。所以就可以得出第一个结论:写端目前没有向管道中写入数据,此时读端就会等待,直到有数据。如果其次就是进程子进程cnt=0的时候,写完了以后会退出,此时父进程还在while(1)中死循环并不会退出。
此时我们更改一下代码的内容(让读端等待,写端一直写):
int main()
{
//建立管道
...
//创建子进程,父子进程指向同一个文件
...
//child-w
if(id==0)
{
close(tmp[0]);//关闭读文件
char str[1024];int cnt=10000;
while(1)
{
//写入到文件中(文件中是不存在\0的)
...
cout<<"write------------------"<<cnt<<endl;
}
}
//parent-r
if(id>0)
{
close(tmp[1]);//关闭写文件
char buffer[1024];
while(1)
{
sleep(10);
//向管道中读取数据
...
}
//等待子进程退出
...
}
return 0;
}