既然都整到半夜了,那就顺便把这段时间碰到的一个问题整理成博客记录下来吧,其间也和一位好朋友讨论了很久这个问题,最后也终于找到了原因,如果文章中有我理解有误的地方,欢迎指正。

一次为项目中的内部日志库改动后,在本地的测试中发现了日志文件居然有写乱的情况发生,从直觉上来说,这大概率是由并发问题导致的,但是这个日志文件我们是使用O_APPEND模式打开的,这与前辈们口口相传的“ O_APPEND 模式是原子”相违背。为了方便调试这个问题,我先把这个问题简化成了一个可稳定复现的PHP脚本:

/**

* author: LiZhiYang

* email: zhiyanglee@foxmail.com

*/

define('BUFF_SIZE', 8193 - 1);

define('WORKER_NUMS', 20);

define('WORKER_PER_LINES', 50);

$fileName = "sample.txt";

if (file_exists($fileName)) {

unlink($fileName);

}

/**

* 创建20个进程模拟并发写

*/

$pids = [];

for ($i = 0; $i < WORKER_NUMS; $i++) {

$pid = pcntl_fork();

if ($pid == -1) {

echo "pcntl_fork error.\n";

} else if ($pid) {

$pids[] = $pid;

} else {

$appendFd = fopen($fileName, "a+");

/**

* $i + 65得到一个字母编码,并重复BUFF_SIZE次构成一行

*/

$buf = '';

$ch = pack("C", $i + 65);

$buf = str_repeat($ch, BUFF_SIZE);

$buf .= "\n";

for ($i = 0; $i < WORKER_PER_LINES; $i++) {

fwrite($appendFd, $buf);

}

fclose($appendFd);

exit(0);

}

}

/**

* 等待所有子进程完成并发写

*/

$pidSeq = 1;

$curPid = posix_getpid();

foreach ($pids as $pid) {

echo "cur_pid:{$curPid} seq:{$pidSeq} wait {$pid}\n";

pcntl_waitpid($pid, $status);

$pidSeq++;

}

/**

* 因为一行的字母都是一样的,如果一个字母在那一行没有重复BUFF_SIZE次,则代表

* 写入时出现了写乱的情况

*/

$line = 1;

$fd = fopen($fileName, "r");

while (($row = fgets($fd)) !== false) {

$firstChar = $row[0];

if (!preg_match('/^' . $firstChar . '{' . BUFF_SIZE. '}$/', $row)) {

echo "line:{$line} concurrent error.\n";

exit(-1);

} else {

echo "line:{$line} pass.\n";

}

$line++;

}

fclose($fd);

在我的反复测试中,只要大于8192这个值,就会稳定的出现写乱的情况,而低于或者等于这个值则没有任何问题。

我第一个想到的是,O_APPEND模式的原子写入的数据也许有一个大小上限,所以开始查询write 调用的文档,而write调用的文档中提到了这么一句话:

(On Linux, PIPE_BUF is 4096 bytes.) So in Linux the size of an atomic write is 4096 bytes.

这个PIPE_BUF可以通过ulimit -a看到,其中 pipe size 一行就是PIPE_BUF的大小,在我本机Mac上是512字节,在Linux是4KB(4096字节),但很明显的是,这与我前面试出来的8192值,明显不一样。

后面我朋友使用strace 跟踪了一次PHP写入10240字节数据时的系统调用,PHP脚本:

$fileName = "test_fwrite.log";

if (file_exists($fileName)) {

unlink($fileName);

}

$str = str_repeat('a', 10240);

$fd = fopen($fileName,"a+");

fwrite($fd, $str);

echo "ok\n";

跟踪系统调用的结果如下:

fstat(4, {st_mode=S_IFREG|0666, st_size=0, ...}) = 0

lseek(4, 0, SEEK_CUR) = 0

lseek(4, 0, SEEK_CUR) = 0

write(4, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 8192) = 8192

