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 是如何解决的呢?

答案是文件锁。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36protected function write(array $record)

{

if (!is_resource($this->stream)) {

if (null === $this->url || '' === $this->url) {

throw new \LogicException('Missing stream url, the stream can not be opened. This may be caused by a premature call to close().');

}

$this->createDir();

$this->errorMessage = null;

set_error_handler([$this, 'customErrorHandler']);

$this->stream = fopen($this->url, 'a');

if ($this->filePermission !== null) {

@chmod($this->url, $this->filePermission);

}

restore_error_handler();

if (!is_resource($this->stream)) {

$this->stream = null;

throw new \UnexpectedValueException(sprintf('The stream or file "%s" could not be opened: '.$this->errorMessage, $this->url));

}

}

if ($this->useLocking) {

// ignoring errors here, there is not much we can do about them

// 注意,此处使用了阻塞的排他文件锁,多进程时等待

flock($this->stream, LOCK_EX);

}

// 常规的文件写入操作

$this->streamWrite($this->stream, $record);

if ($this->useLocking) {

// 写入完成后解锁

flock($this->stream, LOCK_UN);

}

}

protected function streamWrite($stream, array $record)

{

fwrite($stream, (string) $record['formatted']);

}

文件通过 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() 的实现(源码稍长,只做部分摘录):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63PHP_FUNCTION(file_put_contents)

{

php_stream *stream; // 流的结构体,告知了流的读写操作参数

// ...

char mode[3] = "wb"; // 打开流的标记,默认是写二进制文件格式

// ...

context = php_stream_context_from_zval(zcontext, flags & PHP_FILE_NO_DEFAULT_CONTEXT);

// 如果提供了 FILE_APPEND 标记为则以追加模式打开流

if (flags & PHP_FILE_APPEND) {

mode[0] = 'a';

} else if (flags & LOCK_EX) { // 如果有 LOCK_EX标志则尝试对流上锁

/* check to make sure we are dealing with a regular file */

// ...

mode[0] = 'c';

}

mode[2] = '\0';

stream = php_stream_open_wrapper_ex(filename, mode, ((flags & PHP_FILE_USE_INCLUDE_PATH) ? USE_PATH : 0) | REPORT_ERRORS, NULL, context);

// ...

switch (Z_TYPE_P(data)) {

case IS_RESOURCE: {

// ...

break;

}

case IS_NULL:

case IS_LONG:

case IS_DOUBLE:

case IS_BOOL:

case IS_CONSTANT:

convert_to_string_ex(&data);

case IS_STRING:

if (Z_STRLEN_P(data)) {

// 关键逻辑,实际写入操作

numbytes = php_stream_write(stream, Z_STRVAL_P(data), Z_STRLEN_P(data));

if (numbytes != Z_STRLEN_P(data)) {

php_error_docref(NULL TSRMLS_CC, E_WARNING, "Only %ld of %d bytes written, possibly out of free disk space", numbytes, Z_STRLEN_P(data));

numbytes = -1;

}

}

break;

case IS_ARRAY:

// ...

break;

case IS_OBJECT:

// ...

default:

numbytes = -1;

break;

}

php_stream_close(stream);

if (numbytes < 0) {

RETURN_FALSE;

}

RETURN_LONG(numbytes);

}

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

写入操作的实现

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14static size_t _php_stream_write_buffer(php_stream *stream, const char *buf, size_t count TSRMLS_DC)

