在fork()之后关闭侦听套接字

Closing the listening socket after a fork()

本文关键字:套接字 之后 fork      更新时间:2023-10-16

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

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

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

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

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

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

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

Linux排队等待连接。父进程或子进程对accept的调用将轮询该队列。

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

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

所以只要你不接受子线程中的任何连接,父线程继续接受连接,一切都会正常工作。

但是如果你计划在你的子进程中永远不接受连接,为什么你要在这个进程中为套接字保留资源呢?

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

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

这些都是什么意思呢?

这个想法很简单。如果你在调用execve()时打开了一个文件描述符,那么你就给了子进程对该文件描述符的访问权限,这样它就可以访问它不应该访问的数据。

当你创建一个服务,fork(),然后执行代码,代码通常开始放弃权限(即主apache2服务运行作为根用户,但所有衍生的fork()实际上运行作为httpdwww用户——重要的是,主进程是根为了打开端口80和443,任何端口低于1024,实际上)。现在,如果黑客能够以某种方式获得对子进程的控制,如果很早就关闭,他们至少不会访问该文件描述符。这样更安全。

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

所以在你的情况下,它可能不是那么重要。然而,如果黑客接管了您的进程,他们可以接受其他连接,并使用他们狡猾的服务器版本处理它们……有事情要考虑。如果你的服务是内部的(只在你的内部网上运行),那么危险就会小一些(尽管从我的阅读来看,公司里的大多数小偷都是在那里工作的员工…)

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

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