PHP多进程中使用file_put_contents安全吗?

TL;DR

Linux下,PHP多进程使用 file_put_contents() 方法记录日志时,使用追加模式(FILE_APPEND),简短的日志内容不会重叠,即能安全的记录日志内容。

file_put_contents() 使用 write() 系统调用实现数据的写入,write() 系统调用对普通文件保证写入数据的完整性,O_APPEND 打开模式保证数据写入到文件末尾。

如果愿意的话,也可以考虑在标记位中使用 LOCK_EX

从monolog说起

提起 PHP 日志记录,不得不说到 monolog 这个项目,这几乎是现有大多数项目首选的日志库。

对于日志记录这一场景,无论是 HTTP API 还是 daemon 进程,在应用中总会遇到多个进程的情况。

PHP-FPM 下会存在多个 worker,而 daemon 常选择使用多进程的方式充分利用资源。多个进程之间的竞争是必然存在的,而 monolog 是如何解决的呢?

答案是文件锁。

翻看源码 StreamHandler.php

文件通过 a 模式,即追加模式打开,写入操作使用的是常规的 fwrite 操作。

让人困惑的是,已经使用 a 模式打开为何还需要上锁?这一个上锁操作来源于 GitHub 上的这一个 issue #379

#379 这个 issue 简而言之即用户在使用过程中发现写入一定长度的日志时出现了重叠的情况,于是提交了一个需要上锁的 PR。但是个人认为此处需要上锁的理由并不充分,因为 issue 中提到的问题,个人理解并不能确定是否是因为未上锁引起的。

有人说如果进程写日志过程中挂了没有解锁怎么办?没关系,文件锁在进程退出之后就会被释放。

file_put_contents()的实现

file_put_contents()完成的是open/write/close

翻阅 PHP 5.4.41 源码中的 ext/standard/file.c 文件,可以看到 file_put_contents() 的实现(源码稍长,只做部分摘录):

可以看出,file_put_contents() 实际上是完成了 open -> write -> close 三大操作。

写入操作的实现

我们最为关心的 write 操作,跟踪源码可以发现,实际上是流结构体中的 write 函数指针指向的函数完成的:

那么问题来了,write 指向的函数到底是什么呢?

继续跟踪源码,在函数 _php_stream_open_wrapper_ex() 中找到了一些线索:

main/stream/stream.c 文件中的 php_stream_locate_url_wrapper() 函数中可以看到,对于文件,实际上返回的的是 php_plain_files_wrapper 的全局变量的指针:

而这个变量的结构实际上包含了一个静态变量 php_plain_files_wrapper_ops

当中的 php_plain_files_stream_opener 函数指针指向的函数则明确的告知了如何生成流对象的实现:

在流打开的函数 _php_stream_fopen() 中(位于文件 main/stream/plain_wrapper.c中),我们终于找到了生成流结构的逻辑:

再深入一步,看看 _php_stream_fopen_from_fd_int() (最终都会调用这一函数)这些函数是如何生成流结构中的 ops 结构体的:

write 操作的实现的答案就在 php_stream_stdio_ops 这一变量中:

php_stdiop_write 函数指针指向的函数就是我们要的答案:

跟踪到这里,得到了最终的结论:

file_put_contents() 使用 write() 系统调用实现了数据的写入。

写入安全的保证

造成多进程写入文件内容错误乱的原因很大程度上是因为每个进程打开文件描述符对应的文件位置指针都是独立的,如果没有同步机制,可能后来的写入的位置就会覆盖之前写入的数据,那么 write()O_APPEND 能不能解决这个问题呢?

《Linux系统编程》第二章提到:

对于普通文件,除非发生一个错误,否则write()将保证写入所有的请求。

当fd在追加模式下打开时(通过指定O_APPEND参数),写操作就不从文件描述符的当前位置开始,而是从当前文件末尾开始。

它保证文件位置总是指向文件末尾,这样所有的写操作总是追加的,即便有多个写者。你可以认为每个写请求之前的文件位置更新操作是原子操作。

以上说明了:

  • 每个写操作由操作系统保证完成性,即进程 A 写入 aa,进程 B 写入 bb,文件中不可能出现类似的 abab 这样的数据交叉情况。
  • O_APPEND在多个写入者的情况下已然能保证数据写入文件末尾。

结论

综上,可以放心的使用 PHP 的 file_put_contents() 结合 FILE_APPEND 记录日志。

当然这是对于写入普通文件,如果写入的是管道则要关注是否数据大小超过 PIPE_BUF 的值了,这里有一篇有趣的博文 Are Files Appends Really Atomic? 可以读读。

参考

《PHP扩展开发与内核应用》:15 流的实现

《Linux 系统编程》(第二版)

Is lock-free logging safe?

StackOverflow: Is file append atomic in UNIX?

Appending to a File from Multiple Processes

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">