在 Windows 上,大多数类型的句柄都可以被子进程继承。期望 TCP 套接字也可以被继承。但是,当安装了某些分层服务提供程序时,它不会按预期工作(赛门铁克的 PCTools 等 A/V 产品曾经导致我们为客户的应用程序出现问题)。
微软构建 WinSock 的方式,我们是否应该期望能够正确继承 SOCKET?
不,不应将 SOCKET 标记为可继承。安装某些分层服务提供程序 (LSP) 后,继承的句柄根本无法在子级中使用。
作为一个额外的刺激,请参阅相关问题“可以将 TCP SOCKETS 标记为不可继承吗?” . 简而言之,您不能依赖能够继承套接字,但也不能阻止套接字被继承!
遗憾的是,这违背了微软自己的一些示例和文档(例如KB150523)。简而言之,分层服务提供程序是 Microsoft 为第三方软件提供的一种方式,可以将自己插入到您的应用程序和 Microsoft 的 WinSock DLL 中的 TCP/UDP 堆栈之间。由于一些 LSP 的工作方式,它们使得在进程之间传输套接字变得困难,因为 LSP 将一些本地信息与它需要存在的每个套接字相关联。
LSP 只能挂接到 WinSock 函数;例如,DuplicateHandle
当安装了某些 LSP 时,调用 SOCKET 将不起作用,因为它是句柄级函数,并且 LSP 永远没有机会复制它需要的信息。DuplicateHandle
(这在文档中简要但清楚地说明了)。
类似地,尝试将 SOCKET 句柄设置为可继承将在不通知 LSP 的情况下复制句柄,结果相同:Winsock 在子进程中可能无法识别重复的句柄。典型的错误是 WSAENOTSOCK(10038,“非套接字上的套接字操作”),甚至是 ERROR_INVALID_HANDLE(6,“句柄无效”)。
假设您要编写一个 Windows 程序,该程序使用重定向的 stdin 和 stdout 启动子进程,向其发送一些数据,在子进程的标准输入上发出 EOF 信号,以便它知道处理数据,然后等待子进程返回。
让我们进一步假设执行了某种富有想象力的启动形式,这意味着您的孩子可能根本不是孩子(例如,gksu/runas 启动必须立即退出的包装器,只留下与客户端)。因此,您无需等待孩子的 PID。
行为将与此类似:
int main(int argc, char* argv[]) {
int handles[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, handles);
if (fork()) {
// child
close(handles[0]);
dup2(handles[1], 0);
dup2(handles[1], 1);
execl("clever-app", "clever-app", (char*)0);
}
// parent
close(handles[1]);
char* data[100];
write(handles[0], data, sizeof(data)); // should at least check for EINTR...
// tell the app we called there's nothing more to read from stdin:
shutdown(handles[0], SHUT_WR);
// wait until child has exited (discarding all output)
while (read(handles[0], data, sizeof(data)) >= 0) ;
// now continue with the rest of the program...
}
在没有分层服务提供者的机器上,创建一对连接的 TCP 套接字,并在子级中继承一个作为标准输入/标准输出,确实表现正确。很容易将其用作socketpair
Windows 上行为的解决方法(记得发送随机数!)。
可悲的是,SOCKET 根本无法可靠地继承。要在 Windows 上编写具有几乎相同功能的东西,您需要使用命名管道。在调用之前CreateProcess
,使用CreateNamedPipe
/ConnectNamedPipe
和朋友创建一对连接的 HANDLES(GetOverlappedResult
对于重叠的父句柄)。(孩子的句柄,用作标准输入,不能重叠!)孩子的句柄可以设置为可继承,孩子将通过它正常通信。
当您完成向客户端传输数据后,调用父句柄上的FlushFileBuffers
和。CloseHandle
在继续之前等待孩子退出怎么样,只使用手柄?没有办法直接用管道直接做到这一点。Windows 管道不能半封闭。方法:
OpenProcess
将 pid 转换为句柄,所以如果你在OpenProcess
调用,你可以摆脱在 Unix 上无法实现的竞争条件。)使用这样的进程句柄仍然是一个正确的痛苦,因为你可能会发现你需要第二个命名管道连接来发送它,具体取决于关于如何编写 runas 包装器。一个陷阱:孩子如何收到父母已完成对其标准输入的写入的通知?如果父母试图打电话DisconnectClient
,孩子不会得到正常的EOF。根据您要执行的操作,这可能是个问题。当父级关闭 SOCKET 时,您会得到feof
,但如果句柄连接到子级的标准输入,则子级将收到读取错误,而不会向其发出 EOF 信号。这可能会导致子进程无法以与正常连接到标准输入的方式完全相同的方式工作。在父级中调用 CloseHandle 会在子级中提供正确的行为。