代码史诗,恢弘巨制。今天为大家带来的是muduo网络库之日志篇。
请大家跟随我的脚步,一起学习这段异步,双缓冲,多线程的高速日志模块。
听着就很吊。
muduo的日志模块分为前后端两个部分,主要由两个类负责:
前端:Logger 类
后端:AsyncLogging 类
我们先看前端部分:
Logger.h文件:
(Logger类中又定义了一些其他类,但是不要怕,我会给出注释)
class Logger
{
public:
enum LogLevel //日志级别
{
TRACE,
DEBUG,
INFO,
WARN,
ERROR,
FATAL,
NUM_LOG_LEVELS,
};
class SourceFile //用于从文件路径中提取出文件名
{
public:
template<int N>
SourceFile(const char (&arr)[N])
: data_(arr),
size_(N-1)
{
const char* slash = strrchr(data_, '/'); // 查找最后一个'/'的位置
if (slash)
{
data_ = slash + 1;
size_ -= static_cast<int>(data_ - arr);
}
}
explicit SourceFile(const char* filename)
: data_(filename)
{
const char* slash = strrchr(filename, '/');
if (slash)
{
data_ = slash + 1;
}
size_ = static_cast<int>(strlen(data_));
}
const char* data_;
int size_;
};
//不同情况的构造函数
Logger(SourceFile file, int line);
Logger(SourceFile file, int line, LogLevel level);
Logger(SourceFile file, int line, LogLevel level, const char* func);
Logger(SourceFile file, int line, bool toAbort);
~Logger();
static LogLevel logLevel();
static void setLogLevel(LogLevel level);
typedef void (*OutputFunc)(const char* msg, int len); //指向输出给后端用的函数
typedef void (*FlushFunc)();
static void setOutput(OutputFunc);
static void setFlush(FlushFunc);
static void setTimeZone(const TimeZone& tz);
private:
class Impl //用于封装一条完整的日志,
{
public:
typedef Logger::LogLevel LogLevel;
Impl(LogLevel level, int old_errno, const SourceFile& file, int line);
void formatTime();
void finish();
Timestamp time_; //时间相关
LogStream stream_; //封装了对左移运算符<<的重载和储存单条日志的缓冲区
LogLevel level_;
int line_;
SourceFile basename_;
};
Impl impl_;
LogStream& stream() { return impl_.stream_; } //用法:Logger::stream()<<"日志信息"
};
然后搞了一些宏定义,给我们打日志用:
#define LOG_TRACE if (muduo::Logger::logLevel() <= muduo::Logger::TRACE) \
muduo::Logger(__FILE__, __LINE__, muduo::Logger::TRACE, __func__).stream()
#define LOG_DEBUG if (muduo::Logger::logLevel() <= muduo::Logger::DEBUG) \
muduo::Logger(__FILE__, __LINE__, muduo::Logger::DEBUG, __func__).stream()
#define LOG_INFO if (muduo::Logger::logLevel() <= muduo::Logger::INFO) \
muduo::Logger(__FILE__, __LINE__).stream()
#define LOG_WARN muduo::Logger(__FILE__, __LINE__, muduo::Logger::WARN).stream()
#define LOG_ERROR muduo::Logger(__FILE__, __LINE__, muduo::Logger::ERROR).stream()
#define LOG_FATAL muduo::Logger(__FILE__, __LINE__, muduo::Logger::FATAL).stream()
#define LOG_SYSERR muduo::Logger(__FILE__, __LINE__, false).stream()
#define LOG_SYSFATAL muduo::Logger(__FILE__, __LINE__, true).stream()
经过如上代码,我们可以看出,我们在写日志的时候,是创建了一个临时的 logger 类在初始化的时候填入了文件名,行数和日志级别等信息,然后把我们要输出的日志内容经过左移运算符传入到 stream 的缓冲区中。
然后我们看一下 Logger 类的构造函数:
Logger::Logger(SourceFile file, int line)
: impl_(INFO, 0, file, line)
{
}
Logger::Logger(SourceFile file, int line, LogLevel level, const char* func)
: impl_(level, 0, file, line)
{
impl_.stream_ << func << ' ';
}
Logger::Logger(SourceFile file, int line, LogLevel level)
: impl_(level, 0, file, line)
{
}
Logger::Logger(SourceFile file, int line, bool toAbort)
: impl_(toAbort?FATAL:ERROR, errno, file, line)
{
}
其实就是用这些参数初始化了 impl_ 。
所以我们看一下 Impl 类的构造函数:
Logger::Impl::Impl(LogLevel level, int savedErrno, const SourceFile& file, int line)
: time_(Timestamp::now()),
stream_(),
level_(level),
line_(line),
basename_(file)
{
formatTime();
CurrentThread::tid();
stream_ << T(CurrentThread::tidString(), CurrentThread::tidStringLength());
stream_ << T(LogLevelName[level], 6);
if (savedErrno != 0)
{
stream_ << strerror_tl(savedErrno) << " (errno=" << savedErrno << ") ";
}
}
可以看出 impl_ 用传进来的参数也通过左移运算符传入到了 stream_ 的缓冲区中。其中牵扯到一些其他的实现细节这里不再赘述,否则就要扯远了,本文旨在让大家理解日志模块的大致运行思路。
以上就是单条日志的大致组装过程,现在一条日志已经存在于 stream_ 的缓冲区中了。
现在这个临时的 Logger 类对象最后的使命就是把这条日志传给后端。这个功能是在对象的析构函数中实现的。(就显得很浪漫)
Logger 类的析构函数:
Logger::~Logger()
{
impl_.finish();
const LogStream::Buffer& buf(stream().buffer());
g_output(buf.data(), buf.length());
if (impl_.level_ == FATAL)
{
g_flush();
abort();
}
}
void Logger::setOutput(OutputFunc out)
{
g_output = out;
}
内容并不复杂,故事的最后我们成功通过 g_output 函数把日志传给了后端,一个没有名字的 Logger 类对象默默从程序中消失了。
一刻也来不及为前端日志对象哀悼了,接下来登场的是
后端部分:
AsyncLogging.h 文件:
class AsyncLogging : noncopyable
{
public:
AsyncLogging(const string& basename,
off_t rollSize,
int flushInterval = 3);
~AsyncLogging()
{
if (running_)
{
stop();
}
}
void append(const char* logline, int len);//上文中前端就是通过这个函数把数据传给后端的
void start()
{
running_ = true;
thread_.start();
latch_.wait();
}
void stop() NO_THREAD_SAFETY_ANALYSIS
{
running_ = false;
cond_.notify();
thread_.join();
}
private:
void threadFunc();//主要事件处理线程
typedef muduo::detail::FixedBuffer<muduo::detail::kLargeBuffer> Buffer;
typedef std::vector<std::unique_ptr<Buffer>> BufferVector;
typedef BufferVector::value_type BufferPtr;
const int flushInterval_;
std::atomic<bool> running_;
const string basename_;
const off_t rollSize_;
muduo::Thread thread_;
muduo::CountDownLatch latch_;
muduo::MutexLock mutex_;
muduo::Condition cond_ GUARDED_BY(mutex_);
BufferPtr currentBuffer_ GUARDED_BY(mutex_);//当前缓冲区,接收前端传过来的日志
BufferPtr nextBuffer_ GUARDED_BY(mutex_);//预备缓冲区,currentBuffer_写满的时候第一时间顶上去
BufferVector buffers_ GUARDED_BY(mutex_);//已满缓冲区数组,写满的Buffer都会进到这里
};
}
主要功能主要在 append 函数和 threadFunc 函数中。
append 函数:
void AsyncLogging::append(const char* logline, int len)
{
muduo::MutexLockGuard lock(mutex_); //多线程写到这里,加锁保证安全
if (currentBuffer_->avail() > len) //如果当前缓冲区够写
{
currentBuffer_->append(logline, len); //就直接写
}
else //当前缓冲区写满了
{
buffers_.push_back(std::move(currentBuffer_)); //把当前缓冲区入栈
if (nextBuffer_) //存在预备缓冲区
{
currentBuffer_ = std::move(nextBuffer_); //替补出场,预备缓冲区成为新的当前缓冲区
}
else
{
currentBuffer_.reset(new Buffer);
}
currentBuffer_->append(logline, len); //这时候的当前缓冲区一定够空间存入新日志了
cond_.notify(); //有新日志了生产者发出信号提醒消费者。
}
}
下面是把日志写入文件的核心代码
!24 threadFunc 函数:!
void AsyncLogging::threadFunc()
{
assert(running_ == true);
latch_.countDown();
LogFile output(basename_, rollSize_, false); //初始化文件类,这个文件类会自己更新文件,不会一直写入一个文件中
BufferPtr newBuffer1(new Buffer); //替补缓冲区一号
BufferPtr newBuffer2(new Buffer); //替补缓冲区二号
newBuffer1->bzero();
newBuffer2->bzero();
BufferVector buffersToWrite; //用来给 buffers_ 替补的,
buffersToWrite.reserve(16);
while (running_)
{//元气满满的一轮循环开始
assert(newBuffer1 && newBuffer1->length() == 0);
assert(newBuffer2 && newBuffer2->length() == 0);
assert(buffersToWrite.empty());
{
muduo::MutexLockGuard lock(mutex_); //拿一下锁
if (buffers_.empty()) //没有数据
{
cond_.waitForSeconds(flushInterval_); //等待生产者发出有数据的信号,一直没有就等3秒超时
}
buffers_.push_back(std::move(currentBuffer_)); //放里面等会全都写了
currentBuffer_ = std::move(newBuffer1); //替补登场,每次循环后面会补充替补
buffersToWrite.swap(buffers_); //交换两个数组,把空的数组给前端,把有内容的数组拿到后端来写。
if (!nextBuffer_)
{
nextBuffer_ = std::move(newBuffer2); //替补登场,不能让前端的缓冲区不够用了
}
}
assert(!buffersToWrite.empty());
if (buffersToWrite.size() > 25) //日志太多了,执行罢工操作
{
char buf[256];
snprintf(buf, sizeof buf, "Dropped log messages at %s, %zd larger buffers\n",
Timestamp::now().toFormattedString().c_str(),
buffersToWrite.size()-2);
fputs(buf, stderr);
output.append(buf, static_cast<int>(strlen(buf))); //只写这一条,说一下日志太多了
buffersToWrite.erase(buffersToWrite.begin()+2, buffersToWrite.end()); //把日志全丢了,主打一个潇洒
} //打工人上班前的幻想,大多数时候是工作很多,但是拼尽全力又能做完
//日志没有压垮打工人,那就本本分分的干活
for (const auto& buffer : buffersToWrite) //遍历数组,开始干活
{
output.append(buffer->data(), buffer->length()); //向文件类里面写入日志
}
if (buffersToWrite.size() > 2)
{
buffersToWrite.resize(2); //删掉多余缓冲区,留两个是为了下面补充替补的时候就不需要重复申请了
}
if (!newBuffer1) //补充替补缓冲区一号,如果需要的话
{
assert(!buffersToWrite.empty());
newBuffer1 = std::move(buffersToWrite.back());
buffersToWrite.pop_back();
newBuffer1->reset();
}
if (!newBuffer2) //补充替补缓冲区二号,如果需要的话
{
assert(!buffersToWrite.empty());
newBuffer2 = std::move(buffersToWrite.back());
buffersToWrite.pop_back();
newBuffer2->reset();
}
buffersToWrite.clear();
output.flush(); //累了,文件类的细节不想讲了,反正就是写好了
//别睡了打工人,讲到这里,新的循环要开始了
}
output.flush();
}
总结:
至此我们了解完了muduo网络库的日志模块的主要内容,从多线程的前端生产者生产日志,到后端单线程一个消费者利用双缓冲处理日志。相信各位也大体上了解了日志模块是怎么工作的了,如果看了一遍没看懂,那可能是我废话太多影响各位理解了,再看一遍就好了。
如果看了两遍,还是没看懂。
该休息了,我的朋友。