php文件句柄,file_put_contents相關(guān)問(wèn)題以及日志相關(guān)

fopen()

fopen(filename,mode,include_path,context)會(huì)返回一個(gè)文件句柄。
文件句柄其實(shí)就是一個(gè)指針,指針就是指向文件中的某個(gè)位置,mode參數(shù)決定了指針的位置。

fwrite()

fwrite(file,string,length)將字符串寫(xiě)入到文件句柄的指針處。

  • 參數(shù)file:必需。fopen()返回的文件句柄
  • 參數(shù)string:必需。寫(xiě)入文件的字符串
  • 參數(shù)length:可選。規(guī)定寫(xiě)入的最大字節(jié)數(shù)

注意:小于8k的字符串寫(xiě)入文件時(shí)不是一個(gè)字符一個(gè)字符的寫(xiě)入,而是所有字符串一次性全部寫(xiě)入文件。為什么小于8k的會(huì)這樣?為什么是一次性寫(xiě)入?這些問(wèn)題下面介紹。

fclose()

fclose(file)關(guān)閉文件句柄

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

正常的寫(xiě)入文件代碼如下

<?php
//打開(kāi)文件句柄,并將資源綁定到一個(gè)流上
$status=fopen('./1.txt','a+');
fwrite($status,'88'); 
fclose($status);

flock()

flock(file,lock,block)為鎖定資源或者釋放資源。也稱作文件鎖。

  • file:必需。fopen()返回的文件句柄
  • lock:必需。規(guī)定要使用哪種鎖定類型
  • block:可選。若設(shè)置為 1 或 true,則當(dāng)進(jìn)行鎖定時(shí)阻擋其他進(jìn)程

lock參數(shù)選項(xiàng)如下

  • LOCK_SH:共享鎖定,類似mysql的共享鎖,可讀不可寫(xiě),是阻塞的
  • LOCK_EX:獨(dú)占鎖定,類似mysql的排他鎖,不可讀也不可寫(xiě),是阻塞的,一般使用該類型。
  • LOCK_UN:釋放鎖
  • LOCK_NB:如果不希望flock()在鎖定時(shí)阻塞,則加上該選項(xiàng)

使用加鎖后的普通代碼如下

<?php
//打開(kāi)文件句柄,并將資源綁定到一個(gè)流上
$status=fopen('./1.txt','a+');
$lock=flock($status,LOCK_EX);
if($lock){
    fwrite($status,'88');
    flock($status,LOCK_UN);
}
fclose($status);

file_put_contents()

file_put_contents()函數(shù)是把一個(gè)字符串寫(xiě)入到文件中。
與依次調(diào)用fopen(),fwrite(),fclose()功能一樣。

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

  • file:文件句柄。規(guī)定要寫(xiě)入的文件,文件如果不存在則會(huì)創(chuàng)建。
  • data: 寫(xiě)入文件的內(nèi)容,可以是字符串,數(shù)組(不能是多維數(shù)組)或者數(shù)據(jù)流。
  • mode:規(guī)定如何打開(kāi)/寫(xiě)入文件,值有下面幾個(gè)選項(xiàng)
    • FILE_APPEND:將文件指針指向文件末尾,用于追加內(nèi)容。與fopen()中的a模式相同。
    • LOCK_EX:寫(xiě)文件時(shí)加鎖。與flock()中的LOCK_EX相同。
    • FILE_USE_INCLUDE_PATH:很少用。

追加內(nèi)容

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

追加內(nèi)容并加鎖

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

要知道:file_put_contents()和fwrite()一樣都是:小于8k的字符串在寫(xiě)入文件時(shí)不是一個(gè)字符一個(gè)字符的寫(xiě)入,而是所有字符串于一次性全部寫(xiě)入文件。
為什么是一次性寫(xiě)入?
因?yàn)槊看螌?xiě)入文件都是一次io,io是阻塞耗時(shí)的,所以從性能各個(gè)方面肯定不會(huì)將每次字符都寫(xiě)入一次的,所以是一次性寫(xiě)入的,減少了io次數(shù),相應(yīng)的也就提高了性能。
為什么小于8k的會(huì)這樣?
如果寫(xiě)入的內(nèi)容很大很大,且一次性寫(xiě)入時(shí)消耗的性能也是很大的,所以可以分批次寫(xiě)入<=8k的內(nèi)容,這樣消耗可能更低。

使用file_put_contents()寫(xiě)入日志發(fā)生日志錯(cuò)亂

現(xiàn)象

高并發(fā)情況下,且日志很長(zhǎng)大于8k的情況下,日志會(huì)發(fā)生錯(cuò)亂的現(xiàn)象。
我們可以注意到倆個(gè)關(guān)鍵字,高并發(fā)日志很長(zhǎng)。寫(xiě)入內(nèi)容大于8k的情況下,內(nèi)容會(huì)分批次寫(xiě)入,也就保證不了原子性了,如果現(xiàn)在有并發(fā)情況,就可能在分批次寫(xiě)入這里發(fā)生日志錯(cuò)亂的情況

寫(xiě)入內(nèi)容小于8k的情況下會(huì)一次性寫(xiě)入,這也就說(shuō)明了保證了原子性,所以在高并發(fā)的情況下不會(huì)發(fā)生錯(cuò)亂的情況。

