调用posix_spawn时关闭所有文件句柄

Close all File Handles when Calling posix_spawn

本文关键字:文件句柄 posix spawn 调用      更新时间:2023-10-16

我想使用 posix_spawn(...) (或非常类似的东西)生成一组进程。此函数接受类型 posix_spawn_file_actions_t 的参数,这允许我指定应如何处理打开的文件句柄。从我从文档中可以确定的内容来看,所有文件都是从调用进程继承的,并根据posix_spawn_file_actions_t结构中的信息进行修改。

我希望生成进程取消打开所有文件(stdin、stdout 和 stderr 除外)。有谁知道如何做到这一点?显然,这可以在使用"POSIX_SPAWN_CLOEXEC_DEFAULT"生成属性标志的某些实现上完成,但这在我的平台上不可用。每当我打开文件时,我还可以使用 fcntl(...) 指定"在执行时关闭",但我觉得这个问题的更本地化的解决方案会更可取。

使用文件租约和/或fcntl()锁(记录锁)在多线程应用程序中处理fork()exec*()的开放文件描述符是很困难的。

通常,O_CLOEXEC/fcntl(fd, F_SETFD, FD_CLOEXEC)选项比显式关闭描述符更可取,因为显式关闭描述符会产生一些不良的副作用。特别是,如果描述符上有租约,则在子进程中关闭描述符将释放租约。

注意,在Linux中,fcntl()锁不是跨fork()继承的;参见man 2 fork中的描述。

posix_spawn()在 C 库中实现,文件操作可以通过posix_spawn_file_actions_init()posix_spawn_file_actions_addclose()等进行管理;请查看手册页中的">另请参阅">列表。就个人而言,我不会使用此接口,因为在exec*()之前关闭子进程中的描述符至少一样简单。

由于上述所有原因,我个人更喜欢使用O_CLOEXEC打开文件和/或使用fcntl(fd,F_SETFD,FD_CLOEXEC),以便默认情况下所有描述符都关闭执行。类似的东西

#define _GNU_SOURCE
#define _POSIX_C_SOURCE 200809L
#include <unistd.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/resource.h>
void set_all_close_on_exec(void)
{
struct rlimit  rlim;
long           max;
int            fd;
/* Resource limit? */
#if defined(RLIMIT_NOFILE)
if (getrlimit(RLIMIT_NOFILE, &rlim) != 0)
rlim.rlim_max = 0;
#elif defined(RLIMIT_OFILE)
if (getrlimit(RLIMIT_OFILE, &rlim) != 0)
rlim.rlim_max = 0;
#else
/* POSIX: 8 message queues, 20 files, 8 streams */
rlim.rlim_max = 36;
#endif
/* Configured limit? */
#if defined(_SC_OPEN_MAX)
max = sysconf(_SC_OPEN_MAX);
#else
max = 36L;
#endif
/* Use the bigger of the two. */
if ((int)max > (int)rlim.rlim_max)
fd = max;
else
fd = rlim.rlim_max;
while (fd-->0)
if (fd != STDIN_FILENO  &&
fd != STDOUT_FILENO &&
fd != STDERR_FILENO)
fcntl(fd, F_SETFD, FD_CLOEXEC);
}

是一种非常便携的方法,可以将所有打开的描述符(标准描述符除外)快速设置为关闭执行;库有时在内部使用描述符,并且可能不会设置O_CLOEXEC。在我的系统上,set_all_close_on_exec()运行需要 0.25 毫秒;最大值分别为 4096 和 1024,因此最终尝试设置 4093 个文件描述符。

(请注意,对于所有有效描述符,fcntl(fd,F_SETFD,FD_CLOEXEC)应成功,对于其他(无效/未使用的)描述符,应失败并errno==EBADF

请注意,简单地尝试在所有可能的描述符上设置标志比尝试找出哪些描述符实际上是打开的要快得多。(后者在 Linux 中是可能的,例如/proc/self/fd/.)

其次,我更喜欢使用帮助程序函数来创建到子进程的控制管道,将文件描述符移动到适当的位置(这并不总是微不足道的),并分叉子进程。签名通常类似于

int do_exec(pid_t *const childptr,
const char *const cmd,
const char *const args[],
const int stdin_fd,
const int stdout_fd,
const int stderr_fd);

我的do_exec()函数创建一个 close-on-exec 控制管道,以区分执行子二进制文件失败和子二进制文件退出状态。(如果子进程无法exec(),它会将errno作为签名字符写入控制管道。父进程尝试从控制管道的另一端读取单个签名字符。如果成功,则执行失败;父母使用例如waitpid(),并返回errno错误。否则,管道由于 exec() 而关闭,因此父进程知道子执行已启动,并且可以关闭(控制管道的最后一个打开端)。

最后,如果您有一个多线程服务器类型的进程,需要以最小的延迟和资源使用生成新的子进程,请使用 Unix 域套接字启动连接到原始进程的单个子进程(因为您可以使用辅助消息使用这些消息传输凭据和文件描述符),并让该子进程启动实际的子进程。这正是例如Apache mod_cgid和大多数FastCGI实现所做的。

在创建新进程之前,请在所有打开的文件描述符上设置FD_CLOEXEC(使用fcntl()),或者open()设置O_CLOEXEC标志。

从 posix_spawn() 规范:

如果 file_actions 是空指针,则在调用进程中打开的文件描述符在子进程中应保持打开状态,但设置了 close-on- exec 标志FD_CLOEXEC的文件描述符除外(请参阅 fcntl() )。