文件写对象 AppendFile
高效地将数据追加到写入文件,使用自定义缓冲区减少系统调用次数,提高写入性能。
主要方法:
封装OS提供的创建/打开文件、写文件、关闭文件等操作
相关函数详解
FILE* fopen(const char* filename, const char* mode);
参数说明
filename
:需要打开的文件路径,可以是绝对路径或相对路径。mode
:文件打开模式,控制文件的读写权限和操作方式。
返回值
- 成功时返回指向
FILE
对象的指针。 - 失败时返回
NULL
,并设置全局错误码errno
。
常见打开模式
"r"
:以只读方式打开文件,文件必须存在。"w"
:以只写方式打开文件,文件不存在则创建,存在则清空内容。"a"
:以追加方式打开文件,写入内容自动追加到文件末尾。"r+"
:以读写方式打开文件,文件必须存在。"w+"
:以读写方式打开文件,文件不存在则创建,存在则清空内容。"a+"
:以读写方式打开文件,写入内容自动追加到文件末尾。
二进制模式 在模式字符串中添加'b'
(如"rb"
、"wb+"
),用于处理二进制文件,避免文本模式下的换行符转换。
void setbuffer(FILE *stream, char *buffer, size_t size);
参数说明
stream
:指向需要设置缓冲区的文件流指针。buffer
:指向用于作为缓冲区的字符数组的指针。若为NULL
,表示不使用缓冲区。size
:指定缓冲区的大小,以字节为单位。
功能说明 setbuffer
函数的主要作用是为文件流指定一个自定义的缓冲区,从而控制文件的读写缓冲方式。当对文件进行读写操作时,数据会先在缓冲区中进行处理,而不是直接与文件进行交互。这样可以减少对文件的直接读写次数,提高读写效率。
使用场景
- 向文件写入数据时,数据会先被写入到缓冲区中。
- 当缓冲区满或执行
fflush()
函数时,缓冲区中的数据才会被真正写入到文件中。
成员变量
private:
static const size_t FILE_BUFF_SIZE = 1024 * 1024*10; // 10m;缓冲区大小
std::unique_ptr<char[]> buffer_; //自定义文件缓冲区
FILE *fp_; //文件指针
size_t writeBytes_; //已写入文件的总字数
size_t write(const char *msg, const size_t len);
成员函数
- 构造函数
//构造函数,打开指定文件并初始化为追加模式
AppendFile(const std::string &filename)
:buffer_(new char[FILE_BUFF_SIZE]),fp_{nullptr},writenBytes_{0}
{
fp_=fopen(filename.c_str(),"a"); //打开或创建文件
assert(fp_!=nullptr);
setbuffer(fp_,buffer_.get(),FILE_BUFF_SIZE);
//当数据写满缓冲区时会自动触发系统调用将数据写入磁盘
}
//析构函数,确保文件被正确关闭
~AppendFile()
{
fclose(fp_);
fp_=nullptr;
buffer_.reset();
}
- 追加数据到文件
void append(const char *msg, const size_t len)
{
size_t n=write(msg,len); //写入的字节数
size_t remain=len-n; //还剩多少没写进去,可能缓冲区满了
//文件是可以动态增长的
while(remain>0){
size_t x=write(msg+n,remain);
if(x==0){ //还未写进去,可能文件满了
int err=ferror(fp_);
if(err){
fprintf(stderr,"appendFile::append failed %s\n",strerror(err));
break;
}
}
}
n+=x;
wrtieBytes_+=n;
}
日志文件对象 LogFile
主要方法:
1. 构建日志文件名称
2. 构建新的回滚文件
3. 日志信息写入文件缓冲区
4. 刷新函数,将文件缓冲区的数据写入文件
此项目回滚只实现了基于文件大小和基于时间(按天)的日志回滚功能,当文件超出所设定的上限时,产生一个新文件;按天回滚,每到00:00就会产生一个新文件。
未实现当文件超过所设定的时间范围就会被删除的功能
成员变量
private:
const string basename_; //日志文件基础名
const size_t rollSize_; //日志文件滚动的大小阈值
const int flushInterval_; //刷新间隔 s
//用于控制检查频率,避免每次写入日志都耗时进行条件检查(大小,时间)
const int checkEventN_; //检查滚动条件的事件次数阈值
int count_; //记录日志写入次数,用于触发检查
time_t startOfPeriod_; //当前日志周期开始时间(按天算)
time_t lastRoll_; //上次滚动文件的时间
time_t lastFlush; //上次刷新文件的时间
static const int kRollPerSeconds_ = 60 * 60 * 24; // 一天的秒数,用于按天滚动
std::unique_ptr<AppendFile>file_; //文件写入器
std::unique_ptr<std::mutex> mutex_; //互斥锁指针
重要成员函数
- 构造函数
LogFile(const std::string &basename,
size_t rollSize=1024*1024,
int flushInterval=3,
int checkEventN=30,
bool threadSafe=true)
: basename_(basename),
rollSize_(rollSize),
flushInterval_(flushInterval),
checkEventN_(checkEventN),
mutex_(threadSafe ? std::make_unique<std::mutex>() : nullptr),
count_(0),
startOfPeriod_(0),
lastRoll_(0),
lastFlush_(0)
{
rollFile();
}
- 回滚函数
bool rollFile()
{
tulun::Timestamp t;
t.now();
string filename=getLogFileName(basename_,t);
time_t start=(t.getSecond()/kRollPerSeconds_) *kRollPerSeconds_;
if(t.getSecond()>lastRoll_)
{
lastRoll_=t.getSecond();
lastFlush_=t.getSecond();
startOfPeriod_=start;
//reset()释放当前管理的对象,并接管新传入的指针
file_.reset(new tulun::AppendFile(filename));
return true;
}
return false;
}
};
- 默认文件名
std::string LogFile::getLogFileName(const std::string &basename, const tulun::Timestamp &now)
{
string filename;
filename.reserve(basename.size()+128);
filename=basename;
filename+=".";
filename+=now.toFormattedFile();
filenmae+=".";
filename+=hostname();
std::stringstream ss;
ss<<"."<<pid()<<".log";
filename+=ss.str();
return filename;
}
同步日志分析
当开启线程安全模式(threadSafe为true)当在创建 LogFile对象时将 threadSafe参数设置为 true,LogFile类会创建一个互斥锁 、mutex_。在调用 append方法时,会通过这个互斥锁来保护对 append_unlocked方法的调用,
void LogFile::append(const char *msg, const size_t len)
{
if(mutex_)
{
std::unique_lock<std::mutex> lock(*mutex_);
append_unlocked(msg, len);
}
else
{
append_unlocked(msg, len);
}
}
在 `append_unlocked` 方法中,当满足特定条件时会调用 `file_->flush()` 进行缓冲区刷新,代码如下:
void LogFile::append_unlocked(const char *msg, const size_t len)
{
file_->append(msg, len);
if (file_->getWriteBytes()+len > rollSize_) //预判断
{
rollFile();
}
else
{
count_ += 1;
if (count_ > checkEventN_)
{
count_ = 0;
time_t now = ::time(nullptr);
time_t thisPeriod = (now / kRollPerSeconds_) * kRollPerSeconds_;
if (thisPeriod != startOfPeriod_)
{
rollFile();
}
else if (now - lastFlush_ > flushInterval_)
{
lastFlush_ = now;
file_->flush();
}
}
}
}
在这种模式下,当一个线程获取互斥锁并执行`append_unlocked`方法时,在执行
`file_->flush()` 操作(即缓冲区往磁盘写内容)时,其他线程会被阻塞`std::unique_lock<std::mutex> lock(*mutex_);` 这一行,直到持有锁的线程释放锁,