在Linux fork过程中防止文件描述符的继承

36

如何防止文件描述符在fork()系统调用中被复制继承(当然不能关闭它)?

我正在寻找一种方法标记单个文件描述符,在fork()时不被子进程(复制)继承,类似于FD_CLOEXEC的黑科技,但适用于forks(因此如果您喜欢,可以使用FD_DONTINHERIT功能)。有人做过这个吗?或者研究过这个并给我一个提示开始?

谢谢

更新:

我可以使用libc的__register_atfork

 __register_atfork(NULL, NULL, fdcleaner, NULL)

fork()返回之前关闭子进程中的fds。然而,FD仍在被复制,所以这听起来像是一个愚蠢的hack。问题是如何跳过不需要的FD的子进程中的dup()

我正在考虑一些情况下需要使用fcntl(fd, F_SETFL, F_DONTINHERIT)

  • fork()将复制事件FD(例如epoll());有时这是不想要的,例如FreeBSD将kqueue()事件FD标记为KQUEUE_TYPE,这些类型的FD不会被复制到fork(kqueue FD明确地从被复制中跳过,如果想要从子进程中使用它,则必须使用共享FD表进行fork)

  • fork()将复制100k个不需要的FDs以fork子进程执行某些CPU密集型任务(假设需要fork()的概率非常低,程序员不想为通常不会发生的事情维护一个子进程池)

我们希望复制一些描述符(0、1、2),但大多数描述符可能都不需要复制。我认为完整的FD表复制是出于历史原因,但我可能是错误的。

这听起来多么愚蠢:

  • 修改fcntl()以支持文件描述符上的dontinherit标志(不确定该标志应该是每个FD保留还是在FD表fd_set中保留,就像close-on-exec标志一样被保留)
  • 在内核中修改dup_fd()以跳过复制dontinherit FDs,与FreeBSD对kq FDs所做的相同

考虑这个程序

#include <stdio.h>
#include <unistd.h>
#include <err.h>
#include <stdlib.h>
#include <fcntl.h>
#include <time.h>

static int fds[NUMFDS];
clock_t t1;

static void cleanup(int i)
{
    while(i-- >= 0) close(fds[i]);
}
void clk_start(void)
{
    t1 = clock();
}
void clk_end(void)
{  

    double tix = (double)clock() - t1;
    double sex = tix/CLOCKS_PER_SEC;
    printf("fork_cost(%d fds)=%fticks(%f seconds)\n",
        NUMFDS,tix,sex);
}
int main(int argc, char **argv)
{
    pid_t pid;
    int i;
    __register_atfork(clk_start,clk_end,NULL,NULL);
    for (i = 0; i < NUMFDS; i++) {
        fds[i] = open("/dev/null",O_RDONLY);
        if (fds[i] == -1) {
            cleanup(i);
            errx(EXIT_FAILURE,"open_fds:");
        }
    }
    t1 = clock();
    pid = fork();
    if (pid < 0) {
        errx(EXIT_FAILURE,"fork:");
    }
    if (pid == 0) {
        cleanup(NUMFDS);
        exit(0);
    } else {
        wait(&i);
        cleanup(NUMFDS);
    }
    exit(0);
    return 0;
}

当然,不能把这个视为真正的工作台,但无论如何:
root@pinkpony:/home/cia/dev/kqueue# time ./forkit
fork_cost(100 fds)=0.000000ticks(0.000000 seconds)

real    0m0.004s
user    0m0.000s
sys     0m0.000s
root@pinkpony:/home/cia/dev/kqueue# gcc -DNUMFDS=100000 -o forkit forkit.c
root@pinkpony:/home/cia/dev/kqueue# time ./forkit
fork_cost(100000 fds)=10000.000000ticks(0.010000 seconds)

real    0m0.287s
user    0m0.010s
sys     0m0.240s
root@pinkpony:/home/cia/dev/kqueue# gcc -DNUMFDS=100 -o forkit forkit.c
root@pinkpony:/home/cia/dev/kqueue# time ./forkit
fork_cost(100 fds)=0.000000ticks(0.000000 seconds)

real    0m0.004s
user    0m0.000s
sys     0m0.000s

forkit 运行在戴尔Inspiron 1520上,使用Intel(R) Core(TM)2 Duo CPU T7500 @ 2.20GHz和4GB RAM;平均负载为0.00。


