3. 管道通信(Pipe):匿名管道与命名管道、父子进程通信实战、管道读写阻塞问题
管道通信,说白了就是进程间的一条「数据水管」。一端往里灌,另一端接着喝。这是IPC里最古老、最基础的方式,也是我入行上位机开发时第一个接触的进程通信手段。
我记得刚做第一个工业采集项目时,上位机需要跟一个底层采集程序交换数据。当时领导说「用管道吧,简单」。我心想,管道?不就是Linux里那个 | 符号吗?后来才发现,Windows下的管道通信,坑比想象的多。
3.1 匿名管道:父子进程的「脐带」
匿名管道,顾名思义,没有名字。它只能在有亲缘关系的进程间使用——最常见的就是父子进程。
它的工作方式很直接:父进程创建管道,得到两个句柄——一个读端,一个写端。然后父进程创建子进程,把读端或写端传给子进程。这样父子俩就能通过这根「脐带」通信了。
核心特点:
- 单向通信:数据只能从一个方向流动
- 亲缘关系:只能在父子进程或兄弟进程间使用
- 无名无姓:没有文件系统路径,用完即焚
- 半双工:如果需要双向通信,得创建两根管道
我在项目中遇到过一个问题:父进程往管道里写数据,子进程读不到。查了半天,原来是父进程忘记关闭读端句柄了。嗯,这里要注意——使用匿名管道时,一定要关闭不需要的句柄。父进程如果持有读端不关,子进程的读操作永远不会收到EOF。
3.2 命名管道:跨进程的「公共水管」
命名管道就灵活多了。它有一个名字,像文件一样存在于系统中。任何进程,只要知道这个名字,就能打开它进行通信。不需要亲缘关系。
在Windows上,命名管道的路径格式是:\\.\pipe\管道名称。你可以把它想象成一个公共水管,多个进程都能接上去。
| 特性 | 匿名管道 | 命名管道 |
|---|---|---|
| 进程关系 | 仅限父子/兄弟进程 | 任意进程 |
| 通信方向 | 单向 | 双向(双工模式) |
| 生命周期 | 随进程结束而销毁 | 可持久存在 |
| 网络支持 | 不支持 | 支持(远程管道) |
| 使用场景 | 简单父子进程通信 | 复杂多进程、C/S架构 |
命名管道还有一个好处:它支持消息模式。你可以发送固定长度的消息,接收方按消息边界读取,不会出现数据粘包的问题。这一点在工业上位机里特别实用——你想想看,采集的数据包如果被拆成两半,解析起来多麻烦。
3.3 父子进程通信实战
光说不练假把式。我直接给你看一段我常用的代码模板。这是Windows下用匿名管道实现父子进程通信的典型写法。
// 父进程:创建管道并启动子进程
HANDLE hRead, hWrite;
SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };
// 1. 创建匿名管道
CreatePipe(&hRead, &hWrite, &sa, 0);
// 2. 设置子进程启动信息
STARTUPINFO si = { sizeof(STARTUPINFO) };
PROCESS_INFORMATION pi;
si.hStdOutput = hWrite; // 子进程的标准输出重定向到管道写端
si.dwFlags = STARTF_USESTDHANDLES;
// 3. 创建子进程
CreateProcess(NULL, "child.exe", NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
// 4. 关闭写端句柄(父进程不需要写)
CloseHandle(hWrite);
// 5. 从管道读取子进程的输出
char buffer[1024];
DWORD bytesRead;
while (ReadFile(hRead, buffer, sizeof(buffer), &bytesRead, NULL) && bytesRead > 0) {
// 处理数据...
}
// 6. 清理
CloseHandle(hRead);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
我的经验:这段代码里最容易踩的坑是第4步——关闭写端句柄。如果你不关,ReadFile会一直阻塞等待数据,永远不会返回EOF。我曾经因为这个bug排查了整整一个下午。
3.4 管道读写阻塞问题
管道通信最让人头疼的就是阻塞问题。说白了就是:读操作在等数据,写操作在等空间,两边互相等着,谁也不让谁。
具体来说,有几种典型场景:
- 读阻塞:管道里没数据,ReadFile就卡在那里,直到有数据进来
- 写阻塞:管道缓冲区满了,WriteFile就卡住,直到对方读走数据腾出空间
- 死锁:父子进程互相等待对方先操作,结果谁也动不了
我曾经在一个数据采集项目里遇到过这个问题。上位机通过管道给采集程序发指令,采集程序回传数据。结果数据量一大,管道缓冲区满了,写操作阻塞,两边都卡死了。
怎么解决?我总结了几个实战经验:
- 使用异步I/O:用OVERLAPPED结构体配合事件对象,读写操作不阻塞主线程
- 设置管道模式:用SetNamedPipeHandleState把管道设为消息模式,避免粘包
- 增加缓冲区:创建管道时指定更大的缓冲区,减少写阻塞概率
- 独立线程处理:读写操作放在独立线程里,主线程该干嘛干嘛
避坑指南:我曾经在一个多线程项目里,多个线程同时往一个管道写数据。结果数据乱成一锅粥。后来才意识到——管道不是线程安全的。多线程写管道,必须加锁,或者用命名管道的消息模式,每个消息独立发送。
还有一个容易被忽略的点:管道断开检测。当对端进程意外退出时,你的ReadFile会返回FALSE,GetLastError返回ERROR_BROKEN_PIPE。很多新手没做这个判断,程序就莫名其妙卡死了。
嗯,说到这儿,我想起一个真实案例。有个同事写的采集程序,运行一段时间后就无响应。我一看代码,ReadFile没有做错误处理,对端进程崩溃后,这边一直阻塞在ReadFile上。加了个错误判断,问题就解决了。
最后说一句:管道通信虽然简单,但用好了很强大。尤其是在Windows上位机开发中,命名管道配合消息模式,能实现非常稳定的进程间通信。我个人习惯在需要简单C/S架构时优先考虑命名管道,比Socket配置简单,比共享内存安全。