我看到很多C代码都试图在调用fork()
和调用exec...()
之间关闭所有文件描述符。为什么这样做很常见,以及如何在我的代码中以最佳方式执行此操作,因为我已经看到了许多不同的实现?
我看到很多C代码都试图在调用fork()
和调用exec...()
之间关闭所有文件描述符。为什么这样做很常见,以及如何在我的代码中以最佳方式执行此操作,因为我已经看到了许多不同的实现?
fork()
时,操作系统通过克隆现有进程来创建一个新的进程。新进程与其克隆自的进程基本相同,除了进程ID和任何已记录在fork()
调用中被替换或重置的属性。exec...()
时,调用进程的进程镜像被替换为一个新的进程镜像,但除此之外,进程状态得到保留。一个后果是,在调用exec...()
之前,进程文件描述符表中打开的文件描述符仍然存在于该表中,因此新进程代码继承了对它们的访问权限。我猜这可能是为了让子进程自动继承STDIN
、STDOUT
和STDERR
。exec...()
之前不关闭这些文件描述符,则新的子进程将获得对它们的访问权限,即使它甚至没有所需的访问权限。想象一下,一个根进程创建一个非根子进程,但这个子进程将获得根父进程的所有打开文件描述符的访问权限,包括只能由根或受保护的服务器套接字下端口1024以下写入的打开文件。fcntl()
来设置FD_CLOEXEC
标志,但请注意,在多线程环境中这样做是不安全的。考虑以下代码:int so = socket(...);
fcntl(so, F_SETFD, FD_CLOEXEC);
fork()
,这当然是可能的,那么标志位还没有被设置,因此这个文件描述符不会被关闭。for (int i = STDERR_FILENO + 1; i < 256; i++) close(i);
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)
被记录为在使用setrlimit()
提高打开文件限制(RLIMIT_NOFILE
)后能够正确更新。资源限制(rlimits
)是运行进程和文件的有效限制,它们总是需要在_POSIX_OPEN_MAX
(文档中记录为进程始终允许打开的最小文件描述符数,必须至少为20
)和OPEN_MAX
(必须至少为_POSIX_OPEN_MAX
并设置上限)之间。
虽然在循环中关闭所有可能的描述符在技术上是正确的,并且将按预期工作,但它可能会尝试关闭几千个文件描述符,其中大部分通常不存在。即使对于不存在的文件描述符,close()
调用很快(这不被任何标准保证),在较弱的系统上可能需要一段时间(考虑嵌入式设备,考虑小型单板计算机),这可能是一个问题。
closefrom()
和fdwalk()
,它们得到了BSD和Solaris系统的支持。不幸的是,The Open Group投票反对将closefrom()
添加到标准中(引用):"在保证符合环境的情况下关闭任意文件描述符以上的接口无法标准化。"(来源)这显然是无稽之谈,因为他们自己制定规则,如果他们定义某些文件描述符可以始终从关闭中静默地省略,那么这不会破坏该函数的任何现有实现,并且仍然为我们提供所需的功能。如果没有这些函数,人们将使用循环来做The Open Group试图避免的事情,因此不添加它只会使情况变得更糟。open
返回的文件描述符是4。我想:“这很奇怪,标准输入、输出和错误通常是文件描述符0、1和2,所以你打开的第一个文件描述符通常是3。”/etc/group
上打开,而不是密码文件。]
printf()
的结果将是负数,并设置errno)。如果你关闭stdin,任何试图从中读取数据的尝试也会失败。父进程必须为子进程提供这些流,可以通过继承自己的流或创建新的流来实现(例如,你可以打开一个文件,然后将此文件句柄作为stdout,现在所有的printf()
调用都会写入该文件;或者你可以将管道的一端作为stdout并捕获子进程的输出)。 - Mecki/dev/null
并使用该文件ID作为stdin/out/err,如果您不希望子进程拥有任何这些内容。写入/dev/null
只是无处可去,始终是安全的,从中读取只返回“流结束”(EOF)。 - Mecki