【Linux】进程间通信(1)
进程是操作系统进行资源分配的基本单位,各进程的地址空间相互独立(受操作系统内存隔离机制保护),无法直接访问对方数据。进程间通信(Inter-Process Communication, IPC) 是操作系统提供的一套机制,用于打破这种隔离,实现进程间的数据交换、资源共享、事件通知或进程控制。
1.1 进程间通信的目的
进程间通信的设计核心是解决多进程协作的关键需求,具体可分为四类:
数据传输:一个进程需将数据(如用户输入、计算结果)发送给另一个进程。例如:即时通讯软件中,消息发送进程将用户输入的文字发送给消息接收进程;数据分析进程将计算结果发送给数据展示进程。
资源共享:多个进程需共享同一资源(如文件、内存块、硬件设备),避免资源冗余存储。例如:多个视频播放进程共享系统显卡资源以渲染画面;多个文档编辑进程共享同一磁盘文件(需通过 IPC 保证读写同步,避免数据错乱)。
通知事件:一个进程需向其他进程发送 “事件信号”,告知特定事件已发生。例如:子进程执行完毕后,通过信号通知父进程回收其资源;监控进程检测到磁盘空间不足时,通知文件写入进程暂停操作。
进程控制:一个进程(如调试器
gdb
)需完全控制另一个进程的执行流程,包括拦截异常、查看寄存器状态、暂停 / 继续执行。例如:gdb
通过 IPC 机制附加到目标进程,设置断点后可实时监控目标进程的执行状态。
1.2 进程间通信的发展
IPC 机制随操作系统演进不断优化,主要经历三个阶段,不同阶段的机制适配不同场景的需求:
早期 Unix 阶段:核心机制为管道(Pipe),是最古老的 IPC 形式,设计简单但仅支持亲缘进程(如父子进程)通信,适用于简单的流式数据传输。
System V IPC 阶段:推出System V 消息队列、System V 共享内存、System V 信号量,支持非亲缘进程通信,提供更灵活的资源管理,但接口较复杂且可移植性差(不同 Unix 系统实现存在差异)。
POSIX IPC 阶段:基于 POSIX 标准统一接口,推出POSIX 消息队列、POSIX 共享内存、POSIX 信号量,以及适用于线程 / 进程同步的互斥量、条件变量、读写锁,可移植性强(支持 Linux、macOS、BSD 等),成为现代操作系统的主流 IPC 方案。
1.3 进程间通信的分类
根据实现原理和功能,主流 IPC 机制可分为以下类别,后续章节将重点展开 “管道” 的细节:
分类 | 具体机制 | 核心特点 |
---|---|---|
管道类 | 匿名管道(Pipe)、命名管道(FIFO) | 基于 “一切皆文件” 思想,数据流式传输,半双工通信 |
System V IPC | 消息队列、共享内存、信号量 | 基于内核对象管理,独立于进程生命周期,支持非亲缘进程 |
POSIX IPC | 消息队列、共享内存、信号量、互斥量、条件变量 | 标准化接口,可移植性强,支持线程 / 进程共享 |
其他 | 信号(Signal)、Socket | 信号用于紧急事件通知;Socket 支持跨主机进程通信 |
1.4 匿名管道
1.4.1 管道的定义
管道是 Unix 系统中最古老的 IPC 形式,核心是 “从一个进程连接到另一个进程的数据流”。它遵循 Linux “一切皆文件” 的设计思想 —— 内核在内存中维护一个临时缓冲区,进程通过读写该缓冲区实现通信,操作方式与读写普通文件一致(如read()
、write()
系统调用)。
1.4.2 匿名管道(Pipe)
匿名管道是最基础的管道类型,通过pipe()
系统调用创建,仅支持亲缘进程(如父子、兄弟进程)通信。
1. pipe()
函数接口
#include <unistd.h>
// 功能:创建匿名管道,返回两个文件描述符
// 参数:fd[2] - 输出型数组,fd[0]表示“读端”,fd[1]表示“写端”
// 返回值:成功返回0;失败返回-1,同时设置errno(如EMFILE表示文件描述符耗尽)
int pipe(int fd[2]);
2. 实例代码:基础管道通信
以下代码实现 “从键盘读取数据→写入管道→读取管道→输出到屏幕” 的完整流程,直观展示匿名管道的使用:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>int main(void) {int fds[2]; // 存储管道的读端(fds[0])和写端(fds[1])char buf[100] = {0}; // 用于存储读写的数据int len; // 记录读取/写入的数据长度// 1. 创建匿名管道,失败则报错退出if (pipe(fds) == -1) {perror("make pipe"); // 打印错误原因(如“make pipe: Too many open files”)exit(1);}// 2. 循环从键盘读取数据(标准输入stdin)while (fgets(buf, 100, stdin) != NULL) {len = strlen(buf); // 获取输入数据的长度(包含换行符)// 3. 将数据写入管道(写端fds[1])if (write(fds[1], buf, len) != len) {perror("write to pipe");break; // 写入失败则退出循环}// 4. 清空缓冲区,准备读取管道数据memset(buf, 0x00, sizeof(buf));// 5. 从管道读取数据(读端fds[0])if ((len = read(fds[0], buf, 100)) == -1) {perror("read from pipe");break; // 读取失败则退出循环}// 6. 将读取到的数据写入屏幕(标准输出stdout)if (write(1, buf, len) != len) {perror("write to stdout");break;}}return 0;
}
3. 用fork()
共享管道的原理
匿名管道仅支持亲缘进程通信,核心依赖fork()
的 “文件描述符继承” 特性,具体流程如下:
- 父进程调用
pipe(fds)
创建管道,获得fds[0]
(读端)和fds[1]
(写端); - 父进程调用
fork()
创建子进程,子进程会复制父进程的文件描述符表,因此也拥有fds[0]
和fds[1]
,指向同一内核缓冲区; - 父进程和子进程通过关闭 “不需要的端” 实现单向通信(如父进程关闭读端
fds[0]
、子进程关闭写端fds[1]
,则父进程写、子进程读)。
4. 从文件描述符角度理解管道
- 管道的本质是内核维护的 “伪文件”,没有实际的磁盘存储,仅存在于内存中;
- 进程通过
fd[0]
(读端)和fd[1]
(写端)操作管道,这两个文件描述符与普通文件的描述符无差异,可通过read()
、write()
、close()
等系统调用操作; - 只有持有管道文件描述符的进程才能访问管道,非亲缘进程无该描述符,因此无法通信。
5. 从内核角度理解管道本质
内核为管道维护三个核心结构:
- 缓冲区:用于存储管道数据,大小由内核定义(通常为
PIPE_BUF
,默认 4096 字节); - 读写指针:分别记录当前读取和写入的位置,确保数据按 “先进先出(FIFO)” 顺序传输;
- 引用计数:记录当前持有管道读端和写端的进程数,当读端引用计数为 0 时,写操作会触发
SIGPIPE
信号;当写端引用计数为 0 时,读操作会返回 0(类似文件 EOF)。
“Linux 一切皆文件” 的思想在此体现:管道的操作逻辑与普通文件完全一致,进程无需区分 “操作的是管道还是文件”,降低了编程复杂度。
1.4.3 匿名管道的进阶示例
1. 测试管道读写(父子进程通信)
以下代码通过fork()
创建父子进程,实现 “子进程写、父进程读” 的单向通信,验证管道的基本功能:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>void ChildWrite(int wfd)
{char buffer[1024];int cnt = 0;while (true){snprintf(buffer, sizeof(buffer), "I am child, pid: %d, cnt: %d", getpid(), cnt++);write(wfd, buffer, strlen(buffer));sleep(1);}
}
void FatherRead(int rfd)
{char buffer[1024];while (true){buffer[0] = 0;ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::cout << "child say: " << buffer << std::endl;}}
}int main()
{// 创建管道int fds[2] = {0};int n = pipe(fds); // fds[0] 读端,fds[1] 写端口if (n < 0){std::cerr << "pipe error" << std::endl;return 1;}std::cout << "fds[0] " << fds[0] << std::endl;std::cout << "fds[1] " << fds[1] << std::endl;// 创建子进程// f -> r, c -> wpid_t id = fork();if (id == 0){// childclose(fds[0]);ChildWrite(fds[1]);close(fds[1]);exit(0);}close(fds[1]);FatherRead(fds[0]);waitpid(id, nullptr, 0);close(fds[0]);return 0;
}
2. 创建进程池处理任务
当需要多个子进程并发处理任务时(如批量数据处理、多任务调度),可通过 “父进程创建多个管道 + 多个子进程” 实现进程池,核心是通过管道实现 “父进程派发任务、子进程执行任务” 的协作模式。
以下是进程池的核心代码实现(含头文件和主函数):
(1)ProcessPool.hpp
:管道通信信道封装和进程池管理
用于封装 “管道写端” 和 “子进程 ID”,方便父进程定位子进程并发送任务:
#ifndef __PROCESS_POOL_HPP__
#define __PROCESS_POOL_HPP__#include <iostream>
#include <cstdlib> // stdlib.h stdio.h -> cstdlib cstdio
#include <vector>
#include <unistd.h>
#include <sys/wait.h>
#include "Task.hpp"// 先描述
class Channel
{
public:Channel(int fd, pid_t id) : _wfd(fd), _subid(id){_name = "channel-" + std::to_string(_wfd) + "-" + std::to_string(_subid);}~Channel(){}void Send(int code){int n = write(_wfd, &code, sizeof(code));(void)n; // ?}void Close(){close(_wfd);}void Wait(){pid_t rid = waitpid(_subid, nullptr, 0);(void)rid;}int Fd() { return _wfd; }pid_t SubId() { return _subid; }std::string Name() { return _name; }private:int _wfd;pid_t _subid;std::string _name;// int _loadnum;
};// 在组织
class ChannelManager
{
public:ChannelManager() : _next(0){}void Insert(int wfd, pid_t subid){_channels.emplace_back(wfd, subid);// Channel c(wfd, subid);// _channels.push_back(std::move(c));}Channel &Select(){// 采用轮询的方式选择一个信道auto &c = _channels[_next];_next++;_next %= _channels.size();return c;}void PrintChannel(){for (auto &channel : _channels){std::cout << channel.Name() << std::endl;}}void StopSubProcess(){for (auto &channel : _channels){channel.Close();std::cout << "关闭: " << channel.Name() << std::endl;}}void WaitSubProcess(){// for (auto &channel : _channels)// {// channel.Wait();// std::cout << "回收: " << channel.Name() << std::endl;// }for (int i = _channels.size() - 1; i >= 0; i--){_channels[i].Close();std::cout << "关闭: " << _channels[i].Name() << std::endl;_channels[i].Wait();std::cout << "回收: " << _channels[i].Name() << std::endl;}}~ChannelManager() {}private:std::vector<Channel> _channels;int _next;
};const int gdefaultnum = 5;class ProcessPool
{
public:ProcessPool(int num) : _process_num(num){_tm.Register(PrintLog);_tm.Register(Download);_tm.Register(Upload);}void Work(int rfd){while (true){int code = 0;ssize_t n = read(rfd, &code, sizeof(code));if (n > 0){if (n != sizeof(code)){continue;}std::cout << "子进程[" << getpid() << "]收到一个任务码: " << code << std::endl;_tm.Execute(code);}else if (n == 0){std::cout << "子进程退出" << std::endl;break;}else{std::cout << "读取错误" << std::endl;break;}}}bool Start(){for (int i = 0; i < _process_num; i++){// 1. 创建管道int pipefd[2] = {0};int n = pipe(pipefd);if (n < 0)return false;// 2. 创建子进程pid_t subid = fork();if (subid < 0)return false;else if (subid == 0){// 子进程// 3. 关闭不需要的文件描述符close(pipefd[1]);Work(pipefd[0]); //??close(pipefd[0]);exit(0);}else{// 父进程// 3. 关闭不需要的文件描述符close(pipefd[0]); // 写端:pipefd[1];_cm.Insert(pipefd[1], subid);// wfd, subid}}return true;}void Debug(){_cm.PrintChannel();}void Run(){// 1. 选择一个任务int taskcode = _tm.Code();// 2. 选择一个信道[子进程],负载均衡的选择一个子进程,完成任务auto &c = _cm.Select();std::cout << "选择了一个子进程: " << c.Name() << std::endl;// 2. 发送任务c.Send(taskcode);std::cout << "发送了一个任务码: " << taskcode << std::endl;}void Stop(){// 关闭父进程所有的wfd即可// _cm.StopSubProcess();// 回收所有子进程_cm.WaitSubProcess();// 为什么两个不是合在一起写呢?// 如果合在一起写会发现,父进程关闭写端后,父进程还在等待,会导致父进程阻塞,无法退出。// 这是因为父进程的文件描述表会被后面的子进程继承,父进程的管道写端将会被多个子进程共享,当父进程关闭写端时并不是真的关闭了// 这时管道文件就没有结束,所以子进程还在等待。// 要想解决这个问题:// 1. 可以倒着关, 因为最后面的管道文件没有被子进程继承,所以关掉它的写端,子进程就能正常退出。// 2. 让子进程自己关闭自己继承下来的他的哥哥进程的写端就可以了}~ProcessPool(){}private:ChannelManager _cm;int _process_num;TaskManager _tm;
};#endif // __PROCESS_POOL_HPP__
(2)Task.hpp
:任务管理
定义任务类型和任务管理器,实现任务的选择和执行:
#pragma once#include <iostream>
#include <vector>
#include <ctime>typedef void (*task_t)();////////////////debug/////////////////////
void PrintLog()
{std::cout << "我是一个打印日志的任务" << std::endl;
}void Download()
{std::cout << "我是一个下载的任务" << std::endl;
}void Upload()
{std::cout << "我是一个上传的任务" << std::endl;
}
//////////////////////////////////////class TaskManager
{
public:TaskManager(){srand(time(nullptr));}void Register(task_t t){_tasks.push_back(t);}int Code(){return rand() % _tasks.size();}void Execute(int code){if(code >= 0 && code < _tasks.size()){_tasks[code]();}}~TaskManager(){}
private:std::vector<task_t> _tasks;
};
1.5 命名管道
在 Unix/Linux 系统的进程间通信(IPC)机制中,命名管道(FIFO,First In First Out)是对匿名管道的关键扩展。它解决了匿名管道仅能在具有亲缘关系进程(如父子、兄弟进程)间通信的限制,通过在文件系统中创建一个可见的 “特殊文件”,实现了无亲缘关系进程间的可靠数据传输。
1.5.1 命名管道的核心概念
命名管道本质是一种特殊类型的文件,其核心特性如下:
- 文件系统可见性:创建后会在文件系统中生成一个对应的路径(如
./mypipe
),进程可通过该路径识别并访问管道,这是其与匿名管道(仅存在于内存)的核心区别。 - FIFO 特性:数据传输遵循 “先进先出” 原则,保证写入顺序与读取顺序一致。
- 半双工通信:与匿名管道一致,数据传输为单向流,需双向通信时需创建两个命名管道。
- 面向字节流:传输的数据为无结构字节流,不保留消息边界,需开发者自行定义数据解析规则(如固定长度、分隔符)。
1.5.2 创建命名管道
命名管道的创建有两种方式:命令行创建和程序内创建,两种方式最终生成的管道功能完全一致。
1. 命令行创建:mkfifo
命令
mkfifo
是专门用于创建命名管道的命令行工具,语法如下:
$ mkfifo [选项] 管道文件名
- 常用选项:无特殊选项时,默认创建的管道权限受
umask
影响(通常为0644
或0664
)。 - 示例:创建名为
mypipe
的命名管道$ mkfifo mypipe $ ls -l mypipe # 查看管道文件类型,显示为"p"(pipe) prw-r--r-- 1 user user 0 Oct 2 14:00 mypipe
2. 程序内创建:mkfifo
函数
C 语言中通过 mkfifo()
系统调用创建命名管道,函数原型如下:
#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *filename, mode_t mode);
- 参数说明:
filename
:指向管道文件路径的字符串(如"p2"
)。mode
:管道文件的权限位(如0644
,表示所有者可读可写,组和其他用户可读),最终权限会与当前进程的umask
进行按位与(~umask & mode
),因此创建前常通过umask(0)
清除权限屏蔽。
- 返回值:成功返回 0;失败返回 -1,并设置
errno
(如EEXIST
表示文件已存在,EACCES
表示权限不足)。
示例:程序内创建命名管道
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>#define ERR_EXIT(m) \
do { \perror(m); \exit(EXIT_FAILURE); \
} while(0)int main(int argc, char *argv[])
{umask(0); // 清除权限屏蔽,确保设置的mode生效// 创建名为"p2"、权限为0644的命名管道if (mkfifo("p2", 0644) == -1) {ERR_EXIT("mkfifo failed");}printf("命名管道\"p2\"创建成功\n");return 0;
}
1.5.3 命名管道的打开规则
命名管道创建后需通过 open()
函数打开才能进行读写,其打开行为受阻塞标志(O_NONBLOCK) 和打开模式(读 / 写) 共同影响,这是使用命名管道的核心注意点。
打开模式 | O_NONBLOCK 禁用(默认阻塞) | O_NONBLOCK 启用(非阻塞) |
---|---|---|
为读打开(O_RDONLY) | 阻塞,直到有进程以 “写模式” 打开该管道 | 立即返回成功,即使暂无写进程打开管道 |
为写打开(O_WRONLY) | 阻塞,直到有进程以 “读模式” 打开该管道 | 立即返回失败,错误码设为 ENXIO |
读写打开(O_RDWR) | 立即返回成功(无需等待其他进程) | 立即返回成功(无需等待其他进程) |
注意:实际开发中极少使用 “读写打开”,因为这会绕过管道的 “两端配对” 机制,可能导致数据读写逻辑混乱。
1.5.4 匿名管道与命名管道的区别
二者在数据传输语义(阻塞、字节流、半双工)上完全一致,核心差异仅集中在创建与访问方式,具体对比如下:
特性 | 匿名管道(Pipe) | 命名管道(FIFO) |
---|---|---|
创建函数 | pipe(int pipefd[2]) | mkfifo(const char *filename, mode_t mode) |
打开方式 | 创建时自动打开(返回读写端 fd) | 需通过 open() 函数手动打开 |
进程间通信限制 | 仅支持具有亲缘关系的进程 | 支持无亲缘关系的任意进程(通过文件路径访问) |
存在形式 | 仅存在于内存,随进程退出自动释放 | 存在于文件系统,需手动 unlink() 删除 |
标识方式 | 通过文件描述符(pipefd [0]/pipefd [1])标识 | 通过文件系统路径(如 ./mypipe )标识 |
1.5.5 命名管道的实践实例
命名管道的典型应用场景包括 “无亲缘进程间文件传输”“客户端 - 服务器(C/S)通信” 等,以下结合完整代码说明实现逻辑。
实例 1:用命名管道实现文件拷贝
通过两个独立进程(无亲缘关系)配合:进程 A 读取源文件并写入管道,进程 B 从管道读取数据并写入目标文件,实现文件拷贝。
进程 A(读文件→写管道)
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>#define ERR_EXIT(m) \
do \
{ \perror(m); \exit(EXIT_FAILURE); \
} while(0)int main()
{// 1. 创建命名管道(若已存在可跳过,此处简化为直接创建)umask(0);if (mkfifo("tp", 0644) == -1 && errno != EEXIST) {ERR_EXIT("mkfifo failed");}// 2. 打开源文件(只读)int infd = open("abc", O_RDONLY);if (infd == -1) {ERR_EXIT("open source file failed");}// 3. 打开管道(只写,默认阻塞,等待读进程)int outfd = open("tp", O_WRONLY);if (outfd == -1) {ERR_EXIT("open pipe for write failed");}// 4. 循环读写数据char buf[1024];ssize_t n; // 注意用ssize_t接收read/write返回值(支持-1)while ((n = read(infd, buf, sizeof(buf))) > 0) {write(outfd, buf, n); // 写入管道}// 5. 释放资源close(infd);close(outfd);printf("文件数据已写入管道\n");return 0;
}
进程 B(读管道→写目标文件)
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>#define ERR_EXIT(m) \
do \
{ \perror(m); \exit(EXIT_FAILURE); \
} while(0)int main()
{// 1. 打开目标文件(只写,不存在则创建,存在则清空)int outfd = open("abc.bak", O_WRONLY | O_CREAT | O_TRUNC, 0644);if (outfd == -1) {ERR_EXIT("open target file failed");}// 2. 打开管道(只读,默认阻塞,等待写进程)int infd = open("tp", O_RDONLY);if (infd == -1) { // 原代码此处误写为outfd,已修正ERR_EXIT("open pipe for read failed");}// 3. 循环读写数据char buf[1024];ssize_t n;while ((n = read(infd, buf, sizeof(buf))) > 0) {write(outfd, buf, n); // 写入目标文件}// 4. 释放资源并删除管道文件close(infd);close(outfd);unlink("tp"); // 手动删除管道文件printf("文件拷贝完成,目标文件:abc.bak\n");return 0;
}
运行步骤:
- 编译两个程序:
gcc write_pipe.c -o write_pipe
、gcc read_pipe.c -o read_pipe
。 - 启动进程 B(读端):
./read_pipe
(此时阻塞,等待写端)。 - 启动进程 A(写端):
./write_pipe
(开始写入数据,进程 B 接收并写入目标文件)。 - 进程 A 运行结束后,进程 B 读取完数据也随之结束,管道文件被
unlink()
删除。
实例 2:用命名管道实现 Server&Client 通信
构建简单的 C/S 模型:Server 创建管道并监听读端,Client 通过管道向 Server 发送消息,实现单向通信。
1. 工程结构与 Makefile
$ ll
total 12
-rw-r--r-- 1 user user 450 Oct 2 14:30 clientPipe.c # 客户端代码
-rw-r--r-- 1 user user 164 Oct 2 14:30 Makefile # 编译脚本
-rw-r--r-- 1 user user 580 Oct 2 14:30 serverPipe.c # 服务端代码
Makefile
PHONY: all clean
all: client serverclient: client.ccg++ -o $@ $^ -std=c++11server: server.ccg++ -o $@ $^ -std=c++11clean:rm -f client server
2. Server(服务端:创建管道→读消息)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>#define FIFO_FILE "fifo"int main()
{// 创建管道文件umask(0);int n = mkfifo(FIFO_FILE, 0666);if (n < 0) {std::cerr << "mkfifo error" << std::endl;return 1;}std::cout << "mkfifo success" << std::endl;// 打开管道文件std::cout << "Please wait..." << std::endl;int fd = open(FIFO_FILE, O_RDONLY);if(fd < 0){std::cerr << "open fifo error" << std::endl;return 2;}std::cout << "open fifo success" << std::endl;// 正常的 readwhile(true){char buf[1024];int num = read(fd, buf, sizeof(buf) - 1);if(num > 0){buf[num] = 0;std::cout << "client say# " << buf << std::endl;}else if(num == 0){std::cout << "Client quit! me too!" << std::endl;break;}else{std::cout << "read error" << std::endl;break;}}close(fd);// 删除管道文件n = unlink(FIFO_FILE);if (n < 0) {std::cerr << "unlink error" << std::endl;return 1;}std::cout << "unlink success" << std::endl;return 0;
}
3. Client(客户端:打开管道→发消息)
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>#define FIFO_FILE "fifo"int main()
{// writeint fd = open(FIFO_FILE, O_WRONLY);if (fd < 0){std::cerr << "open fifo error" << std::endl;}std::cout << "open fifo sucess" << std::endl;// 写std::string message;int cnt = 0;pid_t id = getpid();while (true){std::cout << "请输入:" << std::endl;std::getline(std::cin, message);message += (", message number: " + std::to_string(cnt++) + ", [" + std::to_string(id) + "]");write(fd, message.c_str(), message.size());}return 0;
}
4. 运行结果
- 编译程序:
make all
,生成client
和server
可执行文件。 - 启动服务端:
- 启动客户端(新终端):
- 服务端接收消息:
- 客户端退出(Ctrl+C)后,服务端提示并退出:
代码封装:
Server.cc:
#include "comm.hpp"int main()
{// 创建管道文件NamedFifo fifo(".", FILENAME);// 文件操作了FileOper readerfile(PATH, FILENAME);readerfile.OpenForRead();readerfile.Read();readerfile.Close();return 0;
}
Client.cc:
#include "comm.hpp"int main()
{FileOper writerfile(PATH, FILENAME);writerfile.OpenForWrite();writerfile.Write();writerfile.Close();return 0;
}
comm.hpp:
#pragma once#include <iostream>
#include <cstdio>
#include <string>
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>#define PATH "."
#define FILENAME "fifo"#define ERR_EXIT(m) \do \{ \perror(m); \exit(EXIT_FAILURE); \} while (0)// 创建管道
class NamedFifo
{
public:NamedFifo(const std::string path, const std::string name): _path(path), _name(name){_fifoname = _path + "/" + _name;umask(0);int n = mkfifo(_fifoname.c_str(), 0666);if (n < 0){ERR_EXIT("mkfifo");}std::cout << "mkfifo success" << std::endl;}~NamedFifo(){int n = unlink(_fifoname.c_str());if (n < 0){ERR_EXIT("unlink");}std::cout << "unlink success" << std::endl;}private:std::string _path;std::string _name;std::string _fifoname;
};// 打开文件
class FileOper
{
public:FileOper(const std::string& path, const std::string& name): _path(path), _name(name), _fd(-1){_fifoname = _path + "/" + _name;}void OpenForRead(){std::cout << "Please wait..." << std::endl;_fd = open(_fifoname.c_str(), O_RDONLY);if (_fd < 0){ERR_EXIT("open");}std::cout << "open fifo success" << std::endl;}void OpenForWrite(){_fd = open(_fifoname.c_str(), O_WRONLY);if (_fd < 0){ERR_EXIT("open");}std::cout << "open fifo sucess" << std::endl;}void Write(){std::string message;int cnt = 0;pid_t id = getpid();while (true){std::cout << "请输入:" << std::endl;std::getline(std::cin, message);message += (", message number: " + std::to_string(cnt++) + ", [" + std::to_string(id) + "]");write(_fd, message.c_str(), message.size());}}void Read(){while (true){char buf[1024];int num = read(_fd, buf, sizeof(buf) - 1);if (num > 0){buf[num] = 0;std::cout << "client say# " << buf << std::endl;}else if (num == 0){std::cout << "Client quit! me too!" << std::endl;break;}else{std::cout << "read error" << std::endl;break;}}}void Close(){if (_fd > 0)close(_fd);}~FileOper(){}private:std::string _path;std::string _name;std::string _fifoname;int _fd;
};
1.5.6 注意事项与常见问题
- 权限问题:创建管道时需确保进程有目标目录的写权限,打开管道时需确保有管道文件的读写权限,建议创建前用
umask(0)
清除权限屏蔽。 - 管道残留:命名管道文件不会随进程退出自动删除,若程序异常退出未执行
unlink()
,需手动删除(rm 管道名
),否则下次创建会报错EEXIST
。 - 阻塞与非阻塞:默认阻塞模式下,读写端需配对才能正常工作;非阻塞模式需做好错误处理(如写端无读端时返回
ENXIO
)。 - 数据完整性:由于管道是面向字节流,若需传输结构化数据(如消息),需自定义协议(如 “消息长度 + 消息内容”),避免数据粘连。