当前位置: 首页 > news >正文

【Linux】(32)详解命名管道 | 日志管理 | 进程池2.0

目录

1. 介绍

2. 理解

3.运用

3.1 简易通信

makefile

comm.hpp

server.cc (服务端,读取显示)

client.cc (客户端,写入)

3.2 日志

log.hpp

1. 定义日志级别

2. 实现日志函数

可变参数

3. 日志输出管理

3.3 进程池 2.0

channel.hpp

tasks.hpp

main.cc

🏷️ 解释


1. 介绍

具有血缘关系的进程进行进程间通信,是匿名管道,如果毫不相关的进程进行通信呢?命名管道 mkfifo

eg. echo 和 cat 的实现,就是一种命名

🎢mkfifo() 函数

mkfifo() 函数用于创建一个FIFO(First In First Out)或者称为命名管道,它允许进程之间进行通信。下面是关于 mkfifo() 函数的详细介绍:man mkfifo

函数原型


#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);
  • pathname:要创建的命名管道的路径名
  • mode:创建命名管道时设置的权限模式,通常以 8 进制表示,比如 0666。

返回值

  • 成功,返回值为 0;若失败,返回值为 -1,并设置errno来指示错误类型。

功能

mkfifo() 函数的作用是在文件系统中创建一个特殊类型的文件,该文件在外观上类似于普通文件,但实际上是一个FIFO,用于进程之间的通信。这种通信方式是单向的,即数据写入FIFO的一端,可以从另一端读取出来,按照先进先出的顺序。

🎢创建命名管道

std::string fifoPath = "/tmp/my_named_pipe";  // 命名管道的路径名
mkfifo(fifoPath.c_str(), 0666); // 创建权限为0666的命名管道

🏷️注意事项

  • 路径名:确保要创建的命名管道路径名合法且没有重复。
  • 权限模式:根据实际需求设置合适的权限模式,确保可被需要访问该管道的进程所访问。
  • 错误处理:对 mkfifo() 函数的返回值进行适当的错误处理,根据具体的错误原因进行相应的处理和日志记录。

创建命名管道并处理错误的示例

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cerrno>
int main() {std::string fifoPath = "/tmp/my_named_pipe";  // 命名管道的路径名// 尝试创建命名管道if (mkfifo(fifoPath.c_str(), 0666) == -1) {// 检查错误类型if (errno == EEXIST) {std::cerr << "Named pipe already exists" << std::endl;} else {// 输出错误信息perror("Error creating named pipe");}} else {std::cout << "Named pipe created successfully" << std::endl;}return 0;
}

使用命名管道进行读写操作

open后,可以通过 read()write() 函数对其进行读写操作。

关闭命名管道

关闭命名管道是确保在进程使用完毕后释放相关资源的重要步骤。

#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>
int main() {int fd = open("/tmp/my_named_pipe", O_RDONLY);  // 以只读方式打开命名管道// 进行读取操作...// 关闭命名管道if (close(fd) == -1) {perror("Error closing named pipe");} else {std::cout << "Named pipe closed successfully" << std::endl;}return 0;
}

注意事项

  • 关闭顺序:如果有多个文件描述符指向同一个命名管道,需要依次关闭这些文件描述符,直到所有相关资源都得到释放。

通信方式

  • 单向通信:命名管道提供单向通信方式,一个进程写入,另一个进程读取。
  • 持久性:命名管道以文件形式存在于文件系统中,即使创建进程终止,管道仍然存在。
  • 阻塞和非阻塞:可以选择阻塞或非阻塞模式进行通信。

用法示例

进程 A 写入数据到命名管道

int fd = open("/tmp/my_named_pipe", O_WRONLY);  // 以只写方式打开命名管道
write(fd, "Hello, named pipe!", 18);  // 向管道中写入数据
close(fd);  // 关闭命名管道

进程 B 从命名管道读取数据

int fd = open("/tmp/my_named_pipe", O_RDONLY);  // 以只读方式打开命名管道
char buffer[50];
read(fd, buffer, 50);  // 从管道中读取数据
close(fd);  // 关闭命名管道

2. 理解

对创建的文件,进行只读/只写的 open

  1. 如果两个不同的进程,打开同一个文件的时候,在内核中,操作系统会维持几份呢?一份

