目录
前言
一、进程间通信
二、匿名管道的概念
三、匿名管道的代码实现
四、管道的四种情况
1.管道无数据,读端需等待
2.管道被写满,写端需等待
3.写端关闭,读端一直读取
4.读端关闭,写端一直写入
五、管道的特性
前言
之前我们学习的进程,都是进程自己干自己的事情,最多就父进程等待一下子进程,两个进程自己完成自己的任务,并没有太多关系,虽说进程具有独立性,但进程并不孤僻,进程之间交流是可以完成的,今天我们就来学习进程通过匿名管道进行通信,匿名管道并不难,不要被它名字唬住了。
一、进程间通信
进程间通信的目的如下
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。
要实现这些目的的前提,就是进程间可以通信起来。但是我们知道进程具有独立性,如果数据传输通过数据拷贝的方式进行,也就是将A进程的数据直接拷贝给B进程,这样就破坏了进程的独立性。这时就需要一个AB进程都能去到的地方,往这个共享区进行读写,这样就能保证进程独立性的同时,还实现了通信的功能,操作系统就很适合当这个中间人。
进程间通信的本质:让不同的进程看到同一份资源。
进程间通信分类分为管道、System V进程间通信、POSIX进程间通信。今天我们着重学习管道中的匿名管道。
二、匿名管道的概念
首先,我们知道一个进程有他的task_struct,这里面有一个指向文件结构体的指针,该文件结构体里面存在文件标识符数组,数组0号下标指向键盘(stdin),1指向显示器(stdout),2指向显示器(stderr)。
现在,我新打开一个文件,那么该文件的文件标识符就为3,此时我fork创建子进程,子进程要继承父亲的task_struct、虚拟地址空间、页表等等,都继承了task_struct了,那么files_struct也要继承一份(因为如果共用files_struct,那么子进程想打开新的文件,肯定会往文件标识符数组里面写内容,那么父进程也会看见,这岂不是破坏了进程的独立性)。
既然files_struct也继承了,里面指向的新文件也肯定继承了,文件有文件缓冲区,如果我父进程往该文件进行写入,子进程对文件进行读取,这样是不是就完成了进程间的通信。(注意:虽然文件缓冲区一般情况是要刷新到磁盘中的,但是这里文件只充当了管道的作用,刷新到磁盘再读取磁盘的效率非常低,因此这里并不会涉及到磁盘,只是内存级别的数据拷贝)
但是这里还存在一点小问题,就是父进程如果只是只读方式打开文件,那么子进程也是只读的方式打开文件,并不能写入,因此需要父进程同时对一个文件进行打开读取和打开写入操作,子进程继承下来后,既可以读取又可以写入,那么到时候,我想让子进程写,父进程读,就只需关闭子进程读,父进程写就可以了,反之亦然。
我们对同一文件进行读和写,在内核数据结构中实际上会生成两个struct file,只不过这两个struct file,都指向的是同一个inode,同一个缓冲区。进程关闭文件时,将文教标识符数组清空,file里面的引用计数 -- ,进程就不管文件了,但操作系统还得判断当前文件引用计数是否为0,为0证明没有进程还在使用该文件,就可以关闭,不为0就不会真正的关闭。
这就是管道名字的由来,只支持一边读,另一边写,是半双工的一种特殊方式。 同时这个管道我们并不关心它的名字,因此叫做匿名管道。
管道是Unix中最古老的进程间通信的形式。
在linux命令中,| 就是管道,可以将输出的信息读取给后面的命令处理。
三、匿名管道的代码实现
由于管道接口是不需要将数据刷新到外设,因此接口需要特殊设计。
#include 功能:创建一无名管道
原型 int pipe(int fd[2]);
参数 fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
我们直接上代码,利用pipe创建管道,子进程去写数据,父进程来读数据
#include <iostream>
#include <cstdio>
#include <stdlib.h>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
#define MAX 1024
int main()
{
// 建立管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n==0); //release模式不会执行
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 1;
}
// 子写 父读
if (id == 0)
{
// child
close(pipefd[0]);
char message[MAX];
int cnt = 10;
while(cnt)
{
snprintf(message, MAX, "hello father,I am child,pid: %d,cnt: %d ", getpid(),cnt);
write(pipefd[1],message,strlen(message));
cnt--;
sleep(1);
}
exit(0);
}
// 父进程
close(pipefd[1]);
char buff[MAX];
while (true)
{
ssize_t n = read(pipefd[0], buff, MAX - 1);
if (n > 0)
{
buff[n] = '\0';
cout << getpid()<<", child say:" << buff << "to me!" << endl;
}
}
pid_t rid = waitpid(id, NULL, 0);
if (rid = id)
{
cout << "wait success" << endl;
}
return 0;
}
成功进行数据通信
四、管道的四种情况
1.管道无数据,读端需等待
如果管道中没有数据了,读端需要等待,直到写段写入数据。
我们让子进程休眠100秒
发现父子进程都在休眠,印证了管道中没有数据,读端并不会一直运行。
2.管道被写满,写端需等待
管道被写满了,写端需要等待,等读端读走数据才可以继续写。
我们让写端死循环写入,读端进行休眠。
发现写端写到一定数据后不动了。 他需要等待读端读取数据再写入。
3.写端关闭,读端一直读取
我们关闭写端,读端一直读取,读端会读到read返回值为0,表示文件结尾。
以下是写端休眠100秒,一直不写,读端读数据,看n返回多少,都会打印。
程序没有结果,这也印证了上面的情况,写端不写入,读端需等待,read不会返回值。
后面让读端关闭,查看写端打印结果
写端关闭了,因此可以返回值,n == 0,表示读到文件末尾。
4.读端关闭,写端一直写入
读端关闭,写端一直写入,操作系统会发送 SIGPIPE 直接杀掉写端进程。
退出码为13。
发送了13号 SIIGPIPE
五、管道的特性
- 匿名管道,允许有血缘关系的进程通信
- 匿名管道,默认读写段要提供同步机制
- 面向字节流
- 管道的声明周期随进程(进程死亡,文件自然close,管道也不存在了)
- 管道式单向通信的,半双工通信的一种特殊情况。