因为是同步写入,所以不具有在生产环境中的实际应用价值,只是基于对无锁写入方案的一个探讨。
- 高效,作为一个日志系统,不应该占据太多资源
- 简洁,尽量不要引入太复杂的依赖(log4cpp库),给系统开发带来难度
- 线程安全,服务端的各个线程都能同时读写日志
- 轮替问题,如果半年到一年的日志放到一个文件会导致文件过大
- 及时保存,程序故障导致异常退出,如果日志还留在缓冲区就会导致丢失
当我选择弃用log4cpp等庞大的库的时候,就意味着需要自己去解决3-5等问题。
解决线程安全应当考虑不引入锁,因为加锁会带来复杂性和性能问题,所以应当考虑更wakuwaku的解决方案。
- 使用O_APPEND方式打开文件,这个标记让write写出的内容添加到文件末尾,移动文件指针与输出内容是原子的,由操作系统来保证原子性。因此这个标记保证在多线程/多进程调用write也能够保持输出的内容不会相互覆盖错乱,nginx的日志也利用了这个标记来达到多进程不干扰。
- 每一次log,都会生成包括了时间的最终输出字符串,调用write,写出到日志系统的文件描述符fd。当write返回时,日志已经写到操作系统,不管程序是否崩溃,只要操作系统不崩溃,那么输出的内容就会保存到日志文件中。
轮替的过程中,需要关闭当前文件并打开新文件,让新的内容写到新文件中,在多线程环境下就需要锁来同步所有线程的日志输出操作,避免写入到不合法的文件描述符中。
可以使用posix中的dup2来实现无锁轮替文件。
//轮替时,首先重命名已打开的日志文件,保持打开状态,
rename(filename, newname);
//然后创建新的日志文件
fd = open(filename,...);
//使用dup2系统函数把fd(新)复制到fd_(旧)上
dup2(fd, fd_);
//关闭fd(新)
close(fd);
其中dup2是原子操作,它会关闭fd_并且把fd_也指向fd打开的文件。因此fd_这个文件描述符总是保持打开状态,并且值不变,但是前后指向了不同的文件,完全不会影响其他线程调用write(fd_, ...)等操作。另一边write也是个原子操作,它与dup2不会交叉进行,因此保证了日志系统的正确性。