当前位置: 首页 > news >正文

深入解析Linux匿名管道机制与应用

目录

一、匿名管道的基本概念

二、工作原理

三、关键特性

1、内核维护的共享资源

2、纯内存操作

3、匿名管道的特点

四、应用限制

五、pipe函数(创建管道)

1、函数原型

2、功能描述

3、参数说明

4、返回值

5、注意事项

六、匿名管道的使用步骤与原理

1、基本使用步骤

2、关键注意事项

3、代码示例分析

七、管道读写规则

1、pipe2函数概述

1. 读取时的行为

2. 写入时的行为

3. 写端关闭的情况

4. 读端关闭的情况

5. 写入原子性保证

O_NONBLOCK

EAGAIN 和 EWOULDBLOCK

SIGPIPE 和 EPIPE

PIPE_BUF

pathconf() 和 fpathconf()

2、Linux pipe2 函数使用示例

说明:

八、管道的主要特性分析

1、管道内部的同步与互斥机制

2、进程绑定的生命周期

3、流式数据传输服务

4、半双工通信模式

九、管道的四种特殊运行情况分析

情况一:读阻塞

情况二:写阻塞

情况三:写端关闭

情况四:读端关闭

实验验证:SIGPIPE信号

十、管道容量探究

方法一:查阅man手册

方法二:使用ulimit命令

方法三:实际测试验证

代码功能概述

代码逐段解析

子进程部分 (id == 0)

父进程部分

程序行为分析

关键知识点

十一、Linux管道实现Shell命令ls -l | grep main

1、代码结构概述

2、详细解析

1. 创建管道

2. 第一个子进程:执行ls -l

dup2() 的具体操作步骤

步骤①:关闭 newfd(如果它已打开)

步骤②:让 newfd 指向 oldfd 相同的文件对象

步骤③:增加文件对象的引用计数

3. 第二个子进程:执行grep main

4. 父进程处理

3、关键概念详解

1. 文件描述符重定向

2. 进程关系

3. 管道通信流程

4、为什么需要关闭不需要的文件描述符?

5、错误处理

7、实际执行效果

8、扩展思考


一、匿名管道的基本概念

        匿名管道(Anonymous Pipe)是Linux中进程间通信(IPC)的一种基本方式,它允许有亲缘关系的进程(通常是父子进程)之间进行单向数据通信。


二、工作原理

匿名管道实现进程间通信的核心原理是:让不同进程访问同一份文件资源。具体实现方式如下:

  1. 共享文件资源操作系统(OS)创建一个特殊的文件资源,父子进程都能访问这个资源

  2. 通信机制

    • 父进程可以通过该文件进行写入操作

    • 子进程可以通过同一文件进行读取操作

    • 反之亦然,实现双向通信

子进程继承父进程文件描述符表!!! 匿名管道:不用文件路径,内存级的,没有文件名!!!

从内核视角看,管道的本质:

        使用管道就像操作普通文件一样简单。这种设计完全遵循了"Linux一切皆文件"的理念,使管道的使用方法与文件操作保持高度一致。


三、关键特性

1、内核维护的共享资源

  • 操作系统内核负责维护这个共享文件资源

  • 父子进程对管道的操作直接作用于内核维护的缓冲区

  • 无写时拷贝:当进程向管道写入数据时,不会发生写时拷贝(Copy-on-Write)

2、纯内存操作

匿名管道具有以下与常规文件不同的特点:

  1. 无磁盘IO

    • 操作系统不会将通信数据刷新到磁盘

    • 完全在内存中完成所有操作

  2. 效率优势

    • 避免了磁盘IO带来的性能损耗

    • 通信速度显著高于常规文件操作

  3. 特殊文件类型

    • 这类文件仅存在于内存中

    • 在磁盘上没有对应的实体文件

    • 体现了"内存文件"与"磁盘文件"的非一一对应关系

