日志log的实现原理
将我以前写的一些学习笔记上传到博客上
日志系统
Log的实现了学习了muduo,Log的实现分为前端和后端,前端往后端写,后端往磁盘写。为什么要这样区分前端和后端呢?因为只要涉及到IO,无论是网络IO还是磁盘IO,肯定是慢的,慢就会影响其它操作,必须让它快才行。
这里的Log前端是前面所述的IO线程,负责产生log,后端是Log线程,设计了多个缓冲区,负责收集前端产生的log,集中往磁盘写。这样,Log写到后端是没有障碍的,把慢的动作交给后端去做好了。
后端主要是由多个缓冲区构成的,集满了或者时间到了就向文件写一次。采用了muduo介绍了“双缓冲区”的思想,实际采用4个多的缓冲区(为什么说多呢?为什么4个可能不够用啊,要有备无患)。4个缓冲区分两组,每组的两个一个主要的,另一个防止第一个写满了没地方写,写满或者时间到了就和另外两个交换指针,然后把满的往文件里写。
与Log相关的类包括FileUtil、LogFile、AsyncLogging、LogStream、Logging。 其中前4个类每一个类都含有一个append函数,Log的设计也是主要围绕这个append函数展开的。
- FileUtil是最底层的文件类,封装了Log文件的打开、写入并在类析构的时候关闭文件,底层使用了标准IO,该append函数直接向文件写。
- LogFile进一步封装了FileUtil,并设置了一个循环次数,每过这么多次就flush一次。
- AsyncLogging是核心,它负责启动一个log线程,专门用来将log写入LogFile,应用了“双缓冲技术”,其实有4个以上的缓冲区,但思想是一样的。AsyncLogging负责(定时到或被填满时)将缓冲区中的数据写入LogFile中。
- LogStream主要用来格式化输出,重载了<<运算符,同时也有自己的一块缓冲区,这里缓冲区的存在是为了缓存一行,把多个<<的结果连成一块。
- Logging是对外接口,Logging类内涵一个LogStream对象,主要是为了每次打log的时候在log之前和之后加上固定的格式化的信息,比如打log的行、文件名等信息。
AppendFile.h
在该头文件中定义了打开文件,关闭文件,写数据的相关操作,是对文件描述符的具体封装。
这个类作为日志系统的底层实现,不对外提供接口来直接进行操作。
这个类的append方法的内部调用的是C语言标准库中的标准IO函数fwrite_unlocked方法。
class AppendFile
{
public:
explicit AppendFile( std::string filename );
~AppendFile();
//通过append函数将日志写入文件
void append( const char *logline, const size_t len );
// flush函数将缓冲区的内容写入文件
void flush();
private:
size_t wirte( const char *logline, size_t len );
//文件描述符
FILE *fp_;
//输出缓冲区
char buffer_[ 64 * 1024 ];
private:
AppendFile( const AppendFile & ) = delete;
AppendFile( const AppendFile && ) = delete;
AppendFile &operator=( const AppendFile & ) = delete;
};
AppendFile::AppendFile( std::string filename )
: fp_( fopen( filename.c_str(), "ae" ) )
{
//该函数用来设置文件流的缓冲区,参数stream为文件流指针,buf为缓冲区的地址,size为缓冲区的大小
//函数 setbuffer() 是 C 语言标准 I/O 库中的一个函数,用于设置给定文件流的缓冲区。
//它允许程序员手动指定一个缓冲区和其大小,以代替库默认的缓冲机制。
setbuffer( fp_, buffer_, sizeof( buffer_ ) );
}
AppendFile::~AppendFile() { fclose( fp_ ); }
void AppendFile::append( const char *logline, const size_t len )
{
size_t n = this->wirte( logline, len );
size_t remain = len - n;
while ( remain > 0 )
{
//写剩余的内容
size_t x = this->wirte( logline + n, remain );
//判断写入是否成功
if ( x == 0 )
{
int err = ferror( fp_ );
if ( err )
{
fprintf( stderr, "AppendFile::append() failed %d\n", err );
}
break;
}
n += x;
remain = len - n;
}
}
void AppendFile::flush() { fflush( fp_ ); }
size_t AppendFile::wirte( const char *logline, size_t len ) { return fwrite_unlocked( logline, 1, len, fp_ ); }
LogFile.h
这个头文件的LogFile是对AppendFile类的进一步封装,并且提供了对共享资源安全访问的线程锁,这个类的append方式是对AppendFile类的append方法的封装。方法内部调用的是AppendFile类的append方法。并且会定期刷新缓冲区
该类定义的功能函数:
- append函数:其内部是调用append_unlocked函数来实现的,为了保证线程安全该函数内部使用了线程锁。
- flush函数:调用AppendFile类的flush函数,其底层实现是使用的标准函数fflush函数。为了保证线程安全这个函数使用了线程锁
- append_unlocked函数:其内部调用的是AppendFile类的append函数,并且记录append执行的次数,当次数大于需要刷新的次数时,执行刷新。
class LogFile
{
public:
LogFile( std::string baseName, int flushEveryN = 1024 );
~LogFile();
//将日志写入文件,这个方法是对AppendFile进行操作的封装
void append( const char *logline, int len );
//刷新缓冲区
void flush();
private:
void append_unlocked( const char *logline, int len );
//属性
const std::string baseName_;
//触发刷新的阈值
const int flushEveryN_;
//记录当前写入的次数
int count_;
//定义锁
std::unique_ptr< monsoon::RWMutex > mutex_;
//定义操作的appendFile类
std::unique_ptr< AppendFile > file_;
private:
LogFile( const LogFile & ) = delete;
LogFile( const LogFile && ) = delete;
LogFile &operator=( const LogFile & ) = delete;
};
LogFile::LogFile( std::string baseName, int flushEveryN )
: baseName_( baseName )
, flushEveryN_( flushEveryN )
, count_( 0 )
, mutex_( new monsoon::RWMutex )
{
file_.reset( new AppendFile( baseName ) );
}
LogFile::~LogFile() {}
//将日志写入文件,这个方法是对AppendFile进行操作的封装
void LogFile::append( const char *logline, int len )
{
//获取锁
monsoon::RWMutex::WriteLock lock( *mutex_ );
append_unlocked( logline, len );
}
//刷新缓冲区
void LogFile::flush()
{
monsoon::RWMutex::WriteLock lock( *mutex_ );
file_->flush();
}
void LogFile::append_unlocked( const char *logline, int len )
{
file_->append( logline, len );
count_++;
if ( count_ >= flushEveryN_ )
{
count_ = 0;
file_->flush();
}
}
AsyncLogging.h
AsyncLogging类,这是一个核心类,主要功能是通过往缓冲区中写入数据,并且执行一个线程用来专门将缓冲区中的数据写入到日志文件中。应用了四缓冲技术。这个线程的主要操作就是从缓冲区容器中取出缓冲区并将缓冲区中的数据写入到log文件中。
class AsyncLogging
{
public:
explicit AsyncLogging( const std::string basename, int flushInterval = 2 );
~AsyncLogging();
//该方法用来将日志写入到缓冲区,并且将写满的缓冲区写入到缓冲区容器中。
void append( const char *logline, int len );
void start()
{
running_ = true;
thread_.start();
//使用条件变量阻塞在这里,等待线程执行线程函数时唤醒
sem_.wait();
}
void stop()
{
running_ = false;
cond_.notify_all();
thread_.join();
}
private:
//线程执行的函数,线程执行的函数是从缓冲区容器中取出缓冲区,将缓冲区中的数据写入到文件中。
void threadFunc();
//定义一个线程
Thread thread_;
//定义一个互斥锁
std::mutex mutex_;
//定义一个条件变量
std::condition_variable cond_;
//定义一个信号量
Semaphore sem_;
std::string basename_;
int flushInterval_;
//运行的标志位
bool running_;
//定义两个缓冲区,当前缓冲区,下一个缓冲区
std::shared_ptr< FixedBuffer< kLargeBuffer > > currentBuffer_;
std::shared_ptr< FixedBuffer< kLargeBuffer > > nextBuffer_;
//定义一个缓冲区容器,如果当前缓冲区写满,将当前缓冲区写入到文件中后,清零放入这个容器中。
std::vector< std::shared_ptr< FixedBuffer< kLargeBuffer > > > buffers_;
};
这个类定义的功能函数有:
- start函数,用来启动线程。
- stop函数,用来中止线程。
- 类私有函数threadFunc():该函数是由另一个线程所执行,功能是将缓冲区的数据写入到日志文件中。
- append函数:该函数内部调用LogFile类的append函数来往缓冲区中写数据。
AsyncLogging::AsyncLogging( std::string logFileName_, int flushInterval )
: basename_( logFileName_ )
, flushInterval_( flushInterval )
, running_( false )
, thread_( std::bind( &AsyncLogging::threadFunc, this ), "Logging" )
, currentBuffer_( new FixedBuffer< kLargeBuffer > )
, nextBuffer_( new FixedBuffer< kLargeBuffer > )
, buffers_()
, mutex_()
{
assert( logFileName_.size() > 1 );
//初始化两个缓冲区
currentBuffer_->bzero();
nextBuffer_->bzero();
//将vector的容量变成16
buffers_.reserve( 16 );
}
//往缓冲区中写入数据
void AsyncLogging::append( const char *logline, int len )
{
ScopedLockImpl< std::mutex > lock( mutex_ );
//如果当前缓冲区可写入长度为len的数据,则将数据写入到当前缓冲区中
if ( currentBuffer_->avail() > len )
{
//将数据写入缓冲区
currentBuffer_->append( logline, len );
}
else
{
//当前缓冲区不足以写入长度为len的数据,则将当前缓冲区放入缓冲区容器中
buffers_.push_back( currentBuffer_ ); //这段代码将复制一个shared_ptr对象,引用计数加1,所以currentBuffer_的引用计数加1。
//将当前缓冲区的智能指针重置
currentBuffer_.reset();
//判断下一个缓冲区的指针是否不为空
if ( nextBuffer_ )
{
//将预备缓冲区的指针赋值给当前缓冲区
currentBuffer_ = std::move( nextBuffer_ );
}
else
{
//如果下一个缓冲区的指针为空,则重新创建一个缓冲区
currentBuffer_.reset( new FixedBuffer< kLargeBuffer > );
}
//将数据写入到当前缓冲区
currentBuffer_->append( logline, len );
//通知已经写入了数据
cond_.notify_one();
}
}
//会有一个专门的线程用来处理从缓冲区中写数据到文件中
void AsyncLogging::threadFunc()
{
//首先断言写数据到文件的线程是否启动
assert( running_ == true ); // assert里面的条件为真时,断言不做任何事,只有为假时才中断程序
//线程运行了此函数,因此唤醒start函数
sem_.notify();
LogFile output( basename_ );
//定义两个缓冲区,一个用来写入数据,一个用来交换
std::shared_ptr< FixedBuffer< kLargeBuffer > > newBuffer1( new FixedBuffer< kLargeBuffer > );
std::shared_ptr< FixedBuffer< kLargeBuffer > > newBuffer2( new FixedBuffer< kLargeBuffer > );
//定义一个缓冲区容器
std::vector< std::shared_ptr< FixedBuffer< kLargeBuffer > > > buffersToWrite;
//将缓冲区容器的容量设置为16
buffersToWrite.reserve( 16 );
while ( running_ )
{
//判断这两个缓冲区是否存在且长度为0
assert( newBuffer1 && newBuffer1->length() == 0 );
assert( newBuffer2 && newBuffer2->length() == 0 );
assert( buffersToWrite.empty() );
{
std::unique_lock< std::mutex > lock( mutex_ );
//如果缓冲区容器为空,则等待
if ( buffers_.empty() )
{
cond_.wait_for( lock, std::chrono::seconds( flushInterval_ ) );
}
//将当前缓冲区放入缓冲区容器中
buffers_.push_back( currentBuffer_ );
//释放当前缓冲区的指针,这个指针不在指向任何对象
currentBuffer_.reset();
//将上面的指针指向一个新的预备缓冲区
currentBuffer_ = std::move( newBuffer1 ); //这是所有权转移
//将缓冲区容器的内容交换给buffersToWrite
buffersToWrite.swap( buffers_ );
//判断预备缓冲区nextbuffer是否为空
if ( !nextBuffer_ )
{
//如果为空,说明没有预备缓冲区可用,则将预备缓冲区指向一个新的缓冲区
nextBuffer_ = std::move( newBuffer2 );
}
} //这段代码的目的就是将缓冲区写入容器后,交换缓冲区的内容,然后将两个缓冲区的智能指针指向新的缓冲区
//断言写缓冲区不为空
assert( !buffersToWrite.empty() );
//如果缓冲区容器的大小大于25,则删除掉后面的23个缓冲区,仅保留前两个缓冲区
if ( buffersToWrite.size() > 25 )
{
buffersToWrite.erase( buffersToWrite.begin() + 2, buffersToWrite.end() );
}
//遍历缓冲区容器,将缓冲区中的数据写入到文件中
for ( size_t i = 0; i < buffersToWrite.size(); ++i )
{
//缓冲区的本质就是一个char数组,所以可以直接将数据写入到文件中
output.append( buffersToWrite[ i ]->data(), buffersToWrite[ i ]->length() );
}
//写完之后仅保留两个缓冲区再容器里面,这么做的目的是为了减少内存的使用,这两个缓冲区会被重复利用,一个为当前缓冲区,一个为预备缓冲区
if ( buffersToWrite.size() > 2 )
{
//保留前两个缓冲区的目的是为了复用newBuffer1和newBuffer2。
buffersToWrite.resize( 2 );
}
//复用之前创建的newBuffer1和newBuffer2,这样做可以减少内存的分配
if ( !newBuffer1 )
{
//断言buffersToWrite容器不为空
assert( !buffersToWrite.empty() );
//从容器中取出一个缓冲区给newBuffer1用
newBuffer1 = buffersToWrite.back();
//将使用了的这个缓冲区从容器中删除
buffersToWrite.pop_back();
//初始化缓冲区中的数据,该reset不是智能指针std::shared_ptr的reset,而是FixedBuffer中的reset
newBuffer1->reset();
}
if ( !newBuffer2 )
{
//断言buffersToWrite容器不为空
assert( !buffersToWrite.empty() );
//从容器中取出一个缓冲区给newBuffer2用
newBuffer2 = buffersToWrite.back();
//将使用了的这个缓冲区从容器中删除
buffersToWrite.pop_back();
//初始化缓冲区中的数据
newBuffer2->reset();
}
//清空缓冲区容器
buffersToWrite.clear();
output.flush();
}
output.flush();
}
append函数的执行流程图如下:
ThreadFunc函数的执行流程
LogStream.h
LogStream类的作用是主要用来格式化输出,重载了<<运算符,同时也有自己的一块缓冲区,这里缓冲区的存在是为了存储一行,把多个<<的结果连在一块。
首先程序运行的日志,首先会通过LogStream类的重载的<<运行符,写入到该类定义的一个缓冲区中,然后再将这个缓冲区中的内容写入到AsyncLogging类的缓冲区中,然后由另一个线程写入到日志文件中。该类的源码不在此展示。
Logging.h
该文件的Logger类是对外接口,Logging类内涵一个LogStream对象,主要是为了每次打log的时候在log之前和之后加上固定的格式化的信息,比如打log的行、文件名等信息。
功能函数:
- once_init:这个函数的作用就是创建AsyncLogging对象,并启动线程,并且通过 pthread_once(&once_control, once_init)函数来调用的,这保证了再多线程的环境下该函数只会执行一次。线程启动后,就会开始往文件中写数据,但是这个线程执行的函数内部封装了检查缓冲区是否为空。如果为空则等待。
- output函数:这个函数的功能是往缓冲区中写入数据。
- 析构函数:执行写数据到缓冲区的动作。
最后定义了一个宏来方便进行日志的输入。
//定义一个Logger类,这个类对日志系统的对外接口。
class Logger
{
public:
Logger( const char *fileName, int line );
~Logger();
LogStream &stream() { return impl_.stream_; }
static void setLogFileName( std::string fileName ) { logFileName_ = fileName; }
static std::string getLogFileName() { return logFileName_; }
private:
class Impl
{
public:
Impl( const char *fileName, int line );
void formatTime();
LogStream stream_;
int line_;
std::string basename_;
};
Impl impl_;
static std::string logFileName_;
};
#define LOG Logger( __FILE__, __LINE__ ).stream() //定义log宏,用于输出日志信息
#define LOG Logger( FILE, LINE ).stream() 当使用LOG时,会先创建Logger对象,然后在调用stream返回LogStream流,这样就创建好了Logger对象,并返回了LogStream流,这样就可以使用重载<<来将日志输入到LogStream的缓冲区中了。当进行析构时,会将数据写入到输入缓冲区中。并由另一个线程写入到文件中。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)