还是这样的:

管道是文件缓冲区,不要进行刷盘!所以就有了内存级文件的存在

  1. 怎么打开同一个文件?为什么要打开同一个文件?

同路径下同一个文件名=路径+文件 因为唯一性,就可以保证看到同一份资源了

3.运用

3.1 简易通信

makefile

.PHONY:all
all:server clientserver:server.ccg++ -o $@ $^ -g -std=c++11
client:client.ccg++ -o $@ $^ -g -std=c++11.PHONY:clean
clean:rm -f server client
  • .PHONY:all这行声明 all 是一个伪目标。即使文件系统中存在一个名为 all 的文件,make all 命令也会执行与 all 相关的规则,而不是认为目标已经是最新的。
  • all:server client这行定义了 all 伪目标的依赖,即 serverclient。当运行 make all 时,Makefile 会首先尝试构建 serverclient 目标

comm.hpp

#ifndef COMM_HPP
#define COMM_HPP#include <iostream>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <cstdlib>
#include <cstdio>#define FIFO_FILE "/tmp/my_fifo"
#define FIFO_OPEN_ERR 1#endif // COMM_HPP

server.cc (服务端,读取显示)

#include <iostream>
#include "comm.hpp"using namespace std;int main()
{// 创建命名管道文件if (mkfifo(FIFO_FILE, 0666) == -1){if (errno != EEXIST){perror("mkfifo");exit(FIFO_OPEN_ERR);}}// 打开管道int fd = open(FIFO_FILE, O_RDONLY);if (fd < 0){perror("open");exit(FIFO_OPEN_ERR);}cout << "Server open file done" << endl;// 开始通信while (true){char buffer[1024] = {0};int x = read(fd, buffer, sizeof(buffer) - 1);if (x > 0){buffer[x] = 0;cout << "Client says: " << buffer << endl;}else if (x == 0){cout << "Client quit, server will also quit." << endl;break;}else{perror("read");break;}}close(fd);return 0;
}

批注:

  1. 要等待写入方打开之后,自己才会打开文件,向后执行

client.cc (客户端,写入)

#include <iostream>
#include "comm.hpp"using namespace std;int main()
{// 打开管道int fd = open(FIFO_FILE, O_WRONLY);if (fd < 0){perror("open");exit(FIFO_OPEN_ERR);}cout << "Client open file done" << endl;string line;while (true){cout << "Please Enter: ";getline(cin, line);if (!line.empty()){write(fd, line.c_str(), line.size());}}close(fd);return 0;
}
write(fd, line.c_str(), line.size());  //将其字符流化

打开两个窗口,运行测试如下,成功~

接下来我们将来学习将错误日志化

3.2 日志

  1. 日志时间
  2. 日志等级
  • Info:常规消息
  • Warning:报错信息
  • Error:必要严重了,可能需要立即处理
  • Fatal:致命的
  • Debug:调试

    3.日志内容

    4.文件的名称和行号

实现一个简单的日志函数,在自己的代码中慢慢的引入日志

log.hpp

为了实现上述日志系统,我们可以按照以下步骤进行:

  1. 定义日志级别:定义常见的日志级别,如 InfoWarningErrorFatalDebug
  2. 实现日志函数:使用可变参数实现一个通用的日志函数,该函数能够记录不同级别的日志信息,并且包含时间戳、文件名和行号等信息。
  3. 日志输出管理:实现日志的输出方式,如输出到控制台或文件,支持按日志级别分类输出。
  4. 封装日志接口:提供一个简洁的接口,方便在代码中随时记录日志。

以下是代码实现示例:

1. 定义日志级别

log.hpp 中定义日志级别的枚举类型,并实现日志级别到字符串的映射。

// log.hpp
#pragma once#include <string>
#include <ctime>
#include <iostream>
#include <fstream>
#include <cstdarg>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>enum LogLevel {Info,Warning,Error,Fatal,Debug
};inline std::string levelToString(LogLevel level) {switch (level) {case Info: return "INFO";case Warning: return "WARNING";case Error: return "ERROR";case Fatal: return "FATAL";case Debug: return "DEBUG";default: return "UNKNOWN";}
}

