图片取自muduo网络库源码解析(1):多线程异步日志库(上)_李兆龙的技术博客_51CTO博客也是很好的日志讲解博客,这篇讲解流程基本上和它差不多,并且写的比我条理清楚很多
AppendFile::append() 这个函数是日志写入文件的最终函数,并且AppendFile这个类里面也是包含着减少磁盘IO的秘密
先看LogFile
class LogFile : noncopyable
{
public:
LogFile(const string& basename, //日志文件名字
off_t rollSize, //每个文件可以容纳的最大字节数
bool threadSafe = true, //线程是否安全
int flushInterval = 3, //刷新间隔
int checkEveryN = 1024);
~LogFile();
void append(const char* logline, int len);
void flush();
bool rollFile();
private:
void append_unlocked(const char* logline, int len); //append内部调用此函数
static string getLogFileName(const string& basename, time_t* now);//获取时间函数
const string basename_;
const off_t rollSize_;
const int flushInterval_;
const int checkEveryN_;
int count_; //文件已经写入的日志行数
std::unique_ptr<MutexLock> mutex_; //封装了mutex
time_t startOfPeriod_; //
time_t lastRoll_; //上次回滚的时间
time_t lastFlush_; //上次刷新的时间
std::unique_ptr<FileUtil::AppendFile> file_;
const static int kRollPerSeconds_ = 60*60*24; //一天的秒数
};
接下来看看实现
LogFile::LogFile(const string& basename,
off_t rollSize,
bool threadSafe,
int flushInterval,
int checkEveryN)
: basename_(basename), //写入日志的文件名(用户传入)
rollSize_(rollSize), //一个文件中允许的最大字节数(用户传入)
flushInterval_(flushInterval), //允许刷新的最大间隙 (可调整) 默认为 3秒
checkEveryN_(checkEveryN), //允许停留在buffer中的最大日志条数 默认为1024条
count_(0), //目前写入的条数,最大位checkEverN_
mutex_(threadSafe ? new MutexLock : NULL),
startOfPeriod_(0), //记录前一天的时间 以秒记录
lastRoll_(0), //上次rollfile的时间,以秒记录
lastFlush_(0) //上次fulsh的时间,以秒记录
{
assert(basename.find('/') == string::npos); //判断文件名是否合法
rollFile(); //创建新文件
}
LogFile::~LogFile() = default;
//
void LogFile::append(const char* logline, int len)
{
if (mutex_)
{
MutexLockGuard lock(*mutex_);
append_unlocked(logline, len);
}
else
{
append_unlocked(logline, len);
}
}
void LogFile::flush()
{
//这里的if是保证线程安全的,如果上层没有上锁,那么这里上锁
if (mutex_)
{
MutexLockGuard lock(*mutex_); //RALL自动释放锁
file_->flush(); //调用FileUtil::AppendFile中的fulush,把缓冲区的内容刷新到指定流中
}
else
{
file_->flush();
}
}
void LogFile::append_unlocked(const char* logline, int len)
{
file_->append(logline, len); //把logline中的数据写入文件
if (file_->writtenBytes() > rollSize_) //已经写入的字节数是否大于一个文件所容纳的最大字节数
{
rollFile(); //新开一个文件
}
else
{
++count_; //记录当前写入buffer中的日志条数
if (count_ >= checkEveryN_) //大于buffer所容纳的最大条数
{
count_ = 0;
time_t now = ::time(NULL);
time_t thisPeriod_ = now / kRollPerSeconds_ * kRollPerSeconds_;
if (thisPeriod_ != startOfPeriod_) //天数不同重新生成文件
{
rollFile();
}
else if (now - lastFlush_ > flushInterval_) //大于最大刷新间隔数,把buffer中的数据写入文件中
{
lastFlush_ = now; //更新上次刷新时间
file_->flush(); //刷新缓冲区数据写入文件
}
}
}
}
bool LogFile::rollFile()
{
time_t now = 0;
//这里传递指针 可以由now得到标准时间 即Unix时间戳. 是自1970年1月1日00:00:00 GMT以来的秒数
string filename = getLogFileName(basename_, &now);
time_t start = now / kRollPerSeconds_ * kRollPerSeconds_;//相当于 now-(now%kRollPerSeconds_)
if (now > lastRoll_) //当前天数大于lastRoll天数(隔天日志写在不同文件中)
{
lastRoll_ = now;
lastFlush_ = now;
startOfPeriod_ = start;//记录上一次rollfile的日期(天)
file_.reset(new FileUtil::AppendFile(filename));
//换一个文件写日志,即为了保证两天的日志不写在同一个文件中 可能上一天的日志可能并未写到rollSize_大小
return true;
}
return false;
}
//生成文件名并获取
string LogFile::getLogFileName(const string& basename, time_t* now)
{
string filename;
filename.reserve(basename.size() + 64);//开辟大小,+64原因是全部的日期在格式化为字符串后为64字节
filename = basename;
char timebuf[32];
struct tm tm;
*now = time(NULL);
gmtime_r(now, &tm); // FIXME: localtime_r ?
strftime(timebuf, sizeof timebuf, ".%Y%m%d-%H%M%S.", &tm);
filename += timebuf;
filename += ProcessInfo::hostname();
char pidbuf[32];
snprintf(pidbuf, sizeof pidbuf, ".%d", ProcessInfo::pid());
filename += pidbuf;
filename += ".log";
return filename;
}
接下来就是Appendfile
class AppendFile : noncopyable
{
public:
explicit AppendFile(StringArg filename);
~AppendFile();
void append(const char* logline, size_t len); //实际写入文件
void flush();
off_t writtenBytes() const { return writtenBytes_; }
private:
size_t write(const char* logline, size_t len);
FILE* fp_; //打开文件指针
char buffer_[64*1024];//用户缓冲区 64kb
off_t writtenBytes_; //已写入文件字节数
};
FileUtil::AppendFile::AppendFile(StringArg filename)
: fp_(::fopen(filename.c_str(), "ae")), // 'e' for O_CLOEXEC 打开文件流
writtenBytes_(0)
{
assert(fp_);
::setbuffer(fp_, buffer_, sizeof buffer_); //把buffer设置为前端缓冲区64kb 也是文件流的缓冲区
// posix_fadvise POSIX_FADV_DONTNEED ?
}
FileUtil::AppendFile::~AppendFile()
{
::fclose(fp_); //关闭文件流
}
//把logline数据写入文件
void FileUtil::AppendFile::append(const char* logline, const size_t len)
{
size_t written = 0;
while (written != len)
{
size_t remain = len - written;
size_t n = write(logline + written, remain); //调用自己封装的write
if (n != remain)
{
int err = ferror(fp_);
if (err)
{
fprintf(stderr, "AppendFile::append() failed %s\n", strerror_tl(err));
break;
}
}
written += n;
}
writtenBytes_ += written; //更新已写入文件字节数
}
void FileUtil::AppendFile::flush()
{
::fflush(fp_); //刷新流 stream 的输出缓冲区,就是上述指定的缓冲区buffer_
size_t FileUtil::AppendFile::write(const char* logline, size_t len)
{
// #undef fwrite_unlocked
//上层调用已经保证了线程安全,所以这里使用线程不安全的 fwrite_unlock()写入fp_,目的是为了效率,最后通过 //fflush写入文件
return ::fwrite_unlocked(logline, 1, len, fp_);
}
通过这两个类我们可以知道
LogFile:生成一个日志文件名,并且记录下写入文件的数据信息,由此来判断是否需要重新创建日志文件
AppendFile:创建了一个用户缓冲,最后通过fflush()函数刷新到文件中,或者在LofFile类中的rollFile()函数重新创建新 文件写入,并不会析构AppendFile对象,只是重新打开新文件,这样就不会造成缓冲区日志信息丢失
Logging用于提供写日志的接口
#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()
SourceFile处理传入的路径
class SourceFile
{
public:
template<int N>
SourceFile(const char (&arr)[N]) //匹配传入的字符串数组,构造函数可隐式转换
: data_(arr), //传入的字符串首地址
size_(N-1) //传入的字符串数组的size(大小)
{
//返回参二在参一中最后出现的位置的指针
const char* slash = strrchr(data_, '/'); // builtin function
if (slash)
{
data_ = slash + 1; //把data_置为末尾指针
size_ -= static_cast<int>(data_ - arr);
}
}
}
Impl
class Impl
{
public:
//重声明日志级别枚举类
typedef Logger::LogLevel LogLevel;
Impl(LogLevel level, int old_errno, const SourceFile& file, int line);
void formatTime();
void finish();
Timestamp time_; //提供当前时间 可得到1970年到现在的毫秒数与生成标准时间字符串
LogStream stream_; //流
LogLevel level_; //当前日志级别
int line_; //日志行数 由__line__得到
SourceFile basename_; //日志所属文件名 //由__file__与sourcefile_helper类得到
};
看下Impl的构造函数
Logger::Impl::Impl(LogLevel level, int savedErrno, const SourceFile& file, int line)
: time_(Timestamp::now()), //当前时间
stream_(), //构造stream_对象
level_(level), //初始化日志等级
line_(line), //初始化行数
basename_(file) //日志文件名字
{
formatTime(); //写入一条日志信息的时间
CurrentThread::tid();
stream_ << T(CurrentThread::tidString(), CurrentThread::tidStringLength());//当前线程ID写入缓冲区
stream_ << T(LogLevelName[level], 6);//当前日志等级写入缓冲区
if (savedErrno != 0)
{
stream_ << strerror_tl(savedErrno) << " (errno=" << savedErrno << ") ";
}
}
通过这个构造函数就可以确定了一条日志的 时间 线程id 日志等级,并且使用 << 写入 stream_ 这里,从这里可以知道 LogStream 重载了 << 运算符.
formattTme(这个类中的时间处理现在我们不必过于关注,抓住主要流程)
void Logger::Impl::formatTime()
{
int64_t microSecondsSinceEpoch = time_.microSecondsSinceEpoch(); //得到由1970到现在的微秒数
//秒数与微秒数
time_t seconds = static_cast<time_t>(microSecondsSinceEpoch / Timestamp::kMicroSecondsPerSecond);
int microseconds = static_cast<int>(microSecondsSinceEpoch % Timestamp::kMicroSecondsPerSecond);
/*效率提升的地方,t_lastSecond为__thread变量,
意味着如果此次写入与上次写入在同一秒内,不必再生成一次重复的字符串*/
if (seconds != t_lastSecond)
{
t_lastSecond = seconds;
struct DateTime dt;
if (g_logTimeZone.valid())
{
dt = g_logTimeZone.toLocalTime(seconds);
}
else
{
dt = TimeZone::toUtcTime(seconds);
}
//t_time同样是__thread的
int len = snprintf(t_time, sizeof(t_time), "%4d%02d%02d %02d:%02d:%02d",
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second);
assert(len == 17); (void)len;
}
if (g_logTimeZone.valid())
{
Fmt us(".%06d ", microseconds); //Fmt是一个helper类
assert(us.length() == 8);
stream_ << T(t_time, 17) << T(us.data(), 8); //把时间写入 LogStream 中的字符串
}
else
{
Fmt us(".%06dZ ", microseconds);
assert(us.length() == 9);
stream_ << T(t_time, 17) << T(us.data(), 9);
}
}
在Impl中的构造函数完成了一条日志的 时间 线程id 日志等级我们看下Logger析构函数做了什么事情
Logger::~Logger()
{
impl_.finish(); //写入文件名与函数到LogStream中的缓冲区
const LogStream::Buffer& buf(stream().buffer());//声明一个LogStream中的 data_[] 的引用
g_output(buf.data(), buf.length()); /*把上述的引用传入这个回调函数,通过这个回调函数可以使用
同步日志输出函数 或 异步日志输出函数
同步:把 buf 直接传入到 LogFile里面的append函数中,写入文件
异步:把 buf 传入到 AsyncLogging 里面的 append 函数中,然后
通过 FiexdBuffer 的大缓冲来管理数据,最后在线程函数中
传入给 LogFile 里面的append函数写入文件*/
if (impl_.level_ == FATAL) //日志等级为致命
{
g_flush();
abort(); //终止程序,从调用的地方跳出
}
}
void Logger::Impl::finish()
{
stream_ << " - " << basename_ << ':' << line_ << '\n';
}
析构中写入了 - 文件名 : 行数 \n
所以现在就可以知道一条完整的日志 时间 线程id 日志等级 日志内容 - 文件名 : 行数 \n
后续声明了一个 LogStream 对象的引用buf(就是引用 Impl中的 stream_) ,然后把 buf 中的数据传入 回调函数 g_output(),
通过这个回调函数,我们可以直接改变日志的输出方式,同步或异步(看到这里简直叹为观止) 通过封装一层函数,达到不同输出方式
现在我们来看下 LogStream
//缓冲区大小
const int kSmallBuffer = 4000; //同步日志使用
const int kLargeBuffer = 4000*1000; //异步日志使用
首先是FixedBuffer,这个类就是维护一个内存快( INFO << str 就是往这个内存块里面写数据)
class FixedBuffer : noncopyable
{
public:
FixedBuffer()
: cur_(data_)
{
setCookie(cookieStart);
}
~FixedBuffer()
{
setCookie(cookieEnd);
}
//把buf输入到缓冲区cur指向的位置,并且调整cur的位置
void append(const char* /*restrict*/ buf, size_t len)
{
// FIXME: append partially
if (implicit_cast<size_t>(avail()) > len)
{
memcpy(cur_, buf, len);
cur_ += len;
}
}
//返回缓冲区的首地址
const char* data() const { return data_; }
//获取缓冲区的长度
int length() const { return static_cast<int>(cur_ - data_); }
// write to data_ directly
//返回当前可写缓冲区的首地址
char* current() { return cur_; }
//返回当前缓冲区的剩余空间
int avail() const { return static_cast<int>(end() - cur_); }
//调整cur指针
void add(size_t len) { cur_ += len; }
//重置缓冲区
void reset() { cur_ = data_; }
//封装memset(),data_的内存空间置为0
void bzero() { memZero(data_, sizeof data_); }
// for used by GDB
const char* debugString();
void setCookie(void (*cookie)()) { cookie_ = cookie; }
// for used by unit test
string toString() const { return string(data_, length()); }
StringPiece toStringPiece() const { return StringPiece(data_, length()); }
private:
//返回缓冲区末尾地址
const char* end() const { return data_ + sizeof data_; }
// Must be outline function for cookies.
static void cookieStart();
static void cookieEnd();
void (*cookie_)();
char data_[SIZE]; //缓冲区大小,以传入的SIZE为准
char* cur_; //当前缓冲区可写位置的指针
};
Logstream类的主要部分
class LogStream : noncopyable
{
typedef LogStream self;
public:
typedef detail::FixedBuffer<detail::kSmallBuffer> Buffer;
......一串输出运算符的重载
void append(const char* data, int len) { buffer_.append(data, len); }
const Buffer& buffer() const { return buffer_; }
void resetBuffer() { buffer_.reset(); }
private:
void staticCheck();
template<typename T>
void formatInteger(T); //用于数字转字符串,建议大家可以单独找找这个部分的博客看看,也是无敌的设计
Buffer buffer_;
static const int kMaxNumericSize = 32;
};
重载我们看两个就行,其他都是一样的
字符类型:
self& operator<<(char v)
{
buffer_.append(&v, 1);
return *this;
}
数值类型:
template<typename T>
void LogStream::formatInteger(T v)
{
if (buffer_.avail() >= kMaxNumericSize)
{
size_t len = convert(buffer_.current(), v); //int 转 char 并且加入了 buffer_ 中
buffer_.add(len); // 更新buffer_的指针位置,即使是多线程环境,只要线程没有操作共享变量
// 使用的是线程内部自己创建的变量,它就是线程安全的
}
}
LogStream& LogStream::operator<<(short v)
{
*this << static_cast<int>(v); //转为int类型再次调用函数(int类型的) (链式编程)
return *this;
}
LogStream& LogStream::operator<<(int v)
{
formatInteger(v); //调用这个模板函数,并且写入FiexdBUffer中的buffer_
return *this;
}
至此我们的同步日志就完成了,可能还会有一些困惑,我们看一个muduo中的例子来一步步讲解:
#include "muduo/base/LogFile.h"
#include "muduo/base/Logging.h"
#include
std::unique_ptr<muduo::LogFile> g_logFile;
void outputFunc(const char* msg, int len)
{
g_logFile->append(msg, len);
}
void flushFunc()
{
g_logFile->flush();
}
int main(int argc, char* argv[])
{
char name[256] = { '\0' };
strncpy(name, argv[0], sizeof name - 1);
g_logFile.reset(new muduo::LogFile(::basename(name), 200*1000));
muduo::Logger::setOutput(outputFunc); //设置回调函数,调用LogFile中的 append() 同步日志
muduo::Logger::setFlush(flushFunc); //设置回调, 调用LogFile中的flush()
// 等同于调用 AppendFile -> fflush(fp_)
//就是把缓冲区的数据从这个流刷新到文件中
muduo::string line = "1234567890 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ ";
for (int i = 0; i < 10000; ++i)
{
LOG_INFO << line << i;
//usleep(1000);
usleep(10000);
}
}
大家可以先运行下这个Demo,看下日志文件是不是间隔写入数据的.
LOG_INFO << line << i;
底层调用
#define LOG_TRACE if (muduo::Logger::logLevel() <= muduo::Logger::TRACE) \
muduo::Logger(__FILE__, __LINE__).stream()
调用Logger的构造函数
Logger::Logger(SourceFile file, int line)
: impl_(INFO, 0, file, line)
{
}
会调用SourceFile的构造函数,再掉用 impl 的构造函数至此 一条日志的 时间 线程id 日志等级 就完成了
之后
.stream() << line 就是把 line 中的字符输入到 .stream() 中的 buffer_
现在 线程id 日志等级 日志内容 就完成了
最后该条语句结束调用 Impl 的析构函数把日志剩余部分加上,并且调用回调函数,写入用户缓冲区中,最后等待3秒刷新到日志文件中,或者其他条件满足.
接下来就是异步日志,这部分推荐大家看陈硕大神的书图文并茂
主体流程:
前端有两个内存块用于写入日志信息,在线程函数中等待线程 超时唤醒 或 被唤醒后进行加锁处理前端的日志块(由于操作内存块需要mutex_所以在此期间还有日志写入的会被阻塞)在临界区代码段中,直接把存储当前内存块加入到前端数组块中,然后swap与后端数组交换临界区代码结束,之后只需要日志线程操作后端数组写入用户缓冲区即可
class AsyncLogging : noncopyable
{
public:
......
LogFile output(basename_, rollSize_, false);
void threadFunc();
//使用大块SIZE
typedef muduo::detail::FixedBuffer<muduo::detail::kLargeBuffer> Buffer;
//声明一个vector来管理Buffer
typedef std::vector<std::unique_ptr<Buffer>> BufferVector;
//STL 中的一个类型 value_type ==std::unique_ptr
typedef BufferVector::value_type BufferPtr;
BufferPtr currentBuffer_ GUARDED_BY(mutex_); //当前缓冲区块
BufferPtr nextBuffer_ GUARDED_BY(mutex_); //下一个缓冲区块
BufferVector buffers_ GUARDED_BY(mutex_); //前端保存缓冲区块的数组
}
异步类中的append函数()
void AsyncLogging::append(const char* logline, int len)
{
muduo::MutexLockGuard lock(mutex_);
if (currentBuffer_->avail() > len)
{
currentBuffer_->append(logline, len); //currentBuffer还未满,直接写入当前块
}
else
{
buffers_.push_back(std::move(currentBuffer_)); //currentBuffer满了就加入到vector中等待写入用户缓冲区
if (nextBuffer_)
{
currentBuffer_ = std::move(nextBuffer_);
}
else
{
currentBuffer_.reset(new Buffer); // Rarely happens
}
currentBuffer_->append(logline, len);
cond_.notify();
}
}
线程函数:
void AsyncLogging::threadFunc()
{
assert(running_ == true);
latch_.countDown();
LogFile output(basename_, rollSize_, false);
BufferPtr newBuffer1(new Buffer); //后备块1
BufferPtr newBuffer2(new Buffer); //后备块2
newBuffer1->bzero();
newBuffer2->bzero();
BufferVector buffersToWrite;
buffersToWrite.reserve(16); //写入文件使用的 vector 后端保存缓冲区块的数组
while (running_)
{
assert(newBuffer1 && newBuffer1->length() == 0);
assert(newBuffer2 && newBuffer2->length() == 0);
assert(buffersToWrite.empty());
{ //临界区
muduo::MutexLockGuard lock(mutex_);
if (buffers_.empty()) // unusual usage!
{
cond_.waitForSeconds(flushInterval_); //等待三秒或条件唤醒
}
buffers_.push_back(std::move(currentBuffer_)); //把currentBuffer_
currentBuffer_ = std::move(newBuffer1); //把备用块1交给当前块
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)
{
// FIXME: use unbuffered stdio FILE ? or use ::writev ?
output.append(buffer->data(), buffer->length());
}
//留下两块用于填充后备块
if (buffersToWrite.size() > 2)
{
// drop non-bzero-ed buffers, avoid trashing
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();
}