2.3.2 管道(匿名管道)
管道概述与原理
管道基本结构
管道文件
位于内核区,是Linux
进程间通信最古老的方式之一,用于实现拥有公共祖先
的进程间通信。管道
(Pipe
、匿名管道
)是一个拥有一个读端
与一个写端
的队列
数据结构(其底层实际为循环队列),具有先入先出
的特点,管道
内的数据以流的形式存在,称为管道流
。管道
具有内存限制,可以认为是管道通信模式的中转站与缓冲区。管道
是一个半双工
的工作模式,但通常以单工
模式通信。单工
:仅可用于单向数据传输。双工
:允许双向数据传输。半双工
:允许双向数据传输,但不可以同时进行。
- 虽然
管道
只有一个读端
与一个写端
,但两端可以链接数量不受限制的进程。多个进程可以同时操作读端
与写端
,但过程是不可逆的:多个写端
进程写入的数据按时间顺序混杂在管道
,某一管道流
只能被一个读端
进程获取。 管道
读写是不可逆的,不可以使用lseek
等重定位文件指针。
进程操作管道
管道文件
被父进程
创建,以文件描述符
的形式存在于PCB
中,父进程
同时持有读端
与写端
。文件描述符
被父进程
fork
到子进程
,所有分支进程持有完全相同的读端
与写端
文件描述符
,并同时具有操作同一管道文件
的权限。- 注:在实际开发中,如果
子进程
(或父进程
)无需读端
或写端
权限,应在进程初期立即关闭相应文件描述符
,从而使得:- 避免在长期开发中误用不符合需求的权限操作带来的调试负担。
- 减少非必要
文件描述符
占用有限的文件描述符表
。 - 如果
管道
是阻塞的,充分利用管道
阻塞特征。
管道阻塞特征
管道
具有阻塞
(默认,常用)与非阻塞
属性。阻塞
操作可以保障进程同步通信,但某些场景将制约程序效率。管道
内容不足时,可能读取不足数量的管道流
;管道
内容接近满时,可能写入不足数量的管道流
。这些属于正常读写范畴,是多进程编程需要注意的问题。
管道阻塞特征 : { 操作读端 : { s i z e r e f n u m 有进程引用写端 无进程引用写端 管道非空 正常读取,返回实际读取量 正常读取,返回实际读取量 管道为空 阻塞,等待管道写入 E O F ,返回 0 操作写端 : { s i z e r e f n u m 有进程引用读端 无进程引用读端 管道未满 正常写入,返回实际写入量 触发 S I G P I P E 信号,异常退出 管道已满 阻塞,等待管道读出 触发 S I G P I P E 信号,异常退出 管道非阻塞特征 : { 操作读端 : { s i z e r e f n u m 有进程引用写端 无进程引用写端 管道非空 正常读取,返回实际读取量 正常读取,返回实际读取量 管道为空 返回-1,资源暂不可用 E O F ,返回 0 操作写端 : { s i z e r e f n u m 有进程引用读端 无进程引用读端 管道未满 正常写入,返回实际写入量 触发 S I G P I P E 信号,异常退出 管道已满 返回-1,管道已满 触发 S I G P I P E 信号,异常退出 \begin{array}{rl} 管道阻塞特征: & \left\{ \begin{array}{l} 操作读端:\left\{~~~~ \begin{array}{c|cc} {_{size}}{^{refnum}} & 有进程引用写端 & 无进程引用写端 \\ \hline 管道非空 & 正常读取,返回实际读取量 & 正常读取,返回实际读取量 \\ 管道为空 & 阻塞,等待管道写入 & EOF,返回0\\ \end{array} \right. \\ \\ 操作写端:\left\{~~~~ \begin{array}{c|cc} {_{size}}{^{refnum}} & 有进程引用读端 & 无进程引用读端 \\ \hline 管道未满 & 正常写入,返回实际写入量 & 触发SIGPIPE信号,异常退出 \\ 管道已满 & 阻塞,等待管道读出 & 触发SIGPIPE信号,异常退出 \\ \end{array} \right. \end{array} \right. \\ \\ 管道非阻塞特征: & \left\{ \begin{array}{l} 操作读端:\left\{~~~~ \begin{array}{c|cc} {_{size}}{^{refnum}} & 有进程引用写端 & 无进程引用写端 \\ \hline 管道非空 & 正常读取,返回实际读取量 & 正常读取,返回实际读取量 \\ 管道为空 & 返回\text{-1},资源暂不可用 & EOF,返回0\\ \end{array} \right. \\ \\ 操作写端:\left\{~~~~ \begin{array}{c|cc} {_{size}}{^{refnum}} & 有进程引用读端 & 无进程引用读端 \\ \hline 管道未满 & 正常写入,返回实际写入量 & 触发SIGPIPE信号,异常退出 \\ 管道已满 & 返回\text{-1},管道已满 & 触发SIGPIPE信号,异常退出 \\ \end{array} \right. \end{array} \right. \\ \\ \end{array} 管道阻塞特征:管道非阻塞特征:⎩ ⎨ ⎧操作读端:⎩ ⎨ ⎧ sizerefnum管道非空管道为空有进程引用写端正常读取,返回实际读取量阻塞,等待管道写入无进程引用写端正常读取,返回实际读取量EOF,返回0操作写端:⎩ ⎨ ⎧ sizerefnum管道未满管道已满有进程引用读端正常写入,返回实际写入量阻塞,等待管道读出无进程引用读端触发SIGPIPE信号,异常退出触发SIGPIPE信号,异常退出⎩ ⎨ ⎧操作读端:⎩ ⎨ ⎧ sizerefnum管道非空管道为空有进程引用写端正常读取,返回实际读取量返回-1,资源暂不可用无进程引用写端正常读取,返回实际读取量EOF,返回0操作写端:⎩ ⎨ ⎧ sizerefnum管道未满管道已满有进程引用读端正常写入,返回实际写入量返回-1,管道已满无进程引用读端触发SIGPIPE信号,异常退出触发SIGPIPE信号,异常退出
管道函数
- 创建与读写管道
#include <unistd.h>
// create a pipe
// pipefd:
// {file descriptor to read into pipe, file descriptor to write into pipe}
// return value:
// return 0 for success, or -1 for error
int pipe(int pipefd[2]);
// read
ssize_t read(int fd /* pipefd[0] */, void *buf, size_t count);
// writ
ssize_t write(int fd /* pipefd[1] */, const void *buf, size_t count);
- 设置管道非阻塞
// set to non-block mode (default mode is blocked)
#include <fcntl.h>
int flag = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
实际案例:模拟实现ps aux | grep root
ps aux
:获取全部进程详情。grep root
:从管道中读取并筛选后输出。|
:管道符。可以将符号前的标准输出与符号后的标准输入分别重定向到管道两端。
// 匿名管道: 模拟实现 ps aux | grep root
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define stdin_ 0
#define stdout_ 1
#define stderr_ 2
#define piperead pipefd[0]
#define pipewrite pipefd[1]
#define min(x, y) ((x) < (y) ? (x) : (y))
int pipefd[2];
int ret;
// 模拟实现 ps aux, 此处使用 exec 函数簇调用
void ps() {
ret = close(piperead); // 关闭管道读, 避免后续误操作
ret = dup2(pipewrite, stdout_); // 输出重定向到管道写
int pid = fork();
if (pid) {
// 主进程
ret = wait(NULL); // 同步控制
char buf[] = "Exit!\n"; // 发射退出信号
write(pipewrite, buf, sizeof(buf));
} else {
// 子进程 (根进程的二级子进程)
ret = execlp("ps", "ps", "aux", NULL);
}
}
// 检查筛选条件: 以 jamhus_tao 开始的记录
// 将只打印归属 jamhus_tao 用户的进程详情
int check_user(char *buf) {
const char temp[] = "jamhus_tao";
const int len = strlen(temp);
for (int i = 0; i < len; i++) {
if (buf[i] == '\n') {
return 0;
} else if (buf[i] != temp[i]) {
return 0;
}
}
return 1;
}
// 检查退出信号: Exit!
int check_exit(char *buf) {
const char temp[] = "Exit!";
const int len = strlen(temp);
for (int i = 0; i < len; i++) {
if (buf[i] == '\n') {
return 0;
} else if (buf[i] != temp[i]) {
return 0;
}
}
return 1;
}
// 模拟实现 grep jamhus_tao, 实际效果有所差异
void grep() {
ret = close(pipewrite); // 关闭管道写, 避免后续误操作
ret = dup2(piperead, stdin_); // 输出重定向到管道读
char s[1024];
int len = 0;
char c;
while (read(stdin_, &c, 1)) { // 同步控制, 管道 read 默认阻塞
// 以下代码 (包括两个函数) 用于解析管道流, 非本章讨论核心
s[len++] = c;
if (c == '\n') { // 检查换行: 递交记录操作
if (check_exit(s)) {
exit(0); // 退出信号
} else if (check_user(s)) {
s[min(len-1, 150)] = '\0'; // 考虑终端宽度有限, 为使输出整齐, 截去150长度后输出
puts(s);
}
len = 0;
}
}
}
int main() {
ret = pipe(pipefd);
int pid = getpid();
// 多进程框架: 根进程只用于管理子进程, 不直接完成任何工作
for (int i = 0; i < 2; i++) {
pid = fork();
if (pid == 0) {
switch (i) {
case 0:
// ps 子进程函数, 用于完成 ps aux
ps();
break;
case 1:
// grep 子进程函数, 用于完成 grep jamhus_tao
grep();
break;
default:
break;
}
exit(0); // 子进程结束
}
}
// 父进程
ret = close(piperead);
ret = close(pipewrite);
int ret = 0;
while (ret >= 0) {
ret = wait(NULL); // 等待子进程
}
return 0;
}