ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

PHP fflush() 函数

PHP fflush() 函数 前言fflush()只有一行文档说明把指向文件流的缓冲输出强制写出去。签名简单到不需要解释但它背后有两层非常容易被混淆的缓冲理解错了就会写出「日志丢了一截」「写完立刻读却是空的」这类问题。第一个误解是把fflush()当成「落盘」。fflush()只把 PHP 用户态缓冲区里的数据交给操作系统操作系统还会把数据放在页缓存page cache里延迟写入磁盘。真正想保证数据落到存储介质上需要的是fsync()——它在PHP 8.1.0才被加入。第二个误解是把fflush()和「刷新页面输出」搞混。刷新浏览器输出用的是flush()和ob_flush()和文件流没有关系fflush()的参数必须是文件流资源传不了别的。本文按官方手册讲清fflush()的语义边界、PHP 到磁盘之间的缓冲层次以及哪几个场景里它确实能解决问题。一、函数签名与语义fflush(resource $stream): bool手册的说明是该函数强制把所有被缓冲的输出写向$stream指向的资源。参数必须是有效的文件指针也就是由fopen()或fsockopen()打开、且尚未被fclose()关闭的流资源。成功返回true失败返回false。三个需要点明的语义边界它只作用于「本进程写缓冲区」不保证对端已经读到。对 socket 来说fflush()之后数据可能还在操作系统的发送缓冲区里。它不代表数据已经持久化到磁盘。这是最容易误解的一点见下一节。它不改变流的打开模式。对只读流调用没有意义。二、PHP 写数据的完整链路一次fwrite()到最终落盘中间至少有三层层次由谁控制相关手段PHP 的用户态流缓冲区PHP 运行时默认写缓冲stream_set_write_buffer()可调整设为0表示不使用 PHP 层缓冲操作系统的页缓存操作系统fflush()把数据从 PHP 缓冲推到这里fsync()推动它写向存储介质存储介质本身硬件/文件系统fsync()PHP 8.1.0 起、fdatasync()PHP 8.1.0 起所以fwrite() → PHP 缓冲 → [fflush()] → 内核页缓存 → [fsync()] → 存储介质fflush()只跨越第一步。如果程序依赖「写完就能被别的进程读到」fflush()就够用了因为别的进程读的是同一份页缓存。如果程序依赖「断电后数据还在」那就必须fsync()。需要说明的是PHP 8.1.0 起才有fsync()。在老版本上唯一可靠的落盘方式是fclose()关闭时由操作系统负责把内容写出去或者使用带有O_SYNC语义的外部手段。三、什么时候真的需要fflush()它解决的问题只有一个写进缓冲区的数据还没被交出去而消费方在别的地方等着。场景为什么需要配套手段长驻进程CLI daemon写日志运维用tail -f盯着缓冲没满时日志长时间不出现fflush()或对日志流关闭写缓冲写完临时文件后立刻rename()/ 交给外部程序数据要确实在文件里fflush()后fclose()需要抗断电再加fsync()PHP 8.1通过管道与子进程通信子进程要立刻收到数据fflush()后再触发子进程读取写 socket 的交互式协议请求要马上发出去而不是攒着fflush()反过来说如果只是「把数据写进文件然后结束请求」直接用file_put_contents()就够了——它是原子的内部自己处理打开、写入和关闭。实战带刷新的追加日志函数?php // 适用于 PHP 8.1declare(strict_types1);final class FlushingLogger{/** var resource */private $handle;private bool $fsync;public function __construct(string $path, bool $fsync false){$handle fopen($path, ab);if ($handle false) {throw new RuntimeException(无法打开日志文件: {$path});}$this-handle $handle;$this-fsync $fsync;// 设为 0 表示不使用 PHP 层的写缓冲stream_set_write_buffer($this-handle, 0);}public function log(string $level, string $message): void{$line sprintf([%s] %s %s%s,date(Y-m-d H:i:s),strtoupper($level),$message,PHP_EOL);if (fwrite($this-handle, $line) false) {throw new RuntimeException(写入日志失败);}// 交出 PHP 层缓冲如果设为 0 缓冲则这一步是幂等的fflush($this-handle);// 需要抗断电时再推一次内核页缓存PHP 8.1 才有 fsyncif ($this-fsync) {fsync($this-handle);}}public function close(): void{if (is_resource($this-handle)) {fflush($this-handle);fclose($this-handle);}}}$logger new FlushingLogger(__DIR__ . /app.log);$logger-log(info, 服务启动);$logger-log(warn, 队列积压超过阈值);$logger-close();注意$this-fsync这个开关的设计意图fsync()每次调用都会等待存储设备确认写入代价明显高于fflush()。所以只在「掉了这一行会造成严重后果」的日志上开启如审计日志、资金流水普通业务日志不需要。常见坑点❌ 以为调了fflush()数据就等于写进磁盘了 —— 它只把数据从 PHP 缓冲交给操作系统页缓存断电或主机崩溃时仍可能丢。✅ 需要持久化保证时在fflush()之后再调fsync()PHP 8.1.0 起提供更老版本只能靠fclose()。❌ 用fflush()去刷新浏览器看到的输出 —— 它只接受文件流资源跟页面输出缓冲完全无关。✅ 刷新页面输出用ob_flush(); flush();。❌ 对fopen($f, r)得到的只读流调用fflush()—— 没有可写数据可刷调用毫无意义。✅ 只对w、a、r等可写模式打开的流调用。❌ 把已经被fclose()关闭的句柄继续传给fflush()—— PHP 8 起会抛TypeErrorPHP 7 里是警告加false把错误吞掉更难排查。✅ 保持句柄在刷新期间有效刷完再关用is_resource()先判断。❌ 以为fflush()能影响file_put_contents()写出的内容 —— 那是另一个 API内部自己打开、写入、关闭没有可操作的句柄缓冲概念。✅ 需要精细控制缓冲时用fopen()fwrite()fflush()fclose()这一套。❌ 在高频循环里对每一行日志都调fflush()加fsync()—— 每个fsync()都要等存储设备确认会把吞吐压低很多具体幅度取决于磁盘请自行实测。✅ 按批次或按时间间隔刷新或者把关键日志单独走一条路径。❌ 在 Windows 上fflush()之后马上unlink()同一个文件 —— 文件句柄仍然打开着删除会失败或延迟发生。✅ 先fclose()释放句柄再删除文件。❌ 用fflush()来「保证多进程写日志的顺序」—— 它只影响本进程缓冲写入本身在多进程之间并不是原子的长行还会互相穿插。✅ 用flock()加文件锁或者让所有进程把日志发给一个收集进程统一写。总结要点结论签名fflush(resource $stream): bool作用范围把 PHP 用户态写缓冲推给操作系统不负责落盘落盘手段fsync()/fdatasync()PHP 8.1.0 起提供老版本靠fclose()与页面输出无关那是flush()/ob_flush()的职责关闭缓冲stream_set_write_buffer($fp, 0)可让 PHP 层不做缓冲适用场景长驻进程日志、写后立即被外部读取、管道与 socket 通信不适用场景想保证持久化、想刷新浏览器输出、对只读流调用一句话总结fflush()是「把 PHP 缓冲交出去」的动作不是「把数据写进磁盘」的保证把这两件事分开需要持久化时补一个fsync()PHP 8.1需要省性能时就不要无脑刷这才是它真正的用法边界。
返回列表