本节重点
- 匿名管道的概念与原理
- 匿名管道的创建
- 命名管道的概念与原理
- 命名管道的创建
- 两者的差异与联系
- 命名管道实现EchoServer
一、管道
管道(Pipe)是一种进程间通信(IPC, Inter-Process Communication)机制,用于在不同进程之间传递数据。它是一种抽象的数据通道,允许一个进程的输出直接作为另一个进程的输入,类似于现实中的管道将水流从一个地方输送到另一个地方。管道是操作系统提供的一种高效、轻量级的通信方式,广泛应用于命令行工具、父子进程协作等场景。
在Linux中我们知道,当一个指令运行起来时就是一个进程。who命令用来显示当前登录系统的用户信息,而wc是一个统计工具其中-l选项表示统计行数。当两个指令分别运行时就是两个进程,我们可以通过 | (管道)将who指令运行的结果传递给wc,此时wc就可以帮我们统计当前登录系统的用户个数。
who | wc -l
可以用下图来表示这种关系:
二、匿名管道
匿名管道(Anonymous Pipe)是一种进程间通信(IPC, Inter-Process Communication)机制,用于在具有亲缘关系的进程(如父子进程或兄弟进程)之间传递数据。它是一种单向的、半双工的通信通道,数据只能在一个方向上流动(若需双向通信,需创建两个管道)。匿名管道没有名字标识,因此仅适用于具有亲缘关系的进程,无法用于无关进程间的通信。
半双工与全双工:
半双工通信允许数据在两个方向上传输,但同一时刻只能单向传输。即通信双方可以轮流发送和接收数据,但不能同时进行。
全双工通信允许数据在两个方向上同时传输。即通信双方可以同时发送和接收数据。
特性 半双工 全双工 传输方向 同一时刻只能单向传输 双向同时传输 效率 较低(需切换方向) 较高(无需切换方向) 延迟 较高(方向切换可能引入延迟) 较低(无方向切换) 资源占用 较低(单方向占用资源) 较高(双方向占用资源) 应用场景 对讲机、老式电台、早期网络 电话、现代网络、USB接口
2.1 管道的创建
在Linux系统中我们可以系统调用pipe来创建管道文件,返回值是两个文件描述符分别表示文件的读端与写端。
函数原型:
#include <unistd.h>
int pipe(int fd[2]);
参数解析:
- fd 是一个长度为2的整数数组,用来存储管道文件的读端与写端的文件描述符
- fd[ 0 ]:管道的读端(用于读取数据)。
- fd[ 1 ]:管道的写端(用于写入数据)。
返回值:成功时返回 0,失败时返回 -1 并设置 errno。
数据传输的特点:
- 写操作:进程通过
write(pipefd[1], data, len)
将数据拷贝到内核缓冲区。若缓冲区满,写进程阻塞(默认阻塞模式)。 - 读操作:进程通过
read(pipefd[0], buffer, size)
从内核缓冲区拷贝数据到用户空间。若缓冲区空,读进程阻塞(默认阻塞模式)
管道的关闭:
- 读端先关闭:此时写入无意义,操作系统会向写进程发送SIGPIPE信号终止进程。
- 写段先关闭:此时读进程会读到EOF,用于指示文件或数据流已到达末尾。
代码示例:父子进程通过匿名管道实现进程间通信
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
int main()
{
int fd[2] = {0};
int ret = pipe(fd);
if (ret == -1)
{
printf("pipe error!\n");
}
pid_t id = fork();
if (id > 0)
{
// 父进程读
close(fd[1]);
while (1)
{
char read_buf[1024];
int ret = read(fd[0], read_buf, sizeof(read_buf));
if (ret > 0)
{
read_buf[ret] = '0';
printf("child say: %s", read_buf);
}
else if (ret == 0)
{
// 写端退出:
break;
}
else
{
printf("read error!\n");
}
}
//退出前记得回收子进程
waitpid(id,NULL,0);
}
else if (id == 0)
{
// 子进程写
close(fd[0]);
int cnt=5;
while (cnt)
{
char buffer[1024];
read(0, buffer, sizeof(buffer));
int ret = write(fd[1], buffer, sizeof(buffer));
cnt--;
}
exit(1);
}
else
{
// 子进程创建失败:
printf("fork error!\n");
}
return 0;
}
代码说明:
当父进程调用pipe系统调用时,内核会创建一个管道文件(或开辟一段环形缓冲区)并暴露读端与写端。进程通过分配文件描述符来控制管道的读写操作,我们通过一个整数数组来记录两个文件描述符,通常下标为0的元素记录读端的文件描述符,下标为1的元素记录写端的文件描述符。
之后我们fork创建子进程时操作系统会拷贝一份文件描述符表继承给子进程,此时父子进程就可以看到同一个管道文件。之后父子进程根据读写工作的分配关闭不需要的文件描述符后就可以进行进程间通信了。
结束进程间通信时可以关闭写进程,此时读进程就会读到EOF也就是文件末尾,此时read的返回值为0,我们可以根据read的返回值判断写端是否关闭来结束读端。最后不要忘记父进程等待子进程的返回结果。
2.2 原理与特性
2.2.1 工作原理
匿名管道本质是内核维护的环形缓冲区(或文件),用于临时存储待传输的数据。缓冲区大小由系统决定(如Linux默认通常为64KB),数据以先进先出(FIFO)方式处理。与以往的缓冲区不同的是这类缓冲区由内核提供且不会刷盘(将数据刷新到磁盘)。
2.2.2 基础特性
单向通信
默认是半双工(单向),若需双向通信需创建两个管道(如pipe1
和pipe2
)。
示例:
- 进程A通过
pipe1
写,进程B通过pipe1
读(A→B)。 - 进程B通过
pipe2
写,进程A通过pipe2
读(B→A)。
阻塞行为
- 默认阻塞:读写操作在缓冲区满/空时阻塞。
- 非阻塞模式:通过
fcntl(fd, F_SETFL, O_NONBLOCK)
设置后,若缓冲区满/空,读写操作立即返回错误(EAGAIN
或EWOULDBLOCK
)。
亲缘关系限制
仅适用于父子进程或兄弟进程(通过fork()
派生)。无关进程无法直接访问匿名管道(因无名字标识符)。
数据拷贝开销
数据需在用户空间和内核空间之间拷贝两次(写→内核缓冲区,内核缓冲区→读),可能影响性能。
缓冲区容量限制
若写进程速率远高于读进程,缓冲区可能填满,导致写进程阻塞。
三、命名管道
命名管道(Named Pipe),也称为FIFO(First In First Out,先进先出),是一种进程间通信(IPC,Inter-Process Communication)机制,允许不相关的进程通过文件系统中的一个特殊文件(即命名管道文件)进行数据交换。与匿名管道(Anonymous Pipe)不同,命名管道具有一个明确的名称,因此可以在不同进程之间、甚至不同用户或不同主机之间(通过网络命名管道实现)进行通信。
3.1 命名管道的创建
在Linux系统中我们可以通过系统调用mkfifo来创建和设置命名管道,具体的函数原型以及参数解析如下:
函数原型:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数解析:
pathname
:指定命名管道的路径名,可以是相对路径或绝对路径,默认在当前路径。mode
:用于指定管道的权限,是一个八进制数,表示文件的权限掩码。
通常在设置管道文件权限之前,我们将文件掩码暂时设为0。
返回值与错误处理:
- 成功时返回 0,失败时返回 -1 并设置
errno
以指示错误原因。 - 常见错误码:
EACCES
:参数pathname
所指定的目录路径无可执行的权限。EEXIST
:参数pathname
所指定的文件已存在。ENAMETOOLONG
:参数pathname
的路径名称太长。ENOENT
:参数pathname
包含的目录不存在。ENOSPC
:文件系统的剩余空间不足。ENOTDIR
:参数pathname
路径中的目录存在但却非真正的目录。EROFS
:参数pathname
指定的文件存在于只读文件系统内。
代码示例:客户端与服务端通过命名管道实现进程间通信
//Common.hpp
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
class Name_Fifo
{
public:
Name_Fifo(const std::string path, const std::string name)
: _Name(name), _Path(path)
{
umask(0);
_fifoname = _Path + "/" + _Name;
int ret = mkfifo(_fifoname.c_str(), 0666);
if (ret == 0)
{
std::cout << "mkfifo success!\n"
<< std::endl;
}
else
{
std::cout << "mkfifo fail!\n"
<< std::endl;
exit(1);
}
}
~Name_Fifo()
{
unlink(_fifoname.c_str());
}
private:
std::string _Path;
std::string _Name;
std::string _fifoname;
};
// 对指定命名管道进行的操作
class oper_fifo
{
public:
oper_fifo(const std::string path, const std::string name)
: _Path(path), _Name(name), _fd(-1)
{
_fifoname = _Path + "/" + _Name;
}
~oper_fifo()
{
}
void OpenForRead()
{
_fd = open(_fifoname.c_str(), O_RDONLY);
if (_fd < 0)
{
std::cout << "open fifo failed!" << std::endl;
exit(2);
}
else
{
std::cout << "open fifo success!" << std::endl;
}
}
void OpenForWrite()
{
_fd = open(_fifoname.c_str(), O_WRONLY);
if (_fd < 0)
{
std::cout << "open fifo failed!" << std::endl;
exit(2);
}
else
{
std::cout << "open fifo success!" << std::endl;
}
}
void Read()
{
// 具体的读取操作在这里定义
while (true)
{
char rd_buffer[1024];
int ret = read(_fd, rd_buffer, sizeof(rd_buffer) - 1);
if (ret == 0)
{
// 读到了文件末尾表示写入端退出:
std::cout << "write exit me too!" << std::endl;
break;
}
else if (ret < 0)
{
std::cout << "read failed!" << std::endl;
}
else
{
rd_buffer[ret] = '0';
std::cout << "Client say:" << rd_buffer << std::endl;
}
}
}
void Write()
{
// 具体的写入操作在这里定义
while (1)
{
std::cout<<"Please Enter:"<<std::endl;
std::string messager;
int cnt = 1;
pid_t id = getpid();
std::getline(std::cin, messager);
messager += (", message number: " + std::to_string(cnt++) + ", [" + std::to_string(id) + "]");
int ret=write(_fd,messager.c_str(),messager.size());
}
}
void Close()
{
if(_fd>0)
{
close(_fd);
}
}
private:
std::string _Path;
std::string _Name;
std::string _fifoname;
int _fd; // 命名管道的文件描述符
};
//Server.cc
#include"Common.hpp"
int main()
{
//创建管道:
Name_Fifo Fifo(".","myfifo");
oper_fifo my_read(".","myfifo");
my_read.OpenForRead();
my_read.Read();
my_read.Close();
return 0;
//Client.cc
#include"Common.hpp"
int main()
{
oper_fifo my_write(".","myfifo");
my_write.OpenForWrite();
my_write.Write();
my_write.Close();
return 0;
}
代码说明:
在我们的代码中,Common.hpp中定义了两个类一个是命名管道Name_Fifo,一个是对命名管道的操作oper_fifo。在服务端我们创建命名管道后进行读操作,此时由于写端还没有将管道文件打开服务端会阻塞于my_read.OpenForRead();代码处。
这时我们运行客户端代码,需要注意的是客户端不再需要进行mkfifo而是直接对创建的命名管道进行写操作。此时当客户端以写方式打开命名管道时并等待用户输入时,读端才会解除阻塞状态。
3.2 原理与特性
3.2.1 核心特性
命名管道在文件系统中以特殊文件形式存在,进程通过路径名访问,无需亲缘关系(如父子进程)。
单个命名管道默认是单向的,但可通过创建两个管道(一个读、一个写)实现双向通信。
读操作:若管道无数据,读取进程会阻塞,直到有数据写入。写操作:若管道缓冲区满,写入进程会阻塞,直到有空间可用。同步:通过内核的等待队列管理阻塞进程,确保数据有序传输。
命名管道独立于创建它的进程,即使进程退出,管道仍存在,直到被显式删除(如Linux的unlink
)。
3.2.2 工作原理
创建与初始化
通过mkfifo
或mknod
创建,内核在文件系统中生成一个inode
,但不占用磁盘空间,仅维护管道的元数据(如缓冲区大小、等待队列)。
数据传输流程
写入端:进程调用write
,数据被复制到内核缓冲区。若缓冲区满,写入进程阻塞。
读取端:进程调用read
,从内核缓冲区复制数据到用户空间。若缓冲区空,读取进程阻塞。
内核缓冲:数据在内核中临时存储,确保读写操作的原子性。
进程阻塞与唤醒
内核通过等待队列管理阻塞的读写进程。当数据可用或缓冲区有空间时,内核唤醒对应的进程。
关闭与清理
所有读写端关闭后,管道被销毁(Linux)或标记为无效(Windows)。
四、区别与联系 (命名&匿名)
4.1 区别
特性 | 命名管道(Named Pipe) | 匿名管道(Anonymous Pipe) |
---|---|---|
命名方式 | 通过文件系统路径命名(如/tmp/pipe 或\\.\pipe\name ) |
无名称,仅通过文件描述符(如fd[0] 、fd[1] )引用 |
进程关系 | 不相关进程可通过路径名通信 | 仅限亲缘进程(如父子进程、兄弟进程) |
持久性 | 独立于进程存在,需显式删除(如unlink ) |
随进程退出而自动销毁 |
网络通信 | 支持(Windows可通过网络路径访问) | 不支持,仅限本地进程 |
安全性 | 支持访问控制(如Windows的ACL) | 无内置安全机制,依赖进程间信任 |
实现复杂度 | 需创建管道文件(mkfifo 或CreateNamedPipe ) |
简单,通过pipe() 系统调用自动创建 |
数据方向 | 默认半双工(可通过双向管道模拟全双工) | 半双工,需两个管道实现双向通信 |
可见性 | 在文件系统中可见(如ls 命令可列出) |
不可见,仅在内核中维护 |
典型应用场景 | 跨进程、跨网络通信(如日志服务、远程服务) | 本地进程间短生命周期通信(如父子进程协作) |
命名与路径
命名管道通过文件系统路径标识,类似普通文件,但仅用于通信。匿名管道无路径,通过文件描述符传递,通常由pipe()
系统调用创建。
进程关系
命名管道允许无亲缘关系的进程通信(如服务进程与客户端)。匿名管道仅限有亲缘关系的进程(如fork()
创建的子进程)。
持久性与清理
命名管道需手动删除(如Linux的unlink
或Windows的CloseHandle
)。匿名管道随进程退出自动销毁,无需额外清理。
4.2 共同联系
特性 | 命名管道与匿名管道的共同点 |
---|---|
内核机制 | 均依赖内核缓冲区存储数据,支持阻塞式I/O |
数据传输方式 | 均为字节流(默认,可通过消息模式扩展) |
同步机制 | 均通过内核的等待队列管理阻塞的读写进程 |
进程通信模型 | 均属于进程间通信(IPC)机制 |
性能开销 | 均涉及用户态与内核态的上下文切换 |
适用场景 | 均适用于进程间数据交换(匿名适合短生命周期,命名适合长生命周期或跨进程) |
4.3 总结
命名管道更灵活,适合跨进程、跨网络通信,但需手动管理生命周期。
匿名管道更简单,适合亲缘进程间短生命周期通信,无需额外清理。
根据场景选择合适的管道类型,可平衡复杂度与功能需求。