为什么在fork之后关闭文件描述符会影响子进程?

5

我希望能够通过点击按钮在Linux中运行程序,因此我编写了一个名为execute的函数:

void execute(const char* program_call, const char* param )
{
    pid_t child = vfork();

    if(child == 0) // child process
    {
        int child_pid = getpid();

        char *args[2]; // arguments for exec
        args[0] = (char*)program_call; // first argument is program_call
        args[1] = (char*)param;

        // close all opened file descriptors:
        const char* prefix = "/proc/";
        const char* suffix = "/fd/";
        char child_proc_dir[16]; 
        sprintf(child_proc_dir,"%s%d%s",prefix,child_pid, suffix);

        DIR *dir;
        struct dirent *ent;

        if ((dir = opendir (child_proc_dir)) != NULL) {
            // get files and directories within directory
            while ((ent = readdir (dir)) != NULL) {
                // convert file name to int
                char* end;
                int fd = strtol(ent->d_name, &end, 32);
                if (!*end) // valid file descriptor
                {
                    close(fd); // close file descriptor
                    // or set the flag FD_CLOEXEC
                    //fcntl( fd, F_SETFD, FD_CLOEXEC );
                }
            }
            closedir (dir);
        } 
        else 
        {
            cerr<< "can not open directory: " << child_proc_dir <<endl;
        }
        // replace the child process with exec*-function
            execv(program_call,args);
            _exit(2);
        }
    else if (child == -1) // fork error
    {
        if (errno == EAGAIN)
        {
            cerr<<“To much processes"<<endl;
        }
        else if (errno == ENOMEM)
        {
            cerr<<“Not enough space available."<<endl;
        }
    }
    else // parent process
    {
        usleep(50); // give some time 
        if ( errno == EACCES)
        {
            cerr<<“Permission denied or process file not executable."<<endl;
        }
        else if ( errno == ENOENT)
        {
            cerr<<"\n Invalid path or file."<<endl;
        }
        int child_status;
        if ( waitpid(child, &child_status, WNOHANG | WUNTRACED) < 0) // waitpid failed
        {
            cerr<<"Error - Execution failed"<<endl;
        }
        else if ( WIFEXITED( child_status ) &&  WEXITSTATUS( child_status ) != 0)   
        {
            cerr<<“Child process error - Execution failed"<<endl;
        }
    }
}

有两个问题:
  1. 关闭文件描述符会引起一些问题,例如Thunderbird崩溃或VLC没有声音。更准确地说:关闭stdout(1)stderr(2)会引起这些问题。据我所知,在exec之前关闭文件描述符只是防止它们被复制(子进程不需要将信息发送给父进程)。为什么这会影响子进程呢?将close()替换为设置FD_CLOEXEC标志并没有改变任何事情。在fork之前设置FD_CLOEXEC标志也无法解决这个问题。有没有更好的方法来防止文件描述符的继承?

  2. waitpid的返回值通常为0,即使程序调用失败,我认为这是因为有两个(异步)进程。 usleep(50)可以解决我的需求,但我希望有更好的解决方案。

我正在使用vfork,但使用fork也会出现相同的问题。


2
据我所知,文件描述符在子进程和父进程之间是共享的。你需要在子进程中使用dup(2)创建原始描述符的副本,并仅在子进程中使用该副本。 - Philipp Murry
你的应用程序在做什么?它使用哪些文件描述符?你的 execute 如何使用? - Basile Starynkevitch
我的应用程序应该是其他应用程序的简单启动面板。它目前没有使用任何多余的文件描述符,但以后可能会更改。在按钮回调函数(FLTK)中使用execute。@Philipp Murry:仅使用从dup2()复制的文件描述符的优点是什么? - Lexa
使用 dup 命令,您将获得一个新的文件描述符,它与第一个是独立的。这就像再次对文件调用 open 命令一样。因此,关闭第一个文件描述符不会影响第二个(使用 dup 创建的副本)。 - Philipp Murry
2个回答