2. 实现日志函数

使用可变参数实现一个通用的日志函数 logMessage,能够记录日志级别、时间戳、文件名、行号,以及用户自定义的日志内容。

// log.hpp
#define SIZE 1024void logMessage(LogLevel level, const char *filename, int line, const char *format, ...) {time_t t = time(nullptr);struct tm *ctime = localtime(&t);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%02d-%02d %02d:%02d:%02d][%s:%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,filename, line);char rightbuffer[SIZE];va_list args;va_start(args, format);vsnprintf(rightbuffer, sizeof(rightbuffer), format, args);va_end(args);char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);printLog(level, logtxt);
}
可变参数
int sum(int n,...)  //之后会从右向左入栈
{va_list s;//char * va_start(s,n);//s=&n+1 
}

sum函数是一个示例,它是一个可变参数函数,可以接受任意数量的整数参数,并返回它们的和。va_list类型用于遍历这些参数

sum函数中:

  1. int sum(int n,...):声明了一个可变参数函数,它接受一个整数n和任意数量的整数参数。
  2. va_list s;:声明了一个va_list类型的变量s,用于遍历可变参数列表。
  3. va_start(s, n);:初始化va_list变量s,将可变参数列表的起始地址指向n之后的第一个参数
  4. va_end(s);:清理va_list变量s,准备释放它占用的内存。

可变参数函数是C语言中非常强大的特性,它可以使代码更加通用和灵活。

// sum 函数的实例化
int sum(int n, ...) {va_list s;va_start(s, n);int result = 0;for (int i = 0; i <= n; i++) {result += va_arg(s, int);}va_end(s);return result;
}// 示例使用 sum 函数
int main() {int sumResult = sum(3, 1, 2, 3);printf("The sum of the numbers is: %d\n", sumResult);return 0;
}

在这个例子中,定义了一个sum函数,它接受一个整数n和任意数量的整数参数。调用sum(3, 1, 2, 3)来实例化这个函数,它将计算1 + 2 + 3的和,并打印结果。

注意:sum函数的参数n是可选的,如果省略,则默认值为0,意味着它将接受任意数量的整数参数。在这个例子中,n的值为第一个数 3。

3. 日志输出管理

如何实现对多个日志分门别类的管理?

用'std::string _logname = path + logname' 都放到 log 中,实现管理

实现日志输出到控制台或文件的功能,支持按日志级别分类输出。

// log.hpp
enum PrintMethod {Screen,Onefile,Classfile
};PrintMethod printMethod = Screen;
std::string path = "./";
std::string LogFile = "log.txt";void printLog(LogLevel level, const std::string &logtxt) {switch (printMethod) {case Screen:std::cout << logtxt;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(LogLevel level, const std::string &logtxt) {std::string filename = LogFile;filename += ".";filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"printOneFile(filename, logtxt);
}

4. 封装日志接口

定义一个简洁的宏 LOG,方便在代码中使用日志功能。

// log.hpp
#define LOG(level, format, ...) logMessage(level, __FILE__, __LINE__, format, ##__VA_ARGS__)

5. 使用日志系统

在代码中可以使用 LOG 宏来记录日志信息,示例如下:

// main.cpp
#include "log.hpp"int main() {LOG(Info, "Server started successfully.");LOG(Warning, "This is a warning message.");LOG(Error, "An error occurred: %s", "file not found");LOG(Fatal, "Fatal error, shutting down...");LOG(Debug, "Debugging info: variable x = %d", 42);// 更改日志输出方式为按级别分类printMethod = Classfile;LOG(Info, "Server started successfully with classified logs.");return 0;
}

6. 管理多级别日志

通过在 printClassFile 中使用 log.txt.Debug, log.txt.Warning, log.txt.Fatal 等文件名,可以自动将不同级别的日志写入不同的文件中,方便后续查找和调试

🎢 日志管理总结

通过这种方式实现的日志系统能够灵活地处理不同级别的日志,并支持输出到控制台或文件中。通过使用 LOG,日志功能可以很方便地集成到代码中,提供有效的调试和运行时信息支持。

后续还可以不断扩展和完善,例如添加日志轮转、异步日志、网络日志等高级功能,以适应更复杂的应用场景。对于错误不用再 printf,可以直接查日志啦

3.3 进程池 2.0

命名管道,能不能也设计成我们上一篇文章讲的进程池的样子呢?

使用命名管道(FIFO)实现一个进程池可以通过以下步骤完成:

  1. 创建命名管道:使用 mkfifo 创建两个命名管道,一个用于父进程向子进程发送任务,另一个用于子进程向父进程发送结果
  2. 初始化进程池:创建子进程并为每个子进程分配命名管道。
  3. 父进程控制逻辑:父进程从标准输入获取任务,并通过命名管道将任务发送给子进程,然后读取子进程的结果。
  4. 子进程工作逻辑:子进程从命名管道读取任务,执行任务后将结果写回命名管道。

以下是使用命名管道实现进程池的代码:

channel.hpp

#pragma once#include <string>
#include <vector>
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#include <cassert>class channel {
public:channel(const std::string &cmdfifo, const std::string &outfifo, int slaverid, const std::string &processname): _cmdfifo(cmdfifo), _outfifo(outfifo), _slaverid(slaverid), _processname(processname) {}public:std::string _cmdfifo;     // 发送任务的命名管道std::string _outfifo;     // 接收结果的命名管道pid_t _slaverid;          // 子进程的PIDstd::string _processname; // 子进程的名字 -- 方便我们打印日志
};

