目录
[b.2 默认给读写端提供同步机制](#b.2 默认给读写端提供同步机制)
[a.2 如果管道文件被写满了,写端必须等待,直到有空间为止。](#a.2 如果管道文件被写满了,写端必须等待,直到有空间为止。)
a3:写端关闭,读端一直读,读端会读到read的返回值为0,表示读到文件结尾。
1.管道原理
- 进程间通信不能两个进程的数据直接拷贝,不满足进程的独立性原则,因为直接拷贝可以看到其他进程的数据。
- 进程间通信的本质:不同的进程可以看到同一份资源,通常由os提供。
- 进程独立性与文件系统没有关系。因为父子进程指向同一个标准输出,所有才能打印到同一个显示器。文件描述符表也属于进程部分 ,也会拷贝过来,但不会直接拷贝文件。父进程创建子进程时,我们可以先创建一个文件,用两个文件描述符指向,文件描述符表浅拷贝struct file *, 普通文件也可以被父子进程同时看到,就可以完成进程间通信。但是这种方式内存中的文件缓冲区会刷新到磁盘中,而我们不需要,这时需要这个文件只是纯内存级文件,称为管道文件。
管道文件的特点,只允许单向通信,双向通信需要建立两个管道,因为这样方便区分哪部分是要发送的,哪部分是要接受的。又因通信风格像管道所以被称为管道。
如何实现父子管道通信: 父进程先通过os读写同时指向一个管道文件,通过fork()创建子进程,此时父子进程同时可以读写这个管道文件,然后父进程关闭写,子进程关闭读,就实现了字子读父写的进程间通信。 
同一个进程打开同一个文件读和写两次,内存中文件内容和属性会只存在一份,都是相同的。
struct file 是管理内存文件的结构,磁盘上是没有的,只会在内存中存在。 读方式和写方式两次打开文件时,因为 struct file 中读写位置 pos 是不同的,因此struct file 有两个,所以两个struct file*指向不同的struct file ,一个用来读取,一个写入,但他们会指向同一个方法集,inode 和文件缓冲区(内容)。

fork () 创建的子进程也会指向与父进程相同的 struct file。这时父子进程各关闭一个,由于struct file 中存在 引用计数cnt的原因,管道文件 struct file不会被关闭,使进程与文件系统解耦 。
2.管道的建立
使用系统调用创建一个无名管道。
cpp
#include <unistd.h>
int pipe(int fd[2]);
fd 输出型参数,因为传参降维等于传入指针,fd[0] 表示读端,fd[1] 表示写端,都指向同一文件。成功返回0,出错返回错误码。头文件在c++中也要使用**.h**格式,因为是系统头文件,而C语言可以用 c 开头的格式。
cpp
#include <iostream>
#include <cassert>
#include <unistd.h>//系统头文件,只存在c语言风格的 .h
using namespace std;
int main()
{
int pipefd[2] = {0};
int n = pipe(pipefd);//成功返回0,失败返回-1
assert(n == 0);//debuge模式 g++编译时要加-g选项 下才会执行,release不会存在执行
(void)n; //防止编译器告警 变量n未被使用
cout << "pipefd[0]:" << pipefd[0] << ", pipefd[1]:" << pipefd[1] << endl;
// cout << "hello bit" <<endl;
return 0;
}

建立一个子写父读的管道,子进程向管道写数据
cpp
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
//子写父读
if(id == 0)
{
//子进程
close(pipefd[0]);//关闭读
//write
int cnt = 10;
while(cnt)
{
char message[MAX] = {'\0'};
//sizeof字符串包含\0=MAX,而strlen遇到\0停止
snprintf(message,sizeof(message),"hello father,I am child,pid:%d ,cnt = %d",getpid(),cnt);//格式化输入到字符串中
write(pipefd[1],message,strlen(message));//这里strlen最后\0不用加1,这是在文件中,不是c语言中没有联系
cnt--;
sleep(1);
}
//close(pipefd[1]);写不写都行,进程退出文件就会被关闭
exit(0);
}
//父进程
close(pipefd[1]);//关闭写
//read
char buffer[MAX] = {'\0'};
while(true)
{
ssize_t n = read(pipefd[0],buffer,sizeof(buffer)-1);
//希望读取sizeof-1个即MAX-1,这里-1方便为最后一个位置加 \0 //返回值为实际读取的字节数,即发送的strlen(message)
if(n>0)
{
buffer[n] = '\0'; //当作字符串
cout<< "child say: "<<buffer<< " to me!"<<endl;
}
pid_t rid = waitpid(id, nullptr, WNOHANG); // 阻塞等待
if (rid == id)
{
cout << "wait success" << endl;
break;
}
}
操作系统通过维护管道的内部数据结构 和读写指针 来判断应该读取哪些数据,通多输入数据的长度判断数据结尾指针,而不是\0,被读取后更新数据开始指针位置。
3.管道的特点
父进程可以把代码和数据通过fork()给子进程,为什么还要进程间通信?
实现子进程给父进程数据
实现变化的数据进行通信
a. 管道的4种情况正常情况(读写端没有被关闭),如果管道没有数据了,读端 read 就必须等待,直到有数据为止
正常情况(读写端没有被关闭),如果管道被写满了,写端必须等待,直到有空间为 止。
写端关闭,读端一直去读,读端会读到read的返回值为0,表示读到文件结尾。
读端关闭,写端一直写,OS会将写端进程杀掉,通过向目标进程发送SIGPIPE(13)信号,终止目标进程。
b. 管道的5种特性匿名管道,可以允许具有血缘关系的进程之间进行进程间通信,常用于父子,也可以兄弟,爷孙通信,但也仅限于此。
匿名管道,默认给读写端提供同步机制(有顺序 )。子进程写了,父进程才能读。父进程读了,子进程再写
面向字节流的。不是写一个读一个,可能一次读完好几次的写入 ,或分几次读,读写次数没有严格控制对应。
管道的生命周期时随进程的。父进程和子进程都退出了,管道也会关闭。
管道是即不能同时读写,还是单向的。
b.2 默认给读写端提供同步机制
子进程运行下面代码,将数据写入管道文件。
cpp
int cnt = 100000;
while(cnt)
{
char message[MAX];//系统调用接口是c语言, 写代码要c,c++混编
snprintf(message, sizeof(message), "hello father, I an child, pid: %d, cnt: %d", getpid(), cnt);//格式化输入到字符串message中。
cnt--;
write(pipefd[1], message, strlen(message));//不用字符串最后的'\0', 不用string+1 。 写到文件pipefd[1],并没有规定文件大小,可以写很多。
//sleep(1);
cout<< "write...." << cnt <<endl;
}
只有子进程写完后,管道文件有数据,父进程才能读。
a.2 如果管道文件被写满了,写端必须等待,直到有空间为止。
我们将读端sleep(200),发现写端写到一定程度就会停止写入,即停止停止打印cnt。
子进程运行下面代码,写入管道文件:
cpp
//测量管道文件的大小
int cnt = 0;
while(true)
{
char c = 'a';
write(pipefd[1],&c,1);
cnt++;
cout<<"write ..." << cnt <<endl;
}

可以看到我使用的服务器 Linux 管道可以写入65536个字节。即64KB。
我们可是使用ulimit -a 显示目前资源限制的设定

**a3:**写端关闭,读端一直读,读端会读到read的返回值为0,表示读到文件结尾。
我们让子进程只写两条然后退出:
cpp
int cnt = 2;
while(cnt)
{
char message[MAX];//系统调用接口是c语言, 写代码要c,c++混编
snprintf(message, sizeof(message), "hello father, I an child, pid: %d, cnt: %d", getpid(), cnt);//格式化输入到字符串message中。
cnt--;
write(pipefd[1], message, strlen(message));//不用字符串最后的'\0', 不用string+1 。 写到文件pipefd[1],并没有规定文件大小,可以写很多。
sleep(1);
cout<< "write...." << cnt <<endl;
}
cout<< "child close w piont" <<endl;
//close(pipefd[1]); 进程退出自动会关闭
exit(0); //后续只能是父进程
父进程一直读,并打印 read 返回值:正常情况会返回真实,没读到会阻塞。
cpp
char buffer[MAX];
while(true)
{
//sleep(200);
ssize_t n = read(pipefd[0],buffer, sizeof(buffer)-1);//读到buffer中, 希望读sizeof(buffer)-1,返回实际读的数量。
if(n > 0)
{
buffer[n] = 0; //'\0',当做字符串结尾
cout << getpid() << ", child say: " << buffer << " to me!" <<endl;
}
cout<< "father return val(n): " << n <<endl;
}

没数据时,父进程read会阻塞 。如果不阻塞,说明子进程把写端关闭了。我们可以增加一个 n==0 时的判断:
cpp
char buffer[MAX];
while(true)
{
//sleep(200);
ssize_t n = read(pipefd[0],buffer, sizeof(buffer)-1);//读到buffer中, 希望读sizeof(buffer)-1,返回实际读的数量。
if(n > 0)
{
buffer[n] = 0; //'\0',当做字符串结尾
cout << getpid() << ", child say: " << buffer << " to me!" <<endl;
}
else if(n == 0)
{
//没数据时,父进程read会阻塞。如果不阻塞,说明子进程把写端关闭了。
cout<< "child quit, me too !" <<endl;
break;
}
cout<< "father return val(n): " << n <<endl;
}
//close(pipefd[0]); 进程退出自动会关闭
pid_t rid = waitpid(id,nullptr,0);//阻塞等待
if(rid == id)
{
cout << "wait success" <<endl;
}

a4:读端关闭,写端会被操作系统关闭
我们让父进程只读一句就退出。子进程代码与 a3 代码相同。父进程代码:
cpp
char buffer[MAX];
while(true)
{
sleep(1);
ssize_t n = read(pipefd[0],buffer, sizeof(buffer)-1);//读到buffer中, 希望读sizeof(buffer)-1,返回实际读的数量。
if(n > 0)
{
buffer[n] = 0; //'\0',当做字符串结尾
cout << getpid() << ", child say: " << buffer << " to me!" <<endl;
}
cout<< "father return val(n): " << n <<endl;
break;//退出
}
cout<< "read point close" <<endl;
close(pipefd[0]); //父进程主动关闭读端。进程退出自动会关闭
sleep(10);
pid_t rid = waitpid(id,nullptr,0);//阻塞等待
if(rid == id)
{
cout << "wait success" <<endl;
}
运行结果:

我们使用任务管理器检测 mypipe 进程
bash
while :; do ps axj | head -1 && ps axj |grep mypipe;sleep 1;done

发现在父进程关闭读之后,sleep(10)的时候,子进程102624 就已经称为僵尸进程,已经挂掉。对于操作系统来讲,读端关闭,写端就没有意义了,会将其杀掉。
如何证明子进程收到13号信号?
cpp
sleep(5);
int status = 0;
pid_t rid = waitpid(id,&status,0);//阻塞等待
if(rid == id)
{
cout << "wait success, child exit sig: " << (status&0x7F) <<endl;//低七位终止信号
}

通过 waitpid 的输出性参数的第7位得到子进程收到的终止信号
4.管道与重定向的联系
bash
$ sleep 100 | sleep 200 | sleep 300 #代码没有实际意义

他们3个是兄弟进程,父进程都是bash,因为是兄弟间通信,管道是先创建的,所以3个子进程都能看到这两个管道,然后不同管道关闭不需要的读写端就行。

所以,当1号进程想向管道文件中写的时候,可以直接dup2(pipefd[1],1)进行输出重定向,而2号进程使用dup2(pipefd[0],0)进行输入重定向就可以完成从1号进程到2号进程的管道通信,2号进程到3号进程也是这样。然后1号进程的printf,puts,write 都会输入到管道,2号进程的scanf,getline,read都会从管道读取。
本篇结束!