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? 可以讀讀。
參考
總結
以上是生活随笔為你收集整理的php多进程 写入文件_PHP多进程中使用file_put_contents安全吗?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 711 便利店宣布明年起在日本市场大规模
- 下一篇: 丰田新能源车换标志,添加蓝色圆点以示区别