tasks.hpp

#pragma once#include <iostream>
#include <vector>typedef void (*task_t)();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) {tasks->push_back(task1);tasks->push_back(task2);tasks->push_back(task3);tasks->push_back(task4);
}

main.cc

#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <cstdlib>
#include <ctime>
#include <sstream>
#include "channel.hpp"
#include "tasks.hpp"void slaver(const std::vector<task_t> &tasks, const std::string &cmdfifo, const std::string &outfifo) {int cmdfd = open(cmdfifo.c_str(), O_RDONLY);int outfd = open(outfifo.c_str(), O_WRONLY);while (true) {int cmdcode = 0;int n = read(cmdfd, &cmdcode, sizeof(int)); // 从命令管道读取命令码if (n == sizeof(int)) {// 使用 stringstream 重定向 std::cout 到一个字符串std::ostringstream oss;std::streambuf *oldCoutStreamBuf = std::cout.rdbuf();std::cout.rdbuf(oss.rdbuf());if (cmdcode >= 0 && cmdcode < tasks.size()) {tasks[cmdcode]();}std::cout.rdbuf(oldCoutStreamBuf); // 恢复 std::cout 的重定向std::string output = oss.str();write(outfd, output.c_str(), output.size()); // 写到输出管道中}if (n == 0) break; // 父进程关闭写端,子进程退出}close(cmdfd);close(outfd);
}void InitProcessPool(std::vector<channel> *channels, const std::vector<task_t> &tasks, int processnum) {for (int i = 0; i < processnum; i++) {std::string cmdfifo = "/tmp/cmdfifo_" + std::to_string(i);std::string outfifo = "/tmp/outfifo_" + std::to_string(i);mkfifo(cmdfifo.c_str(), 0666);mkfifo(outfifo.c_str(), 0666);pid_t id = fork();if (id == 0) { // child processslaver(tasks, cmdfifo, outfifo);exit(0);} else { // parent processstd::string name = "process-" + std::to_string(i);channels->push_back(channel(cmdfifo, outfifo, id, name));sleep(1);}}
}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, const std::vector<task_t> &tasks) {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;int cmdfd = open(channels[which]._cmdfifo.c_str(), O_WRONLY);write(cmdfd, &cmdcode, sizeof(cmdcode));close(cmdfd);int outfd = open(channels[which]._outfifo.c_str(), O_RDONLY);char buffer[256];int n = read(outfd, buffer, sizeof(buffer) - 1);if (n > 0) {buffer[n] = '\0';std::cout << buffer;}close(outfd);which++;which %= channels.size();}
}void QuitProcess(const std::vector<channel> &channels) {for (const auto &c : channels) {unlink(c._cmdfifo.c_str());unlink(c._outfifo.c_str());waitpid(c._slaverid, NULL, 0);}
}int main() {srand(time(nullptr) ^ getpid() ^ 1023);std::vector<task_t> tasks;LoadTask(&tasks);int processnum = 4;std::vector<channel> channels;InitProcessPool(&channels, tasks, processnum);ctrlSlaver(channels, tasks);QuitProcess(channels);return 0;
}

