为什么在调用fork()之后和调用exec ...()之前关闭所有文件描述符?我该怎么办?

时间:2019-06-18 13:41:45

标签: c fork exec posix file-descriptor

我已经看到很多C代码试图在调用for i, v in enumerate(newdict.values()): 和调用fork()之间关闭所有文件描述符。为什么我通常会这样做,以及在我自己的代码中执行此操作的最佳方法是什么,因为我已经看到了许多不同的实现?

2 个答案:

答案 0 :(得分:8)

调用fork()时,操作系统只需克隆现有进程即可创建一个新进程。除了其进程ID和记录的所有由fork()调用替换或重置的属性外,新进程与从其克隆的进程几乎完全相同。

在调用任何形式的exec...()时,调用过程的过程映像将被新的过程映像替换,但会保留过程状态。结果是,在调用exec...()之前,进程文件描述符表中的打开文件描述符在调用后仍然存在于该表中,因此新的进程代码继承了对其的访问。我想这可能是为了让子进程自动继承STDIN,STDOUT和STDERR。

但是,请记住,在POSIX C中,文件描述符不仅用于访问实际文件,而且还用于所有类型的系统和网络套接字,管道,共享内存标识符等。如果您在调用exec...()之前不关闭它们,则新的子进程将可以访问所有这些子进程,甚至可以访问那些无法获得访问权限的资源,因为它甚至没有所需的访问权限权利。考虑一个根进程创建一个非根子进程,但是该子进程将有权访问根父进程的所有打开文件描述符,包括只能由根或端口1024以下的受保护服务器套接字写入的打开文件。

因此,除非您希望子进程继承对当前打开的文件描述符的访问权限,否则可能会明确要求,例如要捕获某个进程的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从理论上讲可能很大,并且不能反映进程的当前当前运行时限制。

为避免不得不关闭太多不存在的文件描述符,可以改用以下代码:

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之间(记录为始终允许打开进程的最小文件描述符数量,必须至少为20OPEN_MAX(必须至少为_POSIX_OPEN_MAX并设置上限)。

虽然在循环中关闭所有可能的描述符在技术上是正确的,并且可以按需工作,但它可能会尝试关闭数千个文件描述符,但大多数情况下通常不存在。即使close()调用不存在的文件描述符很快(任何标准都不能保证),但在较弱的系统上可能会花费一些时间(考虑嵌入式设备,请考虑使用小型单板计算机) ,这可能是个问题。

因此,一些系统已经开发出更有效的方法来解决此问题。 BSD和Solaris系统支持的closefrom() and fdwalk()是著名的示例。不幸的是,开放组织投票反对在标准(引号)中添加closefrom():“ 不可能标准化一个接口,该接口关闭高于某个值的任意文件描述符,同时仍要保证符合标准的环境。“(Source)这当然是胡说八道,因为它们自己制定规则,并且如果它们定义某些文件描述符总是可以在环境或系统要求或代码本身要求的情况下被静默关闭,那么这将不会破坏该功能的现有实现,并且仍然为我们其他人提供所需的功能。如果没有这些功能,人们将使用循环并完全执行Open Group在这里要避免的事情,因此不添加循环只会使情况变得更糟。

在某些平台上,您基本上不走运,例如完全符合POSIX的macOS。如果您不想在macOS上循环关闭所有文件描述符,则唯一的选择是不使用fork() / exec...()而是使用posix_spawn()posix_spawn()是适用于不支持流程派生的平台的较新API,对于那些确实支持派生并且可以执行以下操作的平台,它可以完全在fork() / exec...()之上的用户空间中实现。否则,请使用平台提供的其他一些API来启动子进程。在macOS上,存在一个非标准标志POSIX_SPAWN_CLOEXEC_DEFAULT,它将像已在CLOEXEC标志上设置了所有文件描述符一样,除了为您明确指定文件操作的标志之外。

在Linux上,您可以通过查看路径/proc/{PID}/fd/来获取文件描述符列表,其中{PID}是您的进程(getpid())的进程ID,也就是说,如果proc文件系统已经全部挂载,并且已经挂载到/proc(但是许多Linux工具都依赖于该文件系统,否则这样做也会破坏许多其他事情)。基本上,您可以限制自己关闭此路径下列出的所有描述符。

答案 1 :(得分:4)

真实的故事:很久以前,我写了一个简单的小C程序打开了一个文件,我注意到open返回的文件描述符是4。“这很有趣,”我想。 “标准输入,输出和错误始终是文件描述符0、1和2,因此您打开的第一个文件描述符通常是3。”

因此,我编写了另一个小C程序,该程序开始从文件描述符3读取(不打开文件描述符,也就是说,假设3是预打开的fd,就像0、1和2一样)。很明显,在我使用的Unix系统上,文件描述符3已在系统密码文件上预打开。显然,这是登录程序中的一个错误,该错误以仍在密码文件上打开的fd 3执行我的登录shell,而流浪fd又被我从shell运行的程序继承了。

自然,我接下来尝试的是一个简单的小C程序,将预写的文件描述符3写入到预打开的文件描述符3中,以查看是否可以修改密码文件并赋予自己root访问权限。但是,这没有用。杂散fd 3以只读模式打开了密码文件。

但是无论如何,这有助于解释为什么执行子进程时不应该打开文件描述符。

[脚注:我说的是“真实的故事”,但大部分是,但是为了叙述,我确实更改了一个细节。实际上,/ bin / login的错误版本使fd 3在组文件/etc/group而不是密码文件上处于打开状态。]