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

lvy¯ 2024-08-10 10:37:01 阅读 98

目录

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() 函数

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

函数原型

<code>

#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

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

还是这样的:

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

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

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

3.运用

3.1 简易通信

makefile

<code>.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

.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;

}

批注:

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

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 日志

日志时间日志等级

Info:常规消息Warning:报错信息Error:必要严重了,可能需要立即处理Fatal:致命的Debug:调试

    3.日志内容

    4.文件的名称和行号

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

log.hpp

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

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

以下是代码实现示例:

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 1024

void 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函数中:

int sum(int n,...):声明了一个可变参数函数,它接受一个整数n和任意数量的整数参数。va_list s;:声明了一个va_list类型的变量s,用于遍历可变参数列表。va_start(s, n);:初始化va_list变量s,将可变参数列表的起始地址指向n之后的第一个参数。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)实现一个进程池可以通过以下步骤完成:

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

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

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; // 子进程的PID

std::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 process

slaver(tasks, cmdfifo, outfifo);

exit(0);

} else { // parent process

std::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;

}

🏷️ 解释

channel 类

channel 类保存了命令和输出的命名管道文件路径 _cmdfifo_outfifo,以及子进程的 PID 和名字。

slaver 函数

子进程在 slaver 函数中运行,从命令管道读取任务码,执行任务并将结果写入输出管道。

InitProcessPool 函数

创建子进程并为每个子进程分配命令和输出的命名管道。使用 mkfifo 创建命名管道,并使用 fork 创建子进程。

ctrlSlaver 函数

父进程从标准输入获取任务码,并通过命令管道发送给子进程。通过输出管道读取子进程的结果并打印出来。

QuitProcess 函数

删除所有命名管道,并等待所有子进程退出。

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



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。