Linux学习日记12:无名通道与有名通道
一、前言
之前我们学习了Linux进程的相关知识,了解了什么是进程以及进程控制相关的内容,今天我们来学习进程间通信其中的两种基础方式——无名通道与有名通道。
二、IPC简介
IPC 是进程间通信的缩写,它是指多个进程之间进行数据交换和同步的机制。由于每个进程都有自己独立的虚拟地址空间,一个进程通常无法直接访问另一个进程的数据,因此操作系统必须提供专门的机制来 facilitated 这种通信。比如:数据交换: A 进程采集的传感器数据传给 B 进程处理;操作同步: A 进程完成任务后,通知 B 进程开始工作;资源共享:比如多个进程共用一块内存区域,提高效率。
三、无名管道—— 最简单的 “单向通道”
3.1、简介
在 Linux 操作系统中,无名管道(Unnamed Pipe) 是最基础、最古老的进程间通信(IPC)机制之一,主要用于解决具有亲缘关系的进程(如父子进程、兄弟进程)之间的单向数据传递问题。它的设计简单直接,是理解 Linux 进程通信的基础。
3.2、本质与创建
无名管道本质是内核中的一段临时缓冲区(环形队列),由内核管理,没有对应的文件系统路径(因此称为 “无名”)。进程通过文件描述符操作这段缓冲区,实现数据读写。如下图所示:

创建方式: pipe()系统调用:
通过pipe()函数创建无名管道,函数原型如下:
#include <unistd.h>
int pipe(int pipefd[2]);其中参数pipefd是一个长度为 2 的数组,用于存储管道的两个文件描述符:
pipefd[0]:管道的读端(只能读取数据);
pipefd[1]:管道的写端(只能写入数据);
返回值:成功返回 0,失败返回 - 1。
3.3、无名通道的特点
1、半双工通信:数据只能单向流动:写端写入的数据,只能从读端读取,无法双向同时传输。若需双向通信,需创建两个无名管道(一个负责 A→B,一个负责 B→A)。
2、亲缘关系限制:仅能在有共同祖先的进程(如父子、兄弟、祖孙进程)间使用。原因是:管道创建后,只有通过fork()复制文件描述符,才能让子进程继承管道的读写端(非亲缘进程无法获取管道的文件描述符)。
3、生命周期与进程绑定:无名管道随创建它的进程(及子进程)的退出而自动释放,没有持久化存储(不占用磁盘空间,仅存在于内存中)。
4、字节流传输:管道中的数据是无结构的字节流(类似文件),没有 “消息边界”。读取时需应用程序自行处理数据拆分(例如约定换行符为分隔符)。
5、有限缓冲区:管道的内核缓冲区大小固定(通常为 4KB,可通过fcntl()或ulimit()查看或者调整)。若缓冲区满,写操作会阻塞;若缓冲区空,读操作会阻塞(除非设置非阻塞模式)。
6、读写端的依赖关系:执行读操作时,如果缓存区有数据的话,正常读,并返回读出的字节数;如果缓存区没有数据(写端被全部关闭),读端读取时会返回 0(类似读到文件末尾EOF);如果没有全部关闭(如写操作停止两秒再写),读端会阻塞两秒后再读。执行写操作时,如果读端全部关闭,则管道会破裂,进程会被终止(内核给当前进程发送信号SIGPIPE-13,默认处理动作),如果读端没全部关闭,会有两种情况:一种是缓冲区写满了,另一种是缓冲区没写满,前者会使写段阻塞,后者会让写段继续写,直到写满阻塞。
3.4、工作流程
1、父进程调用pipe()创建管道,得到 pipefd[0](读)和 pipefd[1](写);
2、父进程调用fork()创建子进程,子进程继承这两个文件描述符;
3、双方关闭不需要的端口(例如父进程写、子进程读,则父进程关闭pipefd[0],子进程关闭 pipefd[1]);
4、父进程通过write( pipefd[1], data,len)写入数据;
5、子进程通过read(pipefd[0], buf, len)读取数据;
6、通信结束后,双方关闭剩余的文件描述符。
3.5、典型示例
首先创建一个pipe.c文件:
![]()
然后输入以下代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main()
{int gd;int fd[2];gd = pipe(fd);if(gd == -1){printf("creat pipe failed!\n");exit(1);}printf("pipe1 is %d\n",fd[0]);printf("pipe2 is %d\n",fd[1]);close(fd[0]);close(fd[1]);return 0;
}
这样我们就创建好了一个基本的无名管道了,编译并运行,结果如下:
可以看到我们将读端和写端都打印了出来,分别为3和4,那为什么会是3和4呢?
是因为Linux 进程启动时,会自动打开三个标准文件描述符:

