前言
在 Linux 后端开发中,日志系统不仅要能 "写得快",更要能 "管得好"------ 比如日志文件过大导致磁盘占满、跨天日志混在一起难以排查、多线程写入乱码、崩溃后日志丢失等问题,都需要一个专业的 "文件管理器" 来解决。
本文的核心主角 LogFile ,就是承担这个职责的核心组件。它基于之前实现的 AppendFile(文件写入器),封装了日志滚动、定时刷盘、线程安全、文件名规范化等关键能力,是企业级日志系统不可或缺的一环。
一、LogFile 核心定位与整体设计
1.1 核心职责
LogFile 不直接执行文件写入(由 AppendFile 负责),而是专注于 "日志文件管理",核心职责包括:
按大小阈值自动滚动日志(如单个文件达 20MB 新建文件);
按时间周期自动滚动日志(每天 0 点生成新文件);
定时自动刷盘,避免日志驻留内存导致丢失;
生成唯一规范的日志文件名(含时间、主机名、进程号);
支持多线程安全写入,避免并发冲突;
统一管理日志写入策略,解耦业务与文件操作。
1.2 类结构设计(LogFile.hpp)
cpp
#include <string>
#include <memory>
#include <mutex>
using namespace std;
#include <time.h>
#include "Timestamp.hpp"
#include "AppendFile.hpp"
#ifndef LOGFILE_HPP
#define LOGFILE_HPP
namespace tulun
{
class LogFile
{
private:
const std::string basename_; // 日志基础文件名(如:mylog)
const size_t rollSize_; // 滚动大小(默认 2MB)
const int flushInterval_; // 自动刷新间隔(默认3秒)
const int checkEventN_; // 每写N次检查一次滚动
int count_; // 写入次数计数器
private:
time_t startOfPeriod_; // 当前时间段起点(每天0点)
time_t lastRoll_; // 上次滚动时间
time_t lastFlush_; // 上次刷新时间
static const size_t kRollPerSeconds_ = 60 * 60 * 24; // 一天秒数
static std::string getLogFileName(const std::string &basename,
const tulun::Timestamp &now);
private:
std::unique_ptr<std::mutex> mutex_; // 线程安全锁(可选)
std::unique_ptr<tulun::AppendFile> file_; // 真正写文件的对象
private:
void append_unlocked(const char *msg, const size_t len);
public:
// 构造函数:可配置滚动大小、刷盘间隔、线程安全开关
LogFile(const std::string &basename,
size_t rollsize = 1024 * 1024 * 20, // 默认 20MB
int flushInterval = 3, // 默认 3 秒
int checkEventN = 30, // 默认每 30 次检查
bool threadSafe = true); // 默认启用线程安全
~LogFile();
LogFile(const LogFile &) = delete;
LogFile &operator=(const LogFile &) = delete;
void append(const std::string &msg);
void append(const char *msg, const size_t len);
// 手动刷盘
void flush();
// 手动触发日志滚动
bool rollFile();
};
} // namespace tulun
#endif
二、核心方法逐行精讲
2.1 辅助工具:获取主机名与进程号
日志文件名需要包含主机名和进程号,确保分布式 / 多进程部署时日志唯一
cpp
namespace tulun
{
// 获取主机名
const std::string hostname()
{
char buff[SMALL_BUFF_LEN] = {};
if (!::gethostname(buff, SMALL_BUFF_LEN))//成功返回 0,失败返回 -1
{
return std::string(buff);
}
else
{
return std::string("nuknownhost");
}
}
pid_t pid()
{
return ::getpid(); // Linux 系统调用,获取当前进程 PID
}
}
2.2 核心工具:生成唯一日志文件名(getLogFileName)
日志文件名格式规范是日志可追溯的关键,该方法生成的文件名包含 "基础名 + 时间戳 + 主机名 + PID + 后缀",示例:syrou.20260408-182803.897262Z.stu-virtual-machine.6893.log
cpp
std::string LogFile::getLogFileName(const std::string &basename, const tulun::Timestamp &now)
{
std::string filename;
filename.reserve(basename.size() + SMALL_BUFF_LEN); // 预分配内存,提升性能
filename += basename; // 基础名:如 "applog"
filename += ".";
filename += now.toFormattedFile(); // 时间戳:如 "20260408-163025.12345Z"
filename += ".";
filename += hostname(); // 主机名:如 "localhost"
filename += ".";
filename += std::to_string(pid()); // 进程号:如 "12345"
filename += ".log"; // 后缀
return filename;
}
核心价值:文件名包含完整上下文,排查问题时可快速定位 "哪个进程、哪个主机、哪个时间" 的日志。
2.3 无锁核心写入:append_unlocked(最关键方法)
该方法是 LogFile 的 "大脑",封装了所有写入策略(滚动判断、刷盘判断),仅在已加锁或单线程场景下调用。
cpp
void LogFile::append_unlocked(const char *msg, const size_t len)
{
// 1. 调用 AppendFile 执行实际写入
file_->append(msg, len);
// 2. 检查:文件大小是否超过滚动阈值
if (file_->getWriteBytes() > rollSize_)
{
rollFile(); // 触发滚动,新建文件
}
else
{
count_+=1; // 写入次数+1
// 3. 每写入 checkEventN_ 次,检查一次时间相关策略
if (count_ > checkEventN_)
{
count_ = 0; // 重置计数器
time_t now = ::time(nullptr); // 获取当前时间(秒级)
time_t thisPeriod = (now / kRollPerSeconds_) * kRollPerSeconds_; // 计算当前周期起点(当天 0 点)
// 4. 检查:是否跨天(进入新的滚动周期)
if (thisPeriod != startOfPeriod_)
{
rollFile(); // 跨天触发滚动
}
// 5. 检查:是否超过自动刷盘间隔
else if (now - lastFlush_ > flushInterval_)
{
lastFlush_ = now;
file_->flush(); // 定时刷盘
}
}
}
}
逻辑拆解:
-
委托
AppendFile写入数据,聚焦策略管理; -
优先判断文件大小,达到阈值立即滚动(避免文件过大);
-
定期检查时间:跨天则滚动,超时则刷盘(平衡性能与可靠性);
-
计数器设计:避免每次写入都检查时间,降低性能损耗。
2.4 对外写入接口:append(支持线程安全)
对外提供统一写入接口,根据配置自动处理线程安全,兼容 std::string 和裸指针两种输入。
cpp
// 重载 1:接收 std::string
void LogFile::append(const std::string &msg)
{
append(msg.c_str(), msg.size()); // 复用裸指针版本
}
// 重载 2:接收裸指针 + 长度(更高效,避免 string 拷贝)
void LogFile::append(const char *msg, const size_t len)
{
if (mutex_)
{ // 如果启用线程安全(默认启用)
std::unique_lock<std::mutex> locker(*mutex_); // 自动加锁/解锁
append_unlocked(msg, len);
}
else
{
append_unlocked(msg, len); // 单线程场景,无锁提升性能
}
}
线程安全设计:
-
使用
std::unique_lock自动管理锁的生命周期(避免死锁); -
支持通过构造函数关闭线程安全(单线程场景性能优化);
-
锁粒度控制在 "单次写入",平衡安全性与并发效率。
2.5 日志滚动:rollFile(新建日志文件)
日志滚动的核心方法,负责关闭旧文件、生成新文件名、创建新 AppendFile 对象,重置时间记录。
cpp
bool LogFile::rollFile()
{
tulun::Timestamp now;
now = tulun::Timestamp::Now();
std::string filename = getLogFileName(basename_, now); // 生成新文件名
// 计算当前周期起点(当天 0 点)
time_t start = (now.getSecond() / kRollPerSeconds_) * kRollPerSeconds_;
// 2026 4 8 16 25 40 // start 2024 4 8 0 0 0
// 仅当当前时间晚于上次滚动时间时执行(避免重复滚动)
if (now.getSecond() > lastRoll_)
{
lastRoll_ = now.getSecond(); // 更新上次滚动时间
lastFlush_ = now.getSecond(); // 新文件同步刷新时间
startOfPeriod_ = start; // 更新当前周期起点
file_.reset(new tulun::AppendFile(filename)); // 创建新文件写入器
return true;
}
return false;
}
关键细节:
-
加了时间判断(
now.getSecond() > lastRoll_),避免短时间内重复调用导致多次创建文件; -
用
std::unique_ptr自动释放旧AppendFile对象(关闭旧文件、释放资源); -
新文件创建后同步更新刷盘时间,确保新日志及时落盘。
2.6 手动刷盘:flush
提供手动刷盘接口,用于程序退出、日志切割等场景,强制将缓冲区数据写入磁盘
cpp
void LogFile::flush()
{
file_->flush();// 直接委托 AppendFile 的 flush 方法
}
2.7 构造函数:初始化配置与资源
构造函数负责初始化所有配置参数、创建锁对象、触发第一次日志滚动(生成初始日志文件)。
cpp
LogFile::LogFile(const std::string &basename,
size_t rollsize,
int flushInterval,
int checkEventN,
bool threadSafe)
: basename_(basename),
rollSize_(rollsize),
flushInterval_(flushInterval),
checkEventN_(checkEventN),
mutex_{threadSafe ? new std::mutex{} : nullptr},// 按需创建锁
count_(0),
startOfPeriod_(0),
lastRoll_(0),
lastFlush_(0),
file_{nullptr}
{
rollFile();// 初始化时触发一次滚动,创建第一个日志文件
}
// 析构函数:无需额外操作,unique_ptr 自动释放资源
LogFile::~LogFile()
{
}
三、实战使用示例
cpp
#include<stdio.h>
#include<iostream>
using namespace std;
#include"LogCommon.hpp"
#include"Logger.hpp"
#include"AppendFile.hpp"
#include"LogFile.hpp"
void func()
{
const int n = 100000;
for(int i = 0;i<n;++i)
{
LOG_TRACE<<"syrou "<<i;
LOG_INFO<<" info syr "<<i;
}
}
tulun::LogFile logfile("syrou");
// 日志输出回调:将日志写入 LogFile
void outputFile(const std::string &msg)
{
logfile.append(msg);
}
// 日志刷新回调
void FlushFile()
{
logfile.flush();
}
int main()
{
tulun::Logger::setLogLevel(tulun::LOG_LEVEL::TRACE);
tulun::Logger::setOutput(outputFile);
tulun::Logger::setFlush(FlushFile);
func();
return 0;
}
输出日志文件名示例
cpp
syrou.20260408-182803.897262Z.stu-virtual-machine.6893.log
四、核心设计亮点与面试考点
4.1 为什么要分 LogFile 和 AppendFile?
-
职责分离:
AppendFile专注 "高效写入"(缓冲区、无锁 IO),LogFile专注 "策略管理"(滚动、刷盘); -
复用性:
AppendFile可独立用于其他文件写入场景,LogFile可灵活替换底层写入器; -
维护性:单一职责让代码更易理解、扩展(如后续支持远程日志,仅需替换
AppendFile)。
4.2 日志滚动的双重判断(大小 + 时间)有什么价值?
-
按大小滚动:避免单个日志文件过大,导致打开、传输、分析困难;
-
按时间滚动:按天拆分日志,排查问题时可快速定位到具体日期的日志;
-
双重保障:既防止文件无限增大,又保证日志按时间维度规整。
4.3 线程安全设计的考量?
-
用
std::mutex+std::unique_lock保证线程安全,避免死锁; -
支持关闭线程安全,适配单线程场景(如嵌入式设备),提升性能;
-
锁粒度仅覆盖 "写入 + 策略判断",不影响其他操作,平衡安全性与并发效率。
4.4 为什么要设计 checkEventN_ 计数器?
-
避免每次写入都调用
time(nullptr)和判断逻辑(系统调用有性能损耗); -
每写入 N 次集中检查一次,平衡 "策略时效性" 与 "性能损耗";
-
可通过构造函数配置 N 值,灵活适配不同写入频率场景。