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.Debug
, log.txt.Warning
, log.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),输入字符串
此时观察老窗口
程序运行成功