fork()后关闭侦听套接字

时间:2015-09-04 17:12:49

标签: c++ c linux sockets

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

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

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

(A)是什么告诉操作系统某个进程是接受某个文件描述符上的连接的候选者?事件是否已调用accept()?或者这个过程调用了listen()

(B)如果这个过程调用了listen()这个事实,那么我们不会遇到竞争条件吗?如果发生这种情况该怎么办:

  1. 父进程侦听套接字S.
  2. 传入连接转到“父进程”。
  3. 父进程分叉一个孩子,孩子有一个套接字S的副本
  4. BEFORE 孩子能够呼叫close(S)第二个传入连接将转到子进程。
  5. 子进程从不调用accept()(因为它不应该),因此传入的连接被删除
  6. 是什么阻止了上述情况的发生?更一般地说,为什么子进程会关闭侦听套接字?

5 个答案:

答案 0 :(得分:7)

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

不关闭子进程中的套接字是资源泄漏,但不是很多。父母仍将获取所有传入的连接,因为它是唯一一个调用accept的连接,但如果父进程退出,则套接字仍然存在,因为它对孩子开放,甚至如果孩子从不使用它。

答案 1 :(得分:2)

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

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

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

有趣的问题是,如果两个进程都在套接字上调用accept()会发生什么。目前我无法找到关于此的明确信息。我能找到的是,你可以肯定的是,每个连接只传递给其中一个进程。

答案 2 :(得分:0)

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

答案 3 :(得分:0)

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

答案 4 :(得分:0)

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

<块引用>

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

不幸的是,这在您调用 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)fork(2) 的同时使用 execve(2) 设置其 close-on-exec 标志。根据执行顺序,竞争可能导致open()返回的文件描述符无意中泄露给fork(2)创建的子进程执行的程序。 (这种竞争原则上对于任何创建文件描述符的系统调用都是可能的,该文件描述符应该设置 close-on-exec 标志,并且各种其他 Linux 系统调用提供了一个等效的 O_CLOEXEC 标志来处理这个问题问题。)

好的,那么这一切意味着什么?

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

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

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

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