默认情况下,由exec()的调用程序所打开的文件描述符在exec()的执行过程中会保持打开状态,而且在新进程中依然有效。这通常很实用,因为调用程序可能会以特定的描述符来打开文件,而在新程序中这些文件将自动有效,无需再去了解文件名或是把它们重新打开。
shell 利用这一特性为其所执行的程序处理 I/O 重定向。例如,假设键入如下的 shell 命令:
$ ls /tmp > dir.txt
$ ls
dir.txt
shell运行该命令时,执行了以下步骤:
- 调用fork()创建子进程,子进程也会运行shell的一份拷贝(包括命令行)
- 子shell以描述符1(标准输出)打开文件dir.txt用于输出。可能会采取以下任一方式
- 子 shell 关闭描述符 1(STDOUT_FILENO)后,随即打开文件 dir.txt。因为 open()在为描述符取值时总是取最小值,而标准输入(描述符 0)又仍处于打开状态,所以会以描述符 1 来打开文件
- shell打开文件dir.txt,获取一个新的文件描述符。之后,如果该文件描述符不是标准输
出,那么 shell 会使用 dup2()强制将标准输出复制为新描述符的副本,并将此时已然无用的新描述符关闭。(这种方法较之前者更为安全,因为它并不依赖于打开文件描述符的低值取数原则。)源代码顺序大体如下
fd = open("dir.txt", O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR | SZ-IRGRP | S_IROTH | S_IWOTH);
if(fd != STDOUT_FILENO){
dup2(fd, STDOUT_FILENO);
close(fd);
}
- 子进程执行ls。ls将其结果输出到标准输出,亦即文件 dir.txt 中
执行时关闭(close-on-exec)标志(FD_CLOEXEC)
在执行exec()之前,程序有时需要确保关闭某些特定的文件描述符。尤其是在特权进程中调用 exec()来启动一个未知程序时(并非自己编写),亦或是启动程序并不需要使用这些已打开的文件描述符时,从安全编程的角度出发,应当在加载新程序之前确保关闭那些不必要的文件描述符。对所有此类描述符施以 close()调用就可达到这一目的,然而这一做法存在如下局限性
- 某些描述符可能是由库函数打开的。但库函数无法使主程序在执行exec()之前关闭相应的文件描述符。作为基本原则,库函数应总是为其打开的文件设置执行时关闭(close-on-exec)标志
- 如果 exec()因某种原因而调用失败,可能还需要使描述符保持打开状态。如果这些描述符已然关闭,将它们重新打开并指向相同文件的难度很大,基本上不太可能
为此,内核为每个文件描述符提供了执行时关闭标志。如果设置了这一标志,那么在成功执行exec()时,会自动关闭该文件描述符。如果exec()失败,文件描述符会保持打开状态。
可以通过系统调用fcntl()来访问执行时关闭标志。fcntl()的F_GETFD操作可以获取文件描述符标志的一份拷贝。
int flags;
flags = fcntl(fd, F_GETFD);
if(flags == -1){
perror("fcntl");
exit(1);
}
获取这些标志之后,可以对FD_CLOEXEC 位进行修改,再调用 fcntl()的 F_SETFD 操作令其生效:
flags != FD_CLOEXEC;
if(fcntl(fd, F_SETFD, flags) == -1){
perror("fcntl");
exit(1);
}
包括 Linux 在内的许多 UNIX 实现,还允许以另外两种非标准的 ioctl()调用来修改执行时关闭标志:以 ioctl(fd, FIOCLEX)为 fd 设置此标志,以 ioctl(fd, FIONCLEX)来清除此标志
当使用 dup()、dup2()或 fcntl()为一文件描述符创建副本时,总是会清除副本描述符的执行时关闭标志.
看个例子:
//为一文件描述符设置执行时关闭标志
#include <cstring>
#include <stdio.h>
#include <cstdlib>
#include <zconf.h>
#include <fcntl.h>
int
main(int argc, char *argv[])
{
int flags;
if (argc > 1) {
flags = fcntl(STDOUT_FILENO, F_GETFD); /* Fetch flags */
if (flags == -1){
perror("fcntl - F_GETFD");
exit(EXIT_FAILURE);
}
flags |= FD_CLOEXEC; /* Turn on FD_CLOEXEC */
if (fcntl(STDOUT_FILENO, F_SETFD, flags) == -1) /* Update flags */
{
perror("fcntl - F_SETFD");
exit(EXIT_FAILURE);
}
}
execlp("ls", "ls", "-l", argv[0], (char *) NULL);
perror("execlp");
exit(EXIT_FAILURE);
}