查看file_put_contents()源碼

  • 腳本服務(wù)寫(xiě)入日志代碼如下:
if ($this->isCli == true) {
    return file_put_contents($messageLogFile, $strLogMsg, FILE_APPEND);
}
  • 查看file_put_contents 的源碼實(shí)現(xiàn),最終寫(xiě)文件會(huì)執(zhí)行到_php_stream_write_buffer 函數(shù),里面有這樣一處代碼:

明確幾個(gè)變量的含義:
count:需寫(xiě)入文件的字符串長(zhǎng)度
stream->chunk_size :默認(rèn)為8192 (8k)

從上面代碼可以看出,當(dāng)寫(xiě)入的字符串長(zhǎng)度 大于8192時(shí),則拆為多次<=8192的字符串,分批次寫(xiě)入,打個(gè)比方,如果寫(xiě)入的內(nèi)容是23k,就分三次寫(xiě)入,第一次寫(xiě)入8k,第二次寫(xiě)入8k,第三次7k。

然后調(diào)用php_stdiop_write函數(shù)寫(xiě)入文件。什么意思呢?

  • php_stdiop_write函數(shù)實(shí)現(xiàn)如下:
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 則調(diào)用的 write函數(shù) 寫(xiě)入文件;write函數(shù)是能保證一次寫(xiě)入的完整的。

所以日志寫(xiě)串的原因也就能分析出來(lái)了,調(diào)用鏈接為:file_put_contents ->_php_stream_write_buffer ->php_stdiop_write(多次調(diào)用,每次最多寫(xiě)入8192字節(jié)) ->write(),是在 多次調(diào)用php_stdiop_write 函數(shù)時(shí)出的問(wèn)題;第一次寫(xiě)完,緊接著在高并發(fā)的情況下,被其他進(jìn)程的 write 函數(shù)追著寫(xiě),此時(shí)就出現(xiàn)寫(xiě)串,也就是前面示例中日志;

總結(jié):寫(xiě)入內(nèi)容小于8k時(shí)是原子性操作,不用加鎖,反之需要。這個(gè)8k的限制可以在php中修改的。

加鎖代碼

由于加鎖是阻塞的,在并發(fā)時(shí)會(huì)影響性能,所以寫(xiě)入內(nèi)容時(shí)最好判斷下大小是否超過(guò)8k,代碼如下

<?php

$str='xxx';
$strlen=strlen($str);
if($strlen > 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()的效果是一樣的。

加鎖后會(huì)有死鎖的問(wèn)題嗎?

這個(gè)鎖并沒(méi)有設(shè)定過(guò)期時(shí)間,那么會(huì)不會(huì)有死鎖的情況呢?比如在執(zhí)行完加鎖還沒(méi)有到解鎖的時(shí)候機(jī)器宕機(jī),該文件會(huì)不會(huì)被鎖死?
答案是:進(jìn)程重啟或者kill掉該進(jìn)程后,系統(tǒng)會(huì)自動(dòng)釋放這個(gè)文件鎖。在沒(méi)重啟或者沒(méi)kill掉進(jìn)程之前,該文件會(huì)被死鎖

在多進(jìn)程模式下,使用file_put_contents()會(huì)影響并發(fā)嗎?

分倆種情況

  • 不加鎖的情況
    首先f(wàn)ile_put_contents()就是一個(gè)阻塞io,所以肯定會(huì)阻塞進(jìn)程的,這點(diǎn)毋庸置疑。
    比如php-fpm一共有10個(gè)進(jìn)程,執(zhí)行file_put_contents()時(shí)會(huì)阻塞1s,那么此時(shí)最高的qps也就是10/s。只有進(jìn)程空閑后才會(huì)繼續(xù)處理別的請(qǐng)求。
  • 加鎖的情況
$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個(gè)進(jìn)程,在寫(xiě)入數(shù)據(jù)時(shí)會(huì)阻塞1s,而且該文件還被加鎖。
第一個(gè)請(qǐng)求在寫(xiě)入阻塞了1s,且該文件已加鎖。第二個(gè)并發(fā)請(qǐng)求寫(xiě)入時(shí)需要等待第一個(gè)請(qǐng)求鎖釋放才能寫(xiě)入,一次類推,此時(shí)qps也就是1/s。

如果前一個(gè)請(qǐng)求沒(méi)有釋放文件鎖就會(huì)導(dǎo)致后面的請(qǐng)求無(wú)法獲得鎖,卡死在獲取鎖的這一步。如果php-fpm一共10個(gè)進(jìn)程,此時(shí)系統(tǒng)最多能處理10個(gè)請(qǐng)求,且這10個(gè)請(qǐng)求都是阻塞狀態(tài)。說(shuō)白了都在阻塞造成的問(wèn)題,

所以在必須加鎖的情況下,我們必須加上LOCK_NB,它可以避免阻塞,也就是說(shuō)此時(shí)的qps也是10/s。

<?php
$fp = fopen('/tmp/lock.txt', 'r+');
if(!flock($fp, LOCK_EX | LOCK_NB)) {
    echo 'Unable to obtain lock';
    exit(-1);
}
fclose($fp);
?>
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容