文章目录
- 🌈 一、管道介绍
- 🌈 二、匿名管道
- ⭐ 1. 匿名管道的概念
- ⭐ 2. 匿名管道的创建
- ⭐ 3. 匿名管道的本质
- ⭐ 4. 匿名管道的使用
- ⭐ 5. 匿名管道的特点
- ⭐ 6. 匿名管道的大小
- 🌈 三、命名管道
- ⭐ 1. 命名管道的概念
- ⭐ 2. 命名管道的创建
- ⭐ 3. 命名管道的使用
- ⭐ 4. 命名管道的特点
- 🌈 四、管道的特殊情况
🌈 一、管道介绍
什么是管道
- 管道是一个进程连接到另一个进程的一个数据流。
举个例子
- 现在有个统计云服务器用户登录个数的指令:
who | wc -l
- who 用于查看当前云服务器的登录用户 (一行一个用户),wc -l 用于统计当前的行数。
- who 和 wc -l 是两个不同的进程,它们却能很好的配合得出想要的结果,靠的就是管道 |。
🌈 二、匿名管道
⭐ 1. 匿名管道的概念
- 匿名管道不需要名字,只用保证具有亲缘关系的进程能够看到即可,常用于父子进程之间的通信。
- 创建子进程时,子进程会拷贝父进程的 PCB、地址空间、页表、文件描述符表 (struct files_struct)。
- 子进程在拷贝时用的是浅拷贝,因此父子进程的文件描述符表中的 fd_array 数组中的指针指向的都是同一份文件资源,这才导致父子进程能看到同一份文件。
- 由于进程间通信的本质是让多个进程看到同一份资源,此时父子进程看到同一个文件及缓冲区不就属于进程间通信了吗。
⭐ 2. 匿名管道的创建
1. 创建匿名管道函数
#include <unistd.h>int pipe(int pipefd[2]);
- 功能:创建一条无名管道。
- 参数:pipefd 是一个文件描述符数组且是个输出型参数,pipefd [0] 表示管道的读端,pipefd [1] 表示管道的写端。
- 返回:创建匿名管道成功返回 0,失败则返回 - 1 并设置错误码。
2. 创建匿名管道实例
#include <cassert>
#include <unistd.h>
#include <iostream>using std::cin;
using std::cout;
using std::endl;int main()
{ // 创建管道int pipefd[2] = { 0 };int n = pipe(pipefd); assert(0 == n);cout << "读端 fd: " << pipefd[0] << endl;cout << "写端 fd: " << pipefd[1] << endl;return 0;
}
⭐ 3. 匿名管道的本质
1. 读写端的本质
- 读端和写端说白了就是两个不同 file 结构体,它们指向同一个文件管道文件。
- 读端的 file 结构体负责从管道文件中读消息,写端的 file 结构体负责往管道文件写消息。
2. 实现单向通信的过程
- 父进程向 OS 申请创建管道,设置管道的读写端。
- 父进程 fork 出子进程,由于子进程会对父进程进行拷贝,子进程也会持有管道的读写端。
- 保证管道单向通信的特性,父子进程不能同时持有管道的读写端。
- 父进程向子进程发送数据:需要父进程关闭读端 pipefd[0],子进程关闭写端 pipefd[1]。
- 子进程向父进程发送数据:需要父进程关闭写端 pipefd[1],子进程关闭读端 pipefd[0]。
⭐ 4. 匿名管道的使用
- 实现一个子进程向父进程发送数据的单向通信管道。
1. 代码展示
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <iostream>
#include <sys/wait.h>
#include <sys/types.h>using std::cin;
using std::cout;
using std::endl;int main()
{/* 1.创建管道 */int pipefd[2] = {0};int n = pipe(pipefd);assert(0 == n);/* 2.创建子进程 */pid_t id = fork();if (id < 0) // 子进程创建失败return 1;/* 3.建立子写,父读的单向通信的管道 */// 父进程关闭写端,子进程关闭读端if (0 == id){// 子进程关闭读端close(pipefd[0]); // 子进程从写端向管道写入消息for (int count = 10; count >= 0; count--){char message[1024];snprintf(message, sizeof(message) - 1,"hello father, I am child, pid: %d, count: %d",getpid(), count);write(pipefd[1], message, strlen(message));sleep(1);}// 子进程退出exit(0); }/* 4.父进程从读端从管道读取消息并打印 */close(pipefd[1]); // 父进程关闭写端char buffer[1024];while (true){ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1);if (n > 0) // 读取成功{buffer[n] = '\0';cout << "child say: " << buffer << " to me!" << endl;}else if (0 == n)// 读取结束{break;}}pid_t rid = waitpid(id, nullptr, 0); // 父进程等待子进程退出if (rid == id) // 等待成功cout << "wait success" << endl;return 0;
}
2. 结果展示
⭐ 5. 匿名管道的特点
- 匿名管道有如下 5 种特性。
1. 匿名管道只能用于具有共同祖先的进程
- 匿名管道只能用于具有亲缘关系的进程之间进行通信,常用于父子进程之间的通信。
2. 匿名管道提供的是流式服务
- 流式概念:提供一个通信的信道,写端就负责写,读端就负责读。但是,具体写多少、读多少完全由上层决定。底层就只提供一个数据通信的信道。它不关心数据本身的一些细节或格式,这叫做面向字节流。
- 流式服务:数据没有明确的分割,一次拿多少数据都行。
3. 匿名管道内部自带同步与互斥机制
- 同步:父子进程在执行时,具有一定的顺序性。
- 互斥:父子进程不管谁是写端,谁是读端,同时只能有一个访问管道文件。
4. 匿名管道的生命周期随进程结束而结束
- 管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统。
- 当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说匿名管道的生命周期随进程的结束而结束。
5. 匿名管道采用半双工的通信方式
- 匿名管道是一个只允许单向通信的共享资源文件。
- 即一个管道只允许进程 A 写进程 B 读,或进程 B 写进程 A 读。
- 如果想实现双向通信,只需要建立两个匿名管道即可。
- 即管道 1 用于进程 A 写进程 B 读,管道 2 用于进程 B 写进程 A 读。
⭐ 6. 匿名管道的大小
- 管道文件的容量也是有极限的,如果管道已满,那么写端将阻塞或写失败,可以通过以下 2 种方式获取管道的大小。
1. 编写代码验证
- 现在已经知道了写端在管道满时会阻塞等待,可以根据这个特性获取管道文件的大小。
- 实现方式:定义一个值为 0 的计数器,写端每次往管道种写入 1 个字节,然后让计数器 +1,直到写端阻塞住时的计数器的值就是管道的大小。
#include <cassert>
#include <unistd.h>
#include <iostream>
#include <sys/wait.h>
#include <sys/types.h>using std::cout;
using std::endl;int main()
{/* 1.创建管道 */int pipefd[2] = {0};int n = pipe(pipefd);assert(0 == n);/* 2.创建子进程 */pid_t id = fork();/* 3.只让子进程不停的向管道写入 */if (0 == id){// 子进程关闭读端close(pipefd[0]); // 子进程不停的从写端向管道写入消息int count = 0;while (true){char ch = 'a';write(pipefd[1], &ch, 1); // 每次向管道中写入 1 个字节的内容cout << "write ......: " << ++count << endl;}exit(0); // 子进程退出}/* 4.父进程停止从管道种读取数据 */close(pipefd[1]); // 父进程关闭写端pid_t rid = waitpid(id, nullptr, 0); // 父进程等待子进程退出if (rid == id) // 等待成功cout << "wait success" << endl;return 0;
}
2. 使用 ulimit -a 指令查看
🌈 三、命名管道
⭐ 1. 命名管道的概念
- 匿名管道的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
- 如果想实现多个不相关的进程之间的通信,可以使用命名管道。
- 命名管道是一种特殊类型的文件 (FIFO 文件),命名管道有自己的路径 + 文件名。
- 进程通过命名管道唯一名字 (路径 + 文件名) 打开同一个管道文件,让多个进程看到同一份资源,即可实现进程间通信。
⭐ 2. 命名管道的创建
- 创建命名管道的方式有如下 2 种。
1. 通过 mkfifo 指令创建命名管道
- 可以在命令行中输入如下指令创建一个命名管道,命名管道的名字可自定义。
mkfifo filename
2. 通过 mkfifo 函数创建命名管道
#include <sys/stat.h>
#include <sys/types.h>int mkfifo(const char* pathname, mode_t mode);
- 参数:
- pathname:如果该参数是个路径,则在指定路径下创建命名管道;如果只是个文件名,则在当前路径下创建命名管道。
- mode:创建命名管道时,要赋予该命名管道文件的默认权限。
- 返回:如果创建命名管道成功则返回 0,失败则返回 -1。
#include <cassert>
#include <sys/stat.h>
#include <sys/types.h>const char* pathname = "name_pipe";int main()
{// 在当前路径创建名为 name_pipe 的命名管道并赋予其 0666 的权限int n = mkfifo(pathname, 0666);assert(0 == n);return 0;
}
⭐ 3. 命名管道的使用
- 命名管道有如下 2 种使用方式。
1. 通过指令使用命名管道
- 使用输出重定向的方式往命名管道中写入文件。
- 如:使用 echo “hello pipe” > name_pipe 向之前创建的命名管道写入数据。
- 使用输入重定向的方式从命名管道中读取文件。
- 如:使用 cat < name_pipe 从命名管道中读取数据,然后打印到屏幕上。
2. 通过代码使用命名管道
- 由于命名管道就是个管道文件,那么只要一个进程以读方式打开管道文件,另一个进程以写方式打开管道文件,即可实现进程之间的互相通信。
- 例:实现一个两个进程打开同一份命名管道,客户端进程负责向管道写入,服务端进程负责从管道读取。
- server.cpp :创建命名管道,以读方式打开命名管道,作为读端从命名管道中读取数据并打印。
#include <fcntl.h>
#include <cassert>
#include <unistd.h>
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>using std::cout;
using std::endl;#define FILENAME "fifo" // 命名管道的名字int main()
{// 创建命名管道int n = mkfifo(FILENAME, 0666);assert(0 == n);// 以只方式打开命名管道文件int rfd = open(FILENAME, O_RDONLY); assert(rfd >= 0);// 从管道中读取数据char buffer[1024];while (true){ssize_t s = read(rfd, buffer, sizeof(buffer) - 1);if (s > 0) // 读取成功{buffer[s] = '\0';cout << "client say: " << buffer << endl;}else if (0 == s) // 写端关闭{break;}}// 关闭读端close(rfd);return 0;
}
- client.cpp :以写方式打开命名管道,作为写端获取用户输入并向命名管道中写入数据。
#include <string>
#include <fcntl.h>
#include <cassert>
#include <unistd.h>
#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>using std::cin;
using std::cout;
using std::endl;
using std::string;
using std::getline;#define FILENAME "fifo" // 命名管道的名字int main()
{// 以写方式打开命名管道文件int wfd = open(FILENAME, O_WRONLY);assert(wfd >= 0);// 获取用户输入并将其写入管道string message;while (true){ cout << "请输入: ";getline(cin, message);ssize_t s = write(wfd, message.c_str(), message.size());assert(s >= 0);}// 关闭写端close(wfd);return 0;
}
- 结果展示:
⭐ 4. 命名管道的特点
1. 任何进程都可以使用命名管道
- 命名管道不受进程间亲缘关系的限制,任何进程都可以通过管道的名称来访问管道进行通信。
2. 匿名管道提供的是流式服务
- 流式概念:提供一个通信的信道,写端就负责写,读端就负责读。但是,具体写多少、读多少完全由上层决定。底层就只提供一个数据通信的信道。它不关心数据本身的一些细节或格式,这叫做面向字节流。
- 流式服务:数据没有明确的分割,一次拿多少数据都行。
3. 匿名管道内部自带同步与互斥机制
- 同步:父子进程在执行时,具有一定的顺序性。
- 互斥:父子进程不管谁是写端,谁是读端,同时只能有一个访问管道文件。
4. 命名管道不随进程的终止而消失
- 命名管道是有全局唯一的名称 (路径 + 文件名),其是作为文件系统中的特殊文件存在的。
- 即使创建它的进程终止,只要该管道文件未被删除,其他进程依然可以通过命名管道的名字来使用它。
5. 命名管道提供半/全双工通信
- 命名管道可以是单向通信,也可以是双向通信,具体取决于管道的打开方式和通信协议。
🌈 四、管道的特殊情况
- 正常情况下,如果管道文件没数据了,读端必须等待,直到有数据 (写端写入数据) 了为止 。
- 正常情况下,如果管道文件被写满了,写端必须等待,直到有空间 (读端读出数据) 了为止 。
- 写端关闭,读端一直读取:读端会读到 read 的返回值为 0,表示读到了文件的结尾。
- 读端关闭,写端一直写入:OS 不会做任何浪费时空的事情,会直接向目标进程发送 SIGPIPE (13 号) 信号将写端进程终止。