write(4, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"..., 2048) = 2048

write(1, "ok\n", 3) = 3

close(4) = 0

close(2) = 0

close(1) = 0

可以很明显的看到PHP分成了两次调用write 去写入,而一次写入的上限从第一次write可以看出是8192字节,和我前面试出来的值刚好一致。这个时候就需要去PHP内核确定一下它具体的流程,通过断点调试定位了PHP的 fwrite最终调用实现在 main/streams/streams.c(1122行)中:

/* 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) {

size_t towrite = count;

if (towrite > stream->chunk_size)

towrite = stream->chunk_size;

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

..........

}

return didwrite;

}

可以看到,PHP内部做了一次分割,一次最大只可以写入stream->chunk_size大小的数据,这就解释了前面跟踪系统调用时为什么看到了两次write系统调用,因为PHP内部会按照stream->chunk_size这个最大值来切割要写入的数据,然后分批写入。

那么stream->chunk_size这个值又在哪定义的呢,同样也是通过断点调试,赋值是在ext/standard/file.c(148行):

static void file_globals_ctor(php_file_globals *file_globals_p)

{

memset(file_globals_p, 0, sizeof(php_file_globals));

file_globals_p->def_chunk_size = PHP_SOCK_CHUNK_SIZE;

}

而PHP_SOCK_CHUNK_SIZE 则定义在 main/php_network.h(222行)

#define PHP_SOCK_CHUNK_SIZE 8192

看到PHP内核会把超过8192字节的数据分批写入后,就明白了为什么O_APPEND也会出现写乱的情况,我们可以将追加模式(O_APPEND)下的write的调用大概简化成下面这个流程:

锁定文件inode->写入前重新获取文件大小并设置为当前写入偏移量->开始写入数据->解锁文件inode

可以看到,一次追加写入调用是原子的,但是如果你将这一次写入的数据分为了两次调用:

第一个追加写操作:锁定文件inode->写入前重新获取文件大小并设置为当前写入偏移量->开始写入数据(8192个字节)->解锁文件inode

第二个追加写操作:锁定文件inode->写入前重新获取文件大小并设置为当前写入偏移量->开始写入数据(2048个字节)->解锁文件inode

那么在第一个追加写8192个字节后,第二个追加写2048个字节的操作可能并不会马上执行,因为受Linux内核的调度,在执行第二个追加写操作的时候,中间可能会穿插了别的进程的追加写操作,所以会出现O_APPEND模式下也出现了写操作错乱的情况。

但是我朋友后面发现PHP内核中的master分支,已经把这个按照最大8192字节分批写入的逻辑给移除了,也就是说后面新的PHP版本的追加写就不存在这个因为超过8192字节会写乱的情况了,但现有的PHP版本如7.1、7.2等还是会有这种情况。

Don't use chunking for stream writes

We're currently splitting up large writes into 8K size chunks, which

adversely affects I/O performance in some cases. Splitting up writes

doesn't make a lot of sense, as we already must have a backing buffer,

so there is no memory/performance tradeoff to be made here.

This change disables the write chunking at the stream layer, but

retains the current retry loop for partial writes. In particular

network writes will typically only write part of the data for large

writes, so we need to keep the retry loop to preserve backwards

compatibility.

If issues due to this change turn up, chunking should be reintroduced

at lower levels where it is needed to avoid issues for specific streams,

rather than unnecessarily enforcing it for all streams.

到最后我也非常好奇,那么一次write调用能够原子写入的大小到底有多大呢,因为我发现Nginx也是用O_APPEND打开后,直接就调用write进行日志写入,并没有额外的同步机制。我用C也实现了一遍上面PHP复现脚本的逻辑,发现不管写多大都不会乱:

#include

#include

#include

#include

#include

#include

#include

#include

void print_error_and_exit(int exit_code)

{

fprintf(stderr, "%s\n", strerror( errno ));

exit(exit_code);

}

void check_rt(int rt)

{

if (rt < 0) {

print_error_and_exit(-1);

}

}

int main(int argc,char **argv) {

size_t num_workers = 20;

size_t lines_per_worker = 50;

size_t buf_len = 0;

if (argc > 1) {

size_t input_buf_len = atoi(argv[1]);

if (input_buf_len > 0) {

buf_len = input_buf_len;

printf("set buf length to input value:%zu\n", input_buf_len);

}

}

if (buf_len <= 0) {

printf("set buf length to default value:4096.\n");

buf_len = 4096;

}

pid_t pids[num_workers];

for (size_t i = 0; i < num_workers; i++)

{

pid_t pid = fork();

if (pid == -1) {

printf("fork error.\n");

} else if (pid) {

pids[i] = pid;

} else {

int fd = open("./sample.txt", O_WRONLY|O_CREAT|O_APPEND);

check_rt(fd);

char c = i + 65;

char buf[buf_len];

for (size_t i = 0; i < (buf_len - 1); i++)

{

buf[i] = c;

}

buf[buf_len - 1] = '\n';

for (size_t i = 0; i < lines_per_worker; i++) {

int r = write(fd, &buf, buf_len);

check_rt(r);

}

exit(0);

}

}

for (size_t i = 0; i < num_workers; i++)

{

pid_t pid = pids[i];

int status;

printf("wating process[%d]\n", pid);

waitpid(pid, &status, WUNTRACED|WCONTINUED);

}

}

我这里的C程序没有实现最后的验证逻辑,我是把PHP复现脚本底下的验证逻辑单独写成一个脚本,来验证这个C程序输出的文件,你同样可以这也做(主要是懒没写完。

抱着试一试的心态,我去翻了一下Linux内核文件系统的源代码,当应用层调用write时流程如下:

write -> _libc_write -> ksys_write -> vfs_write -> [具体的文件系统实现] -> write_iter

ext4的写入实现如下:

static ssize_t

ext4_file_write_iter(struct kiocb *iocb, struct iov_iter *from)

{

.........

if (!inode_trylock(inode)) {

if (iocb->ki_flags & IOCB_NOWAIT)

return -EAGAIN;

inode_lock(inode);

}

.........

out:

inode_unlock(inode);

return ret;

}

对Linux内核不是很熟悉,但就这么看的话,似乎在所有写流程(阻塞同步写为例子)开始之前,会先上锁文件的inode(“一般情况下,一个文件对应一个inode),那么其实可以理解为一次write 调用就是原子的?

但总得来说这一次问题追踪还是学习到挺多的.......

php 原子性,PHP下O_APPEND模式的原子性相关推荐

  1. 帧中继环境下NBMA模式的配置

    帧中继环境下NBMA模式的配置 1.  实验目的: 通过本次的实验,我们可以掌握如下技能 1)        帧中继静态映射及其broadcast参数的含义. 2)        NBMA模式下的DR ...

  2. linux下文本模式不能登录,图形可以登录

    问题描述 : 输入用户名密码后弹回,重复提示用户输入行,表示不登陆!!! 问题出现前的操作 : 在图形界面将启动配置文件中的启动模式由runlevel 5 改为 3,然后重启电脑. 解决方式 : 在项 ...

  3. linux下桥接模式设置静态IP实现上网

    桥接网络连接模式的虚拟机就当作主机所在以太网的一部分,虚拟系统和宿主机器的关系,就像连接在同一个Hub上的两台电脑,可以像主机一样可以访问以太网中的所有共享资源和网络连接,可以直接共享主机网络的互联网 ...

  4. 如何盘活新零售5大线上线下交互模式?拥抱用户与收益增长

    新零售各类应用模式层出不穷,花样迭出.即使如此,细化到不同模式,普及度与效果,以及用户接受度也各有不同.新零售模式究竟有哪些?本篇,我们回归新零售模式本质,解析如何盘活5大典型线上线下交互模式,拥抱用 ...

  5. Lumerical---FDE和模式光源下寻找模式的技巧

    Lumerical---FDE和模式光源下寻找模式的技巧 引言 应用场景 采取措施 名称解释 引言 许多FDE本征模式求解器的设置都会影响模式寻找.本教程旨在指导在一些情境下当你找寻不到正确的模式或者 ...

  6. 360wifi linux ad hoc,360随身wifi支持Windows XP下ad-hoc模式吗

    360随身wifi支持Windows XP下ad-hoc模式吗 360随身wifi不支持Windows XP下ad-hoc模式,但支持AP模式. 360随身wifi"瞄准的主要是学校.办公室 ...

  7. 《唱吧CEO陈华:“下大雪”模式倒逼新员工快速成长》读后感

    <唱吧CEO陈华:"下大雪"模式倒逼新员工快速成长>读后感 唱吧CEO陈华:"下大雪"模式倒逼新员工快速成长 一.用好中层员工. "所以, ...

  8. 计算机电源计划为节能模式,Win10下电源模式会自动更改为“节能”模式的解决方法...

    将电脑系统升级到win10正式版后,会体验到不少新功能,但是同时可能也会碰到一些新问题.比如,有位用户反馈自己的电脑升级win10后,每次开机或重启,电源模式都会自动更改为"节能" ...

  9. 线上线下O2O模式为什么会这么火呢?

    "互联网+"在引入到传统产业的过程中,更强调用户体验.这也意味着传统产业将有机会为用户创造新的价值,寻找到新的价值点,为进一步发展奠定基础.那么,线上线下O2O模式为什么会这么火呢 ...

最新文章

  1. 皮一皮:时代不同了...
  2. 4.2 One-Shot 学习-深度学习第四课《卷积神经网络》-Stanford吴恩达教授
  3. java中多个输入框搜索_如何在一个搜索框中输入多个字段的值进行查询?
  4. Java并发编程之AbstractQueuedSynchronizer(AQS)源码解析
  5. linux设置蓝牙可连接网络,Linux下蓝牙参数设置程序
  6. VMware中linux访问共享文件夹设置流程
  7. Visual Studio 2015开发Android App问题集锦
  8. 插件开发之360 DroidPlugin源码分析(二)Hook机制
  9. 采用ArcGIS 10.6制作漂亮的点阵世界地图,完美!!!
  10. 解决MATLAB的xlsread函数读取表格失败
  11. Excel图表1——双坐标图(双柱图)
  12. 加密邮箱的数字签名和加密原理
  13. Apache Calcite介绍
  14. 三极管流水灯电路设计
  15. dea_des 简介
  16. mysql rebuild index_批量rebuild索引
  17. strcpy和strncpy用法和区别
  18. 中间服务器代理解决跨域
  19. 在不同操作系统上安装Python的详细教程
  20. DB2密码过期的解决办法-创建新密码

热门文章

  1. 自定义导航--wx.getMenuButtonBoundingClientRect() 万机兼容
  2. SQL语句操作优先级顺序
  3. mac上的Android虚拟机,android虚拟机能在retina MacBook pro上跑吗?
  4. linux简单邮件系统,怎样简单搭建一个Linux操作系统邮件服务器
  5. 计算机基础与应用32页,《计算机基础与应用》2次作业及答案
  6. slot多作用域 vue_vue插槽(slot)详解
  7. html5tab页高德地图,高德地图系列web篇——目的地公交导航
  8. mysql 报错跳过_mysql跳过主从同步错误
  9. idam oracle_oracle中的wm_concat对应达梦的是什么?
  10. 微信小程序引用php函数,微信小程序Page中data数据操作和函数调用详细介绍