元旦快乐,期末周悲伤,这两个星期没时间学习计算机相关内容啦,今天是开年的的一篇博客,延续上一篇博客内容,今天我们来谈谈匿名管道的五大特征~~
先说结论,五大特征分别是:
- 匿名管道,只能用来进行具有血缘关系的进程进行进程间通信 (常用与父子)
- 管道文件,自带同步机制
- 管道是面向字节流的属于半双工的一种特殊情况
- 管道是单向通信的任何一个时刻,一个发,一个收 --- 半双工,任何一个时刻,可以同时收发 --- 全双工
- (管道) 文件的生命周期,是随进程的
1.匿名管道,只能用来进行具有血缘关系的进程进行进程间通信 (常用与父子)
因为匿名管道创建之初就是想在父子关系中进行通信,所以匿名管道只能在具有血缘关系的进程进行进程间通信
cpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>
#include <cstring>
void ChildWrite(int wfd)
{
char buffer[1024];
int cnt = 0;
while (true)
{
buffer[0] = '\0';
snprintf(buffer, sizeof(buffer), "我是子进程, pid : %d, cnt : %d", getpid(), cnt++);
write(wfd, buffer, strlen(buffer));
sleep(1);
}
}
void FutherRead(int rfd)
{
char buffer[1024];
int cnt = 0;
while (true)
{
buffer[0] = '\0';
ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
std::cout << buffer << std::endl;
}
}
}
int main()
{
// 创建管道
int pfd[2] = {0};
int n = pipe(pfd);
if (n == -1)
{
std::cerr << "pipe error!" << std::endl;
return 1;
}
std::cout << "pfd[0] : " << pfd[0] << std::endl;
std::cout << "pfd[1] : " << pfd[1] << std::endl;
// 一般来说,pfd[0] -> 读端 pfd[1] -> 写端
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// 关闭子进程读端
close(pfd[0]);
// 子进程向管道写入内容
ChildWrite(pfd[1]);
// 关闭子进程写端
close(pfd[1]);
}
// 关闭父进程写端
close(pfd[1]);
// 父进程读取管道内容
FutherRead(pfd[0]);
// 等待子进程结束
waitpid(id, nullptr, 0);
// 关闭父进程读端
close(pfd[0]);
return 0;
}
2.管道文件,自带同步机制
在上面的代码中,我们是慢写快读,运行结果如下

父进程会等待子进程写入,只有当子进程写入内容时,父进程才会读取管道内容,更深一步,在子进程没有写入时,父进程处于阻塞状态,这就是管道文件自带的同步机制
3.管道是面向字节流的,属于半双工的一种特殊情况
cpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdio>
#include <cstring>
void ChildWrite(int wfd)
{
char buffer[1024];
int cnt = 0;
while (true)
{
buffer[0] = '\0';
snprintf(buffer, sizeof(buffer), "我是子进程, pid : %d, cnt : %d", getpid(), cnt++);
write(wfd, buffer, strlen(buffer));
//sleep(1);
}
}
void FutherRead(int rfd)
{
char buffer[1024];
int cnt = 0;
while (true)
{
sleep(3);
buffer[0] = '\0';
ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
std::cout << buffer << std::endl;
}
}
}
int main()
{
// 创建管道
int pfd[2] = {0};
int n = pipe(pfd);
if (n == -1)
{
std::cerr << "pipe error!" << std::endl;
return 1;
}
std::cout << "pfd[0] : " << pfd[0] << std::endl;
std::cout << "pfd[1] : " << pfd[1] << std::endl;
// 一般来说,pfd[0] -> 读端 pfd[1] -> 写端
// 创建子进程
pid_t id = fork();
if (id == 0)
{
// 关闭子进程读端
close(pfd[0]);
// 子进程向管道写入内容
ChildWrite(pfd[1]);
// 关闭子进程写端
close(pfd[1]);
}
// 关闭父进程写端
close(pfd[1]);
// 父进程读取管道内容
FutherRead(pfd[0]);
// 等待子进程结束
waitpid(id, nullptr, 0);
// 关闭父进程读端
close(pfd[0]);
return 0;
}

此时是快写慢读,会发现,在3秒后,一次性读取管道内容,并且写的时候不会一直进行,而是在写到一定程度的时候停止,这说明管道是由内存限制的,写满以后就无法继续写了,此时需要读取内容,让管道空间释放(读进程每读取 N 字节,管道就释放 N 字节空间,写进程就能继续写 N 字节),才能继续写入内容,也印证了上面的结论--管道文件自带同步机制,而且我们不关心怎么写入,怎么读取,因为管道是面向字节流的,属于半双工的一种特殊情况!
- 管道不关心数据的 "写入批次"(比如写进程分 10 次写、每次写 1KB),只把数据当成连续的字节流存在缓冲区;
- 读进程慢读(3 秒后才读)时,管道里已经积累了所有未读字节,读进程调用
read()会一次性取走所有可用字节(只要读缓冲区足够大); - 对比 "面向消息" 的 IPC(比如消息队列):消息队列必须按 "消息批次" 读,而管道无此限制 ------ 这是字节流最核心的特征,也是 "不关心怎么写入、怎么读取" 的本质原因。
4.管道是单向通信的,任何一个时刻,一个发,一个收 --- 半双工,任何一个时刻,可以同时收发 --- 全双工
在最开始,我们需要一个即基于现有情况,而且实现简单的进程间通信,所以我们决定使用文件来进行,并且一端关闭读,一端关闭写,我们将这种叫做单向通信,形象的称为管道通信,而匿名管道只是我们为了区分不同的管道通信而取的名字
由于我们双方各关闭了一个端口,于是只能一个写,一个读,不能同时读写,所以我们将这种成为半双工
而全双工,可以类比生活中两个人发生口角,两个人不仅要输出,而且还要接受别人的输出
5.(管道) 文件的生命周期,是随进程的
之前我们就知道,文件的生命周期,是随进程的,比如我们打开了一个文件,却忘了关闭,我们不用担心,当进程结束的时候,操作系统会自动帮我们回收(文件引用计数清零,操作系统帮我们关闭)
同理,管道也是这样的,我们通过pipe打开管道,其实相当于打开两个文件(pfd[2]),在外面忘记关掉的时候,系统会帮我们解决这个问题
尽管操作系统会兜底,但工程上依然建议你手动调用close()关闭管道 fd:
- 避免 "fd 泄漏":如果进程长期运行(比如守护进程),反复创建管道却不关闭 fd,会耗尽进程的 fd 资源(每个进程的 fd 数量有限,默认几百 / 几千)
- 明确逻辑:手动关闭 fd 能让代码逻辑更清晰,也符合 "谁打开、谁关闭" 的资源管理原则