php文件句柄,file_put_contents相关问题以及日志相关

fopen()

fopen(filename,mode,include_path,context)会返回一个文件句柄
文件句柄其实就是一个指针,指针就是指向文件中的某个位置mode参数决定了指针的位置。

fwrite()

fwrite(file,string,length)将字符串写入到文件句柄的指针处。

  • 参数file:必需。fopen()返回的文件句柄
  • 参数string:必需。写入文件的字符串
  • 参数length:可选。规定写入的最大字节数

注意:小于8k的字符串写入文件时不是一个字符一个字符的写入,而是所有字符串一次性全部写入文件。为什么小于8k的会这样?为什么是一次性写入?这些问题下面介绍。

fclose()

fclose(file)关闭文件句柄

  • file:fopen()返回的文件句柄

正常的写入文件代码如下

flock()

flock(file,lock,block)为锁定资源或者释放资源。也称作文件锁

  • file:必需。fopen()返回的文件句柄
  • lock:必需。规定要使用哪种锁定类型
  • block:可选。若设置为 1 或 true,则当进行锁定时阻挡其他进程

lock参数选项如下

  • LOCK_SH:共享锁定,类似mysql的共享锁,可读不可写,是阻塞的
  • LOCK_EX:独占锁定,类似mysql的排他锁,不可读也不可写,是阻塞的,一般使用该类型
  • LOCK_UN:释放锁
  • LOCK_NB:如果不希望flock()在锁定时阻塞,则加上该选项

使用加锁后的普通代码如下

file_put_contents()

file_put_contents()函数是把一个字符串写入到文件中。
与依次调用fopen(),fwrite(),fclose()功能一样。

格式为:file_put_contents(file,data,mode,context)

  • file:文件句柄。规定要写入的文件,文件如果不存在则会创建
  • data: 写入文件的内容,可以是字符串,数组(不能是多维数组)或者数据流
  • mode:规定如何打开/写入文件,值有下面几个选项
    • FILE_APPEND:将文件指针指向文件末尾,用于追加内容。与fopen()中的a模式相同。
    • LOCK_EX:写文件时加锁。与flock()中的LOCK_EX相同。
    • FILE_USE_INCLUDE_PATH:很少用。

追加内容

file_put_contents('./1.txt','ff',FILE_APPEND);

追加内容并加锁

file_put_contents('./1.txt','ff2',FILE_APPEND | LOCK_EX);

要知道:file_put_contents()和fwrite()一样都是:小于8k的字符串在写入文件时不是一个字符一个字符的写入,而是所有字符串于一次性全部写入文件
为什么是一次性写入?
因为每次写入文件都是一次io,io是阻塞耗时的,所以从性能各个方面肯定不会将每次字符都写入一次的,所以是一次性写入的,减少了io次数,相应的也就提高了性能。
为什么小于8k的会这样?
如果写入的内容很大很大,且一次性写入时消耗的性能也是很大的,所以可以分批次写入<=8k的内容,这样消耗可能更低。

使用file_put_contents()写入日志发生日志错乱

现象

高并发情况下,且日志很长大于8k的情况下,日志会发生错乱的现象。
我们可以注意到俩个关键字,高并发日志很长。写入内容大于8k的情况下,内容会分批次写入,也就保证不了原子性了,如果现在有并发情况,就可能在分批次写入这里发生日志错乱的情况

写入内容小于8k的情况下会一次性写入,这也就说明了保证了原子性,所以在高并发的情况下不会发生错乱的情况。

查看file_put_contents()源码

  • 脚本服务写入日志代码如下:
if ($this->isCli == true) {
    return file_put_contents($messageLogFile, $strLogMsg, FILE_APPEND);
}
  • 查看file_put_contents 的源码实现,最终写文件会执行到_php_stream_write_buffer 函数,里面有这样一处代码:

明确几个变量的含义:
count:需写入文件的字符串长度
stream->chunk_size :默认为8192 (8k)

