程序写日志文件时该不该加锁
程序写日志文件时该不该加锁
日志(log)
为了让自己的思路更加清晰,下面我都会称日志为 log。因为日志这个词有两种含义,详情见百度百科释义或者*释义。
我们这里说的当然是服务器日志,也就是 server log 。
写入 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 不用加锁。
为什么 php 语言写日志时用了 append 也要加锁?
如果根据上面的说法,咱们可以设置好 write 的 append 标志,然后就可以睡大觉去了,文件永远不会冲突。
但是(一般都有个但是)你去看 php 的框架中都会在 file_put_contents 的 append 之前加锁。
于是,怀疑是因为 file_put_contents 的底层实现没有实现原子性。
跟进源码(非 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 = (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); } }
这个函数最终调用的是函数 php_stdiop_write
函数 _php_stream_write_buffer 中会将字符串分成多个 chunksize ,每个 chunksize 为 8192 (8k) 字节,分别进行 write。
如果不加锁,那么超过 8192 字节之后,多个进程写日志就会出现混乱。
而且,php 文档也说明了:
所以,最终需要根据不同的语言,具体分析。