【Linux C++ 日志系统实战】高性能文件写入 AppendFile 核心方法解析

前言

在之前的日志系统文章中,我们实现了日志消息拼装(LogMessage)日志器入口(Logger)

但日志最终要落地到文件 ,这就需要一个高性能、高可靠、低开销 的文件写入工具。今天我们要解析的 AppendFile 就是承担这个使命的核心组件。

它解决了以下关键问题:

  1. 避免频繁 IO:使用大缓冲区减少系统调用

  2. 线程安全写入 :使用 fwrite_unlocked 提升性能

  3. 容错处理:写入失败自动报错

  4. 原子追加:保证日志不丢失、不乱序

  5. 低开销:无冗余拷贝,直接写入


一、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);//设置缓冲区大小 / 模式
    }

关键点解析:

  1. a 模式打开:原子追加,日志永远不会被覆盖

  2. 128KB 大缓冲区:减少系统调用,大幅提升性能

  3. setbuffer:将文件缓冲区替换为我们自己管理的内存

  4. 异常安全:内存分配 / 文件打开失败直接退出并提示


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

深度解析:

  1. 循环写入保证完整性 一次 fwrite 不一定能写完所有数据,必须循环写入直到全部完成。

  2. 错误处理写入失败立即获取错误码并打印,方便排查磁盘满、权限不足等问题。

  3. 统计写入量 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 次数

无锁写入,超高并发性能

循环写入保证数据完整性

错误捕获,方便排查问题

原子追加,多进程安全

智能指针管理内存,无泄漏

可直接集成到生产环境

相关推荐
C++ 老炮儿的技术栈19 小时前
GCC编译时无法向/tmp 目录写入临时汇编文件,因为设备空间不足,解决
linux·运维·开发语言·汇编·c++·git·qt
橘颂TA19 小时前
【笔试】算法的暴力美学——牛客 NC213140 :除2!
c++·算法·结构与算法
爱莉希雅&&&20 小时前
linux中MySQL数据库备份恢复的四种方法(更新中)
linux·数据库·mysql·数据库备份·mysqldumper
wsoz20 小时前
Leetcode普通数组-day5、6
c++·算法·leetcode·数组
favour_you___20 小时前
2026_4_8算法练习题
数据结构·c++·算法
coppher20 小时前
Ubuntu 22.04 amd64 离线安装 Docker 完整教程
linux·docker
xyz59921 小时前
如何在 WSL 中删除指定版本的 Ubuntu 以及安装
linux·运维·ubuntu
SccTsAxR21 小时前
算法基石:手撕离散化、递归与分治
c++·经验分享·笔记·算法
Q741_14721 小时前
每日一题 力扣 3655. 区间乘法查询后的异或 II 模拟 分治 乘法差分法 快速幂 C++ 题解
c++·算法·leetcode·模拟·快速幂·分治·差分法
亚空间仓鼠21 小时前
OpenEuler系统常用服务(五)
linux·运维·服务器·网络