IO接口
read/write/fsync
- linux底层操作
- 内核调用,涉及到进程上下文的切换,即用户态到核心态的转换,这是个比较消耗性能的操作
fread/fwrite/fflush
- c语言标准规定的io流操作,建立在read/write/fsync之上
- 在用户层,又增加了一层缓冲机制,用于减少内核调用次数,但是增加了一次内存拷贝
两者之间的关系:
对于输入设备,调用fsync/fflush将清空相应的缓冲区,缓冲区内数据将被丢弃
对于输出设备或磁盘文件,fflush只能保证数据到达内核缓冲区,并不能保证数据到达物理设备,应该在调用fflush后,调用fsync,确保数据存入磁盘
fflush/fsync功能区别
fflush(FILE *)
是libc.a中提供的方法,用来将流中未写的数据传送到内核。如果参数为null,将导致所有流冲洗
把C库中的缓冲调用write函数写到磁盘(实际是写到内核的缓冲区)
fsync(int fd)
参数是一个int型的文件描述符,fsync是系统提供的系统调用,将数据写到磁盘上
把内核缓冲刷到磁盘上
总体流程:
c库缓冲/用户缓冲 —fflush—> 内核缓冲 —fsync—> 磁盘
sync()/fflush()/fsync()三个函数的区别
-
三者的用途不同
sync, 是同步整个系统的磁盘数据
fsync, 将打开的一个文件到缓冲区的数据同步到磁盘上
fflush, 刷新打开的流
-
都是同步,但三者的同步等级不同
sync, 将缓冲区数据写回磁盘,保持同步(无参数)
fsync, 将缓冲区的数据写到文件中(有一个参数 int fd)
fflush, 将文件流里未写出的数据立刻写出
log4cpp开源日志库
同步日志方式性能比较差只能用在客户端,异步日志方式可以用在服务端
每次写日志都要去检查一次日志文件的大小,性能会很差
1s钟写入1w条左右日志的日志库 是性能比较差的,会影响服务的性能
log-src/src-log4cpp/4-RollingFileAppender.cpp 测试同步日志每秒写文件次数
log-src/src-log4cpp/5-StringQueueAppender.cpp 测试异步日志每秒写文件次数
在build目录中,进行编译运行
同步日志
运行测试文件,ops为每秒写入日志行数(电脑是ssd硬盘)
以下两种都是同步日志方式,RollingFileAppender差于FileAppender
滚动日志 RollingFileAppender
-
简介
可配置日志文件个数,每个日志文件的大小
_append函数在每次写入日志的时候,都要去获取文件大小,如果超过设定的大小就新创建一个日志文件
性能较差
-
滚动原理:
1)每次写入先获取日志文件大小,如果没有超过设定大小,直接写入
2)如果超过设定大小,进行滚动
3)比如最后一个日志文件log.3,删除log.3
4)log.2->log.3, log.1->log.2, log->log.1
5)新创建 .log,将日志写入 .log中
类似一个固定大小的队列,先入先出原理,满了就把最先进来的踢出去
-
日志性能分析
实时写入磁盘,单笔write
回滚日志每次都读取日志文件大小,不能每次读取文件大小
普通日志 FileAppender
异步日志
StringQueueAppender
功能是将日志记录到一个字符串队列中,该字符串队列使用了STL中的两个容器,
即字符串容器std::string和队列容器std::queue,具体如下:
std::queue<std::string> _queue;
_queue变量是StringQueueAppender类中用于具体存储日志的内存队列。
先将日志存到队列中,再异步获取存日志的队列,写入文件,可以实现异步日志
muduo开源日志库
日志写入队列的时候用mutex+notify进行通知
mutex:多线程互斥
日志写入磁盘是批量写
该日志库如何做到高性能?
双缓存机制
- 日志notify的问题
- 写满一个buffer才notify一次,进行插入日志
- 另外一个线程通过wait_timeout去读取日志,然后写入磁盘(当收到notify/timeout超时,就去执行)
-
使用两个buffer,能够避免buffer不断分配,一个用来读,一个写
-
buffer 默认4M一个
写满4M ,notify一次
双队列
代码重点:双缓冲、双队列、锁的粒度、move语义、批量日志插入队列、日志读取写入磁盘
-
append为前台线程
维护双缓冲区currentBuffer_,nextBuffer_;一个队列buffers_(是vector)
当需要写入日志,如果currentBuffer_剩余空间够,直接写入;
否则,将currentBuffer_加入到buffers_中,nextBuffer_空间给currentBuffer_,如果日志信息写入过快,把currentBuffer_和nextBuffer_都用光了,只好分配一块新的currentBuffer_;并通知后台线程
-
threadFunc为后台线程
维护双缓冲区newBuffer1,newBuffer2;一个队列buffersToWrite(用来和前端buffers_交换)
等待超时,将currentBuffer_写入buffers_,置空,nextBuffer_也置空(加锁)
感觉后台线程2个缓冲区的作用是用于清空前台缓冲区
接下来交换bufferToWrite和buffers_,这就完成了将记录了日志消息的buffer从前端到后端的传输,后端日志线程慢慢进行IO即可
个人总结
前台线程全程加锁,因为用到的缓冲buffer与后台线程是共享数据
前台线程向1个缓冲buffer中写入日志,使用2个缓冲buffer做交替,避免不断分配buffer
缓冲buffer写满之后,通知后台线程开始工作
后台线程同时有个定时逻辑,到时间自动开始;将有数据的前台buffer放到队列中,清空前台2个缓冲buffer
swap两个队列,用新的队列去写日志文件
代码实现主要在AsyncLogging.h/cc
class AsyncLogging : noncopyable
{
public:
AsyncLogging(const string& basename,
off_t rollSize,
int flushInterval = 3);
~AsyncLogging()
{
if (running_)
{
stop();
}
}
void append(const char* logline, int len); // 负责收集业务线程发来的日志
void start() // 启动线程
{
running_ = true;
thread_.start();
latch_.wait();
}
void stop() NO_THREAD_SAFETY_ANALYSIS // 关闭线程
{
running_ = false;
cond_.notify();
thread_.join();
}
private:
void threadFunc(); // 异步线程处理逻辑
typedef detail::FixedBuffer<detail::kLargeBuffer> Buffer;
typedef std::vector<std::unique_ptr<Buffer>> BufferVector;
typedef BufferVector::value_type BufferPtr;
const int flushInterval_;
std::atomic<bool> running_;
const string basename_;
const off_t rollSize_;
Thread thread_;
CountDownLatch latch_;
MutexLock mutex_;
Condition cond_ GUARDED_BY(mutex_);
BufferPtr currentBuffer_ GUARDED_BY(mutex_); // 当前写入的buffer
BufferPtr nextBuffer_ GUARDED_BY(mutex_); // 下一个备用buffer
BufferVector buffers_ GUARDED_BY(mutex_); // 写入完毕,待打印的buffer集合
};