多进程写同一个日志并发问题分析

背景问题

在优化日志组件项目中有如下场景:两个进程,A、B进程往同一个文件写日志的时候,使用C语言的库函数写,fopen文件追加方式打开, fwrite等。如果每一次写的时候都强制fflush操作,则写的时序是正常的。如果不是每次fflush操作的时候,则会出现写入日志的时候的时序问题,即是A进程的一条日志没打完,B进程的日志中间插进来了。

问题分析

原子性

  • 系统调用
    先从最基本的系统调用说起,磁盘IO效率的简单分析,该篇的问题2中提到,如果是系统调用,多进程需要保证写磁盘有序,则需要open的时候使用O_APPEND选项,其本质是open的时候,写磁盘的时候会锁住文件的inode节点,全局唯一,所以进程间可以实现互斥。

  • 库函数IO
    对于C语言的库函数而言,它在系统调用的上层,做了一层缓冲。之所以自己做一层缓冲,是为了提升IO效率,在用户空间就对数据进行缓冲,最终使用合并写操作,以提升IO的效率。而多进程同时写文件的时候,由于flush的时机,由库函数自动决定,因此,多进程状态下,存在刷新操作的非原子性问题。

追问

库函数的追加写不是通过O_APPEND方式打开的吗?为什么这里刷新操作为什么不是原子的, 底层不是通过锁定innode方式实现的吗?

Write原子性

  • 系统调用原子性
    O_APPEND进行open的文件而言,系统调用,它能够保证原子性,在系统调用的过程之中,不会有其他系统调用打断它

  • 写数据的原子性
    这里有个非常重要的概念,write函数原型如下:ssize_t write(int fd, const void *buf, size_t count);它有个返回值,它表示此次调用成功写入了多少个字节。在这些成功写入的字节中,它是原子的。并不是你写1G缓冲,write就给你保证这1G数据就原子性写入磁盘,不要太天真!

flush的非原子性

通过上面的分析,我们知道,flush操作的时候,缓冲区中的数据,它是调用write系统调用来写磁盘,写入的数据能保证原子性。但是,但是,这里的缓冲区数据,它未必能够保证一次性全部写完。即是A、B进程都有1K字节缓冲区,调用write一次写1K字节,可能返回500字节,那此时,B进程可以调用write写入500字节,A再写完剩下的500字节,再写剩下的B进程的500字节。这种情况下,那么,flush操作则会导致日志的乱序,原因找到。

如何优化

对于日志组件而言,在多进程并发写的情况下,一个优化方式无非就是合并写,但这里利用库函数做合并,flush的时候存在并发问题。如何解决?

解决方案

  • 手动加锁刷新
    通过手动刷新代替库函数的自动刷新,在缓冲区满之前,手动加上进程锁进行刷新操作。例如:满100条日志的时候,就手动刷新一次。但这里会存在一个问题,当流量特别小的时候,日志会特别久才出来一次,何解?且看方案二
  • 定时线程刷新
    每一个进程创建一个分离线程,进行定时刷新,例如:每一秒钟刷新一次。这样就能解决小流量调试的时候,日志的查看及时度问题

你可能感兴趣的:(C/C++,工作,linux)