3
你可以这样说FD_CLOEXEC是无用的,因为你可以在执行exec()之前关闭fd。第三方库正在进行fork(),我不准备干涉那段代码并为自己使用而分支。 - user237419
2
你必须有点创意。你可以尝试fork()并在库fork之前关闭它。 - sjr
3
谁说FD_CLOEXEC没用了?! - user237419
2
我可以提供另一个原因,说明为什么这将是一个有用的特性。在我的代码中,我遇到了一些问题,其中一个进程关闭了TCP端口,然后尝试立即再次打开它,但由于子进程已经打开了它,所以不允许再次打开。为了解决这个问题,我让子进程关闭FD。然而,这仍然存在竞争条件,因此需要额外的代码来使子进程能够向父进程发出已关闭FD的信号,并且父进程在继续之前等待此信号。如果我们有一个FD_DONTINHERIT标志,那么就可以省去父进程中的阻塞代码。 - Alex Zeffertt
2
@AlexZeffertt 另一种出现相同问题的情况是在类似于“make”的系统中,多个线程编写shell脚本,然后fork()+exec()这些脚本。当一个fork()进程恰好继承了另一个线程编写的shell脚本时,就会出现“文本文件忙”的错误提示。 - user239558
显示剩余14条评论
3个回答

9

如果你使用fork来调用一个exec函数,你可以使用fcntlFD_CLOEXEC一起使用,以便在exec后关闭文件描述符:

int fd = open(...);
fcntl(fd, F_SETFD, FD_CLOEXEC);

这样的文件描述符会在fork后继续存在,但不会在exec函数族中生效。


6
问题中提到了FD_CLOEXEC,这也解释了为什么它不适用于这种情况(没有调用exec)。此外,fcntl并不是设置关闭-on-exec标志的最佳方式。 - Ben Voigt
1
@BenVoigt,您说“fnctl”不是设置close-on-exec标志的最佳方法是什么意思? - Akronix
2
@Akronix:如果另一个线程在此线程调用openfcntl之间调用exec()(类似于fork()),会发生什么情况?两阶段初始化无法解决此问题。这在fcntlopen手册中明确提到是一个问题,解决方案是使用O_CLOEXEC标志来调用open() - Ben Voigt
我想在特定的多线程程序中,你必须像手册上所说的那样关注这些情况。 问题在于,在我的情况下,例如,我需要让某些子进程继承一个fd,然后阻止该fd再次被继承。换句话说,我需要能够在程序执行期间设置和取消fd的FD_DONT_INHERIT标志。 - Akronix
@Akronix:如果你只需要在一个子进程中使用它,请在fork()之后再打开它。 - Ben Voigt
4
或者创建一个带有额外参数的“fork”调用,该参数是需要显式继承的文件描述符列表,无论标志位如何。但不要设计出一种情况,其中不能控制另一个线程上的“fork()”是否能看到标志位。 - Ben Voigt

7

不需要我来关闭,因为你知道哪些需要关闭。


10
“不”是对问题的回答,但“自己关闭它们”则不是。正如在原始问题的评论中所指出的,这会导致多种情况下的竞态条件问题。 - user239558
2
如果您将对Oracle数据库的访问嵌入到软件中,Oracle本身会创建一个继承您打开的文件和套接字的进程分支。特别是TCP服务器套接字仍然处于活动状态,并防止在主应用程序中关闭和重新打开它们。没有地方可以关闭自己的fh,只有描述的fcntl可能有所帮助。 - Daniel Alder

6
据我所知,没有标准的方法来实现这个功能。
如果你想要正确地实现它,最好的方式可能是添加一个系统调用,将文件描述符标记为在fork后关闭,并拦截sys_fork系统调用(系统调用号2),在调用原始的sys_fork之后对这些标志进行操作。
如果你不想添加新的系统调用,也许可以通过拦截sys_ioctl(系统调用号54)并只添加一个新命令来标记一个文件描述符为close-on-fork来解决问题。
当然,如果你能控制你的应用程序,那么维护用户级表格,其中包含你想要在fork时关闭的所有文件描述符,并调用你自己的myfork可能会更好。这样,它会fork,然后浏览用户级表格关闭那些被标记的文件描述符。
那么,你就不必再Linux内核中摆弄了,这种解决方案可能只有在你无法控制fork过程(例如,第三方库正在进行fork()调用)时才是必要的。

1
听起来太复杂了,我认为这不是正确的方法。我在主问题中更新了另外两种可能的解决方案。谢谢你,Diablo! - user237419
2
考虑一个更通用的解决方案,添加一个新的fcntl标志并修改内核中的dup_fd()(补丁似乎很容易应用)来测试它...这听起来太过于侵入性了吗?至少乍一看比syscall/ioctl方式要少工作。dup_fd是在fork时进行fdcopy的地方,而且似乎这个函数只与fork()系统调用有关。 - user237419

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接