extern int pipe (int __pipedes[2])上面是函数原型 传入一个字符数组,创建两个文件描述符,[0]为读端[1]为写端
下面给一个代码案例,一步步解析过程
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char const *argv[]) { int pipefd[2]; //管道传入的数组 pid_t cpid; //子进程pid if(argc != 2) { printf("%s 请填写需要传递的信息\n",argv[0]); exit(EXIT_FAILURE); } if(pipe(pipefd) == -1) { perror("创建管道失败"); exit(EXIT_FAILURE); } //fork创建子进程 cpid = fork(); if(cpid == -1) { perror("fork"); exit(EXIT_FAILURE); } if(cpid == 0) { //子进程 close(pipefd[1]); //关闭读端 printf("子进程 %d 收到数据\n",getpid()); char buf; while(read(pipefd[0],&buf,1) > 0) { write(STDOUT_FILENO,&buf,1); } printf("\n"); close(pipefd[0]); //关闭读端 _exit(EXIT_SUCCESS); //系统调用关闭子进程,无需回收资源 } else { //父进程,写数据 close(pipefd[0]); //关闭读端 printf("父进程 %d 写入数据\n",getpid()); write(pipefd[1],argv[1],strlen(argv[1])); close(pipefd[1]); //写完后关闭写端 waitpid(cpid,NULL,0); //等待子进程结束 exit(EXIT_SUCCESS); } return 0; }使用 ./unnamed_pipe_test "test" 运行代码
输出:
父进程 12259 写入数据
子进程 12260 收到数据
test
交互动画演示
1. 文件描述符 (File Descriptor, FD)
把文件描述符想象成一个遥控器。
内核层:操作系统内核维护着真正的“管道”对象(实际上是一块内存缓冲区)。
用户层:进程手里拿的
pipefd[0](值为3)和pipefd[1](值为4)只是遥控器上的按钮编号。pipefd[0]是 Read 按钮。pipefd[1]是 Write 按钮。进程不直接操作管道内存,只能通过拿着这些号码(FD)去请求内核(read/write)。
2.fork()时的复制机制
这是理解一切的关键。
当你调用
fork()时,操作系统复制了父进程的PCB(进程控制块)。这其中包含了文件描述符表的拷贝。
可以把它想象成复印了一把钥匙。父进程手里有开门(访问管道)的钥匙,子进程复制了一把一模一样的钥匙。
虽然有两把钥匙(两个不同的进程,各自有自己的 FD 表),但它们开的是同一扇门(指向同一个内核管道对象)。
3. 引用计数 (Reference Count) —— 管道生命的维持者
内核中的管道对象有一个“生命值”,这就是引用计数。它记录了“现在有多少个文件描述符指向我”。
正常流程
fork后,写端引用计数 = 2(父进程持有 + 子进程持有)。子进程
close(pipefd[1])-> 写端引用计数降为 1。父进程写完数据。
父进程
close(pipefd[1])->写端引用计数降为 0。核心时刻:内核检测到写端计数为 0,意味着“世界上再也没有人能往这个管道写数据了”。
内核向读端发送EOF (End Of File)。
子进程的
read函数收到 EOF,返回 0,循环结束,程序正常退出。
错误流程(忘记关闭 close)
fork后,写端引用计数 = 2。子进程没有关闭写端
close(pipefd[1])。父进程写完数据,关闭自己的写端 -> 写端引用计数降为 1(因为子进程手里还捏着一个写端 FD 呢!虽然它不用)。
死锁时刻:子进程去
read。因为写端计数是 1(不是 0),内核认为“还有人可能会写数据”,所以不发送 EOF。子进程一直傻傻地阻塞在
read上,等待那个其实就在它自己手里的写端写入数据(但它自己由于阻塞在读上,永远不会去写)。程序挂起(Hang)。
这就是 Linux 进程间通信优雅而严谨的底层逻辑!