前言:
对于muduo库,我觉得,光Linux多线程上提到的一些实现,还是不够的,在base/里面,还有/net里面提供了很多不错的实现,值得去学习,暑假算是看看muduo的百分之八十的源码,并对其进行了一次实现,(剩下的在最近总结的时候,也会开始看看,并实现一遍),对于muduo库,简单谈谈自己对其实现的理解。
日志缓存流LogStream
在muduo实现的基础日志类,一共用来三个文件,LogFilem, LogStream,Loggin。本着自底向上原则,我们从底层的实现慢慢讲起。
在LogStream中,定义了三个类,两个底层的类分别是FixedBuffer和Fmt。
muduo在实现写日志的时候,并没有直接写向stderr或者IO流,而是会写入自己定义的FixedBuffer(缓冲流中),下面就让我们看看缓冲流的定义和实现吧!
FixedBuffer定义在namespace detail算是用户不可见,在创建FixedBuffer的时候可以向其输出大小来控制内部的buf的大小,也定义两个常量分别对应大小buf
const int kSmallBuffer = 4000; const int kLargeBuffer = 4000 * 1024;
一共有三个成员,data就是缓存的数据,cur指向缓存数据的末尾的下一个和cookie_指针
我们需要重点关注以下函数
//追加字符串
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;
}
}
//设置缓存
void setCookie(void (*cookie)())
{ cookie_ = cookie; };
值得注意的是下面在实现转化的时候在detail中定义了
const char digits[] = "9876543210123456789";
const char* zero = digits + 9;
BOOST_STATIC_ASSERT(sizeof(digits) == 20);
const char digitsHex[] = "0123456789ABCDEF";
BOOST_STATIC_ASSERT(sizeof digitsHex == 17);
// 先用求余除法求出 value转化成字符串的倒着顺序的,然后用std::reverse对其进行倒转
// 另一方面用了取数的方法,相比直接申请赋值会更有效率一些
template<typename T>
size_t convert(char buf[], T value)
{
T i = value;
char* p = buf;
do
{
int lsd = static_cast<int>(i % 10);
i /= 10;
*p++ = zero[lsd];
} while (i != 0);
if (value < 0)
{
*p++ = '-';
}
*p = '\0';
std::reverse(buf, p);
return p - buf;
}
/// 同样的实现不过是16进制的而已
size_t convertHex(char buf[], uintptr_t value);
下面我们就开始看看LogStream
它有两个数据成员,一个是缓冲流buffer,另一个是一个成员是表示数值的最大字节数(用于后面的优化)。
在里面重载的若干函数,它是通过一个模板函数,对其进行统一起来书写
template <typename T>
void LogStream::formatInteger(T v)
{
if (buffer_.avail() >= kMaxNumericSize)
{
size_t len = convert(buffer_.current(), v);
buffer_.add(len);
}
}
LogStream& LogStream::operator<<(int v)
{
formatInteger(v);
return *this;
}
值得注意的是,里面有一个重载函数的形参是StringPrice。这也是一个亮点的地方,它实现了无论是字符串还是string都可以进行零拷贝数据,只拷贝指针来传递,也是一个不错的方法,具体怎么实现的,看看它的构造函数也就明白了一切。
class StringPiece {
public:
StringPiece() : ptr_(nullptr), length_(0) {}
StringPiece(const char *str)
: ptr_(str), length_(static_cast<int>(strlen(ptr_))) {}
StringPiece(const unsigned char* str)
: ptr_(reinterpret_cast<const char*>(str)),
length_(static_cast<int>(strlen(ptr_))) {}
StringPiece(const string &str)
: ptr_(str.c_str()), length_(static_cast<int>(str.size())) {}
StringPiece(const char* offset, int len)
: ptr_(offset), length_(len) { }
private:
const char* ptr_;
int length_;
}
但是现在的Logstream也并不全面,对于给定一个格式的输入无从下手,于是Fmt就有了
class Fmt
{
public:
template<typename T>
Fmt(const char *fmt, T val);
const char *data() const
{ return buf_; }
int length() const
{ return length_; }
private:
char buf_[32];
int length_;
};
template <typename T>
Fmt::Fmt(const char *fmt, T val)
{
// 判断必须为算数类型
BOOST_STATIC_ASSERT(boost::is_arithmetic<T>::value == true);
length_ = snprintf(buf_, sizeof buf_, fmt, val);
assert(static_cast<size_t>(length_) < sizeof buf_);
}
inline LogStream& operator<<(LogStream& s, const Fmt& fmt)
{
s.append(fmt.data(), fmt.length());
return s;
}
写日志Logger的实现
先讲讲Logger,Logger的实现的机制就是靠初始化一个impl,再向impl内写入数据,在析构的时候再将impl_中的buffer提取出来,使用用户定义的outputFunc,输出buffer。如果未定义,它会使用默认的输入输出函数,即直接输出到stdout中。
impl的意思是implement,它仅仅负责向stream中进行输入,让我们先看看lmpl_类里面到底包含什么吧。
class Impl
{
public:
typedef Logger::LogLevel LogLevel;
Impl(LogLevel level, int old_errno, const SourceFile& file, int line);
void formatTime(); //格式化时间,时间是以毫秒计算的
void finish(); // 将文件名和line写入
Timestamp time_; //保存写日志当前的时间
LogStream stream_; //日志流
LogLevel level_; //日志等级
int line_; //出错的文件行号
SourceFile basename_; // 保存这文件名和文件名的大小
};
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(); // 获取线程id
stream_ << T(CurrentThread::tidString(), CurrentThread::tidStringLength()); //写入线程id
stream_ << T(LogLevelName[level], 6); //写入loglevel
if (savedErrno != 0)
{
stream_ << strerror_tl(savedErrno) << "(errno = " << savedErrno << ")";
}
}
每当有一个Logger对象就会初始化一个impl,并且向里面写入数据
Logger::Logger(SourceFile file, int line, LogLevel level, const char* func)
: impl_(level, 0, file, line)
{
impl_.stream_ << func << ' ';
}
在析构的时候调用析构函数,使用outputFunc函数写入数据
Logger::~Logger()
{
impl_.finish(); //写入文件名
const LogStream::Buffer& buf(stream().buffer());
g_output(buf.data(), buf.length());
if (impl_.level_ == FATAL)
{
g_flush();
abort();
}
}
void defaultOutput(const char* msg, int len)
{
size_t n = fwrite(msg, 1, len, stdout); //默认向stdout写入数据
//FIXME check n
(void)n;
}
void defaultFlush()
{
fflush(stdout);
}
请注意,在Loggin.cc中定义了两个全局指针,所以我们在使用Loggin类之前只需要将两个函数指针set一下,就可以在整个文件中获得相同的效果了。
Logger::OutputFunc g_output = defaultOutput; Logger::FlushFunc g_flush = defaultFlush;
但是我们每次使用这个类,都要写一个对象来使用,是不是未免它麻烦,我们可以使宏定义和重载<<来实现
#define LOG_TRACE if (jmuduo::Logger::logLevel() <= jmuduo::Logger::TRACE) \ jmuduo::Logger(__FILE__, __LINE__, jmuduo::Logger::TRACE, __func__).stream() #define LOG_DEBUG if (jmuduo::Logger::logLevel() <= jmuduo::Logger::DEBUG) \ jmuduo::Logger(__FILE__, __LINE__, jmuduo::Logger::DEBUG, __func__).stream() #define LOG_INFO if (jmuduo::Logger::logLevel() <= jmuduo::Logger::INFO) \ jmuduo::Logger(__FILE__, __LINE__).stream() #define LOG_WARN jmuduo::Logger(__FILE__, __LINE__, jmuduo::Logger::WARN).stream() #define LOG_ERROR jmuduo::Logger(__FILE__, __LINE__, jmuduo::Logger::ERROR).stream() #define LOG_FATAL jmuduo::Logger(__FILE__, __LINE__, jmuduo::Logger::FATAL).stream() #define LOG_SYSERR jmuduo::Logger(__FILE__, __LINE__, false).stream() #define LOG_SYSFATAL jmuduo::Logger(__FILE__, __LINE__, true).stream()
在这里我们使用了一个匿名类,将日志写入后,还将其LogStream返回出来便于使用LogStream写入其他数据。
等等,这里我们是不是忘记介绍了一个东西,就是日志的级别和默认记录的日志级别
enum LogLevel
{
TRACE,
DEBUG,
INFO,
WARN,
ERROR,
FATAL,
NUM_LOG_LEVELS,
};
muduo的日志级别一般都在INFO,即INFO级别的日志才会打印出来,具体实现也是依赖一个全局的日志级别的LogLevel,通过读取环境变量设置LogLevel,也可以设置
Logger::LogLevel initLogLevel()
{
if (::getenv("MUDUO_LOG_TRACE"))
return Logger::TRACE;
else if (::getenv("MUDUO_LOG_DEBUG"))
return Logger::DEBUG;
else
return Logger::INFO;
}
Logger::LogLevel g_logLevel = initLogLevel();
void Logger::setLogLevel(Logger::LogLevel level)
{
g_logLevel = level;
}
到此,整个Loggin类就全部剖析完了,但是还有一个宏,及宏的小小用法我觉得讲一讲也未尝不可。
// 用了宏的字符串的拼接"'"#val"'Must be non NULL"
#define CHECK_NOTNULL(val) \
::jmuduo::CheckNotNull(__FILE__, __LINE__, "'"#val"'Must be non NULL", (val))
template <typename T>
T* CheckNotNull(Logger::SourceFile file, int line, const char *names, T* ptr)
{
if (ptr == NULL)
{
Logger(file, line, Logger::FATAL).stream() << names;
}
return ptr;
}
}
输出到文件LogFIle
在上面我们实现了如何写日志,并定义了日志的输出接口,在LogFile中我们就可以使用这些接口,将日志输出到文件中,但是这里又有新的问题产生了
1.文件名要如何规定才能方便查阅?
2.程序崩溃了日志怎么办?
3.文件要如何滚动?时间?大小?还是两个都要?
在muduo中,一个典型的日志文件的文件名如下:
LogFile_test.20180821-072337.hostname.19238.log
-
第一部分是进程名字,用于区分那个服务的程序日志 第二部分是文件创建的事件,这样我们就可以用通配符来查询一段事件的日志 第三部分是机器名称 第四部分是进程ID,如果程序反复重启,我们就会得到不同的日志文件
如果程序崩溃了怎么办?
-
定时将缓冲区的日志flush硬盘上 日志消息带有cookie,其为某个函数的地址,这样就可以在core dump文件中查找cookie尚未来得及写入磁盘的消息
日志滚动
-
写满固定的日志的大小,就滚动 即使大小没有写满,固定时间也滚动日志
在muduo中,LogFIle主要负责日志滚动及其他逻辑,namespace FileUtil中负责日志的创建和写入、flush日志的接口。
我们首先看看FileUtil中实现了什么功能
/// read small file < 64kb
class ReadSmallFile: boost::noncopyable
{
public:
ReadSmallFile(StringArg filename);
~ReadSmallFile();
/***
* @maxsize 读入最大字节数
* @content 传出string
* @filesize 传入传出参数文件大小
* @modifyTime 传出参数修改时间
* @createTime 传出参数创建事件
* @return 返回err,没有就是0
*/
template <typename String>
int readToString(int maxSize,
String *content,
int64_t *filesize,
int64_t *modifyTime,
int64_t *createtime);
// 将文件内容加载到内存buf_中
int readToBuffer(int *size);
const char* buffer() const { return buf_; }
static const int kBuffSize = 64 * 1024;
private:
int fd_;
int err_;
char buf_[kBuffSize];
};
//读取filename对应的文件中,并将其通过content返回
template <typename String>
int readFile(StringArg filename,
int maxSize,
String content,
int64_t* fileSize = NULL,
int64_t* modifyTime = NULL,
int64_t* createTime = NULL)
{
ReadSmallFile file(filename);
return file.readToString(maxSize, content, fileSize, modifyTime, createTime);
};
这个虽然与本节无关,但是还是总结了,以后就直接提提就可以了,本节重点关注下面另一个类的实现
class AppendFile : boost::noncopyable
{
public:
explicit AppendFile(StringArg filename);
~AppendFile();
void append(const char* logline, const 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];
off_t writtenBytes_;
};
共有三个成员,一个是缓冲区buffer,一个是文件指针fp,一个是已经写入的字节数,muduo在实现文件写入操作的时候,为了实现异步写入磁盘(磁盘的写入速度远小于内存的写入的速度),使用了流缓冲技术,向缓冲中写入日志,再统一::flush()进入磁盘中。
FileUtil::AppendFile::AppendFile(StringArg filename)
:fp_(fopen(filename.c_str(), "we")), // e for O_CLOEXEC
writtenBytes_(0)
{
assert(fp_);
::setbuffer(fp_, buffer_, sizeof buffer_); //设置文件流缓冲
}
在实现的时候,由于日志会被当做背景线程使用,所以是在单线程的情况下使用,所以使用了::fwrite_unlocked,进一步提示写入速度。
下面我们开始介绍LogFIle,muduo的日志滚动机制是,一个根据文件大小滚动,二是在每天的零点更新一个日志。并且定义了一个FlushInterval,用来规定日志的flush时间。
class LogFile: boost::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);
static string getLogFileName(const string& basename, time_t* now);
const string basename_; /// 文件名
const off_t rollSize_; /// 滚动大小
const int flushInterval_; /// 写入周期
const int checkEveryN_; /// 检测时间的写入次数
int count_; /// 写入次数
boost::scoped_ptr<MutexLock> mutex_;
time_t startOfPeriod_; //此次日志写入时间 以每天零点为准
time_t lastRoll_; //上次滚动时间
time_t lastFlush_; //上次flush时间
boost::scoped_ptr<FileUtil::AppendFile> file_; //文件对象
const static int kRollPerSecond_ = 60*60*24; //滚动时间常量
};
在这里我们重点关注下面这些函数的实现
void LogFile::append_unlocked(const char* logline, int len)
{
file_->append(logline, len);
if (file_->writtenBytes() > rollSize_) //根据大小滚动
{
rollFile();
}
else
{
++count_;
if (count_ >= checkEveryN_) //如果超过检测次数,就检查一次时间
{
count_ = 0;
time_t now = ::time(NULL);
time_t thisPeriod_ = now / kRollPerSecond_* kRollPerSecond_; /// 小数点后面的默认舍去,所以就可以判断是今天的时间还是明天的时间
if (thisPeriod_ != startOfPeriod_) //检测滚动时间
{
rollFile();
}
else if (now - lastFlush_ > flushInterval_) //检查flush时间
{
lastFlush_ = now;
file_->flush();
}
}
}
//滚动日志
bool LogFile::rollFile()
{
time_t now = 0;
string filename = getLogFileName(basename_, &now);
time_t start = now / kRollPerSecond_ * kRollPerSecond_; /// 时间取整
if (now > lastRoll_)
{
lastRoll_ = now;
lastFlush_ = now;
startOfPeriod_ = start;
file_.reset(new FileUtil::AppendFile(filename));
return true;
}
return false;
}