当调用pipe(fd) 创建无名管道时,内核会为管道的读端(fd[0])和写端(fd[1])分配最小的、未被使用的文件描述符。在上述程序中这三个默认描述符(0、1、2)已经被占用,因此内核会分配下一个可用的描述符:读端(fd[0])被分到3,写端(fd[1])被分到4。
接着我们无名管道实现一个经典案例:ps aux | grep "bash":
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main()
{int gd;int fd[2];gd = pipe(fd);if(gd == -1){printf("creat pipe failed!\n");exit(1);}pid_t pid = fork();//ps auxif(pid > 0){close(fd[0]);//关闭读端dup2(fd[1],STDOUT_FILENO);//重定向到输出端execlp("ps","ps","aux",NULL);//执行程序}//grep "bash"if(pid == 0){close(fd[1]);//关闭写端dup2(fd[0],STDIN_FILENO);//重定向到输入端execlp("grep","grep","bash","--color=auto",NULL);//执行程序}printf("pipe1 is %d\n",fd[0]);printf("pipe2 is %d\n",fd[1]);close(fd[0]);close(fd[1]);return 0;
}
使用gcc编译器进行编译,然后运行:
可以看到成功执行了ps aux | grep "bash"这个进程。
补:当执行ps aux | grep "bash"时,Shell 进程(父进程)会先fork()出两个子进程——ps和grep,这两个子进程互为兄弟,它们继承了 Shell 创建的无名管道,从而实现数据传递(ps的输出通过管道传给grep)。
同时我们可以使用ulimit和fpathconf指令来查看管道缓冲区大小:
可以看到管道(pipe)的缓冲区大小为512×8=4KB。
然后使用fpathconf来进行查看:
可以看到也查看到管道(pipe)的缓冲区大小为4096个字节。
四、有名管道
在 Linux 操作系统中,有名管道(FIFO)是一种进程间通信(IPC)机制,它通过文件系统中的路径名来标识,突破了无名管道的 “亲缘进程限制”,允许任意进程(无论是否有亲缘关系) 进行通信。
4.1、有名管道的本质与创建
本质:有名管道是一种特殊的文件(在文件系统中可见,类型为p),但它并不存储数据,仅作为进程间通信的 “标识和通道”,数据仍在内存的管道缓冲区中传递。
创建方式:
1、命令行工具:mkfifo
格式:mkfifo[选项]管道路径
示例:创建一个名为/usr/bin/mkfifo的有名管道:
mkfifo /usr/bin/myfifo2、C语言函数:mkfifo()
头文件:#include <sys/stat.h>
函数原型:int mkfifo(const char *pathname, mode_t mode); pathname:管道的文件系统路径(如"/usr/bin/mkfifo");mode:管道的权限(如0666表示所有用户可读写);返回值:成功返回0,失败返回-1。
4.2、有名管道的核心特点
1、无亲缘关系限制:只要进程能通过文件路径访问有名管道,即可通信(如不同用户的进程、系统服务与应用程序等)。
2、半双工通信:数据只能单向流动,若需双向通信,需创建两个有名管道(一个用于 A→B,一个用于 B→A)。
3、文件系统可见性:有名管道以文件(伪文件)形式存在于文件系统中(可通过ls -l 查看,类型为p),但不占用磁盘空间(数据在内存中)。
4、打开时的阻塞特性:读阻塞:当一个进程以读模式打开 FIFO 时,它会阻塞,直到有另一个进程以写模式打开同一个 FIFO。同样,如果读取时管道内没有数据,读进程也会阻塞,直到有数据写入;写阻塞:当一个进程以写模式打开 FIFO 时,它会阻塞,直到有另一个进程以读模式打开同一个 FIFO。这种阻塞行为是一种天然的进程同步机制。可以使用 O_NONBLOCK 标志在open时设置为非阻塞模式,并处理EAGAIN错误。
5、生命周期与文件系统绑定:有名管道的生命周期由文件系统管理,除非显式删除(如rm /usr/bin/mkfifo),否则会一直存在于文件系统中。
4.3、工作流程
1、先创建有名通道:命令行工具和函数均可;
2、启动读进程(会阻塞,等待写进程):./reader;
3、另开终端,启动写进程:./writer;
4、读进程会输出:read:字符串;写进程输出:write:字符串;
4.4、有名管道和无名管道的对比
| 维度 | 有名管道(FIFO) | 无名管道(Pipe) |
|---|---|---|
| 标识方式 | 文件系统路径(如/tmp/myfifo) | 无文件名,仅通过文件描述符标识 |
| 亲缘关系限制 | 无(任意进程可通过路径访问) | 有(仅亲缘进程,如父子、兄弟进程) |
| 生命周期 | 与文件系统绑定(需手动删除) | 与创建它的进程生命周期绑定(进程退出则销毁) |
| 创建方式 | mkfifo命令或mkfifo()函数 | pipe()函数 |
| 典型场景 | 无亲缘关系的进程通信、服务 - 客户端交互 | 父子进程间的简单数据传递 |
4.5、典型示例
首先我们先创建一个mkfifo.c文件:

