代码之家  ›  专栏  ›  技术社区  ›  Nicholas Wilson

TCP SOCKET句柄是可继承的吗?

  •  7
  • Nicholas Wilson  · 技术社区  · 12 年前

    在Windows上,大多数类型的句柄都可以由子进程继承。预期TCP套接字也可以被继承。然而,当安装了某些分层服务提供商时,这并不能按预期工作(赛门铁克的PCTools等A/V产品曾导致我们为客户提供的应用程序出现问题)。

    微软构建WinSock的方式,我们是否应该期望能够正确地继承SOCKET?

    1 回复  |  直到 12 年前
        1
  •  11
  •   Community CDub    8 年前

    简短回答

    不,SOCKET不应标记为可继承。当安装了某些分层服务提供商(LSP)时,继承的句柄根本无法在子级中使用。

    作为额外的刺激,请参阅相关问题 "Can TCP SOCKETS be marked non-inheritable?" 。简单地说,你不能依赖于能够继承套接字,但也不能阻止套接字被继承!

    解释

    遗憾的是,这与微软自己的一些例子和文档(例如 KB150523 )。简单地说,分层服务提供商是微软为第三方软件提供的一种方式,它可以在您的应用程序和微软的WinSock DLL中的TCP/UDP堆栈之间插入自己。由于某些LSP的工作方式,它们使得很难在进程之间传输套接字,因为LSP将一些本地信息与它需要存在的每个套接字相关联。

    LSP只能挂接到WinSock函数中;例如,调用 DuplicateHandle 当安装了某些LSP时,SOCKET上的功能将不起作用,因为这是一个句柄级别的功能,LSP永远不会有机会复制它需要的信息。(这在 重复句柄 documentation )。

    类似地,试图将SOCKET句柄设置为可继承的将在不通知LSP的情况下复制该句柄,结果相同:Winsock可能无法在子进程中识别重复的句柄。典型的错误是WSAENOTSOCK(10038,“非套接字上的套接字操作”),甚至ERROR_INVALID_HANDLE(6,“句柄无效”)。

    实例

    假设您想编写一个Windows程序,该程序使用重定向的stdin和stdout启动子级,向其发送一些数据,在子级的stdin上发出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套接字,并在子代中继承一个作为stdin/stdout,行为确实正确。将此作为 socketpair Windows上的行为(记得发送一个nonce!)。

    遗憾的是,SOCKET根本无法可靠地继承。要在Windows上编写几乎具有同等功能的东西,您需要使用命名管道。打电话前 CreateProcess ,创建一对连接的HANDLES,而不是使用 CreateNamedPipe / ConnectNamedPipe 和朋友( GetOverlappedResult 对于重叠的父句柄)。(要用作stdin的子级句柄不能重叠!)子级句柄可以设置为可继承,子级将通过它正常通信。

    完成将数据管道传输到客户端后,调用 FlushFileBuffers CloseHandle 在父句柄上。

    进一步的问题

    1. 在只使用句柄继续操作之前,等待孩子退出该怎么办?没有办法只通过管道直接做到这一点;窗户管道不能半封闭。方法:

      1. (Unix方式,为了适应Windows而扭曲)制作另一对虚拟的连接句柄,并在子进程中继承其中一个句柄,但不要告诉子进程(不要将其作为std句柄附加)。然后,您可以等待父级中的第二个句柄来检测子级何时退出。
      2. (Windows-y方式)虽然很痛苦,但功能相当强大:使用命名管道获取父级中子级的实际进程句柄。这是Windows上等待孩子退出的“正确”方式。(这实际上是Unix上无法做到的;只有进程的父进程才能直接等待进程退出; OpenProcess 将pid转换为句柄,因此如果您在 开放进程 调用,您可以消除在Unix上无法实现这一点的竞争条件。)尽管如此,使用这样的进程句柄还是很痛苦的,因为您可能会发现需要第二个命名管道连接来发送它,这取决于您如何编写runas包装器。
    2. 一个棘手的问题:孩子如何接收到父母已经完成写入其stdin的通知?如果家长试图呼叫 DisconnectClient ,孩子没有得到正常的EOF。根据您试图执行的内容,这可能是一个问题。当家长关闭SOCKET时,你会得到 feof ,但如果句柄连接到子级的stdin,则该子级将在没有收到EOF信号的情况下收到读取错误。这可能会导致该子级无法以与正常连接到stdin完全相同的方式工作。相反,在父对象中调用CloseHandle会为子对象提供正确的行为。