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()三个函数的区别

  1. 三者的用途不同

    sync, 是同步整个系统的磁盘数据

    fsync, 将打开的一个文件到缓冲区的数据同步到磁盘上

    fflush, 刷新打开的流

  2. 都是同步,但三者的同步等级不同

    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:多线程互斥

日志写入磁盘是批量写

该日志库如何做到高性能?

双缓存机制

  1. 日志notify的问题
  • 写满一个buffer才notify一次,进行插入日志
  • 另外一个线程通过wait_timeout去读取日志,然后写入磁盘(当收到notify/timeout超时,就去执行)

在这里插入图片描述

  1. 使用两个buffer,能够避免buffer不断分配,一个用来读,一个写

  2. 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集合
};

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