然后输入以下代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int ret;ret = mkfifo("/home/liang/myfifo",0777);if(ret == -1){perror("mkfifo failed:");printf("creat failed!\n");return -1;}printf("creat succeed!\n");
使用gcc编译器进行编译,然后运行:

可以看到运行成功,我们再次使用 ls -l 查看该文件的权限:
![]()
可以看到该文件类型为p,和其他文件不一样。
注:如果再次运行mkfifo.c文件,管道会创造失败,因为mkfifo函数的规则是:如果指定路径已经存在同名文件(无论是否是有名管道),再次调用mkfifo会直接返回 - 1(创建失败),如下图所示:

然后创建一个读进程:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int ret;int fd;int nread;char readbuf[50]={0};ret = mkfifo("/home/liang/myfifo",0777);if(ret == -1){perror("mkfifo failed:");printf("creat failed!\n");return -1;}printf("creat succeed!\n");fd = open("/home/liang/myfifo",O_RDONLY);if(fd == -1){printf("open failed!\n");return -2;}printf("open succeed!\n");nread = read(fd,readbuf,50);printf("read %d byte from fifo %s\n",nread,readbuf);close(fd);return 0;
}
使用gcc编译器进行编译,然后运行:
我们发现读端并没有读到数据,因为并没有写数据,所以目前是阻塞的,然后我们再创建一个写进程:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main()
{int fd;char writebuf[50]={"hello world!"};fd = open("/home/liang/myfifo", O_WRONLY);if(fd == -1){printf("open failed!\n");return -1;}printf("open succeed!\n");write(fd,writebuf,strlen(writebuf));close(fd);return 0;
}
接着打开两个终端,依次启动读进程和写进程:
打开读进程后因为没有数据写入,目前处于阻塞状态:
接着执行写进程后,读进程收到数据,读取所收到的数据并打印了出来。
