【问题标题】:Closing the listening socket after a fork()在 fork() 之后关闭监听套接字
【发布时间】:2015-11-30 22:10:37
【问题描述】:

Linux/UNIX 系统上常见的服务器套接字模式是侦听套接字,接受连接,然后fork() 处理连接。

所以,似乎在您accept()fork() 之后,一旦您进入子进程,您将继承父进程的侦听文件描述符。我已经读到此时,您需要从子进程中关闭侦听套接字文件描述符。

我的问题是,为什么?这仅仅是为了减少监听套接字的引用计数吗?还是这样子进程本身不会被操作系统用作路由传入连接的候选者?如果是后者,我有点困惑,原因有二:

(A) 什么告诉操作系统某个进程可以接受某个文件描述符上的连接?是不是进程调用了accept()?还是进程调用了listen()这一事实?

(B) 如果进程调用了listen(),我们这里不是有竞争条件吗?如果发生这种情况怎么办:

  1. 父进程监听socket S。
  2. 传入连接转到父进程。
  3. 父进程派生一个子进程,子进程拥有套接字 S 的副本
  4. 子进程能够调用close(S)之前,第二个传入连接转到子进程。
  5. 子进程从不调用accept()(因为它不应该调用),因此传入的连接被丢弃

是什么阻止了上述情况的发生?更一般地说,为什么子进程要关闭监听套接字?

【问题讨论】:

    标签: c++ c linux sockets


    【解决方案1】:

    Linux 将挂起的连接排队。从父进程或子进程调用accept 将轮询该队列。

    不关闭子进程中的套接字是资源泄漏,但仅此而已。父级仍然会抓取所有传入的连接,因为它是唯一调用accept 的连接,但如果父级退出,套接字仍然存在,因为它在子级上打开,即使子级从未使用它。

    【讨论】:

      【解决方案2】:

      传入的连接将被“传递”到任何进程正在调用accept()。在关闭文件描述符之前分叉后,您可以在两个进程中接受连接。

      因此,只要您从不接受子线程中的任何连接并且父线程继续接受连接,一切都会正常工作。

      但是如果您打算在子进程中永远不接受连接,为什么要在这个进程中为套接字保留资源?

      有趣的问题是如果两个进程都在套接字上调用accept() 会发生什么。我暂时找不到这方面的确切信息。我能找到的是,你可以肯定,每个连接都只传递给这些进程中的一个。

      【讨论】:

      • 如果两个进程在套接字上调用 accept() 会发生什么。”两个进程之一将以未指定的方式accept() 连接。
      • "unspecified way." 在我之前的评论中会说它是 "unspecified" 哪个进程将获得accept() 连接。
      • @alk 幸运的是,从两个进程中调用accept() 是明确定义的。它已被 Apache 服务器使用了数十年。其中一个过程将获得连接。另一个会坐稳。当然,如果两个连接同时到达,那么这两个进程很可能会在纳秒内被唤醒。
      【解决方案3】:

      除非调用了 accept(),否则子进程不会监听套接字,在这种情况下,传入的连接可以转到任一进程。

      【讨论】:

      • 至少在 Linux 中,listen()s 不是真正的任何进程,而是操作系统。
      【解决方案4】:

      子进程从其父进程继承所有文件描述符。子进程应关闭所有侦听套接字以避免与其父进程发生冲突。

      【讨论】:

      • 这不能回答问题。提问者想知道为什么必须关闭所有文件描述符以及如果不这样做会发生什么。
      【解决方案5】:

      socket() 手册中,有一段说:

      SOCK_CLOEXEC
      在新文件描述符上设置 close-on-exec (FD_CLOEXEC) 标志。请参阅open(2)O_CLOEXEC 标志的描述 这可能有用的原因。

      不幸的是,当你调用fork() 时,它不会做任何事情,它只适用于你调用execv() 和其他类似函数时。无论如何,阅读我们看到的open()功能手册中的信息:

      O_CLOEXEC(自 Linux 2.6.23 起)
      为新文件描述符启用 close-on-exec 标志。指定此标志允许程序避免额外的fcntl(2) F_SETFD 操作来设置FD_CLOEXEC 标志。

      请注意,在某些多线程程序中使用此标志是必不可少的,因为使用单独的fcntl(2)F_SETFD 操作来设置FD_CLOEXEC 标志并不足以避免一个线程打开文件描述符和尝试使用fcntl(2) 设置其close-on-exec 标志,同时另一个线程执行fork(2) 加上execve(2)。根据执行顺序,竞态可能会导致open() 返回的文件描述符无意中泄露给fork(2) 创建的子进程执行的程序。 (这种竞争原则上对于任何创建文件描述符的系统调用都是可能的,该文件描述符应该设置其 close-on-exec 标志,并且各种其他 Linux 系统调用提供了一个等效的 O_CLOEXEC 标志来处理这个问题。 )

      好的,那么所有这些是什么意思?

      这个想法很简单。如果您在调用execve() 时让文件描述符保持打开状态,则您将授予子进程对该文件描述符的访问权限,因此它可能会获得对其不应访问的数据的访问权限。

      当您创建fork()s 的服务然后执行代码时,该代码通常以删除权限开始(即主 apache2 服务以 root 身份运行,但所有生成的 fork() 实际上以 httpdwww 用户——主进程必须是 root 才能打开端口 80 和 443,实际上是 1024 以下的任何端口)。现在,如果黑客能够以某种方式控制该子进程,那么如果过早关闭,他们至少将无法访问该文件描述符。这样更安全。

      另一方面,我的 apache2 示例的工作方式不同:它首先打开一个套接字并将其绑定到端口 80、443 等,然后使用 fork() 创建子代,每个子代调用 accept()(默认情况下会阻塞)。第一个传入连接将通过从accept() 调用返回来唤醒其中一个孩子。所以我想这毕竟不是那么冒险。它甚至会保持该连接打开并再次调用accept(),直至达到最大值。在您的设置中定义(默认为 100,取决于您使用的操作系统)。最大后。 accept() 调用,该子进程退出并且服务器创建一个新实例。这是为了确保内存占用不会增长太多。

      所以在你的情况下,它可能并不那么重要。但是,如果黑客接管了您的进程,他们可以接受其他连接并使用您的服务器的精明版本来处理它们……这有什么关系。如果您的服务是内部服务(仅在您的 Intranet 上运行),那么危险就较小(尽管据我所知,公司中的大多数窃贼都是在那里工作的员工......)

      【讨论】:

        猜你喜欢
        • 2011-11-09
        • 1970-01-01
        • 2013-05-20
        • 1970-01-01
        • 1970-01-01
        • 2011-02-15
        • 1970-01-01
        • 1970-01-01
        • 2013-03-29
        相关资源
        最近更新 更多