用C语言从系统命令启动的进程继承父进程文件描述符

13

我有一个示例应用程序,是一个同时监听tcp和udp端口5060的SIP服务器。 在代码的某个点上,我执行了system("pppd file /etc/ppp/myoptions &");

执行了这个命令之后,如果我运行netstat -apn命令,它会显示出pppd也打开了5060端口! 有没有什么方法可以避免这种情况发生?这是Linux系统函数的标准行为吗?

谢谢, Elison

5个回答

15

默认情况下,每当您分叉一个进程(system 这样做时),子进程都会继承所有父进程的文件描述符。如果子进程不需要这些描述符,则应该将它们关闭。使用 system(或任何执行分叉+执行的方法)的方法是,在不应由您的进程的子进程使用的所有文件描述符上设置 FD_CLOEXEC 标志。这将导致它们在任何子进程执行其他程序时自动关闭。

一般来说,无论何时您的程序打开任何类型的文件描述符,并且该描述符将存活一段时间(例如您示例中的监听套接字),并且不应与子进程共享,您都应该执行此操作。

fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC);

从POSIX.1的2016年修订版开始,您可以将SOCK_CLOEXEC标志或'd应用于套接字的类型,以便在创建套接字时自动获得此行为:

在文件描述符上。

listenfd = socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC, 0);
bind(listenfd, ...
listen(listemfd, ...

这可以确保即使其他同时运行的线程执行了`system`或`fork`+`exec`调用,它也会被正确关闭。幸运的是,这个标志在Linux和BSD Unix上已经支持了一段时间(不幸的是,在OSX上并不支持)。


是的,我确实遇到过这个问题。在我改用fork()/exec()之前,我将继续使用这种方法。 - Elison Niven
3
使用 POSIX 2008 或 GNU,您还可以在打开文件时使用 O_CLOEXEC 标志。这样可以避免进行单独的调用,并使操作原子化,如果另一个线程(或信号处理程序)在 open 调用和 fcntl 调用之间进行 fork-and-exec,则此操作很重要。 - R.. GitHub STOP HELPING ICE

3

您应该避免使用system()函数。它本质上是危险的,因为它会调用shell,可以被篡改且不太可移植,即使在不同的Unix系统之间也是如此。

您应该使用fork()/exec()来实现。步骤如下:

if(!fork()){
     //close file descriptors
     ...

    execlp("pppd", "pppd", "file", "/etc/ppp/myoptions", NULL);
    perror("exec");
    exit(-1);
}

如果除了套接字之外还有其他文件描述符,您可能需要添加一些建议来找出要关闭哪些文件描述符。 - Spudd86
谢谢。是的,我一定会用fork()/exec()替换system()。目前我的代码多次调用system()!我使用system()是因为我可以通过WIFEXITED获取返回状态,并且编码速度更快!顺便问一下,如果我需要从我的C代码中完成很多事情,比如写/ etc/resolv.conf、启动pppd、杀死pppd、运行iptables -F等,你建议我遵循什么方法?现在所有这些都是通过system()完成的。 - Elison Niven

1

是的,这是Linux中fork()的标准行为,system()也是由它实现的。

socket()调用返回的标识符是一个有效的文件描述符。该值可用于文件导向的函数,例如read()write()ioctl()close()

反之,每个文件描述符都是套接字的说法并不正确。不能使用open()打开常规文件,然后将该描述符传递给bind()listen()等函数。

当您调用system()时,子进程会继承与父进程相同的文件描述符。这就是子进程如何继承stdout(0)、stdin(1)和stderr(2)的方式。如果您安排使用文件描述符为0、1或2的套接字,则子进程将继承该套接字作为标准I/O文件描述符之一。

您的子进程继承了父进程中打开的每个文件描述符,包括您打开的套接字。


我在两个网络接口上打开SIP服务器,支持udp和tcp端口。所以,在system()调用之前,netstat会显示5060的4个条目,其中2个是用于eth0的tcp,另外2个是用于eth1的udp。在system(pppd...)调用之后,netstat会给出6个pppd()的条目。pppd会继承eth0的2个套接字。可能是因为一旦建立pppd连接,我就关闭了主应用程序中eth1接口上的SIP服务器套接字。这解释了为什么只有2个被继承。我每隔2秒钟从shell脚本中调用netstat,所以可能错过了pppd继承所有4个套接字的时机。 - Elison Niven

1

正如其他人所述,这是程序依赖的标准行为。

当涉及到防止它时,您有几个选项。首先是像Dave建议的在fork()后关闭所有文件描述符。其次,有使用fcntlFD_CLOEXEC一起使用的POSIX支持,以在每个fd基础上设置“close on exec”位的方法。

最后,由于您提到正在运行Linux,因此有一组更改旨在让您在打开事物的时候就正确地设置该位。自然,这取决于平台。可以在http://udrepper.livejournal.com/20407.html找到概述。

这意味着您可以在套接字创建调用中使用位或运算符来设置SOCK_CLOEXEC标志。前提是您正在运行2.6.27或更高版本的内核。


我正在使用2.6.30版本,所以我将使用带有FD_CLOEXEC的fcntl。 - Elison Niven

-1

system()函数会复制当前进程并在其之上启动一个子进程。(当前进程已经不存在了。这可能就是pppd使用5060的原因。你可以尝试使用fork()/exec()来创建一个子进程并保持父进程活着。


1
-1:系统不会替换/终止父进程。实际上,system()使用了fork()/exec() - Heath Hunnicutt
Heath Hunnicutt: 谢谢,所以在 system() 执行后会产生2个进程?父进程和子进程? - hari
system() 之后,会有两个进程,即父进程和子进程 -- 但是调用还没有返回。system() 的实现会在子 PID 上调用 waitpid。在 pppd 的情况下,该进程会自我后台化(使用 fork/exec 而不带有 waitpid),因此瞬间会有 三个 进程,其中两个是 pppd,一个是父进程。当 system 返回时,子进程已经退出,因为 C 中的返回值包含由生成程序提供给 exit 的值。一旦 system 返回,子进程必须已经退出。 - Heath Hunnicutt
@Heath Hunnicutt:酷,谢谢你的解释。现在有点明白了。 - hari

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