我见过很多 C 代码试图在调用 fork()
之间关闭所有文件描述符。并调用 exec...()
.为什么通常会这样做,在我自己的代码中这样做的最佳方法是什么,因为我已经看到了很多不同的实现?
最佳答案
打电话时fork()
,您的操作系统通过简单地克隆现有进程来创建一个新进程。新进程将与从其克隆的进程几乎相同,除了它的进程 ID 和记录为由 fork()
替换或重置的任何属性之外。称呼。
拨打任何形式的 exec...()
时,调用进程的进程镜像被一个新的进程镜像替换,但除此之外,进程状态被保留。一个后果是在调用 exec...()
之前打开进程文件描述符表中的文件描述符。调用后仍然存在于该表中,因此新的进程代码继承了对它们的访问。我想这可能是为了 STDIN
, STDOUT
, 和 STDERR
由子进程自动继承。
但是,请记住,在 POSIX C 中,文件描述符不仅用于访问实际文件,还用于所有类型的系统和网络套接字、管道、共享内存标识符等。如果您在调用 exec...()
之前没有关闭这些,您的新子进程将可以访问所有这些资源,即使是那些它自己无法访问的资源,因为它甚至没有所需的访问权限。考虑一个根进程创建一个非根子进程,但是这个子进程可以访问根父进程的所有打开的文件描述符,包括只能由根或端口 1024 以下的 protected 服务器套接字写入的打开文件。
因此,除非您希望子进程继承对当前打开的文件描述符的访问权限,否则可能是明确需要的,例如捕获 STDOUT
通过 STDIN
获取流程或馈送数据对于该过程,您需要在调用 exec...()
之前关闭它们。 .不仅是因为安全性(有时可能根本不起作用),而且还因为否则子进程可用的可用文件描述符较少(并考虑一长串进程,每个打开文件然后生成一个子进程。 .. 可用的免费文件描述符将越来越少)。
一种方法是始终使用标志 O_CLOEXEC
打开文件,确保在 exec...()
时自动关闭此文件描述符曾经被称为。该解决方案的一个问题是您无法控制外部库打开文件的方式,因此您不能指望所有代码都将始终设置此标志。
另一个问题是此解决方案仅适用于使用 open()
创建的文件描述符。 .创建套接字、管道等时不能传递该标志。这是一个已知问题,一些系统正在通过提供非标准 acccept4()
来解决这个问题。 , pipe2()
, dup3()
,以及 SOCK_CLOEXEC
套接字的标志,但是这些还不是 POSIX 标准,不知道它们是否会成为标准(这是计划中的,但在新标准发布之前我们无法确定,所有系统都需要数年才能采用它们)。
您可以做的是稍后设置标志 FD_CLOEXEC
使用 fcntl()
但是,在文件描述符上,请注意这在多线程环境中是不安全的。只需考虑以下代码:
int so = socket(...);
fcntl(so, F_SETFD, FD_CLOEXEC);
如果另一个线程调用
fork()
在第一行和第二行之间,这当然是可能的,标志尚未设置,因此该文件描述符不会被关闭。所以真正安全的唯一方法是明确关闭它们,这并不像看起来那么容易!
我见过很多代码做这样的蠢事:
for (int i = STDERR_FILENO + 1; i < 256; i++) close(i);
但仅仅因为某些 POSIX 系统的默认限制为
256
并不意味着不能提高这个限制。同样在某些系统上,默认限制总是更高。使用
FD_SETSIZE
而不是 256
同样是错误的,因为 select()
API 在大多数系统上默认有一个硬限制并不意味着一个进程不能有比这个限制更多的打开文件描述符(毕竟你不必使用 select()
,你可以使用 poll()
API 作为替换和 poll()
对文件描述符数量没有上限)。总是正确的是使用
OPEN_MAX
而不是 256
因为这实际上是进程可以拥有的文件描述符的绝对最大值。缺点是OPEN_MAX
理论上可能很大,并且不能反射(reflect)进程的实际当前运行时限制。为避免关闭太多不存在的文件描述符,您可以改用以下代码:
int fdlimit = (int)sysconf(_SC_OPEN_MAX);
for (int i = STDERR_FILENO + 1; i < fdlimit; i++) close(i);
sysconf(_SC_OPEN_MAX)
如果使用 RLIMIT_NOFILE
提高了打开文件限制( setrlimit()
),则记录为正确更新.资源限制 ( rlimits
) 是正在运行的进程的有效限制,对于文件,它们始终必须介于 _POSIX_OPEN_MAX
之间。 (记录为始终允许进程打开的最小文件描述符数,必须至少为 20
)和 OPEN_MAX
(必须至少为 _POSIX_OPEN_MAX
并设置上限)。虽然在循环中关闭所有可能的描述符在技术上是正确的并且可以按需要工作,但它可能会尝试关闭数千个文件描述符,其中大多数通常不存在。即使
close()
调用不存在的文件描述符很快(任何标准都不能保证),在较弱的系统上可能需要一段时间(想想嵌入式设备,想想小型单板计算机),这可能是一个问题。所以有几个系统开发了更有效的方法来解决这个问题。著名的例子是
closefrom()
and fdwalk()
哪些 BSD 和 Solaris 系统支持。不幸的是,开放组投票反对添加 closefrom()
对标准(引用):“不可能标准化一个接口(interface),该接口(interface)关闭高于某个值的任意文件描述符,同时仍保证符合环境。” ( Source ) 这当然是无稽之谈,因为他们自己制定规则,并且如果他们定义某些文件描述符总是可以在环境或系统需要或代码本身要求的情况下悄悄地忽略关闭,那么这将破坏没有该功能的现有实现,并仍然为我们其他人提供所需的功能。如果没有这些函数,人们将使用循环并执行 The Open Group 在这里试图避免的操作,因此不添加它只会使情况变得更糟。在某些平台上,您基本上不走运,例如macOS,完全符合 POSIX。如果您不想在 macOS 上循环关闭所有文件描述符,您唯一的选择是不使用
fork()
/exec...()
而是 posix_spawn()
. posix_spawn()
是一个较新的 API,适用于不支持进程 fork 的平台,它可以纯粹在 fork()
之上的用户空间中实现。/exec...()
对于那些支持 fork 并且可以使用平台提供的其他 API 来启动子进程的平台。在 macOS 上存在一个非标准标志 POSIX_SPAWN_CLOEXEC_DEFAULT
,它将像 CLOEXEC
一样遍历所有文件描述符已在它们上设置标志,但您明确指定的文件操作除外。在 Linux 上,您可以通过查看路径
/proc/{PID}/fd/
来获取文件描述符列表。与 {PID}
是您的进程的进程 ID ( getpid()
),也就是说,如果 proc 文件系统已完全挂载并且已挂载到 /proc
(但是很多 Linux 工具都依赖于此,不这样做也会破坏许多其他东西)。基本上,您可以限制自己关闭此路径下列出的所有描述符。
关于c - 为什么要在调用 fork() 之后和调用 exec...() 之前关闭所有文件描述符?我该怎么做?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56650579/