前言
在之前的日志系统文章中,我们实现了日志消息拼装(LogMessage)和日志器入口(Logger)。
但日志最终要落地到文件 ,这就需要一个高性能、高可靠、低开销 的文件写入工具。今天我们要解析的 AppendFile 就是承担这个使命的核心组件。
它解决了以下关键问题:
避免频繁 IO:使用大缓冲区减少系统调用
线程安全写入 :使用
fwrite_unlocked提升性能容错处理:写入失败自动报错
原子追加:保证日志不丢失、不乱序
低开销:无冗余拷贝,直接写入
一、AppendFile 整体定位
AppendFile 是日志系统的 "落地器":
接收 Logger 输出的字符串
缓冲、批量写入文件
提供 ** 原子追加(append only)** 能力
是高性能日志库的基
二、头文件结构:LogCommon.hpp
全局公共定义,所有模块共用。
cpp
#ifndef LOG_COMMON_HPP
#define LOG_COMMON_HPP
namespace tulun
{
// 缓冲区大小定义
static const int SMALL_BUFF_LEN = 128;
static const int MEDIAN_BUFF_LEN = 512;
static const int LARGE_BUFF_LEN = 1024;
// 强类型枚举:日志级别
enum class LOG_LEVEL // C++11
{
TRACE = 0,
DEBUG,
INFO,
WARN,
ERROR,
FATAL,
NUM_LOG_LEVELS,
};
// 日志级别字符串映射
static const char * LLTOSTR[]=
{
"TRACE", // 0
"DEBUG",
"INFO",
"WARN",
"ERROR",
"FATAL",
"NUM_LOG_LEVELS",
};
} // namespace tulun
#endif
三、AppendFile 类定义(AppendFile.hpp)
对外提供构造、析构、追加写入、刷新、统计接口。
cpp
// C API
#include <stdio.h>
// C++ STL
#include <string>
#include <memory>
using namespace std;
#ifndef APPEND_FILE_HPP
#define APPEND_FILE_HPP
namespace tulun
{
class AppendFile
{
private:
// 128KB 内部缓冲区(非常关键)
static const size_t FILE_BUFF_SIZE = 1024 * 128;
std::unique_ptr<char[]> buffer_; // 缓冲区智能指针
FILE *fp_; // 文件指针
size_t writeBytes_; // 累计写入字节数
// 底层无锁写入
size_t write(const char *msg, const size_t len);
public:
// 构造:打开文件 + 初始化缓冲区
AppendFile(const std::string &logfilename);
// 析构:关闭文件 + 释放资源
~AppendFile();
// 禁止拷贝(单例写入模式)
AppendFile(const AppendFile &) = delete;
AppendFile &operator=(const AppendFile &) = delete;
// 对外写入接口
void append(const std::string &msg);
void append(const char *msg, const size_t len);
// 刷新缓冲区到磁盘
void flush();
// 获取总写入字节数
size_t getWriteBytes() const;
};
} // namespace tulun
#endif
四、AppendFile 方法实现深度解析(核心)
1. 私有方法:write(无锁底层写入)
cpp
size_t AppendFile::write(const char *msg, const size_t len)
{
// 线程安全高性能写入
return fwrite_unlocked(msg, sizeof(char), len, fp_);
}
核心亮点:
-
使用
fwrite_unlocked而非fwrite -
无锁,减少线程同步开销
-
日志系统内部已保证单线程写入,无并发竞争
2. 构造函数:初始化 + 打开文件
cpp
AppendFile::AppendFile(const std::string &logfilename)
: buffer_{nullptr}, fp_{nullptr}, writeBytes_{0}
{
// 分配 128KB 缓冲区
buffer_.reset(new(std::nothrow) char[FILE_BUFF_SIZE]);//释放当前管理的对象,管理新的
if(!buffer_)
{
fprintf(stderr,"new error \n");
exit(EXIT_FAILURE);
}
// 以 "追加模式" 打开日志文件
fp_ = fopen(logfilename.c_str(),"a"); //"wb" "ab" "rb"
if(nullptr == fp_)
{
fprintf(stderr,"fopen error \n");
exit(EXIT_FAILURE);
}
// 将文件缓冲区绑定到我们自定义的 buffer
setbuffer(fp_,buffer_.get(),FILE_BUFF_SIZE);//设置缓冲区大小 / 模式
}
关键点解析:
-
a模式打开:原子追加,日志永远不会被覆盖 -
128KB 大缓冲区:减少系统调用,大幅提升性能
-
setbuffer:将文件缓冲区替换为我们自己管理的内存 -
异常安全:内存分配 / 文件打开失败直接退出并提示
3. 析构函数:安全关闭
cpp
AppendFile::~AppendFile()
{
fclose(fp_);
fp_ = nullptr;
buffer_.reset();// 释放缓冲区
}
作用:
-
确保程序退出时所有日志刷入磁盘
-
自动释放内存,无内存泄漏
4. 对外写入接口:append(字符串版本)
cpp
void AppendFile::append(const std::string &msg)
{
append(msg.c_str(), msg.size());
}
-
直接复用底层
append方法 -
代码简洁、无冗余
5. 核心写入方法:append(裸指针 + 长度)
这是 AppendFile 最核心、最复杂、最关键的方法!
cpp
void AppendFile::append(const char *msg, const size_t len)
{
size_t n = write(msg,len);// 0 1 2 3 4 5 6 7 8 9
size_t remain = len - n;
// 循环写入,确保所有字节都写入
while(remain > 0)
{
size_t x = write(msg + n,remain);
if(x == 0)
{
int err = ferror(fp_);
if(err)
{
fprintf(stderr,"appendFile::append failed %s \n",strerror(err));
break;
}
}
n +=x;
remain = len - n;
}
writeBytes_ += n;
}
深度解析:
-
循环写入保证完整性 一次
fwrite不一定能写完所有数据,必须循环写入直到全部完成。 -
错误处理写入失败立即获取错误码并打印,方便排查磁盘满、权限不足等问题。
-
统计写入量
writeBytes_用于日志滚动、大小监控。
6. flush:手动刷新缓冲区
cpp
void AppendFile::flush()
{
fflush(fp_);
}
-
强制将缓冲区数据写入磁盘
-
用于程序崩溃前、日志切割前保证日志不丢失
7. getWriteBytes:获取写入总量
cpp
size_t AppendFile::getWriteBytes() const
{
return writeBytes_;
}
用于:
-
日志文件大小判断
-
滚动切分(超过大小自动新建文件)
五、使用示例(整合进 Logger)
cpp
#include <stdio.h>
#include <iostream>
using namespace std;
#include "LogCommon.hpp"
#include "Logger.hpp"
#include "AppendFile.hpp"
void func()
{
LOG_TRACE << " 1 ";
LOG_DEBUG << " 2 ";
LOG_INFO << " 3 ";
LOG_WARN << " 4 ";
LOG_ERROR << " 5";
LOG_FATAL << " 6 ";
// 输出日志
LOG_ERROR << " hello";
}
// 全局唯一文件写入器
tulun::AppendFile appfile("syrlog.log");
// 输出回调:写入文件
void outputFile(const std::string &msg)
{
appfile.append(msg);
}
// 刷新回调
void FlushFile()
{
appfile.flush();
}
int main()
{
// 设置日志级别
tulun::Logger::setLogLevel(tulun::LOG_LEVEL::ERROR);
// 设置输出到文件
tulun::Logger::setOutput(outputFile);
tulun::Logger::setFlush(FlushFile);
func();
return 0;
}
六、关键技术亮点
1. 为什么用 128KB 缓冲区?
-
减少 ** 系统调用(syscall)** 次数
-
大幅提升写入吞吐量
-
避免频繁磁盘 IO 导致性能瓶颈
2. 为什么用 fwrite_unlocked?
-
无锁,性能比 fwrite 高 20%~50%
-
日志库内部单线程写入,无需锁
-
高性能后端必备优化
3. 为什么用 "a" 追加模式?
-
原子追加,多进程写入不乱码
-
日志永远不会被覆盖
-
重启 / 崩溃后日志继续追加,不丢失
4. 为什么要循环写入?
-
fwrite不保证一定写入全部长度 -
循环保证日志绝对完整,不丢数据
5. 为什么禁止拷贝?
-
文件流、缓冲区不具备拷贝语义
-
全局单例使用,防止重复打开文件
七、性能优势总结
大缓冲区减少 IO 次数
无锁写入,超高并发性能
循环写入保证数据完整性
错误捕获,方便排查问题
原子追加,多进程安全
智能指针管理内存,无泄漏
可直接集成到生产环境