🏷️ 解释

  1. channel 类
    • channel 类保存了命令和输出的命名管道文件路径 _cmdfifo_outfifo,以及子进程的 PID 和名字。
  1. slaver 函数
    • 子进程在 slaver 函数中运行,从命令管道读取任务码,执行任务并将结果写入输出管道。
  1. InitProcessPool 函数
    • 创建子进程并为每个子进程分配命令和输出的命名管道。
    • 使用 mkfifo 创建命名管道,并使用 fork 创建子进程。
  1. ctrlSlaver 函数
    • 父进程从标准输入获取任务码,并通过命令管道发送给子进程。
    • 通过输出管道读取子进程的结果并打印出来。
  1. QuitProcess 函数
    • 删除所有命名管道,并等待所有子进程退出。

通过这些步骤,父进程就可以通过命名管道与子进程通信,并实现一个简单的进程池啦

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 基于python的百度迁徙迁入、迁出数据分析(七)
  • C# 报表功能
  • Nginx隐藏欢迎页Welcome to CentOS
  • 百日筑基第四十五天-从JAVA8走到JAVA9
  • Spring的代理模式
  • Omit<T, K> 解释
  • 【电子数据取证】支持最新版微信、企业微信、钉钉等重点应用数据提取分析!
  • 网络安全知识讲解
  • C语言典型例题30
  • Vue 3 中,组件间传值有多种方式
  • 【知识】pytorch中的pinned memory和pageable memory
  • Android Fragment:详解,结合真实开发场景Navigation
  • Java开发笔记--通用基础数据校验的设计
  • 思科CCIE最新考证流程
  • 工业三防平板助力MES系统打造工厂移动式生产管理
  • idea + plantuml 画流程图
  • mockjs让前端开发独立于后端
  • Mysql优化
  • SSH 免密登录
  • TCP拥塞控制
  • 基于Vue2全家桶的移动端AppDEMO实现
  • 记一次用 NodeJs 实现模拟登录的思路
  • 可能是历史上最全的CC0版权可以免费商用的图片网站
  • 坑!为什么View.startAnimation不起作用?
  • 学习使用ExpressJS 4.0中的新Router
  • 译有关态射的一切
  • # Kafka_深入探秘者(2):kafka 生产者
  • #AngularJS#$sce.trustAsResourceUrl
  • #图像处理
  • (4)事件处理——(2)在页面加载的时候执行任务(Performing tasks on page load)...
  • (9)目标检测_SSD的原理
  • (C语言)球球大作战
  • (DFS + 剪枝)【洛谷P1731】 [NOI1999] 生日蛋糕
  • (办公)springboot配置aop处理请求.
  • (动手学习深度学习)第13章 计算机视觉---微调
  • (二)Linux——Linux常用指令
  • (附源码)node.js知识分享网站 毕业设计 202038
  • (接上一篇)前端弄一个变量实现点击次数在前端页面实时更新
  • (四) 虚拟摄像头vivi体验
  • (一) 初入MySQL 【认识和部署】
  • (译)2019年前端性能优化清单 — 下篇
  • (转)jdk与jre的区别
  • *Algs4-1.5.25随机网格的倍率测试-(未读懂题)
  • . ./ bash dash source 这五种执行shell脚本方式 区别
  • .bat批处理(九):替换带有等号=的字符串的子串
  • .NET 使用 ILMerge 合并多个程序集,避免引入额外的依赖
  • .NET:自动将请求参数绑定到ASPX、ASHX和MVC(菜鸟必看)
  • .net专家(张羿专栏)
  • :class的用法及应用
  • @Autowired 与@Resource的区别
  • [100天算法】-实现 strStr()(day 52)
  • [2015][note]基于薄向列液晶层的可调谐THz fishnet超材料快速开关——
  • [C++]:for循环for(int num : nums)
  • [Go WebSocket] 多房间的聊天室(五)用多个小锁代替大锁,提高效率
  • [GXYCTF2019]禁止套娃1