一、Linux IPC 通信方式总览
常见的 Linux IPC 通信方式可分为三大类,覆盖从单机到跨主机的不同场景:
- 传统基础类:无名管道、有名管道、信号 ------ 特点是实现简单、开销小,适用于单机进程通信
- IPC 对象类:共享内存、信号量集、消息队列 ------ 专为高并发、大数据量的单机进程通信设计
- 网络跨主机类:Socket 通信 ------ 突破单机限制,支持跨主机甚至跨网络的进程通信
本文聚焦管道通信 ,详细拆解无名管道与有名管道的原理、用法,并补充 fileno()/fdopen() 函数的实战应用,最后通过一个字典查询实战案例,完整演示管道在父子进程协同工作中的落地场景。
二、核心工具:fileno () 与 fdopen () 函数
管道默认通过 read()/write() 等系统 IO 接口 操作,而实际开发中常需结合 fprintf()/fgets() 等标准 IO 接口 (带缓冲区、使用更便捷),fileno() 和 fdopen() 正是连接两者的桥梁:
1. fdopen ():文件描述符转标准 IO 流
#include <stdio.h>
FILE *fdopen(int fd, const char *mode);
- 功能 :将已打开的文件描述符(如管道的读 / 写端 fd)转换为标准 IO 流(
FILE*指针) - 参数 :
fd:待转换的文件描述符(管道的pipefd[0]/pipefd[1]、FIFO 的 open 返参等)mode:IO 流模式,与fopen()一致(如"r"读、"w"写、"r+"读写)
- 返回值 :成功返回
FILE*流指针;失败返回NULL,并设置errno
2. fileno ():标准 IO 流转文件描述符
#include <stdio.h>
int fileno(FILE *stream);
- 功能 :将标准 IO 流(
FILE*)转换回文件描述符(fd) - 参数 :
stream:已打开的标准 IO 流指针(如stdin/stdout或fdopen()生成的流) - 返回值 :成功返回文件描述符;失败返回 -1,并设置
errno
核心使用场景
- 管道默认返回文件描述符,若想使用
fprintf()/fgets()等便捷接口,需通过fdopen()转换; - 若需对标准 IO 流执行
fcntl()/dup()等系统调用(仅支持 fd),需通过fileno()转换。
三、无名管道(匿名管道):亲缘进程的专属通信通道
无名管道(对应系统调用 pipe)是 Linux 内核提供的轻量级通信机制,仅支持父子、兄弟等有亲缘关系的进程间通信,是管道家族的基础形态。
1. 核心特性与读写规则
- 半双工通信:数据只能单向流动,实际开发中通常固定为 "一端写、一端读" 的单工模式
- 伪文件属性 :管道是内核中的内存缓冲区,并非真实磁盘文件,不支持
lseek()定位操作,只能顺序读写 - IO 操作兼容 :可直接用
read()/write(),也可通过fdopen()转标准 IO 流操作 - 缓冲区限制:默认 64KB 内核缓冲区,超过则写端阻塞;管道为空时读端阻塞
- 关键读写规则 :
- 读端存在时,写端写入超 64KB → 写进程阻塞,直到读端读取数据;
- 写端存在时,读端读取过快 → 读进程阻塞,直到写端写入新数据;
- 读端关闭后写端继续写 → 触发
SIGPIPE信号,写进程终止; - 写端关闭且管道无数据 → 读端
read()返回 0,标志通信结束。
2. 核心编程接口
#include <unistd.h>
int pipe(int pipefd[2]);
- 功能:在内核中创建无名管道,返回读写端文件描述符
- 参数 :
pipefd[0](读端)、pipefd[1](写端) - 返回值 :成功返回 0;失败返回 -1(设置
errno)
3. 基础实战:父子进程简单通信
功能:父进程通过 fprintf() 写管道,子进程通过 fgets() 读管道,演示 fdopen()/fileno() 用法。
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main() {
int pipefd[2];
pid_t pid;
char buf[128] = {0};
const char *msg = "Hello from Parent (Standard IO)!";
FILE *stream; // 标准IO流指针
// 1. 创建无名管道
if (pipe(pipefd) == -1) {
perror("pipe create failed");
return -1;
}
// 2. fork创建子进程
pid = fork();
if (pid == -1) {
perror("fork failed");
close(pipefd[0]);
close(pipefd[1]);
return -1;
}
// 子进程:读管道(标准IO方式)
if (pid == 0) {
close(pipefd[1]); // 关闭写端
// 将管道读端fd转换为标准IO读流("r"模式)
stream = fdopen(pipefd[0], "r");
if (stream == NULL) {
perror("fdopen failed");
close(pipefd[0]);
return -1;
}
// 用标准IO接口fgets读数据(替代read())
fgets(buf, sizeof(buf), stream);
printf("Child Received: %s\n", buf);
// 可选:将流转回fd(演示fileno用法)
int fd = fileno(stream);
printf("Child: Stream converted back to fd = %d\n", fd);
// 关闭流(会自动关闭底层fd)
fclose(stream);
return 0;
}
// 父进程:写管道(标准IO方式)
else {
close(pipefd[0]); // 关闭读端
// 将管道写端fd转换为标准IO写流("w"模式)
stream = fdopen(pipefd[1], "w");
if (stream == NULL) {
perror("fdopen failed");
close(pipefd[1]);
wait(NULL);
return -1;
}
// 用标准IO接口fprintf写数据(替代write())
fprintf(stream, "%s", msg);
fflush(stream); // 标准IO有缓冲区,需手动刷新
// 关闭流(触发写端关闭)
fclose(stream);
wait(NULL);
printf("Parent Finished (Standard IO)\n");
return 0;
}
}
输出结果
Child Received: Hello from Parent (Standard IO)!
Child: Stream converted back to fd = 3
Parent Finished (Standard IO)
4. 进阶实战:父子进程协同实现字典查询
下面通过一个完整的实战案例,演示无名管道在父子进程分工协作中的应用:父进程负责读取字典文件并写入管道,子进程负责接收用户输入的单词,从管道中匹配单词对应的释义。
4.1 需求分析
- 父进程:循环读取字典文件
/home/linux/dict.txt的内容,写入无名管道 - 子进程:将管道读端转为标准 IO 流,接收用户输入的单词,匹配后输出释义
- 退出条件:用户输入
#quit时,子进程终止,父进程回收资源后退出
4.2 完整代码实现
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAXLINE 19661 // 字典最大行数限制
#define BUF_SIZE 1024 // 读写缓冲区大小
int main(int argc, char **argv)
{
int fd[2] = {0};
int ret = pipe(fd);
if (-1 == ret)
{
perror("pipe error");
return 1;
}
pid_t pid = fork();
if (pid > 0)
{
// ========== 父进程:读取字典文件并写入管道 ==========
close(fd[0]); // 关闭读端,只写管道
int fd_dict = open("/home/linux/dict.txt", O_RDONLY);
if (-1 == fd_dict)
{
perror("open dict failed");
close(fd[1]);
return 1;
}
char buf[BUF_SIZE] = {0};
// 读取字典文件并写入管道(仅需一次写入,避免管道缓冲区溢出)
ssize_t rd_ret;
while ((rd_ret = read(fd_dict, buf, sizeof(buf))) > 0)
{
write(fd[1], buf, rd_ret);
memset(buf, 0, sizeof(buf));
}
// 关键:关闭写端,触发子进程读管道的EOF
close(fd_dict);
close(fd[1]);
wait(NULL); // 等待子进程退出,避免僵尸进程
printf("父进程退出\n");
}
else if (0 == pid)
{
// ========== 子进程:接收用户输入,匹配字典释义 ==========
close(fd[1]); // 关闭写端,只读管道
// 将管道读端fd转为标准IO读流
FILE *fp = fdopen(fd[0], "r");
if (NULL == fp)
{
perror("fdopen failed");
close(fd[0]);
return 1;
}
while (1)
{
char want_word[100] = {0};
printf("\n请输入要查询的单词(输入#quit退出):");
fgets(want_word, sizeof(want_word), stdin);
// 去除换行符
want_word[strcspn(want_word, "\n")] = '\0';
// 退出条件判断
if (0 == strcmp(want_word, "#quit"))
{
printf("退出查询程序\n");
break;
}
// 空输入跳过
if (strlen(want_word) == 0)
{
continue;
}
// 逐行读取管道内容,匹配单词
int num = 0;
int find_flag = 0;
char line_buf[BUF_SIZE] = {0};
// 重置文件流指针到开头,支持重复查询
fseek(fp, 0, SEEK_SET);
while (fgets(line_buf, sizeof(line_buf), fp) != NULL)
{
// 分割单词和释义(假设字典格式:单词 释义)
char *word = strtok(line_buf, " ");
char *mean = strtok(NULL, "\r\n"); // 兼容Windows/Linux换行符
if (word == NULL || mean == NULL)
{
continue;
}
// 匹配成功则输出释义
if (0 == strcmp(word, want_word))
{
printf("【%s】的释义:%s\n", word, mean);
find_flag = 1;
break;
}
num++;
// 超过最大行数则判定无此单词
if (num > MAXLINE)
{
break;
}
}
if (!find_flag)
{
printf("未找到单词:%s\n", want_word);
}
}
// 关闭流和fd
fclose(fp);
close(fd[0]);
}
else
{
perror("fork failed");
close(fd[0]);
close(fd[1]);
return 1;
}
return 0;
}
4.3 代码优化说明
原代码存在几个核心问题,优化后解决如下:
- 父进程死循环问题 :原代码外层
while(1)会无限写入字典内容,导致管道缓冲区溢出。优化后仅写入一次字典内容,关闭写端触发子进程读 EOF。 - 重复查询支持 :子进程每次查询前通过
fseek(fp, 0, SEEK_SET)重置流指针,实现多次查询。 - 换行符兼容 :使用
strcspn去除换行符,兼容不同系统的换行格式。 - 资源泄漏修复 :补充
wait(NULL)回收子进程,避免僵尸进程;所有 fd 和流都正确关闭。 - 用户体验优化:增加提示信息,空输入自动跳过,交互更友好。
4.4 编译运行步骤
-
编译代码:
gcc dict_query.c -o dict_query -
运行程序:
./dict_query
4.5 输出效果
请输入要查询的单词(输入#quit退出):linux
【linux】的释义:一套免费使用和自由传播的类UNIX操作系统
请输入要查询的单词(输入#quit退出):apple
【apple】的释义:苹果
请输入要查询的单词(输入#quit退出):#quit
退出查询程序
父进程退出
四、有名管道(FIFO):任意单机进程的通信桥梁
无名管道仅支持亲缘进程,而有名管道(FIFO)通过文件系统中的管道文件,实现任意单机进程通信,同样可结合 fileno()/fdopen() 简化操作。
1. 核心特性
- 继承无名管道所有特性(半双工、64KB 缓冲区等);
- 文件系统可见(
ls -l显示类型为p),进程通过文件名访问; - 默认阻塞式打开(无对应读写端时,
open()阻塞); - 支持
fdopen()/fileno()转换,兼容标准 IO 操作。
2. 核心编程接口
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
- 功能:创建有名管道文件
- 参数 :
pathname(管道路径,如/tmp/my_fifo)、mode(权限,如0664) - 返回值 :成功返回 0;失败返回 -1(已存在则
errno=EEXIST)
3. 实战代码示例(结合标准 IO)
(1)写进程(fifo_write_stdio.c)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#define FIFO_PATH "/tmp/my_fifo"
#define MSG "Hello from FIFO Write (Standard IO)!"
int main() {
int fd;
FILE *write_stream;
// 1. 创建有名管道(已存在则忽略)
if (mkfifo(FIFO_PATH, 0664) == -1 && errno != EEXIST) {
perror("mkfifo failed");
return -1;
}
// 2. 以写模式打开管道,阻塞等待读进程连接
fd = open(FIFO_PATH, O_WRONLY);
if (fd == -1) {
perror("open failed");
return -1;
}
// 3. 转换为标准IO写流
write_stream = fdopen(fd, "w");
if (write_stream == NULL) {
perror("fdopen failed");
close(fd);
return -1;
}
// 4. 标准IO写数据
fprintf(write_stream, "%s", MSG);
fflush(write_stream); // 刷新缓冲区
printf("Write Process: Sent via stdio: %s\n", MSG);
// 5. 关闭流(自动关闭fd)
fclose(write_stream);
unlink(FIFO_PATH); // 删除管道文件
return 0;
}
(2)读进程(fifo_read_stdio.c)
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#define FIFO_PATH "/tmp/my_fifo"
int main() {
int fd;
char buf[128] = {0};
FILE *read_stream;
// 1. 打开管道读端
fd = open(FIFO_PATH, O_RDONLY);
if (fd == -1) {
perror("open failed");
return -1;
}
// 2. 转换为标准IO读流
read_stream = fdopen(fd, "r");
if (read_stream == NULL) {
perror("fdopen failed");
close(fd);
return -1;
}
// 3. 标准IO读数据
fgets(buf, sizeof(buf), read_stream);
printf("Read Process: Received via stdio: %s\n", buf);
// 4. 演示fileno:流转回fd
int fd_convert = fileno(read_stream);
printf("Read Process: Stream -> fd = %d\n", fd_convert);
// 5. 关闭流
fclose(read_stream);
return 0;
}
编译运行
# 编译
gcc fifo_write_stdio.c -o fifo_w_stdio
gcc fifo_read_stdio.c -o fifo_r_stdio
# 终端1运行写进程
./fifo_w_stdio
# 终端2运行读进程
./fifo_r_stdio
五、核心知识点总结
1. fileno ()/fdopen () 核心价值
| 函数 | 作用 | 典型场景 |
|---|---|---|
fdopen() |
fd → FILE*(系统 IO 转标准 IO) | 管道操作中使用 fprintf()/fgets() 等便捷接口 |
fileno() |
FILE* → fd(标准 IO 转系统 IO) | 对标准流执行 fcntl()/dup() 等系统调用 |
2. 管道核心对比(补充 IO 类型)
| 特性维度 | 无名管道 | 有名管道 |
|---|---|---|
| 核心接口 | pipe() |
mkfifo() + open() |
| 通信范围 | 亲缘进程 | 任意单机进程 |
| IO 操作方式 | 系统 IO / 标准 IO(fdopen) | 系统 IO / 标准 IO(fdopen) |
| 文件系统可见性 | 不可见 | 可见(管道文件) |
| 资源释放 | 进程退出自动释放 | 需 unlink() 删除文件 |
3. 开发注意事项
- 标准 IO 缓冲区 :使用
fdopen()后,标准 IO 有默认缓冲区,需通过fflush()手动刷新(写管道时),避免数据滞留; - 流与 fd 的关闭 :
fclose()会自动关闭底层文件描述符,无需重复close(fd),否则会导致双重关闭错误; - 错误处理 :
fdopen()失败时,需先关闭原 fd,避免资源泄漏; - 非阻塞模式 :若需非阻塞通信,打开 FIFO 时加
O_NONBLOCK标志,转换为标准流后仍生效; - 管道破裂处理 :写进程需捕获
SIGPIPE信号,避免读端关闭后写进程被意外终止。