简答
不,不应将 SOCKET 标记为可继承。安装某些分层服务提供程序 (LSP) 后,继承的句柄根本无法在子级中使用。
另外,请参阅相关问题"Can TCP SOCKETS be marked non-inheritable?"。简而言之,您不能依赖能够继承套接字,但也不能阻止套接字被继承!
说明
遗憾的是,这与 Microsoft 自己的一些示例和文档(例如 KB150523)背道而驰。简而言之,分层服务提供程序是 Microsoft 为第三方软件提供的一种方式,可以将自己插入到您的应用程序和 Microsoft 的 WinSock DLL 中的 TCP/UDP 堆栈之间。由于某些 LSP 的工作方式,它们使得在进程之间传输套接字变得很困难,因为 LSP 将一些本地信息与它需要存在的每个套接字相关联。
LSP 只能挂接到 WinSock 函数;例如,在安装了某些 LSP 时,在 SOCKET 上调用 DuplicateHandle 将不起作用,因为它是句柄级函数,并且 LSP 永远没有机会复制它需要的信息。 (这在DuplicateHandledocumentation中有简要但明确的说明)。
类似地,尝试将 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 套接字,并在子节点中继承一个作为标准输入/标准输出,确实可以正常运行。很容易将其用作 Windows 上 socketpair 行为的解决方法(记得发送随机数!)。
遗憾的是,SOCKET 根本无法可靠地继承。要在 Windows 上编写具有几乎相同功能的东西,您需要使用命名管道。在调用 CreateProcess 之前,创建一对连接的 HANDLES,而不是使用 CreateNamedPipe/ConnectNamedPipe 和朋友(GetOverlappedResult 用于重叠的父句柄)。 (子句柄,作为标准输入,不能重叠!)子句柄可以设置为可继承,子句可以正常通信。
当您完成向客户端传输数据后,在父句柄上调用 FlushFileBuffers 和 CloseHandle。
其他问题
-
在继续之前等待孩子退出怎么办,只使用句柄?没有办法直接用管道直接做到这一点。 Windows 管道不能半封闭。方法:
- (Unix 方式,扭曲以适应 Windows)制作另一对连接的虚拟句柄,并在子进程中继承其中一个,但不要告诉子进程(不要将其附加为 std 句柄)。然后,您可以等待父级中的第二个句柄来检测子级何时退出。
- (Windows-y 方式)正确的痛苦,但相当强大:使用命名管道获取父级中子级的实际进程句柄。这是 Windows 上等待孩子退出的“正确”方式。 (这实际上是你在 Unix 上做不到的;只有进程的父进程才能直接等待进程退出;
OpenProcess 将 pid 转换为句柄,所以如果你仔细检查 pid 秒在OpenProcess 调用之后的时间,您可以摆脱在 Unix 上无法实现的竞争条件。)使用像这样的进程句柄仍然是一种痛苦,因为您可能会发现您需要第二个命名管道连接发送它,这取决于您如何编写 runas 包装器。
一个问题:孩子如何收到父母已完成对其标准输入的写入的通知?如果父母试图打电话给DisconnectClient,孩子不会得到正常的EOF。根据您要执行的操作,这可能是个问题。当父级关闭 SOCKET 时,您会得到feof,但如果句柄连接到子级的标准输入,子级将收到读取错误而不会收到 EOF 信号。这可能会导致子进程无法以与正常连接到标准输入的方式完全相同的方式工作。在父级中调用 CloseHandle 会在子级中提供正确的行为。