Linux 高并发学习笔记 - 管道(匿名管道)实现进程间通信

2.3.2 管道(匿名管道)

Linux 高并发学习笔记 - 笔记索引

管道概述与原理
管道基本结构
  • 管道文件位于内核区,是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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值