1. 日志(log)
為了讓自己的思路更加清晰,下面我都會稱日志為 log,因為日志這個詞有兩種含義,詳情見百度百科釋義或者維基百科釋義,
- 日記的另一種說法,“志”字本身為“記錄”的意思,日志就為每日的記錄(通常是跟作者有關的),
- 服務器日志(server log),記錄服務器等電腦設備或軟體的運作,
我們這里說的當然是服務器日志,也就是 server log ,
2. 寫入 log
一般寫入 log 都會遵循以下步驟:
int fd = open(path)
write(fd, sign_append) fclose(fd)
解釋一下上面的代碼:
1. int fd = open(path)
會通過系統呼叫打開一個檔案描述符,或者在其他語言中也可以稱作資源描述符,資源型別,或句柄,
2. write(fd, append = 1)
write 系統呼叫,并加上 append 標志,會執行 seek 和 write 兩個系統呼叫,但是這種系統呼叫是原子性的,
原子性意味著 seek 和 write 會同時執行,不會有兩個執行緒產生交叉,必須 a 執行緒執行完 seek 和 write ,b 執行緒才能繼續執行(這里說執行緒,是因為執行緒才是 cpu 調度的基本單位),
所以在 nginx 中,我們加上 append 標志,就不用對執行緒上鎖了,
3. fclose(fd)
關閉描述符,
linux 一般對打開的檔案描述符有一個最大數量的限制,如果不關閉描述符,很有可能造成大 bug,
查看 linux 中限制的方法如下(其中 open files 代表可以打開的檔案數量):
$ ulimit -a core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 15732 max locked memory (kbytes, -l) 64 max memory size (kbytes, -m) unlimited open files (-n) 1024 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 8192 cpu time (seconds, -t) unlimited max user processes (-u) 15732 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited

所以,如果是系統呼叫,那么 append 不用加鎖,
3. 為什么 php 語言寫日志時用了 append 也要加鎖?
如果根據上面的說法,咱們可以設定好 write 的 append 標志,然后就可以睡大覺去了,檔案永遠不會沖突,
但是(一般都有個但是)你去看 php 的框架中都會在 file_put_contents 的 append 之前加鎖,
于是,懷疑是因為 file_put_contents 的底層實作沒有實作原子性,
3.1 跟進原始碼(非 php 程式員或者對 php 底層原始碼無興趣的可以跳過了):
file_put_contents 底層實作:
// file.c /* {{{ proto int|false file_put_contents(string file, mixed data [, int flags [, resource context]]) Write/Create a file with contents data and return the number of bytes written */ PHP_FUNCTION(file_put_contents) { ... 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, E_WARNING, "Only %zd of %zd bytes written, possibly out of free disk space", numbytes, Z_STRLEN_P(data)); numbytes = -1; } } break; ... } // php_streams.h PHPAPI ssize_t _php_stream_write(php_stream *stream, const char *buf, size_t count); #define php_stream_write_string(stream, str) _php_stream_write(stream, str, strlen(str)) #define php_stream_write(stream, buf, count) _php_stream_write(stream, (buf), (count)) // streams.c PHPAPI ssize_t _php_stream_write(php_stream *stream, const char *buf, size_t count) { ... if (stream->writefilters.head) { bytes = _php_stream_write_filtered(stream, buf, count, PSFS_FLAG_NORMAL); } else { bytes = _php_stream_write_buffer(stream, buf, count); } if (bytes) { stream->flags |= PHP_STREAM_FLAG_WAS_WRITTEN; } return bytes; } /* Writes a buffer directly to a stream, using multiple of the chunk size */ static ssize_t _php_stream_write_buffer(php_stream *stream, const char *buf, size_t count){ ... while (count > 0) { ssize_t justwrote = stream->ops->write(stream, buf, count); if (justwrote <= 0) { /* If we already successfully wrote some bytes and a write error occurred * later, report the successfully written bytes. */ if (didwrite == 0) { return justwrote; } return didwrite; } buf += justwrote; count -= justwrote; didwrite += justwrote; /* Only screw with the buffer if we can seek, otherwise we lose data * buffered from fifos and sockets */ if (stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0) { stream->position += justwrote; } } } // php_streams.h /* operations on streams that are file-handles */ typedef struct _php_stream_ops { /* stdio like functions - these are mandatory! */ ssize_t (*write)(php_stream *stream, const char *buf, size_t count); ssize_t (*read)(php_stream *stream, char *buf, size_t count); int (*close)(php_stream *stream, int close_handle); int (*flush)(php_stream *stream); const char *label; /* label for this ops structure */ /* these are optional */ int (*seek)(php_stream *stream, zend_off_t offset, int whence, zend_off_t *newoffset); int (*cast)(php_stream *stream, int castas, void **ret); int (*stat)(php_stream *stream, php_stream_statbuf *ssb); int (*set_option)(php_stream *stream, int option, int value, void *ptrparam); } php_stream_ops; // plain_wrapper.c static ssize_t php_stdiop_write(php_stream *stream, const char *buf, size_t count) { php_stdio_stream_data *data = https://www.cnblogs.com/wudanyang/p/(php_stdio_stream_data*)stream->abstract; assert(data != NULL); if (data->fd >= 0) { #ifdef PHP_WIN32 ssize_t bytes_written; if (ZEND_SIZE_T_UINT_OVFL(count)) { count = UINT_MAX; } bytes_written = _write(data->fd, buf, (unsigned int)count); #else ssize_t bytes_written = write(data->fd, buf, count); #endif if (bytes_written < 0) { if (errno == EWOULDBLOCK || errno == EAGAIN) { return 0; } if (errno == EINTR) { /* TODO: Should this be treated as a proper error or not? */ return bytes_written; } php_error_docref(NULL, E_NOTICE, "write of %zu bytes failed with errno=%d %s", count, errno, strerror(errno)); } return bytes_written; } else { #if HAVE_FLUSHIO if (data->is_seekable && data->last_op == 'r') { zend_fseek(data->file, 0, SEEK_CUR); } data->last_op = 'w'; #endif return (ssize_t) fwrite(buf, 1, count, data->file); } }View Code
這個函式最終呼叫的是函式 php_stdiop_write
函式 _php_stream_write_buffer 中會將字串分成多個 chunksize ,每個 chunksize 為 8192 (8K) 位元組,分別進行 write,
如果不加鎖,那么超過 8192 位元組之后,多個行程寫日志就會出現混亂,
而且,php 檔案也說明了:


最近看到了另外一種防止多次系統呼叫的方法:使用 stream_set_chunk_size 函式進行控制一次寫入的 chunksize
<?php $str = str_pad('a', 9000,'-'); // file_put_contents('stream_size.txt', $str); $f = fopen('stream_size.txt', 'w'); stream_set_chunk_size($f, 9000); fwrite($f, $str);

可以看到,寫入的時候現在只有一個系統呼叫了
3.2 系統呼叫跟進法 (2020年03月18日更新)
因為已經知道了 file_put_contents 會將內容分成 8192 位元組的資料,所以我們跟進一下 php 在做系統呼叫的時候,是怎么做的,貼一下測驗代碼
<?php file_put_contents('a.log', str_pad('111', 8192, '-'));
file_put_contents('a.log', str_pad('111', 8199, '-'));
shell:
$ sudo strace php file_put.php
輸出如下

所以,最終需要根據不同的語言,具體分析,
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/144534.html
標籤:Linux
上一篇:C、C++和C#區別概述
