一尘不染

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

linux

我想使用posix_spawn(…)(或非常相似的东西)生成一个进程集合。此函数接受posix_spawn_file_actions_t类型的参数,该参数允许我指定应如何对待打开的文件句柄。从文档中可以确定,所有文件都是从调用进程继承的,并根据posix_spawn_file_actions_t结构中的信息进行了修改。

我希望所有文件都不会被生成的进程打开(stdin,stdout和stderr除外)。有谁知道如何做到这一点?显然,这可以在某些实现中使用’POSIX_SPAWN_CLOEXEC_DEFAULT’派生属性标志来完成,但这在我的平台上不可用。每当我打开文件时,我也可以使用fcntl(…)指定“在exec上关闭”,但我认为更可本地化解决此问题的方法是可取的。


阅读 489

收藏
2020-06-07

共1个答案

一尘不染

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

通常,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.25ms才能运行;最大值分别为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()函数创建了一个执行时关闭控制管道,以区分执行子二进制文件的失败状态和子二进制文件退出状态。(如果子进程失败exec(),它将errno作为签名的char
写入控制管道。父进程尝试从控制管道的另一端读取单个签名的char。如果成功,则exec失败;父进程收获子进程使用例如waitpid(),并返回errno错误。否则,由于exec()导致管道已关闭,因此父进程知道子进程已开始执行,并可以关闭(控制管道的最后打开端)。

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

2020-06-07