3、匿名管道的特点

  1. 单向通信:数据只能从一个方向流动,一端写入,另一端读取

  2. 亲缘关系:通常用于父子进程或兄弟进程间通信

  3. 内存结构:基于内核缓冲区实现的FIFO(先进先出)队列

  4. 生命周期:随进程的结束而自动销毁

  5. 无名称:没有磁盘节点,只存在于内存中


四、应用限制

  • 单向通信:通常实现为半双工(同一时间只能单向通信)

  • 亲缘关系要求:仅限父子进程或兄弟进程间使用

  • 生命周期:随创建进程的终止而自动销毁

  • 数据是字节流,没有消息边界概念


五、pipe函数(创建管道)

1、函数原型

int pipe(int pipefd[2]);

2、功能描述

    pipe()函数用于创建一个匿名管道(unnamed pipe),这是一种进程间通信的基本机制。管道提供了一个单向数据流通道:

如果进行子进程创建后,再关闭对应的读写端,就可以允许数据从一个进程流向另一个进程:

3、参数说明

    pipefd参数是一个输出型参数,它是一个包含2个整数的数组,用于返回管道的两个文件描述符:(形象记忆0为张嘴(读),1为笔(写))

数组元素文件描述符用途
pipefd[0]管道的读端(接收数据)
pipefd[1]管道的写端(发送数据)

4、返回值

  • 成功时:返回0

  • 失败时:返回-1,并设置errno来指示错误类型

5、注意事项

  1. 管道是单向的,数据只能从写端流向读端

  2. 管道通常用于具有亲缘关系的进程间通信(如父子进程)

  3. 当管道写端关闭后,读取进程在读取完所有数据后会收到EOF(read返回0)

  4. 如果管道读端关闭,继续写入会导致写入进程收到SIGPIPE信号


六、匿名管道的使用步骤与原理

        匿名管道是Unix/Linux系统中实现父子进程间通信的一种简单机制,主要通过pipe()fork()函数配合使用。

1、基本使用步骤

  1. 创建管道:父进程调用pipe()函数创建管道,该函数返回两个文件描述符,fd[0]用于读,fd[1]用于写。

  2. 创建子进程:父进程调用fork()创建子进程,子进程会继承父进程的文件描述符表,包括管道的读写端。

  3. 关闭不必要的文件描述符

    • 父进程关闭写端(fd[1]),只保留读端

    • 子进程关闭读端(fd[0]),只保留写端

2、关键注意事项

  • 单向通信:管道是半双工的,只能实现单向通信。需要明确父子进程间的数据流向(父→子或子→父)。

  • 内核缓冲:从管道写端写入的数据会被内核缓冲,直到从读端被读取。

  • 阻塞特性:当管道为空时,读操作会阻塞;当管道满时,写操作会阻塞。

3、代码示例分析

以下示例展示了子进程向父进程发送数据的完整过程:

