本文只讨论单线程,文本类型的日志,其实Linux下有许多优秀的开源的效率很高的日志库,例如log4cplus等。我不敢说自己的实现要比它们更好,但是这至少是给了一些想自己实现日志功能的人一些更多的选择。
谈到日志,一开始我们就会想到是直接将日志信息一条一条的写入到文件中,利用直接的I/O操作。但是只要你稍稍一考虑你就会发现这是一个糟糕的做法。一旦你的项目代码越来越多,添加的日志数目越来越多,那么你操作I/O的次数就会非常频繁,而文件操作又是一个比较耗时的操作,那么你的系统就会随着日志的增加而变得越来越慢。
为了不让文件的读写操作随着日志条数的增加而线性的增长。你可能会想到的是将日志先缓存在一个缓冲区中然后定时的去将缓冲区的内容写到文件中。这样做的确可以使I/O的操作不随着日志的增加而线性的增长。但是这也有一个致命的问题,一旦程序在中途发生了崩溃,而定时器还没来得及去将缓冲区的内容写到文件中,那缓冲区中的日志信息就丢失了。记录着程序崩溃的最重要的那些最后的日志信息就丢了。
这似乎是一个解不开的死结,想要解除I/O操作和日志增加之间的线性关系就需要定时的去刷新文件,但是这样又有可能导致信息的丢失。如果需要让日志不丢失,就必须将日志立马写入到文件中。
看来要解开这个死结我们只能一步一步的慢慢解。在本篇文章中我只是凭我自己的经验先解决I/O的读写速度问题。其他的问题,在以后的博文中再去慢慢解开。在Linux系统下提高I/O的执行速率我首先是想到了文件映射,将文件映射到内存空间中,由对文件的操作转换成对内存的操作,因为操作内存的速率,要比文件操作快得多。文件映射的几个基本函数是:
1. void *mmap(void *addr, size_t length, int prot, int flags,int fd,off_t offset);
2. int msync(void *addr, size_t length, int flags);
3. int munmap(void *addr, size_t length);
关于这三个函数,我在此不详述。
文件到内存映射的基本思路是:1.获取文件的大小,将文件的大小扩展,因为mmap无法回写大于源文件大小的内存内容。2.将整个文件映射到内存中提高访问速度。3.更新内存的数据。4.回写内容到磁盘。不过这里值得一提的是当扩展文件的时候,最好将文件的大小扩展到页的整数倍,回写到磁盘的时候也最好回写页的整数倍长度。因为磁盘操作以块为单位,以页的整数倍长度去刷新磁盘速度会更快。CMMapLog::CMMapLog(const string &strName, const int iLogSize) //构造函数
:g_iMapSize(256*getpagesize())
,m_strFileSrc(strName)
,m_iIndex_(0)
,m_iWrite_(0)
,fd_(-1)
,m_bFlushBrock(true)
{
m_strFileName_ = strName;
m_iLogNumber_ = iLogSize/g_iMapSize > 0 ?(iLogSize/g_iMapSize) :1;
if(iLogSize%g_iMapSize >0)
++m_iLogNumber_;
fd_ = open(strName.c_str(),O_RDWR|O_CREAT,S_IRUSR|S_IWUSR);
assert(fd_>=0);
int iSeek = g_iMapSize-strlen("\r\nEnd!");
assert(iSeek>0);
lseek(fd_,iSeek,SEEK_SET); //将文件扩展
int iWrite=write(fd_,"\r\nEnd!",strlen("\r\nEnd!"));
assert(iWrite>0);
lseek(fd_,0,SEEK_SET);
m_pMem_ = static_cast<char *>(mmap(NULL,g_iMapSize,PROT_READ|PROT_WRITE,MAP_SHARED,fd_,0)); //将文件映射到内存
assert(m_pMem_);
bzero(m_pMem_,g_iMapSize);
}
int CMMapLog::PushLog(const string &strLog) //添加日志
{
if(!m_pMem_)
return -1;
if(access(m_strFileName_.c_str(),F_OK)!=0)
{//文件被删除了,重新塑造
if(m_pMem_)
{
munmap(m_pMem_,g_iMapSize);
m_pMem_ = NULL;
}
if(fd_>=0)
{
close(fd_);
}
m_iWrite_ = 0;
fd_ = -1;
fd_ = open(m_strFileName_.c_str(),O_CREAT|O_RDWR,S_IRUSR|S_IWUSR);
if(fd_<0)
return -1;
int iSeek = g_iMapSize-strlen("\r\nEnd!");
if(iSeek<=0)
return -1;
lseek(fd_,iSeek,SEEK_SET);
int iWrite=write(fd_,"\r\nEnd!",strlen("\r\nEnd!"));
if(iWrite<0)
{
close(fd_);
return -1;
}
lseek(fd_,0,SEEK_SET);
m_pMem_ = static_cast<char *>(mmap(NULL,g_iMapSize,PROT_READ|PROT_WRITE,MAP_SHARED,fd_,0));
if(m_pMem_)
{
memset(m_pMem_,0,g_iMapSize);
}
}
int iLen = strLog.length();
if(m_iWrite_+iLen>=g_iMapSize)
{//超出了范围
++m_iIndex_;
if(m_iIndex_>=m_iLogNumber_)
{
m_iIndex_=0;
m_strFileName_ = m_strFileSrc;
}
else
{
char pIndex[32]={0};
sprintf(pIndex,".%d",m_iIndex_);
m_strFileName_ = m_strFileSrc;
m_strFileName_ += (const char*)pIndex;
}
munmap(m_pMem_,g_iMapSize);
m_pMem_ = NULL;
m_iWrite_ = 0;
if(fd_>=0)
{
close(fd_);
fd_ = -1;
}
fd_ = open(m_strFileName_.c_str(),O_CREAT|O_RDWR,S_IRUSR|S_IWUSR);
assert(fd_>0);
int iSeek = g_iMapSize-strlen("\r\nEnd!");
assert(iSeek>0);
lseek(fd_,iSeek,SEEK_SET);
int iWrite=write(fd_,"\r\nEnd!",strlen("\r\nEnd!"));
assert(iWrite>0);
lseek(fd_,0,SEEK_SET);
m_pMem_ =static_cast<char *>(mmap(NULL,g_iMapSize,PROT_READ|PROT_WRITE,MAP_SHARED,fd_,0));
assert(m_pMem_);
memset(m_pMem_,0,g_iMapSize);
}
memcpy(m_pMem_+m_iWrite_,strLog.c_str(),iLen); //跟新内存数据
if(m_bFlushBrock)
msync(m_pMem_,g_iMapSize,MS_ASYNC|MS_INVALIDATE); //整块回写到磁盘
else
msync(m_pMem_+m_iWrite_,iLen,MS_ASYNC|MS_INVALIDATE);//只回写跟新的那部分
m_iWrite_ += iLen;
return iLen;
}
void CMMapLog::SetFlushBrock(bool bValue)//是不是刷新整个内存块
{
m_bFlushBrock = bValue;
}
这仅仅是提高了I/O的读写速率其实,系统的性能还是会随着日志的增加而线性的下降,只不过是I/O读写的速度有了显著的提高,大概是比直接的I/O操作提升了10倍以上(ubuntu下做的测试,不具有普遍性),至于要如何脱开这种线性的依赖关系而又不影响日志的实时性,以后的博文中可能会再讨论。