当前位置 博文首页 > wanggao的专栏:muduo学习笔记:base部分之高性能日志库-LogFile
LogFIle
类是muduo日志库后端的实际管理者,主要负责日志的滚动。内部的AppendFile
类型成员变量是最终用于操作本地文件的类,负责将日志数据写入本地文件,记录写入的日志总长度。
代码类结构如下:
AppendFile
类位于moduo::FileUtil
命名空间。由于写日志到缓冲中使用非加锁的fwrite函数,因此要求外部单线程处理或加锁,保证缓冲区日志不会发生交织混乱。
功能单一,直接上代码和注释,如下。
class AppendFile : noncopyable
{
public:
explicit AppendFile(StringArg filename)
: fp_(::fopen(filename.c_str(), "ae")), // 'e' for O_CLOEXEC
writtenBytes_(0)
{
assert(fp_);
::setbuffer(fp_, buffer_, sizeof buffer_);
// posix_fadvise POSIX_FADV_DONTNEED ?
}
~AppendFile() { ::fclose(fp_); } // 关闭文件,会强制flush缓冲区
void append(const char* logline, size_t len) // 写数据到缓冲区
{
size_t n = write(logline, len); // 缓冲区长度可能不足以写下当前日志数据
size_t remain = len - n;
while (remain > 0) // 需要循环写入当前日志剩余未写的数据
{
size_t x = write(logline + n, remain);
if (x == 0) {
int err = ferror(fp_);
if (err) {
fprintf(stderr, "AppendFile::append() failed %s\n", strerror_tl(err));
}
break;
}
n += x;
remain = len - n; // remain -= x
}
writtenBytes_ += len; // 统计写入缓冲区的总数据长度
}
void flush() { ::fflush(fp_); } // flush缓冲区数据到文件
off_t writtenBytes() const { return writtenBytes_; } // 获取已写入日志的总字节数
private:
size_t write(const char* logline, size_t len){
// #undef fwrite_unlocked
return ::fwrite_unlocked(logline, 1, len, fp_); // 非加锁的写
}
FILE* fp_;
char buffer_[64*1024]; // 文件输出缓冲区,64kB大小
off_t writtenBytes_; // 已写日志数据的总字节数
};
日志消息记录长度达到设定值、日志记录时间超过当天进行日志滚动。当短时间内日志长度较小时,不能将日志信息长时间放如缓存中,因此日志每记录1024次数就检查一次距前一次flush到文件的时间是否超过3s,若是则flush到文件中。另外,可以选择是否使用互斥锁锁保证线程安全。
定义如下:
namespace FileUtil
{
class AppendFile;
}
class LogFile : noncopyable
{
public:
LogFile(const string& basename, // 日志文件名,默认保存在当前工作目录下
off_t rollSize, // 日志文件超过设定值进行roll
bool threadSafe = true, // 默认线程安全,使用互斥锁操作将消息写入缓冲区
int flushInterval = 3, // flush刷新时间间隔
int checkEveryN = 1024); // 每1024次日志操作,检查一个是否刷新、是否roll
~LogFile();
void append(const char* logline, int len);
void flush();
bool rollFile();
private:
void append_unlocked(const char* logline, int len);
static string getLogFileName(const string& basename, time_t* now); // 获取roll时刻的文件名
const string basename_;
const off_t rollSize_;
const int flushInterval_;
const int checkEveryN_;
int count_;
std::unique_ptr<MutexLock> mutex_; // 操作AppendFiles是否加锁
time_t startOfPeriod_; // 用于标记同一天的时间戳(GMT的零点)
time_t lastRoll_; // 上一次roll的时间戳
time_t lastFlush_; // 上一次flush的时间戳
std::unique_ptr<FileUtil::AppendFile> file_;
const static int kRollPerSeconds_ = 60*60*24; // 一天的秒数 86400
};
日志的滚动的实现主要由rollFile()函数。
/*
* 滚动日志
* 相当于重新生成日志文件,再向里面写数据
*/
bool LogFile::rollFile()
{
time_t now = 0;
string filename = getLogFileName(basename_, &now); //获取生成一个文件名称
//注意,这里先除KRollPerSeconds然后乘KPollPerSeconds表示对齐值KRollPerSeconds的整数倍,
// 也就是事件调整到当天零点(/除法会引发取整)
time_t start = now / kRollPerSeconds_ * kRollPerSeconds_;
if (now > lastRoll_) //如果大于lastRoll,产生一个新的日志文件,并更新lastRoll
{
lastRoll_ = now;
lastFlush_ = now;
startOfPeriod_ = start;
file_.reset(new FileUtil::AppendFile(filename));
return true;
}
return false;
}
/*
* 构造一个日志文件名
* 日志名由基本名字+时间戳+主机名+进程id+加上“.log”后缀
*/
string LogFile::getLogFileName(const string& basename, time_t* now)
{
string filename;
//reserve()将字符串的容量设置为至少basename.size() + 64,因为后面要添加时间、主机名、进程id等内容,
// 预先设置容量大小,为了避免反复重新分配缓冲区内存而导致效率降低,
// 或者在使用某些STL操作(例如std::copy)之前保证缓冲区够大
filename.reserve(basename.size() + 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; // 机上进程id
filename += ".log";
return filename;
}
主机名和进程ID获取方式在 ProcessInfo命名空间,
string ProcessInfo::hostname()
{
// HOST_NAME_MAX 64
// _POSIX_HOST_NAME_MAX 255
char buf[256];
if (::gethostname(buf, sizeof buf) == 0){
buf[sizeof(buf)-1] = '\0';
return buf;
}
else{
return "unknownhost";
}
}
pid_t ProcessInfo::pid()
{
return ::getpid();
}
关于日志超过当天就进行roll的判断,记录当天00:00时刻的秒数,那么从当天任何时间计算当天零点的秒数time_t start = now / kRollPerSeconds_ * kRollPerSeconds_;
都是相同的。
例如:2021-07-18 00:00:00 对应的GMT秒数为 1626566400,那么
取值范围在[1626566400,1626566400+ 86400) 的秒数now, 恒有 now * 86400 / 86400 = 1626566400。
注意:time(NULL)得到的实际是本地的时间秒数, 也就是UTC +8的时间戳。
LogFile::append()函数添加日志内容,实际是调用AppendFile::append()函数。但是,这里还设计是否使用互斥锁、日志滚动的策略判断。
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 (mutex_){
MutexLockGuard lock(*mutex_);
file_->flush();
}
else{
file_->flush();
}
}
void LogFile::append_unlocked(const char* logline, int len)
{
file_->append(logline, len);
if (file_->writtenBytes() > rollSize_){
rollFile(