5
首先,在2014年,不要使用vfork,而是直接使用fork(2)。(因为自POSIX 2001以来,vfork(2)已经被废弃,并在POSIX 2008中删除)。
其次,关闭大多数文件描述符的最简单方法只是:
for (int fd=3; fd<256; fd++) (void) close(fd);

关于关闭文件描述符的说明

(提示:如果一个 fd 是无效的,close(fd)会失败,我们忽略这种情况;从 3 开始保持打开状态,以便让 0==stdin, 1==stdout, 2==stderr;所以在原则上,所有上面的 close 都将失败)。

然而,良好行为和编写良好的程序不应该需要这样的关闭循环(因此这是一种克服先前错误的粗略方式)。

当然,如果你知道除了 stdin、stdout、stderr 之外的某些文件描述符对子进程 program_call 是有效的且需要使用(这是不太可能的),那么你需要显式地跳过它。

然后尽可能使用 FD_CLOEXEC

很少会出现你的程序会有很多文件描述符而你却不知道它们。

也许你想要使用daemon(3)或(由 vality 提供的评论)posix_spawn

如果你需要显式关闭 STDIN_FILENO(即 0),或 STDOUT_FILENO(即 1),或 STDERR_FILENO(即 2),最好使用 open("/dev/null",... 和在调用 exec 之前调用 dup2 将它们重定向,因为大多数程序都期望这些文件描述符存在。


1
我不建议使用for循环来关闭所有文件描述符,因为这样可能会关闭有效的文件描述符。我曾经看到过类似这样的代码,因为某个人在那个循环之前打开了一个文件并在之后使用了文件描述符而导致程序崩溃。您能详细说明一下为什么不建议使用vfork吗? - Alexander Oh
请参阅vfork(2)。请注意,它不在POSIX 2008中(并且在POSIX 2001中已过时)。 - Basile Starynkevitch
2
我认为vfork在很大程度上是无害的,并且很可能更好地表达意图,尽管现在已从posix中删除,但从vfork的最佳迁移路径是posix_spawn,因为它比任何一种方式都要快,并立即执行另一个程序,避免了逻辑错误的可能性。 - Vality
1
看起来你在进行某种任务,Basile。如果你所在的系统允许vfork共享地址空间,并且不允许超额提交虚拟内存,那么vfork并不是“无用”的。而且,在vfork进程中专门操作文件描述符是安全的(例如设置输出管道),因为这些都会被复制。话虽如此,OP的示例代码在使用vfork时确实存在一些巨大的问题,因为它会严重干扰子进程的堆栈。 - codetaku
如果execve-d程序需要打开一些文件,那么这就是必要的。但是一个表现良好的程序应该关闭所有无用的fd-s(因此在“close”上的循环应该是无用的)。 - Basile Starynkevitch
显示剩余2条评论

4

第一个问题:除非您自己关闭它们或设置FD_CLOEXEC,否则无法防止文件描述符的继承,请查看此链接

第二个问题:在waitpid中指定了WNOHANG,因此您经常会得到waitpid的返回值为0。

waitpid(): on success, returns the process ID of the child whose state has changed; 
if WNOHANG was specified  and  one  or  more  child(ren) specified by pid exist, 
but have not yet changed state, then 0 is returned.  On error, -1 is returned.

我的应用程序应该像其他程序的启动面板一样工作,因此我需要指定WNOHANG,否则 - 据我所知 - 我无法同时调用多个程序。我只想获取信息,以便在执行失败时找出失败原因并显示消息。问题是,waitpit不能可靠地返回-1表示失败。 - Lexa
1
我认为你最好为 SIGCHLD 设置一个处理程序,当一个程序在执行时失败,你会收到信号 SIGCHLD。这比使用 waitpidWNOHANG 检查要更加健壮。 - D3Hunter

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