前言
本文详细讲解了匿名管道的背景、原理和特点,并且站在文件描述符角度深度帮助理解管道,下面就让我们开始吧。
有兴趣的同学可以也看看匿名管道的实际案例:
匿名管道实例--进程控制【Linux】
匿名管道实例--进程池【Linux】
命名管道详解:
进程间通信--命名管道【Linux】
一、进程间通信目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
通信背景
1.由于进程是具有独立性的,进程想交互数据,成本会非常高。但是有些情况下需要多进程处理一件事情。
2.进程独立并不是彻底独立,有时候我们需要双方能够进行一定程度的信息交互。
我们要学的进程间通信,不是告诉我们如何通信,是他们两个如何先看到同一份资源。(文件,内存块...等方式)
两个进程同时访问磁盘上的一个文件进行读写
但由于进程在磁盘上读写太慢,所以进程间通信一般读写内存中的文件。
两个进程同时访问内存上的一个文件进行读写
二、管道
什么是管道
- 管道是Unix中最古老的进程间通信的形式。
- 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
2.1 原理
父进程指向一个管道文件,子进程继承父进程的指向关系,从而子进程也指向管道,进行通信。
2.2 特点
生活中管道的特点
- 都是单向的
- 管道是为了传输资源的 -- 数据
所以计算机中的管道 -- 单向的,传输数据的
三、匿名管道
3.1 站在文件描述符角度-深度理解管道
1.父进程创建管道,打开fd[0]、fd[1]分别作为读写端
2.父进程fork出子进程,子进程继承读写端
3.父进程关闭fd[0],子进程关闭fd[1]
这里就会有以下三个问题:
1.为什么,父进程要分别打开读和写?
为了让子进程继承,让子进程不用再打开了。
2.为什么父子要关闭对应的读写?
管道必须是单向通信的。操作系统内部设计的管道就是单向的。
3.谁决定,父子关闭什么读写?
不是由管道本身决定的,而是由需求决定的。
3.2 管道
pipe (管道生成函数)原型
int pipe (int pipefd[2])
作用:创建两个管道文件描述符,保存在pipefd中。
参数:一个文件描述符fd数组,包含两个fd,其中pipefd[0]为读端,pipefd[1]为写端。
返回值:一个整数,0表示成功,-1表示失败。
例子:从父进程向管道写入数据,子进程读取数据
1.创建管道
int pipefd[2] = {0};
if (pipe(pipefd) != 0)
{
cerr << "pipe error" << endl;
return 1;
}
2.创建子进程
pid_t id = fork();
if (id < 0)
{
cerr << "fork error" << endl;
return 2;
}
子进程
else if (id == 0)
{
// child
// 子进程来进行读取,子进程就应该关掉写端
close(pipefd[1]);
char buffer[NUM];
while (true)
{
cout << "时间戳:" << (uint64_t)time(nullptr) << endl;
//子进程没有带sleep,为什么子进程也会休眠呢?
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
//read的返回值大于0时读取成功,等于0时读取失败
if (s > 0)
{
// 读取成功
buffer[s] = '\0'; // 把最后一位设置为'\0',使字符串为C风格字符串
cout << "子进程收到信息,内容是:" << buffer << endl;
}
else if (s == 0)
{
// 当s为0时,父进程写完数据关闭写端,子进程读完剩下的数据
cout << "父进程写完了,我也退出啦" << endl;
break;
}
else{
// do nothing
}
}
close(pipefd[0]);
exit(0);
}
父进程
else
{
// parent
// 父进程来进行写入,就应该关闭读端
close(pipefd[0]);
const char *msg = "你好子进程,我是父进程,这次发送的信号编号是";
int cnt = 0;
while (cnt < 5)
{
char sendBuffer[1024];
//用sprintf格式化senBuffer数组,类似于printf
sprintf(sendBuffer, "%s : %d", msg, cnt);
write(pipefd[1], sendBuffer, strlen(sendBuffer));
sleep(1); // 这里是为了一会看现象明显
cnt++;
}
close(pipefd[1]);
cout << "父进程写完了" << endl;
}
父进程等待子进程退出
pid_t res = waitpid(id, nullptr, 0);
if (res > 0)
{
cout << "等待子进程成功" << endl;
}
return 0;
源代码
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctime>
using namespace std;
#define NUM 1024
int main()
{
// 1.创建管道
int pipefd[2] = {0};
if (pipe(pipefd) != 0)
{
cerr << "pipe error" << endl;
return 1;
}
// 2.创建子进程
pid_t id = fork();
if (id < 0)
{
cerr << "fork error" << endl;
return 2;
}
else if (id == 0)
{
// child
// 子进程来进行读取,子进程就应该关掉写端
close(pipefd[1]);
char buffer[NUM];
while (true)
{
cout << "时间戳:" << (uint64_t)time(nullptr) << endl;
//子进程没有带sleep,为什么子进程也会休眠呢?
memset(buffer, 0, sizeof(buffer));
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (s > 0)
{
// 读取成功
buffer[s] = '\0';
cout << "子进程收到信息,内容是:" << buffer << endl;
}
else if (s == 0)
{
// 当s为0时,父进程写完数据关闭写端,子进程读完剩下的数据
cout << "父进程写完了,我也退出啦" << endl;
break;
}
else{
// do nothing
}
}
close(pipefd[0]);
exit(0);
}
else
{
// parent
// 父进程来进行写入,就应该关闭读端
close(pipefd[0]);
const char *msg = "你好子进程,我是父进程,这次发送的信号编号是";
int cnt = 0;
while (cnt < 5)
{
char sendBuffer[1024];
sprintf(sendBuffer, "%s : %d", msg, cnt);
write(pipefd[1], sendBuffer, strlen(sendBuffer));
sleep(2); // 这里是为了一会看现象明显
cnt++;
}
close(pipefd[1]);
cout << "父进程写完了" << endl;
}
pid_t res = waitpid(id, nullptr, 0);
if (res > 0)
{
cout << "等待子进程成功" << endl;
}
// cout << "fd[0]: " << pipefd[0] << endl;
// cout << "fd[1]: " << pipefd[1] << endl;
return 0;
}
输出样例
注意事项
-
当父进程没有写入数据的时候,子进程在等。所以,父进程写入之后,子进程才能
read(会返回)到数据,子进程打印读取数据要以父进程的节奏为主。 -
父进程和子进程读写的时候是有一定的顺序性的。
管道内部,没有数据,读端就必须阻塞等待(read)。
管道内部,如果数据被写满,写端就必须阻塞等待(write)。
阻塞等待时,父子进程会把自己放在管道的等待队列里。 -
也就是说,
pipe内部,自带访问控制机制(同步和互斥机制)。 而在父子进程各自printf(向显示器写入)时没有顺序,此时缺乏访问控制。
特征总结
- 管道只能用来进行具有血缘关系的进程之间,进行进程间通信。常用于父子通信。
- 管道只能单向通信(内核实现决定的),是半双工的一种特殊情况。
- 管道自带同步机制 -- 自带访问控制。
- 管道是面向字节流的,没有格式边界,需要用户来自定义区分内容的边界。
- 管道的生命周期,随进程退出而退出。
总结
本文详细介绍了进程间通信的一种方式 -- 管道,介绍了匿名管道的原理和实现。大家也可以尝试去使用一下管道,这样可以更深入地了解其中的细节。喜欢的话,欢迎点赞支持和关注~