文件描述符
每个文件、套接字、管道等......在您的进程中由一个称为文件描述符的数字唯一标识。
如果您创建一个新的文件描述符,您将获得进程中最低的未使用文件描述符编号,从 0 开始。
每个文件的前 3 个文件描述符都有特殊的作用:
FD |
C 常数 |
0 |
STDIN_FILENO |
1 |
STDOUT_FILENO |
2 |
STDERR_FILENO |
如果您愿意,您可以随时通过查询来查看文件描述符(以及它们所指的内容)/proc
,例如:
ls -l /proc/<pid of your process>/fd
execve 及其朋友
execve
用参数指定的新进程替换当前进程。
您的进程打开的所有文件描述符将保持打开状态¹,新进程可以使用它们。
¹ 标记为的除外close-on-exec
你的程序做什么
在您的程序启动后,您的文件描述符可能如下所示:
0 -> /dev/pts/1
1 -> /dev/pts/1
2 -> /dev/pts/1
(只是普通的标准输入、标准输出、标准错误,连接到普通终端)
之后你分配一个套接字:
int sock = socket(AF_INET, SOCK_STREAM, 0);
0 -> /dev/pts/1
1 -> /dev/pts/1
2 -> /dev/pts/1
3 -> [socket:12345]
然后你连接套接字并到达 dup2 的。
dup2
克隆一个文件描述符并 - 不像dup
- 为其分配一个特定的文件描述符编号(如果该 fd 已在使用中,它将首先关闭)
所以在dup2(sock, STDIN_FILENO);
你的fd之后看起来像这样:
0 -> [socket:12345]
1 -> /dev/pts/1
2 -> /dev/pts/1
3 -> [socket:12345]
所以在execl
fd之前是:
0 -> [socket:12345]
1 -> [socket:12345]
2 -> [socket:12345]
3 -> [socket:12345]
然后你的进程执行到/bin/sh
,用 shell 替换当前进程。
所以现在你有一个shell,它的输入和输出连接到你创建的套接字,有效地允许套接字另一端的程序发送任意shell命令,这些命令将由套接字执行/bin/sh
并通过套接字返回输出。
正如@JonathanLeffler 在评论中指出的那样,fd 3 可以在执行之前关闭,因为它不是必需的。
为什么不使用dup
代替dup2
?
使用dup
,就像您引用的那样,将为您提供流程中可用的最低可用 fd。
因此,可以执行以下操作:
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
dup(sock);
dup(sock);
dup(sock);
关闭将关闭 fd 0-2:
3 -> [socket:12345]
并且 dup 会将 fd 3 复制到 0-2 (你总是得到最低的可用数字,即使那些是标准输入、标准输出或标准错误)
0 -> [socket:12345]
1 -> [socket:12345]
2 -> [socket:12345]
3 -> [socket:12345]
但是,如果您有其他线程正在创建 fd,这可能会出错(例如,另一个线程可能只是在您关闭 stdin 后创建一个新 fd,因此它得到 fd 0,而您的 dup() 稍后将得到 4)。
这dup2()
就是问题所在:精确分配特定的 fd(在本例中为 stdin、stdout、stderr)。
dup2() 系统调用执行与 dup() 相同的任务,但它不使用编号最小的未使用文件描述符,而是使用 newfd 中指定的文件描述符编号。换句话说,文件描述符 newfd 被调整,以便它现在引用与 oldfd 相同的打开文件描述。
还有dup3
, 除了dup2
可以做什么之外,它还允许您指定标志,例如O_CLOEXEC
,在执行时会自动关闭 fd。