文章目录
- 3. 进程间通信
- 3.1 进程间通信介绍
- 3.1.1 进程间通信目的
- 3.1.2 进程间通信发展
- 3.2 什么是进程间通信
- 3.3 管道
- 3.4 匿名管道pipe()
- 3.4.1 站在文件描述符角度-深度理解管道
- 3.4.2 站在内核角度-管道本质
- 3.4.3 用fork来共享管道原理
- 3.4.5 管道相关知识
- 3.4.6 代码一:实现父子进程间的单向通信
- 3.4.7 代码二:验证杀掉正在写入进程的信号
- 3.4.8 代码三:使用管道实现一个简易版本的进程池
- 3.5 命名管道FIFO
- 3.5.1 为什么命名管道可以在不相关的进程之间交换数据
- 3.5.2 命名管道的打开规则
- 3.5.3 一个简单的命名管道代码
- 3.5.4 命名管道代码:一个简单的日志函数实现
- 3.5.5 匿名管道与命名管道的区别
3. 进程间通信
3.1 进程间通信介绍
3.1.1 进程间通信目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另
一个进程的所有陷入和异常,并能够及时知道它的状态改变。
3.1.2 进程间通信发展
管道
System V进程间通信
POSIX进程间通信
进程间通信分类
管道
匿名管道pipe
命名管道
System V IPC
System V 消息队列
System V 共享内存
System V 信号量
POSIX IPC
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
3.2 什么是进程间通信
什么是进程间通信?
是两个或者多个进程实现数据层面的交互。
通信是有成本的,因为进程独立性的存在,导致进程通信的成本比较高
进程间通信的本质:必须让不同的进程看到同一份"资源
资源:特定形式的内存空间
这个"资源"谁提供?
一般是操作系统(第三方空间)。
为什么不是我们两个进程中的一个呢?
假设一个进程提供,这个资源属于谁?
如果是这个进程独有,就会破坏进程独立性。
我们进程访问这个空间,进行通信,本质就是访问操作系统。
进程代表的就是用户,(一般而言)“资源”从创建,使用,释放。
这整个过程通过系统调用接口。
从底层设计,从接口设计,都要由操作系统独立设计。一般操作系统,会有一个独立的通信模块(隶属于文件系统),这个模块在系统里叫做:
IPC
通信模块。出现了很多的通信方案,所以要定制标准 – 进程间通信是有标准的:
system V
和posix
两套标准。基于文件级别的通信方式:管道
直接原理:
进程间通信本质前提是需要先让不同的进程,看到同一份资源。
管道就是文件。
多执行流共享的,难免出现访问冲突的问题(临界资源竞争的问题)。
3.3 管道
什么是管道
管道是
Unix
中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
管道读写规则:
当没有数据可读时
O_NONBLOCK disable
:read
调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable
:read
调用返回-1
,errno
值为EAGAIN
。(EAGAIN是一个错误码(errno),表示"再试一次"(Try again))当管道满的时候
O_NONBLOCK disable
:write
调用阻塞,直到有进程读走数据
O_NONBLOCK enable
:调用返回-1,errno
值为EAGAIN
。如果所有管道写端对应的文件描述符被关闭,则
read
返回0
如果所有管道读端对应的文件描述符被关闭,则
write
操作会产生信号SIGPIPE
,进而可能导致write
进程退出当要写入的数据量不大于
PIPE_BUF
时,linux
将保证写入的原子性。当要写入的数据量大于
PIPE_BUF
时,linux
将不再保证写入的原子性。
管道特点
只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用
fork
,此后父、子进程之间就可应用该管道。管道提供流式服务
一般而言,进程退出,管道释放,所以管道的生命周期随进程
一般而言,内核会对管道操作进行同步与互斥
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
3.4 匿名管道pipe()
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
3.4.1 站在文件描述符角度-深度理解管道
3.4.2 站在内核角度-管道本质
3.4.3 用fork来共享管道原理
3.4.5 管道相关知识
如果我们想要双向通信,可以建立两个管道。
如果两个进程没有关系,就不能用这个原理进行通信了。
(因为管道创建后,文件描述符只在当前进程可见。无法将文件描述符传递给无关进程,fork()后子进程才会继承父进程的文件描述符。)
(但是可以使用FIFO(命名管道),消息队列,共享内存。这些IPC机制都可以通过某种标识符(如文件路径、key值)在无关进程间建立关联,从而实现通信。)
父子关系,兄弟关系,爷孙关系也可以用管道通信。
管道通信适用于:进程之间需要有血缘关系,常用于父子关系。
至此,我们父进程和子进程还没有通信,只是建立了通信的信道。
这里看起来很费劲,实际上是因为进程具有独立性,通信有成本导致的。
管道的特征:
具有血缘关系的进程进行进程间通信
管道只能单向通信
父子进程是会进程协同的,同步与互斥的 — 保护管道文件的数据安全
管道是面向字节流的
管道是基于文件的,而文件的生命周期是随进程的
管道的
4
种情况:
读写端正常,管道如果为空,读端就要阻塞(有数据就读,没数据就等)
读写端正常,管道如果被写满,写端就要阻寒(管道是有固定大小的,在不同内核里可能不一样,所以可以被写满,写满了之后就只能等读端读,才能继续写)
读端正常读,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会被阻塞
写端是正常写入,读端关闭了。操作系统就要杀掉正在写入的进程(因为操作系统是不会做低效,浪费等类似的工作的。如果做了,就是操作系统的bug)
如何干掉?通过信号杀掉。这个信号是13号信号。
3.4.6 代码一:实现父子进程间的单向通信
子进程通过管道每隔1
秒向父进程发送一个字符'c'
,总共发送5
次,父进程负责接收并显示这些数据。
makefile
testPipe:testPipe.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f testPipe
testPipe.cc
// 必要的头文件包含
#include <iostream> // C++输入输出
#include <cstdio> // C标准输入输出
#include <string> // C++字符串
#include <cstring> // C字符串操作
#include <cstdlib> // C标准库函数
#include <unistd.h> // Unix标准函数定义
#include <sys/types.h> // 基本系统数据类型
#include <sys/wait.h> // wait()函数
#define N 2 // 管道的两端:读端和写端
#define NUM 1024 // 缓冲区大小
using namespace std;
// 写数据的函数
void Writer(int wfd) // wfd是管道的写端文件描述符
{
string s = "hello, I am child"; // 待发送的消息
pid_t self = getpid(); // 获取进程ID
int number = 0; // 计数器
char buffer[NUM]; // 数据缓冲区
while (true)
{
sleep(1); // 每次写入间隔1秒
// 以下是发送/写入字符串的方式(被注释)
//buffer[0] = 0; // 清空缓冲区
//snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
//write(wfd, buffer, strlen(buffer));
// 当前使用的是发送单个字符的方式
char c = 'c';
write(wfd, &c, 1); // 向管道写入一个字符
number++;
cout << number << endl; // 显示写入次数
if(number >= 5) break; // 写入5次后退出
}
}
// 读数据的函数
void Reader(int rfd) // rfd是管道的读端文件描述符
{
char buffer[NUM]; // 数据缓冲区
while(true)
{
buffer[0] = 0; // 清空缓冲区
// 从管道读取数据
ssize_t n = read(rfd, buffer, sizeof(buffer));
if(n > 0) // 读取成功
{
buffer[n] = 0; // 添加字符串结束符
cout << "father get a message[" << getpid() << "]# " << buffer << endl;
}
else if(n == 0) // 读到EOF(管道写端关闭)
{
printf("father read file done!\n");
break;
}
else break; // 读取出错
}
}
int main()
{
// 创建管道
int pipefd[N] = {0}; // pipefd[0]读端,pipefd[1]写端
int n = pipe(pipefd); // 调用了 pipe() 函数,用于创建一个管道,并将管道的文件描述符存储在 pipefd 数组中。
if (n < 0) // 管道创建失败
return 1;
// 创建子进程
pid_t id = fork();
if (id < 0) // 进程创建失败
return 2;
if (id == 0) // 子进程
{
close(pipefd[0]); // 关闭子进程读端
Writer(pipefd[1]); // 子进程写数据,写入5次字符'c',每次间隔1秒
close(pipefd[1]); // 关闭子进程写端
exit(0); // 子进程退出
}
// 父进程
close(pipefd[1]); // 关闭父进程写端
Reader(pipefd[0]); // 父进程读数据
// 等待子进程结束
pid_t rid = waitpid(id, nullptr, 0);
if(rid < 0) return 3;
close(pipefd[0]); // 关闭父进程读端
sleep(5); // 暂停5秒
return 0;
}
运行结果:
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson26/pipe$ ./testPipe
1
father get a message[276150]# c
2
father get a message[276150]# c
3
father get a message[276150]# c
4
father get a message[276150]# c
5
father get a message[276150]# c
father read file done!
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson26/pipe$
执行时序:
主进程创建管道
↓
fork()创建子进程
↓
子进程 父进程
关闭读端 关闭写端
↓ ↓
写入'c' 等待读取
sleep(1) 显示读取的数据
写入'c' 等待读取
sleep(1) 显示读取的数据
写入'c' 等待读取
sleep(1) 显示读取的数据
写入'c' 等待读取
sleep(1) 显示读取的数据
写入'c' 等待读取
↓ ↓
关闭写端 显示读取的数据
退出 等待子进程结束
关闭读端
sleep(5)
退出
代码主要实现:
- 创建匿名管道用于父子进程通信
fork
创建子进程- 子进程通过管道每秒向父进程发送一个字符
'c'
,发送5
次后退出- 父进程持续从管道读取数据并显示,直到管道关闭
- 父进程等待子进程结束后退出
重要的系统调用:
pipe()
:创建管道fork()
:创建子进程write()
:写入数据read()
:读取数据waitpid()
:等待子进程结束
3.4.7 代码二:验证杀掉正在写入进程的信号
makefile
testPipe:testPipe.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f testPipe
testPipe.cc
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <cstdlib> // 提供exit()函数
#include <unistd.h> // 提供 pipe(), fork(), read(), write(), close()等系统调用
#include <sys/types.h> // 提供pid_t类型
#include <sys/wait.h> // 提供waitpid()函数
#define N 2 // pipe数组大小,0用于读端,1用于写端
#define NUM 1024 // 缓冲区大小
using namespace std;
// 写数据的函数
void Writer(int wfd)
{
string s = "hello, I am child";
pid_t self = getpid(); // 获取当前进程ID
int number = 0; // 消息序号
char buffer[NUM]; // 写缓冲区
while (true)
{
sleep(1); // 每秒写入一次数据
// 构建发送字符串,格式为:"hello, I am child-进程ID-序号"
buffer[0] = 0; // 清空缓冲区
snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
// 通过管道写端写入数据给父进程
write(wfd, buffer, strlen(buffer)); // 写入字符串长度的数据
}
}
// 读数据的函数
void Reader(int rfd)
{
char buffer[NUM]; // 读缓冲区
int cnt = 0; // 读取次数计数
while(true)
{
buffer[0] = 0; // 清空缓冲区
// 从管道读端读取数据
ssize_t n = read(rfd, buffer, sizeof(buffer));
if(n > 0) // 读取成功
{
buffer[n] = 0; // 添加字符串结束符
cout << "father get a message[" << getpid() << "]# " << buffer << endl;
}
else if(n == 0) // 管道写端关闭
{
printf("father read file done!\n");
break;
}
else break; // 读取错误
cnt++;
if(cnt>5) break; // 最多读取5次
}
}
int main()
{
int pipefd[N] = {0}; // 创建管道文件描述符数组
int n = pipe(pipefd); // 创建管道,pipefd[0]为读端,pipefd[1]为写端
if (n < 0) // 创建管道失败
return 1;
// 创建子进程
pid_t id = fork();
if (id < 0) // 创建子进程失败
return 2;
if (id == 0) // 子进程
{
close(pipefd[0]); // 关闭读端
// 执行写操作
Writer(pipefd[1]);
close(pipefd[1]); // 关闭写端
exit(0); // 子进程退出
}
// 父进程
close(pipefd[1]); // 关闭写端
// 执行读操作
Reader(pipefd[0]); // 读取5次数据
close(pipefd[0]); // 关闭读端
cout << "father close read fd: " << pipefd[0] << endl;
sleep(5); // 等待5秒,此时子进程已经成为僵尸进程
// 等待子进程退出并获取退出状态
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid < 0) return 3; // 等待失败
// 打印子进程退出信息
// status>>8 & 0xFF 获取退出码
// status & 0x7F 获取信号值
cout << "wait child success: " << rid << " exit code: " << ((status>>8)&0xFF) << " exit signal: " << (status&0x7F) << endl;
sleep(5); // 再等待5秒
cout << "father quit" << endl; // 父进程退出
return 0;
}
运行结果:
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson27/1.pipe$ ./testPipe
father get a message[277093]# hello, I am child-277094-0
father get a message[277093]# hello, I am child-277094-1
father get a message[277093]# hello, I am child-277094-2
father get a message[277093]# hello, I am child-277094-3
father get a message[277093]# hello, I am child-277094-4
father get a message[277093]# hello, I am child-277094-5
father close read fd: 3
wait child success: 277094 exit code: 0 exit signal: 13
father quit
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson27/1.pipe$
执行时序:
子进程 父进程
close(pipefd[0]) close(pipefd[1])
↓ ↓
Writer(): Reader():
循环写入消息 循环读取消息
格式:"hello,I am child-PID-序号" 最多读5次
↓ ↓
close(pipefd[1]) close(pipefd[0])
exit(0) sleep(5)
waitpid()等待子进程
sleep(5)
退出
子进程会一直写入直到被父进程终止,而父进程只读取5次就结束读取。
3.4.8 代码三:使用管道实现一个简易版本的进程池
makefile
ProcessPool:ProcessPool.cc
g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
rm -f ProcessPool
Task.hpp
#pragma once
#include <iostream>
#include <vector>
typedef void (*task_t)(); //定义了一个函数指针类型task_t,它指向返回类型为void且不接受任何参数的函数。
void task1()
{
std::cout << "lol 刷新日志" << std::endl;
}
void task2()
{
std::cout << "lol 更新野区,刷新出来野怪" << std::endl;
}
void task3()
{
std::cout << "lol 检测软件是否更新,如果需要,就提示用户" << std::endl;
}
void task4()
{
std::cout << "lol 用户释放技能,更新用的血量和蓝量" << std::endl;
}
void LoadTask(std::vector<task_t> *tasks) // 该函数接受一个指向std::vector<task_t>的指针,并将其作为参数
{
tasks->push_back(task1); //将task1函数的地址添加到向量中。
tasks->push_back(task2);
tasks->push_back(task3);
tasks->push_back(task4);
}
ProcessPool.cc
#include "Task.hpp" // 包含任务相关的头文件,定义了任务类型和函数
#include <string> // 字符串操作
#include <vector> // 使用vector容器
#include <cstdlib> // 系统函数
#include <ctime> // 时间函数
#include <cassert> // 断言
#include <unistd.h> // Unix标准函数
#include <sys/stat.h> // 文件状态
#include <sys/wait.h> // 进程等待
const int processnum = 10; // 定义进程池中的进程数量
std::vector<task_t> tasks; // 存储所有可执行的任务
// channel类:管理父子进程间的通信通道
class channel
{
public:
// 构造函数:初始化通信管道
channel(int cmdfd, int slaverid, const std::string &processname)
:_cmdfd(cmdfd), _slaverid(slaverid), _processname(processname)
{}
public:
int _cmdfd; // 命令管道的文件描述符(写端)
pid_t _slaverid; // 对应子进程的进程ID
std::string _processname; // 进程的名称,用于显示和日志
};
// 子进程的主要执行函数
void slaver()
{
while(true)
{
int cmdcode = 0;
// 从标准输入读取命令(标准输入被重定向到了管道)
int n = read(0, &cmdcode, sizeof(int));
if(n == sizeof(int))
{
// 收到命令后打印信息并执行对应任务
std::cout <<"slaver say@ get a command: "<< getpid() << " : cmdcode: " << cmdcode << std::endl;
if(cmdcode >= 0 && cmdcode < tasks.size()) tasks[cmdcode]();
}
if(n == 0) break; // 管道被关闭,退出循环
}
}
// 初始化进程池,创建子进程和通信管道
void InitProcessPool(std::vector<channel> *channels)
{
std::vector<int> oldfds; // 存储已创建的管道文件描述符
for(int i = 0; i < processnum; i++)
{
int pipefd[2];
int n = pipe(pipefd); // 创建管道,pipefd[0]读端,pipefd[1]写端
assert(!n);
(void)n;
pid_t id = fork(); // 创建子进程
if(id == 0) // 子进程执行的代码
{
// 关闭继承自父进程的所有历史文件描述符
std::cout << "child: " << getpid() << " close history fd: ";
for(auto fd : oldfds) {
std::cout << fd << " ";
close(fd);
}
std::cout << "\n";
close(pipefd[1]); // 关闭写端
dup2(pipefd[0], 0); // 将管道读端重定向到标准输入
close(pipefd[0]); // 关闭原读端
slaver(); // 执行子进程的主要逻辑
std::cout << "process : " << getpid() << " quit" << std::endl;
exit(0);
}
// 父进程执行的代码
close(pipefd[0]); // 关闭读端
// 创建新的channel对象并保存
std::string name = "process-" + std::to_string(i);
channels->push_back(channel(pipefd[1], id, name));
oldfds.push_back(pipefd[1]); // 保存文件描述符
sleep(1); // 等待1秒,确保进程创建的有序性
}
}
// 打印所有channel的信息,用于调试
void Debug(const std::vector<channel> &channels)
{
for(const auto &c :channels)
{
std::cout << c._cmdfd << " " << c._slaverid << " " << c._processname << std::endl;
}
}
// 显示操作菜单
void Menu()
{
std::cout << "################################################" << std::endl;
std::cout << "# 1. 刷新日志 2. 刷新出来野怪 #" << std::endl;
std::cout << "# 3. 检测软件是否更新 4. 更新用的血量和蓝量 #" << std::endl;
std::cout << "# 0. 退出 #" << std::endl;
std::cout << "#################################################" << std::endl;
}
// 控制子进程执行任务的主函数
void ctrlSlaver(const std::vector<channel> &channels)
{
int which = 0; // 当前选择的进程索引
while(true)
{
int select = 0;
Menu();
std::cout << "Please Enter@ ";
std::cin >> select;
if(select <= 0 || select >= 5) break; // 退出条件
int cmdcode = select - 1; // 将选项转换为命令代码
// 向选中的子进程发送任务,并打印信息
std::cout << "father say: " << " cmdcode: " <<
cmdcode << " already sendto " << channels[which]._slaverid << " process name: "
<< channels[which]._processname << std::endl;
write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode));
which++; // 轮询选择下一个进程
which %= channels.size();
}
}
// 清理进程池,关闭所有管道和进程
void QuitProcess(const std::vector<channel> &channels)
{
for(const auto &c : channels){
close(c._cmdfd); // 关闭管道
waitpid(c._slaverid, nullptr, 0); // 等待子进程结束
}
}
int main()
{
LoadTask(&tasks); // 加载任务列表
// 初始化随机数种子
srand(time(nullptr)^getpid()^1023);
std::vector<channel> channels;
InitProcessPool(&channels); // 初始化进程池
ctrlSlaver(channels); // 运行任务分发循环
QuitProcess(channels); // 清理资源
return 0;
}
这个程序的详细解释我单独放在了一篇博客里进行讲解,有兴趣的可以看看:
使用管道实现一个简易版本的进程池
这个程序实现了一个简单的进程池系统,主要功能如下:
- 核心功能:
- 创建一个包含10个子进程的进程池
- 通过管道实现父子进程间的通信
- 父进程可以向子进程分配不同的任务
- 使用轮询方式分配任务
- 工作流程:
- 程序启动后创建10个子进程
- 每个子进程都有独立的管道用于接收命令
- 父进程通过菜单界面接收用户输入
- 根据用户选择的任务,轮询分配给子进程执行
- 子进程接收到命令后执行对应的任务
3.5 命名管道FIFO
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件
mkfifo(FIFO_FILE, MODE)
是创建一个命名管道(FIFO)的系统调用
int mkfifo(const char *pathname, mode_t mode);
参数含义:
FIFO_FILE
:命名管道的路径名(字符串)
- 例如:
"/tmp/myfifo"
MODE
:设置管道的访问权限(八进制)
- 常见值:
0666 (rw-rw-rw-)
0644 (rw-r--r--)
0777 (rwxrwxrwx)
返回值:
- 成功返回
0
- 失败返回
-1
3.5.1 为什么命名管道可以在不相关的进程之间交换数据
进程间通信的前提:先让不同进程看到同一份资源
一般进程通信,我们只想要使用它的内存级缓冲区,不想要把数据写入磁盘文件(刷盘)。但是打开普通文件的话就会把数据写进去,所以就出现了一个新的文件类型:管道文件。
管道文件是一个内存级文件,不需要把数据写进磁盘去(刷盘)。
理解:
如果两个不同的进程,打开同一个文件的时候,在内核中,操作系统会打开几个文件?
打开一个文件。
你怎么知道你们两个打开的是同一个文件?为什么要打开同一个文件?
同路径下同一个文件名 = 路径 +文件名
3.5.2 命名管道的打开规则
如果当前打开操作是为
读
而打开FIFO
时
O_NONBLOCK disable
:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable
:立刻返回成功如果当前打开操作是为
写
而打开FIFO
时
O_NONBLOCK disable
:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable
:立刻返回失败,错误码为ENXIO
3.5.3 一个简单的命名管道代码
写进程 (writer.cpp):
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string>
int main() {
const char* FIFO = "/tmp/myfifo"; // 管道文件路径
// 创建命名管道
mkfifo(FIFO, 0666);
// 打开管道的写端
int fd = open(FIFO, O_WRONLY);
if(fd == -1) {
std::cout << "打开管道失败" << std::endl;
return 1;
}
// 写入数据
std::string msg = "Hello from writer!";
write(fd, msg.c_str(), msg.size());
// 关闭管道
close(fd);
return 0;
}
读进程 (reader.cpp):
#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
int main() {
const char* FIFO = "/tmp/myfifo"; // 管道文件路径
char buf[100] = {0}; // 读取缓冲区
// 打开管道的读端
int fd = open(FIFO, O_RDONLY);
if(fd == -1) {
std::cout << "打开管道失败" << std::endl;
return 1;
}
// 读取数据
read(fd, buf, sizeof(buf));
std::cout << "收到消息: " << buf << std::endl;
// 关闭管道
close(fd);
return 0;
}
使用方法:
- 编译两个程序:
g++ writer.cpp -o writer
g++ reader.cpp -o reader
- 在两个终端中分别运行:
# 终端1
./reader
# 终端2
./writer
运行结果:
reader将显示:“收到消息: Hello from writer!”
注意事项:
- 需要先运行reader,再运行writer
- 管道文件会在/tmp目录下创建
- 使用完可以删除管道文件:
rm /tmp/myfifo
3.5.4 命名管道代码:一个简单的日志函数实现
makefile
.PHONY:all
all:server client
server:server.cc
g++ -o $@ $^ -g -std=c++11
client:client.cc
g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
rm -f server client
comm.hpp
#pragma once // 防止头文件重复包含
// 包含必要的系统头文件
#include <iostream>
#include <string>
#include <cerrno> // 错误号定义
#include <cstring> // 字符串操作
#include <cstdlib> // 标准库函数
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h> // 文件状态
#include <unistd.h> // UNIX标准函数
#include <fcntl.h> // 文件控制
// 定义命名管道文件名和权限
#define FIFO_FILE "./myfifo"
#define MODE 0664 // 用户读写,组读写,其他读
// 错误码枚举
enum
{
FIFO_CREATE_ERR = 1, // 创建管道失败
FIFO_DELETE_ERR, // 删除管道失败
FIFO_OPEN_ERR // 打开管道失败
};
// 初始化类,用于管理命名管道的创建和删除
class Init
{
public:
Init()
{
// 创建命名管道
int n = mkfifo(FIFO_FILE, MODE);
if (n == -1)
{
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
}
~Init()
{
// 删除命名管道
int m = unlink(FIFO_FILE);
if (m == -1)
{
perror("unlink");
exit(FIFO_DELETE_ERR);
}
}
};
log.hpp
#pragma once
// 包含必要的系统头文件
#include <iostream>
#include <time.h> // 时间相关
#include <stdarg.h> // 变参函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024 // 缓冲区大小
// 日志级别定义
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
// 日志输出方式
#define Screen 1 // 输出到屏幕
#define Onefile 2 // 输出到单个文件
#define Classfile 3 // 根据日志级别输出到不同文件
#define LogFile "log.txt" // 日志文件名
class Log
{
public:
Log()
{
printMethod = Screen; // 默认输出到屏幕
path = "./log/"; // 日志文件路径
}
// 设置日志输出方式
void Enable(int method)
{
printMethod = method;
}
// 将日志级别转换为字符串
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
// void logmessage(int level, const char *format, ...)
// {
// time_t t = time(nullptr);
// struct tm *ctime = localtime(&t);
// char leftbuffer[SIZE];
// snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
// ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
// ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
// // va_list s;
// // va_start(s, format);
// char rightbuffer[SIZE];
// vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
// // va_end(s);
// // 格式:默认部分+自定义部分
// char logtxt[SIZE * 2];
// snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
// // printf("%s", logtxt); // 暂时打印
// printLog(level, logtxt);
// }
// 打印日志的具体实现
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
// 输出到单个文件
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
// 根据日志级别输出到不同文件
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level);
printOneFile(filename, logtxt);
}
// 重载函数调用运算符,支持格式化输出
void operator()(int level, const char *format, ...)
{
// 获取当前时间
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
// 构造日志前缀(时间和级别信息)
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]",
levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
// 处理变参部分
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 组合完整日志信息
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);
// 输出日志
printLog(level, logtxt);
}
~Log()
{
}
private:
int printMethod; // 日志输出方式
std::string path; // 日志文件路径
};
// int sum(int n, ...)
// {
// va_list s; // char*
// va_start(s, n);
// int sum = 0;
// while(n)
// {
// sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123);
// n--;
// }
// va_end(s); //s = NULL
// return sum;
// }
server.cc
#include "comm.hpp"
#include "log.hpp"
using namespace std;
int main()
{
Init init; // 创建命名管道
Log log; // 创建日志对象
log.Enable(Onefile); // 设置日志输出到文件
// 以只读方式打开管道
int fd = open(FIFO_FILE, O_RDONLY); // 阻塞等待写入方打开
if (fd < 0)
{
log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
exit(FIFO_OPEN_ERR);
}
// 记录不同级别的日志
log(Info, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
log(Warning, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
log(Fatal, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
log(Debug, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
// 循环读取客户端发送的消息
while (true)
{
char buffer[1024] = {0};
// read函数从管道(fd)中读取数据
// sizeof(buffer)指定最多读取的字节数
// x 存储实际读取的字节数
int x = read(fd, buffer, sizeof(buffer));
if (x > 0) // 读取成功
{
buffer[x] = 0; // 在读取到的数据末尾添加字符串结束符'\0'
cout << "client say# " << buffer << endl; // 打印客户端发送的消息
}
else if (x == 0) // x == 0 表示客户端关闭了连接(发送了EOF)
{
log(Debug, "client quit, me too!, error string: %s, error code: %d", strerror(errno), errno); // 记录日志,包含错误信息和错误码
break;
}
else // 读取错误
break;
}
close(fd); // 关闭管道
return 0;
}
client.cc
#include <iostream>
#include "comm.hpp"
using namespace std;
int main()
{
// 以只写方式打开命名管道
int fd = open(FIFO_FILE, O_WRONLY);
if(fd < 0) // 打开失败时fd返回-1
{
perror("open");
exit(FIFO_OPEN_ERR);
}
cout << "client open file done" << endl;
// 循环读取用户输入并发送到服务端
string line;
while(true)
{
cout << "Please Enter@ ";
getline(cin, line); // 读取一行输入
// 将输入写入管道
write(fd, line.c_str(), line.size());
}
close(fd); // 关闭管道
return 0;
}
实现client.cc
和server.cc
的通信
运行结果:
日志:
3.5.5 匿名管道与命名管道的区别
匿名管道由
pipe
函数创建并打开。命名管道由
mkfifo
函数创建,打开用open
。
FIFO
(命名管道)与pipe
(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。