// child->write, father->read
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{int fd[2] = {0};if (pipe(fd) < 0) { // 1. 创建匿名管道perror("pipe");return 1;}pid_t id = fork(); // 2. 创建子进程if (id == 0) {// 子进程close(fd[0]); // 3. 关闭读端const char* msg = "hello father, I am child...";int count = 10;// 子进程向管道写入数据while (count--) {write(fd[1], msg, strlen(msg));sleep(1);}close(fd[1]); // 写入完毕,关闭写端exit(0);}// 父进程close(fd[1]); // 3. 关闭写端char buff[64];// 父进程从管道读取数据while (1) {ssize_t s = read(fd[0], buff, sizeof(buff));if (s > 0) {buff[s] = '\0';printf("child send to father:%s\n", buff);}else if (s == 0) {printf("read file end\n");break;}else {printf("read error\n");break;}}close(fd[0]); // 读取完毕,关闭读端waitpid(id, NULL, 0);return 0;
}

执行结果说明

        程序运行时,子进程会每秒向管道写入一条消息,共写入10次。父进程同步从管道读取并打印这些消息。当子进程关闭写端后,父进程的read()会返回0,表示管道已关闭,通信结束。

        这种机制简单高效,常用于实现父子进程间的单向数据传递,是Unix/Linux进程间通信的基础方式之一。


七、pipe2函数

1、pipe2函数概述

pipe2函数是pipe函数的扩展版本,用于创建匿名管道,其函数原型如下:

int pipe2(int pipefd[2], int flags);

pipe函数不同,pipe2可以通过flags参数设置管道的各种行为选项。

1. 读取时的行为

  • 阻塞模式(O_NONBLOCK未设置)

    当管道中没有数据可读时,read调用会阻塞,进程暂停执行,直到有数据到达为止。
  • 非阻塞模式(O_NONBLOCK设置)

    当管道中没有数据可读时,read调用立即返回-1,并设置errnoEAGAIN(或EWOULDBLOCK)。

2. 写入时的行为

  • 阻塞模式(O_NONBLOCK未设置)

    当管道已满时,write调用会阻塞,直到有其他进程从管道中读取数据腾出空间。
  • 非阻塞模式(O_NONBLOCK设置)

    当管道已满时,write调用立即返回-1,并设置errnoEAGAIN(或EWOULDBLOCK)。

3. 写端关闭的情况

  • 如果管道所有写端对应的文件描述符都被关闭:

    read调用将返回0,表示已经到达文件结尾(EOF)。

4. 读端关闭的情况

  • 如果管道所有读端对应的文件描述符都被关闭:

    • write操作会产生SIGPIPE信号

    • 如果信号未被捕获,将导致写入进程终止

    • write调用返回EPIPE错误

5. 写入原子性保证

  • 小数据量写入(≤PIPE_BUF)

    • Linux保证写入操作的原子性

    • 多个进程同时写入时,数据不会交错

  • 大数据量写入(>PIPE_BUF)

    • Linux不保证写入操作的原子性

    • 多个进程同时写入时,数据可能会出现交错

注意:PIPE_BUF的大小取决于系统实现,通常为4096字节(4KB),可通过pathconf()fpathconf()函数查询具体值。

O_NONBLOCK

  • 作用:通过 pipe2 设置此标志时,管道的读写端会以非阻塞模式打开。

  • 行为

    • 读端:若管道为空,read 立即返回 EAGAIN 或 EWOULDBLOCK,而非阻塞。

    • 写端:若管道已满,write 立即返回 EAGAIN 或 EWOULDBLOCK

  • 示例

    int fd[2];
    pipe2(fd, O_NONBLOCK); // 创建非阻塞管道

EAGAIN 和 EWOULDBLOCK

  • 含义:这两个错误码表示操作因非阻塞模式未能立即完成。

    • EAGAIN (Resource temporarily unavailable)

    • EWOULDBLOCK (Operation would block)

  • 场景

    • 读空管道(非阻塞)时返回 EAGAIN

    • 写满管道(非阻塞)时返回 EWOULDBLOCK

  • 注意:在大多数系统上,两者值相同,但代码中应同时检查以保持可移植性。

SIGPIPE 和 EPIPE

  • 触发条件

    • 当进程向已关闭读端的管道写入数据时,内核会发送 SIGPIPE 信号(默认终止进程)。

    • 若信号被忽略或阻塞,write 返回 EPIPE 错误(Broken pipe)。

  • 预防:忽略 SIGPIPE 并通过 EPIPE 处理错误:

    signal(SIGPIPE, SIG_IGN); // 忽略信号

PIPE_BUF

  • 定义:原子写入的管道缓冲区大小(通常 4KB)。

  • 规则

    • 单次写入 ≤ PIPE_BUF 时,操作是原子的(不与其他写操作交错)。

    • 写入 > PIPE_BUF 时,数据可能被分割。

  • 查看值

    long size = fpathconf(fd[0], _PC_PIPE_BUF);

pathconf() 和 fpathconf()

  • 功能:查询文件系统的运行时限制(如管道缓冲区大小)。

  • 区别

    • pathconf():通过路径名查询(如 pathconf("/", _PC_PIPE_BUF))。

    • fpathconf():通过文件描述符查询(如 fpathconf(fd[0], _PC_PIPE_BUF))。

  • 常用参数

    • _PC_PIPE_BUF:获取 PIPE_BUF 值。

    • _PC_PIPE_SIZE:获取管道容量(Linux 特有)。

2、Linux pipe2 函数使用示例

    pipe2() 是 Linux 系统调用,用于创建管道,相比传统的 pipe() 函数,它允许指定额外的标志(flags)。下面是一个简单的使用示例:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>int main() {int pipefd[2];char buf[256];ssize_t nbytes;// 使用 pipe2 创建管道,设置 O_NONBLOCK 标志使管道非阻塞if (pipe2(pipefd, O_NONBLOCK) == -1) {perror("pipe2");exit(EXIT_FAILURE);}// 写入数据到管道const char *msg = "Hello through the pipe!";write(pipefd[1], msg, strlen(msg) + 1);// 从管道读取数据nbytes = read(pipefd[0], buf, sizeof(buf));if (nbytes > 0) {printf("Received message: %s\n", buf);}// 关闭管道两端close(pipefd[0]);close(pipefd[1]);return 0;
}

说明:

  1. pipe2(int pipefd[2], int flags) 接受两个参数:

    • pipefd[2]:用于返回两个文件描述符的数组

      • pipefd[0] 为读取端

      • pipefd[1] 为写入端

    • flags:可以指定以下标志的组合:

      • O_NONBLOCK:设置非阻塞模式

      • O_CLOEXEC:设置 close-on-exec 标志

  2. 与 pipe() 相比,pipe2() 的主要优势是可以原子性地设置这些标志,避免了先创建管道再设置标志可能存在的竞态条件。

  3. 这个例子展示了基本的管道创建、写入和读取操作,并使用了 O_NONBLOCK 标志使管道操作非阻塞。

注意:pipe2() 是 Linux 特有的系统调用,不是 POSIX 标准的一部分。


八、管道的主要特性分析

1、管道内部的同步与互斥机制

管道作为一种进程间通信机制,其核心特性之一是内置了同步与互斥机制:

  • 临界资源特性:管道属于临界资源,即在同一时刻只允许一个进程进行写入或读取操作。这种限制确保了数据操作的原子性和一致性。

  • 保护机制的必要性:若无保护机制,可能出现多个进程同时操作同一管道的情况,导致:

    • 并发读写冲突

    • 数据交叉污染

    • 读取数据不一致等问题

  • 内核实现的同步与互斥

    • 互斥机制:确保任何时候只有一个进程能够访问管道资源,具有排他性

    • 同步机制:协调多个进程按照特定顺序访问管道

  • 同步与互斥的关系

    • 互斥强调资源的独占访问,不限定访问顺序

    • 同步不仅保证互斥,还规定了进程访问的特定次序

    • 从概念上讲,互斥是同步的特殊形式,同步是更广义的互斥

2、进程绑定的生命周期

管道具有与创建它的进程紧密关联的生命周期特性:

  • 文件系统依赖:管道本质上是通过文件系统实现的通信机制

  • 自动资源管理:当所有关联进程终止时,系统会自动释放管道资源

  • 临时性特征:管道不提供持久化存储,仅作为进程间临时通信通道

3、流式数据传输服务

管道提供的是流式(stream)数据传输服务,其特点包括:

  • 无边界数据流:写入管道的数据被视为连续的字节流

  • 灵活读取:读取进程可以任意指定每次读取的数据量

  • 与数据报服务的对比

    特性流式服务数据报服务
    数据分割无明确边界有固定报文结构
    读取方式任意大小读取按完整报文单位读取
    数据完整性需应用层维护由协议保证

4、半双工通信模式

管道采用半双工通信方式,其通信特性如下:

  • 通信方向限制:数据只能单向流动

  • 双向通信实现:需要建立两个独立的管道

  • 与其他通信模式的对比

    通信类型方向性并发性典型应用场景
    单工通信固定单向不可逆广播、遥测
    半双工通信双向交替分时独占对讲机、管道
    全双工通信双向同时完全并发电话、TCP连接

        管道这种半双工特性虽然在一定程度上限制了通信效率,但简化了实现复杂度,确保了数据在单一方向上的可靠传输。


九、管道的四种特殊运行情况分析

        管道作为进程间通信的重要机制,在使用过程中会出现一些特殊运行情况,这些情况能帮助我们深入理解管道的工作机制和特性。

情况一:读阻塞

        当写端进程不写入数据读端进程持续读取时,读端进程会因为管道中没有数据可读而被挂起(阻塞)。这种阻塞状态会持续到管道中有新数据写入为止,此时读端进程才会被唤醒继续执行。

情况二:写阻塞

        当读端进程不读取数据写端进程持续写入时,一旦管道被写满,写端进程会被挂起(阻塞)。这种阻塞状态会持续到管道中的数据被读端进程读取并腾出空间后,写端进程才会被唤醒继续写入。

        前两种情况充分体现了管道自带的同步与互斥机制:读端进程只在管道有数据时读取,写端进程只在管道有空间时写入。条件不满足时相应进程会被挂起,确保通信的协调性。

情况三:写端关闭

        当写端进程完成写入并关闭写端后,读端进程会读取管道中的所有剩余数据。读取完毕后,读端进程不会阻塞,而是继续执行后续代码逻辑。

情况四:读端关闭

        当读端进程关闭读端写端进程仍在写入时,操作系统会终止写端进程。因为此时管道数据已无读取者,继续写入没有意义。这种情况下写端进程属于异常退出,会收到操作系统发送的信号。

实验验证:SIGPIPE信号

通过以下代码可以验证情况四中写端进程收到的信号类型:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{int fd[2] = { 0 };if (pipe(fd) < 0) { // 创建匿名管道perror("pipe");return 1;}pid_t id = fork(); // 创建子进程if (id == 0) {// 子进程close(fd[0]); // 关闭读端const char* msg = "hello father, I am child...";int count = 10;while (count--) {write(fd[1], msg, strlen(msg)); // 持续写入数据sleep(1);}close(fd[1]); // 写入完毕,关闭写端exit(0);}// 父进程close(fd[1]); // 关闭写端close(fd[0]); // 立即关闭读端(导致子进程被终止)int status = 0;waitpid(id, &status, 0);printf("child get signal:%d\n", status & 0x7F); // 打印子进程收到的信号return 0;
}

运行结果显示子进程收到的是13号信号:

        通过kill -l命令查询可知,13号信号对应的是SIGPIPE信号。这表明当管道读端关闭时,操作系统会向仍在写入的进程发送SIGPIPE信号将其终止。

kill -l


十、管道容量探究

        管道的容量是有限的,当管道已满时,写端操作将会阻塞或失败。那么管道的最大容量究竟是多少呢?我们可以通过以下几种方法来探究。

方法一:查阅man手册

根据Linux man手册的记载:

man 7 pipe
  • 在2.6.11之前的Linux版本中,管道的最大容量与系统页面大小相同

  • 从Linux 2.6.11开始,管道的最大容量固定为65536字节(64KB)

        我们可以使用uname -r命令查看当前Linux内核版本。根据手册说明,如果系统运行的是2.6.11或更新版本,则管道容量应为65536字节:

uname -r

方法二:使用ulimit命令

        另一种方法是使用ulimit -a命令查看系统资源限制设置。该命令输出的"pipe size"值(通常为512个8字节块)表示管道缓冲区大小:512 × 8 = 4096字节

ulimit -a

方法三:实际测试验证

        当发现man手册和ulimit命令给出的结果不一致时,我们可以编写测试程序来实际测量管道容量。

测试原理:

        如果读端进程不读取数据,写端进程持续写入,当管道写满时写端进程会被挂起。通过记录此时的写入量即可确定管道容量。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>int main() {int fd[2] = {0};if (pipe(fd) < 0) {  // 创建匿名管道perror("pipe");return 1;}pid_t id = fork();  // 创建子进程if (id == 0) {// 子进程 - 写端close(fd[0]);  // 关闭读端char c = 'a';int count = 0;// 持续写入,每次1字节while (1) {write(fd[1], &c, 1);count++;printf("%d\n", count);  // 打印写入字节数}close(fd[1]);exit(0);}// 父进程 - 读端close(fd[1]);  // 关闭写端// 父进程不进行读取操作waitpid(id, NULL, 0);close(fd[0]);return 0;
}

代码功能概述

这段程序创建了一个父子进程通过管道通信的场景:

  1. 子进程持续向管道写入数据(每次1字节)

  2. 父进程创建管道但不读取数据

  3. 程序会显示管道缓冲区被填满时的行为

代码逐段解析

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
  • 包含必要的头文件:Unix标准函数、I/O操作、系统调用等

int fd[2] = {0};
if (pipe(fd) < 0) {perror("pipe");return 1;
}
  • 创建匿名管道,fd[0]是读端,fd[1]是写端

  • 如果创建失败则报错退出

pid_t id = fork();
  • 创建子进程

子进程部分 (id == 0)

close(fd[0]);  // 关闭不需要的读端
char c = 'a';
int count = 0;while (1) {write(fd[1], &c, 1);  // 每次写入1字节count++;printf("%d\n", count);  // 打印写入字节数
}
close(fd[1]);
exit(0);
  • 子进程持续向管道写入字母'a',每次1字节

  • 每次写入后计数器+1并打印当前写入总量

  • 这是一个无限循环,直到管道满导致阻塞

父进程部分

close(fd[1]);  // 关闭不需要的写端
// 故意不进行读取操作
waitpid(id, NULL, 0);  // 等待子进程结束
close(fd[0]);
  • 父进程关闭写端但不读取数据

  • 等待子进程结束(虽然子进程理论上不会自行结束)

程序行为分析

  1. 管道容量测试

    • 子进程不断写入数据

    • 父进程不读取,管道缓冲区会逐渐填满

    • 当管道满时(Linux默认64KB),write()会阻塞子进程

  2. 输出观察

    • 程序会打印不断增加的字节数

    • 最终数字会停在65536附近(64KB = 65536字节)

    • 此时子进程阻塞,不再继续写入

  3. 程序终止

    • 需要手动终止程序(如Ctrl+C)

    • 因为父进程在等待子进程,而子进程在阻塞等待写入

关键知识点

  1. 管道容量限制

    • Linux 2.6.11+默认64KB

    • 可通过fcntl(fd[1], F_SETPIPE_SZ, size)调整

  2. 阻塞行为

    • 默认情况下,管道满时write()会阻塞

    • 可设置O_NONBLOCK标志使写入失败而非阻塞

  3. 正确用法

    • 实际应用中应确保读写平衡

    • 本代码故意制造读写不平衡来演示容量限制

        这个程序是一个很好的管道容量和阻塞行为的演示,但在实际开发中应避免这种读写不平衡的设计。测试结果显示,在读端不读取的情况下,写端最多能写入65536字节数据后被操作系统挂起。这表明在当前Linux版本中,管道的实际最大容量确实是65536字节,与man手册的记载一致:


十一、Linux管道实现Shell命令ls -l | grep main

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>int main() {int pipefd[2];pid_t pid1, pid2;if (pipe(pipefd) == -1) {perror("pipe");return 1;}// 第一个子进程:lspid1 = fork();if (pid1 == 0) {close(pipefd[0]); // 关闭读端dup2(pipefd[1], STDOUT_FILENO); // 将标准输出重定向到管道写端close(pipefd[1]);execlp("ls", "ls", "-l", NULL);perror("execlp ls");return 1;}// 第二个子进程:greppid2 = fork();if (pid2 == 0) {close(pipefd[1]); // 关闭写端dup2(pipefd[0], STDIN_FILENO); // 将标准输入重定向到管道读端close(pipefd[0]);execlp("grep", "grep", "main", NULL);perror("execlp grep");return 1;}// 父进程关闭管道两端并等待子进程close(pipefd[0]);close(pipefd[1]);waitpid(pid1, NULL, 0);waitpid(pid2, NULL, 0);return 0;
}

1、代码结构概述

  1. 创建管道

  2. 创建第一个子进程执行ls -l命令

  3. 创建第二个子进程执行grep main命令

  4. 父进程等待两个子进程结束

2、详细解析

1. 创建管道

int pipefd[2];
if (pipe(pipefd) == -1) {perror("pipe");return 1;
}
  • pipe()系统调用创建一个匿名管道

  • pipefd[0]是读取端文件描述符

  • pipefd[1]是写入端文件描述符

  • 如果创建失败,打印错误并退出

2. 第一个子进程:执行ls -l

pid1 = fork();
if (pid1 == 0) { // 子进程1close(pipefd[0]); // 关闭读端dup2(pipefd[1], STDOUT_FILENO); // 重定向标准输出close(pipefd[1]);execlp("ls", "ls", "-l", NULL);perror("execlp ls");return 1;
}

关键操作:

  1. close(pipefd[0]):关闭不需要的读端

  2. dup2(pipefd[1], STDOUT_FILENO)

    • 将管道的写端复制到标准输出文件描述符(STDOUT_FILENO)

    • 这样ls -l的输出就会写入管道而不是终端

  3. close(pipefd[1]):原始的写端文件描述符不再需要

  4. execlp():替换当前进程为ls -l程序

dup2() 的具体操作步骤

当执行 dup2(oldfd, newfd) 时,内核会原子性地完成以下操作:

步骤①:关闭 newfd(如果它已打开)
  • 系统会解除 newfd 当前指向的文件对象(比如终端)

  • 但文件对象本身不会被立即销毁(可能有其他描述符引用它)

步骤②:让 newfd 指向 oldfd 相同的文件对象
  • 内核会让 newfd 和 oldfd 指向同一个文件表条目

  • 这个操作是原子性的(不会出现中间状态)

步骤③:增加文件对象的引用计数
  • 文件对象内部有一个引用计数器

  • dup2 会使计数器 +1(因为现在有两个描述符指向它)

3. 第二个子进程:执行grep main

pid2 = fork();
if (pid2 == 0) { // 子进程2close(pipefd[1]); // 关闭写端dup2(pipefd[0], STDIN_FILENO); // 重定向标准输入close(pipefd[0]);execlp("grep", "grep", "main", NULL);perror("execlp grep");return 1;
}

关键操作:

  1. close(pipefd[1]):关闭不需要的写端

  2. dup2(pipefd[0], STDIN_FILENO)

    • 将管道的读端复制到标准输入文件描述符(STDIN_FILENO)

    • 这样grep main将从管道读取输入而不是键盘

  3. close(pipefd[0]):原始的读端文件描述符不再需要

  4. execlp():替换当前进程为grep main程序

4. 父进程处理

close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
  1. 父进程关闭管道两端(因为父子进程共享文件描述符表)

  2. waitpid()等待两个子进程结束

3、关键概念详解

1. 文件描述符重定向

dup2(oldfd, newfd)系统调用:

  • 使newfd成为oldfd的副本(让 newfd 指向 oldfd 相同的文件对象)

  • 如果newfd已经打开,会先自动关闭

  • 常用于标准输入(0)、标准输出(1)、标准错误(2)的重定向

2. 进程关系

父进程
├── 子进程1 (ls -l) → 写入管道
└── 子进程2 (grep main) ← 从管道读取

3. 管道通信流程

  1. ls -l的输出写入管道写端

  2. grep main从管道读端获取输入

  3. ls -l完成并关闭写端后,grep main会读到EOF

  4. 两个进程都结束后,父进程继续执行

4、为什么需要关闭不需要的文件描述符?

  1. 资源管理:避免文件描述符泄漏

  2. 正确EOF检测

    • 如果写端不关闭,读进程不知道何时停止读取

    • 所有写端关闭后,读操作会返回0(EOF)

  3. 避免死锁:如果所有读端未关闭,管道满时写操作会阻塞

5、错误处理

  • pipe()失败:系统资源不足

  • fork()失败:进程数达到限制

  • execlp()失败:命令不存在或不可执行

7、实际执行效果

这段代码相当于在Shell中执行:

ls -l | grep main

它会:

  1. 列出当前目录的详细文件列表

  2. 筛选出包含"main"的行

  3. 将结果输出到终端

8、扩展思考

  1. 如果要实现更复杂的管道链(如cmd1 | cmd2 | cmd3),可以创建多个管道

  2. 错误处理可以更完善,比如检查子进程的退出状态

  3. 可以添加信号处理,避免僵尸进程

        这个例子很好地展示了Linux进程间通信和文件描述符操作的基本原理,是理解Unix/Linux系统编程的重要案例。

http://www.lryc.cn/news/599253.html

相关文章:

  • 没有 Mac,如何上架 iOS App?多项目复用与流程标准化实战分享
  • AI浪潮涌,数据库“融合智能”奏响产业新乐章
  • 用 Docker 一键部署 Flask + Redis 微服务
  • 如何将 iPhone 备份到 Mac/MacBook
  • 从huggingface上下载模型
  • 接口相关概念
  • 门店管理智能体,为连锁运营开出健康“处方” 智睿视界
  • 【2025年7月25日】TrollStore巨魔商店恢复在线安装
  • Adv. Energy Mater.:焦耳热2分钟制造自支撑磷化物全解水电极
  • Linux 设备驱动模型
  • 如何高效通过3GPP官网查找资料
  • 从数据孤岛到融合共生:KES V9 2025 构建 AI 时代数据基础设施
  • 线段树学习笔记 - 练习题(3)
  • 专题:2025电商增长新势力洞察报告:区域裂变、平台垄断与银发平权|附260+报告PDF、原数据表汇总下载
  • 2025年7月区块链与稳定币最新发展动态深度解析
  • LeetCode 刷题【13. 罗马数字转整数、14. 最长公共前缀】
  • Leetcode力扣解题记录--第21题(合并链表)
  • CentOS8 使用 Docker 搭建 Jellyfin 家庭影音服务器
  • Vim 编辑器全模式操作指南
  • 短剧小程序系统开发:构建影视娱乐生态新格局
  • Java常用命令、JVM常用命令
  • Android Room 持久化库:简化数据库操作
  • pycharm安装教程-PyCharm2023安装详细步骤【MAC版】【安装包自取】
  • PyCharm高效开发全攻略
  • IP证书:构建数字世界知识产权安全防线的基石
  • Java零基础入门学习知识点2-JDK安装配置+Maven
  • Qwen3-235B-A22B-Thinking-2507 - 开源思维推理模型的新标杆
  • 深入解析Hadoop YARN如何避免资源死锁:机制与实战
  • Androidstudio 上传当前module 或本地jar包到maven服务器。
  • C++调用GnuPlot一维绘图