漫话linux:命名管道和日志管理

1.命名管道一般用于无血缘关系进程的管道构建,如echo和cat的实现就是匿名管道

2.mkfifo()函数用于创建一个fifo(命名管道),允许进程间通信

 
#include <sys/types.h>
#include <sys/stat.h>
 
int mkfifo(const char *pathname, mode_t mode);

        1.pathname要创建命名管道的路径名

        2.mode要创建命名管道的权限模式,通常为8进制如0666

        3.成功返回0,不成功返回-1,并使用errno来指示错误类型

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

        5.创建命名管道

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

        6.注意事项:

                1.路径名:确保要创建的命名管道路径名合法且没有重复

                2.权限模式:根据实际需求设置合适的权限模式,确保可被需要访问该管道的进程所访问

                3.错误处理:对 mkfifo() 函数的返回值进行适当的错误处理,根据具体的错误原因进行相应的处理和日志记录

        7.示例:创建命名管道并处理错误

#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;
}

        8.使用命名管道执行读写操作:在open后,可以通过 read() 或 write() 函数对其进行读写操作                

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

        

#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;
}

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

        11.通信方式:

                1.单向通信:命名管道提供单向通信方式,一个进程写入,另一个进程读

                2.持久性:命名管道以文件形式存在于文件系统中,即使创建进程终止,管道仍然存在

                3.阻塞和非阻塞:可以选择阻塞或非阻塞模式进行通信

        12.用法示例:进程 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);  // 关闭命名管道

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

                1.两个不同的进程,打开同一个文件的时候,在内核中,操作系统会维持一份,管道是文件缓冲区。不会进行刷盘,就有了内存级文件的存在,如何打开同一个文件呢?同路径下同一个文件名=路径+文件名 因为唯一性,就可以保证看到同一份资源了

        13.运用:

                1.简易通信: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

.PHONY:all:这行声明 all 是一个伪目标。即使文件系统中存在一个名为 all 的文件,make all 命令也会执行与 all 相关的规则,而不是认为目标已经是最新的

all:server client:这行定义了 all 伪目标的依赖,即 server 和 client。当运行 make all 时,Makefile 会首先尝试构建server和client目标

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());(将字符流化)

接下来是将错误日志化

        14.日志

                1.日志时间

                2.日志等级

                3.消息分类:info:常规消息,warning报错信息,error:严重了,可能需要立刻处理,fatal:致命的,debug:调试

                4.日志内容

                5.文件名称和行号

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

        15.log.hpp

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

                1.定义日志级别:定义常见的日志级别,如info,warning,error,fatal,debug

                2.实现日志函数:使用可变参数实现一个通用的日志函数,该函数能够记录不同级别的日志信息,并且包含时间戳、文件名和行号等信息

                3.日志输出管理:实现日志的输出方式,如输出到控制台或文件,支持按日志级别分类输出

                4.封装日志接口:提供一个简洁的接口,方便在代码中随时记录日志

        16.定义日志级别

                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);
}

      3.可变参数

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.vu_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.Debuglog.txt.Warninglog.txt.Fatal 等文件名,可以自动将不同级别的日志写入不同文件中,方便后序查找和调试

                7.总结:

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

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

        18.上文各种项目实现细节

1.命名管道的路径:进程所在的目录

2.命名管道项目的整体构成:

1.Makefile:

2.hpp:头文件,宏定义的内容是可以在项目中频繁更换的内容,比如命名管道的地址和输出端(现在先打印在屏幕上)

3.服务端

#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;
}

首先按照路径和权限创建命名管道,如果errno没有报错就打印信息,然后按照路径打开管道,然后设置死循环开始通信,首先设置一个用于接收信息的buffer数组,再从管道中读信息,如果x大于0(表示读取的字节数),就给buffer的x位置加一个\0,如果x等于0,说明管道中的信息已经超过了buffer的存储极限,打印信息并退出,x小于0出错直接退出,最后关闭文件描述符

4.客户端

#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;
}

首先根据路径打开管道,输入内容,再写入管道即可

程序运行步骤:

新建窗口,进入同一目录,用于观察子进程

 服务端make

./server

用户端(新建的窗口./client),输入字符串

此时观察老窗口

 程序运行成功

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值