php多进程 写入文件_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 是如何解决的呢?

答案是文件锁。

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? 可以读读。

参考

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/305808.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

虚拟机概述[转贴]

[摘要]描述什么是虚拟机、及运行在各种平台上的多种多样的虚拟机软件PXE2写著 你一定接触过各种各样的虚拟机&#xff0c;Vmware&#xff0c;VirtualPC&#xff0c;甚至JVM或是VBRunDLL&#xff0c;等等等等&#xff0c;在数字的信息世界里虚拟和现实是如此的难解难分。你一定接…

ABP vNext分布式事件总线RabbitMQ注意事项

[https://docs.abp.io/zh-Hans/abp/latest/Distributed-Event-Bus-RabbitMQ-Integration](ABP vNext官方文档链接)&#xff0c;基本使用可直接阅读官方文档&#xff0c;云怀不重复造轮子&#xff0c;只做官方未提到但重要的说明关键配置说明关键配置类&#xff1a;AbpRabbitMqE…

window运行php环境,Windows环境下使用phpstudy搭建php运行环境

首先在百度百科上对于phpstudy的定义是一个PHP调试环境的程序集成包。该程序包集成最新的ApachePHPMySQLphpMyAdminZendOptimizer,一次性安装&#xff0c;无须配置即可使用&#xff0c;是非常方便、好用的PHP调试环境&#xff0c;该程序不仅包括PHP调试环境&#xff0c;还包括了…

去除代码行号的一个小程序(控制台版本)

清风竹林发布了去除代码行号的一个小程序,确实方便大家收集一些文章代码,但个人认为象这样的小东东&#xff0c;要使广大网友能拿来就用&#xff0c;用.Net 2.0做成WinForm&#xff0c;有点贵族化了&#xff0c;于是动手整出个平民化的控制台版本&#xff0c;可以清除指定的文本…

. NET5实战千万高并发项目,性能吊打JAVA,C#排名万年老五,有望逆袭!

“秒杀活动”“抢红包”“微博热搜”“12306抢票”“共享单车拉新”等都是高并发的典型业务场景&#xff0c;那么如何解决这些业务场景背后的难点问题呢&#xff1f;秒杀系统中&#xff0c;QPS达到10万/s时&#xff0c;如何定位并解决业务瓶颈&#xff1f;明星婚恋话题不断引爆…

java不朽神迹,不朽的神迹 Eternal Legacy HD v1.0.8

游戏简介不朽的神迹是一个拥有全3D实时渲染的史诗战斗场面、360度自由调整的视角及丰富的动作特效的游戏。玩家将在游戏中探索壮丽的大陆&#xff0c;体验张力十足的战斗系统。游戏提供了多至3名角色同时参战&#xff0c;可从队伍成员中选择出战的队友&#xff0c;并且定义他们…

ABP vNext 审计日志获取真实客户端IP

背景在使用ABP vNext时&#xff0c;当需要记录审计日志时&#xff0c;我们按照https://docs.abp.io/zh-Hans/abp/latest/Audit-Logging配置即可开箱即用&#xff0c;然而在实际生产环境中&#xff0c;某些配置并不可取&#xff0c;比如今天的主角——客户端IP&#xff0c;记录用…

郭昶

郭 昶左直拳饰演《外来媳妇本地郎》中康家老二康祁宗的演员郭昶6月14日去世了&#xff0c;胃癌&#xff0c;享年50岁。这个消息真令人难以置信&#xff0c;不胜嘘唏。 《外来媳妇本地郎》在广东这边很受欢迎&#xff0c;每集结尾那带有浓厚岭南特色的粤曲小调在胡同小巷时有…

php 常用rpc框架,php的轻量级rpc框架yar

php的轻量级rpc框架yar目的&#xff1a;类方法的远程调用,也就是一个rpc请求。RPC本质上也是一个网络请求&#xff0c;既然是请求&#xff0c;对于效率来说&#xff0c;就需要考虑了。yar是基于http来做的。使用场景&#xff1a;多个项目共享model总的来说这种调用代价挺好的&a…

ABP vNext IOC替换原有Service实现

即 .NET IOC替换原有Service实现背景在使用ABP vNext时&#xff0c;该框架为我们实现了非常多的默认行为&#xff0c;以便开箱即用&#xff0c;但在实际使用中&#xff0c;我们总是需要根据自己的需求定制自己的服务&#xff0c;在.Net框架中&#xff0c;便提供了Service.Repla…

aqs java 简书,Java AQS源码解读

1、先聊点别的说实话&#xff0c;关于AQS的设计理念、实现、使用&#xff0c;我有打算写过一篇技术文章&#xff0c;但是在写完初稿后&#xff0c;发现掌握的还是模模糊糊的&#xff0c;模棱两可。痛定思痛&#xff0c;脚踏实地重新再来一遍。这次以 Java 8源码为基础进行解读。…

仓储模式到底是不是反模式?

【导读】仓储模式我们已耳熟能详&#xff0c;但当我们将其进行应用时&#xff0c;真的是那么得心应手吗&#xff1f;确定是解放了生产力吗&#xff1f;这到底是怎样的一个存在&#xff0c;确定不是反模式&#xff1f;一篇详文我们探讨仓储模式&#xff0c;这里仅我个人的思考&a…

网络工程师必须懂的十五大专业术语!

1、什么时候使用多路由协议&#xff1f; 当两种不同的路由协议要交换路由信息时&#xff0c;就要用到多路由协议。当然&#xff0c;路由再分配也可以交换路由信息。下列情况不必使用多路由协议&#xff1a; 从老版本的内部网关协议&#xff08; Interior Gateway Protocol&…

dnSpy反编译、部署调试神器

一、概要在工作当中&#xff0c;当程序部署了之后就算打了日志遇到极个别的特殊异常没有在程序日志中体现出来或者没有详细的报错原因会让开发者非常头疼&#xff0c;不得不盲猜bug到底出在哪里。这里分享一下工作上经常会用到的工具&#xff0c;这款工具可以反编译并运行调试已…

java中内边距跟外边距,padding和margin——内边距和外边距

一、padding——内边距(内填充)1.1、padding 简写属性在一个声明中设置所有填充属性。该属性可以有1到4个值。div.outer{width: 400px;height: 400px;border: 2px solid #000;}div.inner{width: 200px;height: 200px;background-color:red ;padding: 50px;}运行效果图&#xff…

AJAX将成为移动Web2.0时代首选开发平台

一、 引言  最近,Opera宣布通过他们的浏览器把AJAX技术应用于移动设备开发中。考虑到Opera浏览器在目前浏览器市场(特别是在移动浏览器市场)的流行性,我们可以预计这一宣布对于整个浏览器市场必然会产生重要影响。从加入到移动服务开发市场几年的经验来看&#xff0c;我相信现…

matlab仿真习题,(MATlab仿真部分习题答案.doc

(MATlab仿真部分习题答案[4.1]控制系统结构如图4.1所示利用MATLAB对以上单位负反馈控制系统建立传递函数&#xff1b;将第一问中求得的传递函数模型转化为零极点增益形式和状态空间形式。解:(1)num[2 2];den[1 2 1];[num1,den1]cloop(num,den);systf(num1,den1)程序运行结果如下…

使用 ML.NET 实现峰值检测来排查异常

机器学习中一类问题称为峰值检测&#xff0c;它旨在识别与大部分时序中明显不同但临时突发的数据值。及时检测到这些可疑的个体、事件或观察值很重要&#xff0c;这样才能尽量减少其产生。异常情况检测是检测时序数据离群值的过程&#xff0c;在给定的输入时序上指向“怪异”或…

如何使用Tasklist命令

Tasklist命令用来显示运行在本地或远程计算机上的所有进程&#xff0c;带有多个执行参数。使用格式Tasklist [/S system [/U username [/P [password]]]] [/M [module] | /SVC | /V] [/FI filter] [/FO format] [/NH]参数含义/S system 指定连接到的远程系统。/U [domain]user…

PHP防QQ列表右划,react native 实现类似QQ的侧滑列表效果

如果列表行数据需要更多的操作&#xff0c;使用侧滑菜单是移动端比较常见的方式&#xff0c;也符合用户的操作习惯&#xff0c;对app的接受度自然会相对提高点。最近得空就把原来的react-native项目升级了侧滑操作&#xff0c;轻轻松松支持android和ios双平台&#xff0c;效果如…