从上面代码可以看出,当写入的字符串长度 大于8192时,则拆为多次<=8192的字符串,分批次写入,打个比方,如果写入的内容是23k,就分三次写入,第一次写入8k,第二次写入8k,第三次7k。

然后调用php_stdiop_write函数写入文件。什么意思呢?

  • php_stdiop_write函数实现如下:
static size_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
        int bytes_written;
        if (ZEND_SIZE_T_UINT_OVFL(count)) {
            count = UINT_MAX;
        }
        bytes_written = _write(data->fd, buf, (unsigned int)count);
#else
        int bytes_written = write(data->fd, buf, count);
#endif
        if (bytes_written < 0) return 0;
        return (size_t) bytes_written;
    } else {

#if HAVE_FLUSHIO
        if (!data->is_pipe && data->last_op == 'r') {
            zend_fseek(data->file, 0, SEEK_CUR);
        }
        data->last_op = 'w';
#endif

        return fwrite(buf, 1, count, data->file);
    }
}

php_stdiop_write 则调用的 write函数 写入文件;write函数是能保证一次写入的完整的

所以日志写串的原因也就能分析出来了,调用链接为:file_put_contents ->_php_stream_write_buffer ->php_stdiop_write(多次调用,每次最多写入8192字节) ->write(),是在 多次调用php_stdiop_write 函数时出的问题;第一次写完,紧接着在高并发的情况下,被其他进程的 write 函数追着写,此时就出现写串,也就是前面示例中日志;

总结:写入内容小于8k时是原子性操作,不用加锁,反之需要。这个8k的限制可以在php中修改的。

加锁代码

由于加锁是阻塞的,在并发时会影响性能,所以写入内容时最好判断下大小是否超过8k,代码如下

 8192){
    file_put_contents('./1.txt',$str,FILE_APPEND | LOCK_EX);
}else{
    file_put_contents('./1.txt',$str,FILE_APPEND);
}

file_put_contents()中的LOCK_EX和flock()的效果是一样的。

加锁后会有死锁的问题吗?

这个锁并没有设定过期时间,那么会不会有死锁的情况呢?比如在执行完加锁还没有到解锁的时候机器宕机,该文件会不会被锁死?
答案是:进程重启或者kill掉该进程后,系统会自动释放这个文件锁。在没重启或者没kill掉进程之前,该文件会被死锁

在多进程模式下,使用file_put_contents()会影响并发吗?

分俩种情况

  • 不加锁的情况
    首先file_put_contents()就是一个阻塞io,所以肯定会阻塞进程的,这点毋庸置疑。
    比如php-fpm一共有10个进程,执行file_put_contents()时会阻塞1s,那么此时最高的qps也就是10/s。只有进程空闲后才会继续处理别的请求。
  • 加锁的情况
$fp = fopen("/home/guoxinhua/php.log", "a+");
if (flock($fp, LOCK_EX)) {  //给日志文件加锁
   //do something
    fwrite($fp, "the huge string\n");
    flock($fp, LOCK_UN);    // 释放锁定
}

或者

file_put_contents("/home/guoxinhua/php.log",'111',FILE_APPEND | LOCK_EX);

比如php-fpm有10个进程,在写入数据时会阻塞1s,而且该文件还被加锁。
第一个请求在写入阻塞了1s,且该文件已加锁。第二个并发请求写入时需要等待第一个请求锁释放才能写入,一次类推,此时qps也就是1/s。

如果前一个请求没有释放文件锁就会导致后面的请求无法获得锁,卡死在获取锁的这一步。如果php-fpm一共10个进程,此时系统最多能处理10个请求,且这10个请求都是阻塞状态。说白了都在阻塞造成的问题,

所以在必须加锁的情况下,我们必须加上LOCK_NB,它可以避免阻塞,也就是说此时的qps也是10/s。


你可能感兴趣的:(php文件句柄,file_put_contents相关问题以及日志相关)