
前言先把一个常见前提纠正掉PHP 里没有命名管道这个内置概念也没有fifo_open()、pipe_create()这类函数。命名管道named pipePOSIX 里叫 FIFO即 First In First Out是操作系统提供的进程间通信IPCInter-Process Communication机制PHP 只是通过现有的文件流函数去访问它而已。另一个容易混淆的点是把三个不同的东西当成一回事机制PHP 里的入口跨进程的前提有没有路径名匿名管道proc_open()、popen()必须有亲缘关系没有套接字对stream_socket_pair()必须有亲缘关系没有命名管道 FIFOposix_mkfifo()配合fopen()任意进程只需文件权限有再补充一个平台事实POSIX 的 FIFO只存在于类 Unix 系统Windows 上根本没有这套东西——PHP 的posix扩展在 Windows 上是不可用的posix_mkfifo()自然也不存在。Windows 有自己的命名管道路径形如\\.\pipe\管道名PHP 只能作为客户端去连接创建管道服务端得靠 Windows APIPHP 没有对应函数。本文分四块讲FIFO 的语义尤其是阻塞行为、用posix_mkfifo()的完整示例、Windows 那条不同的路、以及在你并不真的需要命名管道时应该用什么。一、FIFO 的语义关键是搞清楚阻塞FIFO 在文件系统里就是一个路径ls -l看它时权限位第一位是p。它看起来像文件但不是文件它不保存数据。写进去的字节待在内核缓冲区里读完就没了读端关闭后缓冲区里剩下的数据会被直接丢弃。它是字节流没有消息边界。写两次不代表读两次能对上——除非你自己做分帧比如按换行分隔或加长度前缀。有权限的任意进程都能打开它这一点和匿名管道只能用于有亲缘关系的进程完全不同。阻塞行为是理解 FIFO 的关键也是新手最容易以为程序卡死了的地方以只读方式打开一个 FIFO内核会阻塞直到有进程以写方式打开它。以只写方式打开会阻塞到有进程以读方式打开。只有当两端都打开了open()才返回接下来才能读写。读端关闭后写端会拿到 broken pipe 错误写端全部关闭后读端的fgets()之类的调用会读到文件结束。所以我先起读者它卡住了是正常现象不是死锁——它在等写者。诊断时先确认对面那个进程有没有起来再看权限对不对。二、用 posix_mkfifo 创建并使用 FIFO函数签名是posix_mkfifo(string $filename, int $permissions): bool返回值是布尔。注意权限参数同样受 umask 影响传0666实际得到的往往是0644。先写一个创建 FIFO 的小工具顺手处理路径存在但不是 FIFO这种脏情况?php // 适用于 PHP 7.4需要 posix 扩展仅类 Unix 系统declare(strict_types1);function ensure_fifo(string $path, int $mode 0600): void{if (file_exists($path)) {// filetype() 对 FIFO 返回字符串 fifoif (filetype($path) ! fifo) {throw new RuntimeException({$path} 已存在但不是 FIFO请先处理);}return; // 已经是 FIFO直接用}if (!posix_mkfifo($path, $mode)) {throw new RuntimeException(创建 FIFO 失败{$path});}}然后是写端和读端两个独立脚本。它们是两个独立进程需要开两个终端分别运行。?php // writer.php —— 适用于 PHP 7.4仅类 Unixdeclare(strict_types1);$path $argv[1] ?? (__DIR__ . /demo.fifo);ensure_fifo($path);// 以只写方式打开会阻塞直到有读端打开同一个 FIFO$fp fopen($path, wb);if ($fp false) {fwrite(STDERR, 打开 FIFO 失败\n);exit(1);}for ($i 1; $i 5; $i) {$line 第 {$i} 条消息\n;// fwrite 返回实际写入的字节数可能小于请求长度必须检查$written fwrite($fp, $line);if ($written false || $written strlen($line)) {fwrite(STDERR, 写入不完整实际写入 {$written} 字节\n);break;}fflush($fp); // 立刻推给对端不要等缓冲区满sleep(1);}fclose($fp); // 关闭后读端会读到文件结束?php // reader.php —— 适用于 PHP 7.4仅类 Unixdeclare(strict_types1);$path $argv[1] ?? (__DIR__ . /demo.fifo);ensure_fifo($path);// 以只读方式打开会阻塞直到有写端打开$fp fopen($path, rb);if ($fp false) {fwrite(STDERR, 打开 FIFO 失败\n);exit(1);}// 写端全部关闭时fgets() 返回 false循环自然结束while (($line fgets($fp)) ! false) {echo 收到 . $line;}fclose($fp);echo 写端已关闭读取结束\n;运行方式两个终端# 终端 Aphp reader.php /tmp/demo.fifo# 终端 Bphp writer.php /tmp/demo.fifo先起哪个都行因为两端在fopen()处都会等待对方这是 FIFO 的特性。不想被阻塞怎么办有些场景下希望没有数据就立刻返回而不是挂在那里等。有两个办法办法一读写方式打开。在某些类 Unix 系统上以读写模式r打开一个 FIFO不会阻塞因为内核认为读写两端都在即使当前没有另一个进程参与。然后配合stream_set_blocking()关掉阻塞用轮询读?php // 适用于 PHP 7.4行为依赖具体系统请在实际环境确认$fp fopen($path, rb); // 读写方式打开通常不会阻塞if ($fp false) { exit(1); }stream_set_blocking($fp, false); // 之后读不到数据会立刻返回而不是卡住while (true) {$chunk fread($fp, 8192);if ($chunk ! false $chunk ! ) {echo $chunk;}usleep(100_000); // 100 毫秒轮询一次}这个r技巧不是 POSIX 标准保证的行为它是 Linux 等实现上的实际表现移植时必须在目标系统上确认。usleep(100_000)里的下划线数字分隔符需要 PHP 7.4 及以上。办法二用另一个进程当写端保活者。起一个只保持打开状态的进程就不会让读端的fopen()一直阻塞。这个办法可移植性更好代价是多一个进程。三、Windows 上的命名管道Windows 的命名管道是另一套东西路径写在\\.\pipe\下面。PHP 里没有创建管道服务端的函数但可以作为客户端用文件流去连接?php // 适用于 PHP 7.4仅 Windowsdeclare(strict_types1);// 单引号字符串里\\ 表示一个反斜杠// 所以这个字面量对应的真实路径是 \\.\pipe\mypipe$path \\\\.\\pipe\\mypipe;$fp fopen($path, rb); // 注意以二进制可读写模式打开if ($fp false) {fwrite(STDERR, 连接命名管道失败 . $path . \n);exit(1);}fwrite($fp, ping\n);fflush($fp);$reply fgets($fp);if ($reply ! false) {echo 服务端回复 . trim($reply) . \n;}fclose($fp);几个 Windows 特有的注意点路径里的反斜杠数量容易写错。单引号字符串中\\表示一个反斜杠所以要得到\\.\pipe\mypipe必须写成\\\\.\\pipe\\mypipe——四个反斜杠在源码里两个在真实路径里。如果改写成双引号字符串\\\\.\\pipe\\mypipe结果是一样的但更推荐单引号避免再叠加一层转义规则。管道名大小写不敏感这一点和 Unix 上的 FIFO 相反。服务端在别处。通常由 C#、Go、Python 或 Windows 服务通过CreateNamedPipe创建PHP 只负责连上去读写。拒绝访问的错误很常见。命名管道有安全描述符security descriptor服务端可以限制谁能连。报拒绝访问时先确认运行 PHP 的账户有没有被允许。如果你的项目要同时跑 Linux 和 Windows最省事的选择是别用命名管道改用本机 TCPstream_socket_server(tcp://127.0.0.1:9000)在两边都能跑行为一致还免去权限和路径的坑。四、你也许并不需要命名管道在 PHP 里真正用到 FIFO 的场景比想象中少。按需求对号入座需求更合适的做法父进程和 PHP 子进程通信proc_open()的管道原生支持三个标准流只读子进程输出或只写子进程输入popen()配合pclose()拿退出码同一台机器上两个独立进程通信本机 TCP 或 Unix 域套接字跨主机通信TCP、Redis 队列、消息队列只是想解耦、异步执行消息队列或任务队列让另一个进程实时输出数据常驻进程加网络接口stream_socket_pair()会创建一对相互连接的套接字流很适合pcntl_fork()出来的父子进程之间做双向通信socket_create_pair()走的是 sockets 扩展。这两个的跨平台支持情况请以你所用的 PHP 版本官方文档为准不要想当然地假定它在所有系统上都能用。真正非 FIFO 不可的场景通常是对接一个已经用 FIFO 的第三方程序比如某些日志采集器、某些守护进程的输入接口——这时候你只是被动适配没什么可选的。常见坑点❌ 在 Windows 上调用posix_mkfifo()。✅posix扩展只存在于类 Unix 系统Windows 上要用\\.\pipe\路径连命名管道或直接改用 TCP。❌ 运行读端发现卡住了以为程序有 bug。✅ 只读打开一个 FIFO必然阻塞到写端出现这是设计行为先确认另一端进程是否已经启动。❌fopen($fifo, w)之后不检查返回值就fwrite()。✅ 打开可能失败权限、路径被占fopen()返回false后续调用会连锁出错。❌ 认为fwrite()一定把整个字符串写完了。✅ 它返回实际写入的字节数可能小于请求长度大块数据必须循环写并检查返回值。❌ 把 FIFO 当文件看待以为里面存着数据。✅ 它是内核缓冲区读端关闭后剩余数据直接丢弃没有持久化也不支持fseek()之类的位置操作。❌ 用多个写端并发写大块数据靠一次fwrite()保证不交错。✅ 只有不超过PIPE_BUF字节的写入才是原子的超了就可能交错自己加长度前缀或换行做分帧。❌ 把 FIFO 建在/tmp这种所有用户可写的目录里权限还给到0666。✅ 放在程序自己的私有目录权限收紧到0600避免被同名文件抢占或被替换成符号链接。❌ 以为删掉 FIFO 里的数据就等于删掉 FIFO。✅ 必须unlink()才会从文件系统里移除它只关fopen()得到的句柄是不行的。总结问题结论PHP 有命名管道函数吗没有FIFO 是操作系统的PHP 用文件流访问创建 FIFOposix_mkfifo()仅类 Unix需 posix 扩展打开会不会阻塞会只读等写端、只写等读端读写模式通常不阻塞数据会保存吗不会读完即消失读端关闭后缓冲剩余数据丢弃有消息边界吗没有是字节流需要自己分帧Windows 怎么办PHP 只能作为客户端连\\.\pipe\名字或改用 TCP什么时候用它对接已有 FIFO 接口的第三方程序结论命名管道的全部复杂度都集中在阻塞语义和字节流没有边界这两点上把这两点想清楚剩下的就是普通的文件流读写。而在 PHP 生态里绝大多数想跨进程传数据的需求用proc_open()的管道、本机 TCP 或消息队列都会比 FIFO 更省心、更容易跨平台——FIFO 更适合留给必须与既有程序对接的场景。