我想使用posix_spawn(…)(或者非常相似的东西)产生一系列进程。 这个函数接受一个posix_spawn_file_actions_ttypes的参数,它允许我指定如何处理打开的文件句柄。 从文档中我可以确定,所有文件都从调用进程inheritance,并根据posix_spawn_file_actions_t结构中的信息进行修改。
我希望所有文件都未被生成的进程打开(stdin,stdout和stderr除外)。 有谁知道如何做到这一点? 显然这可以在一些实现中使用'POSIX_SPAWN_CLOEXEC_DEFAULT'spawn属性标志来完成,但是这在我的平台上是不可用的。 我也可以使用fcntl(…)在打开文件的时候指定'close on exec',但是我觉得这个问题的一个更加本地化的解决scheme是可取的。
使用文件租约和/或fcntl()
锁(记录锁)在多线程应用程序中通过fork()
和exec*()
打开文件描述符处理是很冒险的。
通常, O_CLOEXEC
/ fcntl(fd, F_SETFD, FD_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/
。)
其次,我更喜欢使用一个辅助函数来为子进程创建一个控制管道,将文件描述符移动到适当的位置(这并不总是微不足道的),然后fork子进程。 签名通常是相似的
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
作为有符号字符写入控制管道,父进程尝试从控制管道的另一端读取一个有符号字符,如果成功,则exec失败;父节点使用例如waitpid()
返回子节点,并返回errno
错误;否则,管道因exec()关闭,所以父进程知道子进程已经启动,并且可以关闭(最后一个打开的结束)控制管道。)
最后,如果你有一个多线程的服务器类型的进程,需要用最少的延迟和资源使用产生新的子进程,用一个Unix域套接字启动一个连接到原始进程的子进程(因为你可以使用辅助消息来传输凭证并使用这些描述符),并让该子进程启动实际的子进程。 这正是Apache mod_cgid和大多数FastCGI实现所做的。
在创建新进程之前,在所有打开的文件描述符上设置FD_CLOEXEC
(使用fcntl()
) ,或者使用open()
设置O_CLOEXEC
标志。
从posix_spawn()规范 :
如果file_actions是一个空指针,那么在调用进程中打开的文件描述符应该在子进程中保持打开状态,除了那些执行了close-on-exec标志FD_CLOEXEC的文件描述符(见fcntl() )。