{

size_t didwrite = 0, towrite, justwrote;

// ...

while (count > 0) {

towrite = count;

if (towrite > stream->chunk_size)

towrite = stream->chunk_size;

// 请注意此处

justwrote = stream->ops->write(stream, buf, towrite TSRMLS_CC);

// ...

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21PHPAPI php_stream *_php_stream_open_wrapper_ex(char *path, char *mode, int options,

char **opened_path, php_stream_context *context STREAMS_DC TSRMLS_DC)

{

php_stream *stream = NULL;

php_stream_wrapper *wrapper = NULL;

// ...

// 生成 wrapper 结构体

wrapper = php_stream_locate_url_wrapper(path, &path_to_open, options TSRMLS_CC);

// ...

if (wrapper) {

if (!wrapper->wops->stream_opener) {

php_stream_wrapper_log_error(wrapper, options ^ REPORT_ERRORS TSRMLS_CC,

"wrapper does not support stream open");

} else {

// 通过结构体中的 wops 中的 stream_opener 指向的函数完成流结构体的生成工作

stream = wrapper->wops->stream_opener(wrapper,

path_to_open, mode, options ^ REPORT_ERRORS,

opened_path, context STREAMS_REL_CC TSRMLS_CC);

}

// ...

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

1

2

3

4

5

6

7

8

9

10

11PHPAPI php_stream_wrapper *php_stream_locate_url_wrapper(const char *path, char **path_for_open, int options TSRMLS_DC)

{

// ...

if (!protocol || !strncasecmp(protocol, "file", n)){

/* fall back on regular file access */

php_stream_wrapper *plain_files_wrapper = &php_plain_files_wrapper;

// ...

return plain_files_wrapper;

}

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19static php_stream_wrapper_ops php_plain_files_wrapper_ops = {

php_plain_files_stream_opener,

NULL,

NULL,

php_plain_files_url_stater,

php_plain_files_dir_opener,

"plainfile",

php_plain_files_unlink,

php_plain_files_rename,

php_plain_files_mkdir,

php_plain_files_rmdir,

php_plain_files_metadata

};

php_stream_wrapper php_plain_files_wrapper = {

&php_plain_files_wrapper_ops,

NULL,

0

};

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

1

2

3

4

5

6

7

8

9static php_stream *php_plain_files_stream_opener(php_stream_wrapper *wrapper, char *path, char *mode,

int options, char **opened_path, php_stream_context *context STREAMS_DC TSRMLS_DC)

{

if (((options & STREAM_DISABLE_OPEN_BASEDIR) == 0) && php_check_open_basedir(path TSRMLS_CC)) {

return NULL;

}

return php_stream_fopen_rel(path, mode, opened_path, options);

}

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28PHPAPI php_stream *_php_stream_fopen(const char *filename, const char *mode, char **opened_path, int options STREAMS_DC TSRMLS_DC)

{

// ...

fd = open(realpath, open_flags, 0666);

if (fd != -1){

if (options & STREAM_OPEN_FOR_INCLUDE) {

// 最终都会调用这一函数

ret = php_stream_fopen_from_fd_int_rel(fd, mode, persistent_id);

} else {

// 注意此处,ret即生成的流结构,即最初实现方法中的stream变量的值

ret = php_stream_fopen_from_fd_rel(fd, mode, persistent_id);

}

if (ret){

// ...

return ret;

}

close(fd);

}

efree(realpath);

if (persistent_id) {

efree(persistent_id);

}

return NULL;

}

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35static php_stream *_php_stream_fopen_from_fd_int(int fd, const char *mode, const char *persistent_id STREAMS_DC TSRMLS_DC)

{

php_stdio_stream_data *self;

self = pemalloc_rel_orig(sizeof(*self), persistent_id);

memset(self, 0, sizeof(*self));

self->file = NULL;

self->is_pipe = 0;

self->lock_flag = LOCK_UN;

self->is_process_pipe = 0;

self->temp_file_name = NULL;

self->fd = fd;

// php_stream_stdio_ops 就是我们想要找到ops操作体

return php_stream_alloc_rel(&php_stream_stdio_ops, self, persistent_id, mode);

}

PHPAPI php_stream *_php_stream_alloc(php_stream_ops *ops, void *abstract, const char *persistent_id, const char *mode STREAMS_DC TSRMLS_DC) /*{{{ */

{

php_stream *ret;

ret = (php_stream*) pemalloc_rel_orig(sizeof(php_stream), persistent_id ? 1 : 0);

memset(ret, 0, sizeof(php_stream));

ret->readfilters.stream = ret;

ret->writefilters.stream = ret;

// ...

// ops即 _php_stream_fopen_from_fd_int 传入的 php_stream_stdio_ops

ret->ops = ops;

// ...

return ret;

}

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

1

2

3

4

5

6

7

8

9PHPAPI php_stream_opsphp_stream_stdio_ops = {

php_stdiop_write, php_stdiop_read,

php_stdiop_close, php_stdiop_flush,

"STDIO",

php_stdiop_seek,

php_stdiop_cast,

php_stdiop_stat,

php_stdiop_set_option

};

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23static size_t php_stdiop_write(php_stream *stream, const char *buf, size_t count TSRMLS_DC)

{

php_stdio_stream_data *data = (php_stdio_stream_data*)stream->abstract;

assert(data != NULL);

if (data->fd >= 0) {

// 最终调用 write 系统调用

int bytes_written = write(data->fd, buf, count);

if (bytes_written < 0) return 0;

return (size_t) bytes_written;

} else {

#if HAVE_FLUSHIO

if (!data->is_pipe && data->last_op == 'r') {

fseek(data->file, 0, SEEK_CUR);

}

data->last_op = 'w';

#endif

return fwrite(buf, 1, count, data->file);

}

}

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

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多进程 写入文件_PHP多进程中使用file_put_contents安全吗?相关推荐

  1. python多进程读写文件_Python多进程写文件时的一些探究

    问题提出 在没有并发控制的情况下,Python多进程向同一个文件写数据(限制单次写入数据大小)是安全的吗? 这里的安全是指: 不会有进程的日志丢失(被覆盖) 两次写入的数据不会相互混着输出(譬如A进程 ...

  2. double 二进制 java_C#中将double值变成二进制然后写入文件,Java中载入该文件读取此二进制double值时不正确...

    目前已定位到是因为C#中的byte范围是0到255,而java中byte值为-128到127导致的错误. 尝试过使用C#的sbyte来解决: bw1 = new BinaryWriter(new Fi ...

  3. python多进程写入mysql_Python实现 多进程导入CSV数据到 MySQL

    前段时间帮同事处理了一个把 CSV 数据导入到 MySQL 的需求.两个很大的 CSV 文件, 分别有 3GB.2100 万条记录和 7GB.3500 万条记录.对于这个量级的数据,用简单的单进程/单 ...

  4. 【Java-IO】File、搜索删除剪切、字符集、字符编码、字节流、将内存中的数据写入文件、字符流、缓冲流、Scanner、格式化输出、数据流、对象流、序列化与反序列化、Files工具类

    IO 文章目录 IO 简介 File 分隔符.大小写 常用方法 练习:搜索.删除.剪切 字符集(Character Set) 字符编码(Character Encoding) 字符编码比较 乱码 字节 ...

  5. python将字符串s和换行符写入文件fp_Python 文件操作

    1. 字符简介 字符:无论什么语言,独立的一个文字就是一个字符 存储单位: Byte 字节 bit 位 1B = 8b B:字节,1 Byte = 8 bit 字符大小: 任何字符集:英文和数字都是一 ...

  6. ae渲染出现错误是什么问题_After Effects错误:写入文件.....时发生渲染错误.输出模块失败.文件可能已损坏。(-1610153464)...

    我来回答一下,你在电脑里安装了其他下载的aex文件格式的插件,你只要把你这些插件删除掉,问题就可以解决,(安装插件不正确,或者有相同的插件也出现提示框)其实,这个提示不重要,你正常开启AE以后,正常使 ...

  7. java写入html,java如何写入文件

    java如何追加写入txt文件 BufferedWriter bw = new BufferedWriter (new OutputStreamWriter (newjava中,对文件进行追加内容操作 ...

  8. java 高性能读写文件_Java写入文件的性能详细分析

    前言 众所周知,Java中有多种针对文件的操作类,以面向字节流和字符流可分为两大类,这里以写入为例: 面向字节流的:FileOutputStream 和 BufferedOutputStream 面向 ...

  9. django 日志多个服务连接_Django多进程日志文件问题

    Django多进程日志文件问题 最近使用Django做一个项目.在部署的时候发现日志文件不能滚动(我使用的是RotatingFileHandler),只有一个日志文件. 查看Log发现一个错误消息:P ...

最新文章

  1. Kotlin威胁、Python逆袭,2018年程序员需要升级哪些技能?(附报告下载)
  2. python中用来捕获异常的是_python – 在一行中捕获多个异常(块除外)
  3. 在ThinkPad W500 A98上升级Windows 7以及安装硬件驱动和相关程序(2/2)
  4. ddos ***之 SYN Flood
  5. 【转】CT层厚、层间距、层间隔的概念是什么,MRI的层厚、层间距、曾间隔是什么
  6. 设计模式之 --- 工厂模式(下)
  7. .net winfrom 定义全局快捷键!
  8. Linux下rpm安装git
  9. 整理了70个Python实战项目列表,都有完整且详细的教程
  10. python怎么调用类中的函数_类中的python函数调用
  11. 【运维】linux shell 编程之函数使用
  12. 你印象中的程序员是什么样子的?
  13. VUE项目(仿商城)
  14. 天猫精灵如何和我们聊天?
  15. 京东关于区块链的发展历程
  16. 最近工作中遇到的问题和解决
  17. 判断浏览器是否是 IE 及 IE8 以下版本
  18. Java 8 新特性,Optional介绍 | 春松客服
  19. 计算机发展有四代 每一代的特点是什么,计算机发展经历了哪几代?每一代各有什么特点?...
  20. 通过IPV6上QQ及自建IPV6代理的方法

热门文章

  1. 轻量级的web框架[Nancy On .Net Core Docker]
  2. Nginx- 实现跨域访问
  3. sql将html转成excel,使用SQL*PLUS,构建完美excel或html输出
  4. [转]IPython介绍
  5. ffmpeg源码分析及mp4文件解析
  6. 解决ArcGIS 9.3卸载时出现invalid install.log file的方法
  7. JavaScript三种弹出框(alert,confirm和prompt)用法举例
  8. C和指针之函数之把数字字符串转为整数并且返回这个数字(ascii_to_integer)
  9. 《看聊天记录都学不会C语言?太菜了吧》(12)循环有多容易?你看一眼就怀...
  10. (附)python3 只需3小时带你轻松入门——python常用一般性术语或词语的简单解释