我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 dup
和 dup2
函数,它们用于复制一个已存在的文件描述符 (file descriptor)。
1. 函数介绍
dup
和 dup2
是 Linux 系统调用,它们的功能是创建一个指向同一文件表项 (open file description) 的新文件描述符。
简单来说,当你调用 dup
或 dup2
时,你得到的是一个别名或副本,这个新文件描述符和原始文件描述符指向同一个打开的文件,共享文件的:
- 文件偏移量 (file offset): 通过一个描述符读写会改变文件位置,通过另一个描述符读写会从新的位置开始。
- 状态标志 (status flags): 如
O_APPEND
,O_NONBLOCK
等。 - 文件锁 (file locks): 通过任何一个描述符获取的锁,对另一个描述符也有效。
它们最常见的用途是重定向标准输入、标准输出或标准错误。例如,将一个程序的输出重定向到文件,而不是终端。
你可以把文件描述符想象成一个指向文件的“把手”。dup
就像是给这个“把手”又做了一个一模一样的复制品。你拿着任何一个“把手”都能操作同一个文件,而且它们的状态是同步的。
2. 函数原型
#include <unistd.h> // 必需
// 复制文件描述符 (返回新的最小可用 fd)
int dup(int oldfd);
// 复制文件描述符到指定的新 fd
int dup2(int oldfd, int newfd);
3. 功能
dup(int oldfd)
:- 复制一个已存在的文件描述符
oldfd
。 - 内核会在当前进程中选择最小的未使用的文件描述符号码作为新的描述符。
- 新的文件描述符和
oldfd
指向同一个文件表项。
- 复制一个已存在的文件描述符
dup2(int oldfd, int newfd)
:- 复制文件描述符
oldfd
,并强制使复制得到的新文件描述符的号码为newfd
。 - 如果
newfd
已经打开(指向另一个文件),dup2
会在复制前先关闭newfd
(相当于先调用close(newfd)
)。 - 如果
oldfd
和newfd
相同,dup2
什么都不做,直接返回newfd
。
- 复制文件描述符
4. 参数
dup
:int oldfd
: 要被复制的现有有效文件描述符。
dup2
:int oldfd
: 要被复制的现有有效文件描述符。int newfd
: 请求的新文件描述符号码。- 如果
newfd
已经打开,它会被关闭。 - 如果
newfd
等于oldfd
,则不执行任何操作。
- 如果
5. 返回值
- 成功时:
- 返回新的文件描述符号码。
- 对于
dup
,这个号码是当前进程中最小的可用号码。 - 对于
dup2
,这个号码就是请求的newfd
。
- 失败时:
- 返回 -1,并设置全局变量
errno
来指示具体的错误原因(例如EBADF
oldfd
或newfd
无效,EMFILE
进程打开的文件描述符已达上限等)。
- 返回 -1,并设置全局变量
6. 相似函数,或关联函数
fcntl
:dup(oldfd)
等价于fcntl(oldfd, F_DUPFD, 0)
。fcntl
提供了更灵活的复制方式,例如F_DUPFD_CLOEXEC
可以在复制时设置FD_CLOEXEC
标志。close
:dup2
在复制前如果newfd
已打开,会隐式调用close(newfd)
。open
: 用于获取最初的文件描述符。read
,write
: 对复制后的文件描述符进行操作。
7. 示例代码
示例 1:使用 dup
和 dup2
重定向标准输出
这个例子演示了如何使用 dup
和 dup2
来保存原始标准输出,然后将标准输出重定向到一个文件,最后再恢复标准输出。
#include <unistd.h> // dup, dup2, close, write
#include <fcntl.h> // open, O_WRONLY, O_CREAT, O_TRUNC
#include <stdio.h> // printf, perror
#include <stdlib.h> // exit
int main() {
int saved_stdout; // 用于保存原始标准输出的文件描述符
int file_fd; // 用于重定向输出的文件描述符
// 1. 保存原始的标准输出 (stdout, 文件描述符 1)
saved_stdout = dup(STDOUT_FILENO);
if (saved_stdout == -1) {
perror("dup saved_stdout");
exit(EXIT_FAILURE);
}
printf("Saved original stdout to fd %d\n", saved_stdout);
// 2. 打开一个文件用于重定向
file_fd = open("output_redirection.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (file_fd == -1) {
perror("open file for redirection");
close(saved_stdout); // 清理
exit(EXIT_FAILURE);
}
printf("Opened file 'output_redirection.txt' with fd %d\n", file_fd);
// 3. 将标准输出重定向到文件
// 方法一:使用 dup2
if (dup2(file_fd, STDOUT_FILENO) == -1) {
perror("dup2 redirect stdout");
close(file_fd);
close(saved_stdout);
exit(EXIT_FAILURE);
}
printf("Standard output redirected to file.\n");
// 注意:从现在开始,printf 的输出将写入到文件中,而不是终端!
// 4. 输出一些内容到文件
printf("This line goes to the file.\n");
printf("This line also goes to the file.\n");
// 5. 恢复标准输出
// 使用 dup2 将保存的原始 stdout 描述符复制回 STDOUT_FILENO
if (dup2(saved_stdout, STDOUT_FILENO) == -1) {
perror("dup2 restore stdout");
// 清理
close(file_fd);
close(saved_stdout);
exit(EXIT_FAILURE);
}
printf("Standard output restored to terminal.\n");
// 注意:从现在开始,printf 的输出将重新显示在终端上!
// 6. 再输出一些内容到终端
printf("This line goes back to the terminal.\n");
// 7. 关闭所有打开的文件描述符
// close(file_fd) 实际上关闭了文件表项的一个引用
// dup2 在重定向时已经关闭了原来的 STDOUT_FILENO 对应的文件表项引用
// 所以这里只需要关闭 file_fd 和我们自己保存的 saved_stdout
if (close(file_fd) == -1) {
perror("close file_fd");
}
// 关闭 saved_stdout 也会关闭它指向的文件表项(即原始的终端 stdout)
if (close(saved_stdout) == -1) {
perror("close saved_stdout");
}
printf("All file descriptors closed.\n");
return 0;
}
代码解释:
- 调用
dup(STDOUT_FILENO)
创建原始标准输出(文件描述符 1)的一个副本,并将其保存在saved_stdout
中。这个副本让我们可以稍后恢复标准输出。 - 使用
open
创建或打开一个名为output_redirection.txt
的文件,用于接收重定向的输出。 - 调用
dup2(file_fd, STDOUT_FILENO)
。这会:- 关闭当前的
STDOUT_FILENO
(文件描述符 1)。 - 将
file_fd
复制一份,并使这个新副本的号码为 1 (即STDOUT_FILENO
)。 - 现在,文件描述符 1 和
file_fd
都指向output_redirection.txt
文件。
- 关闭当前的
- 执行
printf
。因为标准输出已经被重定向,所以这些内容会写入到output_redirection.txt
文件中。 - 调用
dup2(saved_stdout, STDOUT_FILENO)
来恢复标准输出。这会将之前保存的原始终端输出描述符复制回文件描述符 1。 - 再次执行
printf
。现在标准输出已经恢复,内容会显示在终端上。 - 最后,使用
close
关闭所有打开的文件描述符。
示例 2:dup2
自动关闭目标文件描述符
这个例子重点演示 dup2
在目标文件描述符已打开时自动关闭它的行为。
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
int fd1, fd2;
char buffer[100];
ssize_t bytes_read;
// 1. 打开两个不同的文件
fd1 = open("file1.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd1 == -1) {
perror("open file1.txt");
exit(EXIT_FAILURE);
}
write(fd1, "Content of file 1\n", 18);
fd2 = open("file2.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd2 == -1) {
perror("open file2.txt");
close(fd1);
exit(EXIT_FAILURE);
}
write(fd2, "Content of file 2\n", 18);
printf("Initially:\n");
printf(" fd1 points to 'file1.txt' (fd=%d)\n", fd1);
printf(" fd2 points to 'file2.txt' (fd=%d)\n", fd2);
// 2. 使用 dup2 将 fd1 复制到 fd2
// 这会自动关闭 fd2 当前指向的 'file2.txt'
printf("\nCalling dup2(fd1, fd2)...\n");
if (dup2(fd1, fd2) == -1) {
perror("dup2(fd1, fd2)");
close(fd1);
close(fd2);
exit(EXIT_FAILURE);
}
printf("After dup2(fd1, fd2):\n");
printf(" fd1 still points to 'file1.txt' (fd=%d)\n", fd1);
printf(" fd2 now ALSO points to 'file1.txt' (fd=%d)\n", fd2);
printf(" 'file2.txt' has been closed automatically.\n");
// 3. 验证:向 fd2 写入数据,应该出现在 'file1.txt' 中
write(fd2, "Data written via fd2 after dup2\n", 32);
// 4. 关闭文件描述符
// 关闭 fd1 和 fd2 实际上是关闭同一个文件表项的两个引用
// 内核会在最后一个引用关闭时才真正关闭文件
close(fd1);
close(fd2); // 这个 close 实际上是关闭 fd1/fd2 共同指向的文件表项
// 5. 读取 file1.txt 来验证内容
printf("\nContents of 'file1.txt' after operations:\n");
int read_fd = open("file1.txt", O_RDONLY);
if (read_fd != -1) {
while ((bytes_read = read(read_fd, buffer, sizeof(buffer) - 1)) > 0) {
buffer[bytes_read] = '\0';
printf("%s", buffer);
}
close(read_fd);
}
// 6. 检查 file2.txt 是否为空或被截断 (因为它被 dup2 自动关闭了)
printf("\nContents of 'file2.txt' (should be empty or just initial content if not truncated):\n");
read_fd = open("file2.txt", O_RDONLY);
if (read_fd != -1) {
while ((bytes_read = read(read_fd, buffer, sizeof(buffer) - 1)) > 0) {
buffer[bytes_read] = '\0';
printf("%s", buffer);
}
close(read_fd);
}
if (bytes_read == 0) {
printf("(file2.txt is empty)\n");
}
return 0;
}
代码解释:
- 打开两个文件
file1.txt
和file2.txt
,分别得到文件描述符fd1
和fd2
。 - 向两个文件写入不同的初始内容。
- 调用
dup2(fd1, fd2)
。关键点在于:fd2
当前是打开的,指向file2.txt
。dup2
会自动关闭fd2
。- 然后,它将
fd1
(指向file1.txt
)复制一份,并使这个副本的号码为fd2
。
- 现在,
fd1
和fd2
都指向file1.txt
。 - 向
fd2
写入数据,数据会出现在file1.txt
中,证明了fd2
现在确实指向file1.txt
。 - 关闭
fd1
和fd2
。由于它们指向同一个文件表项,文件只会在最后一个引用关闭时才真正关闭。 - 读取
file1.txt
和file2.txt
的内容来验证操作结果。file1.txt
应该包含所有写入的内容,而file2.txt
可能为空(因为它在dup2
时被关闭了,如果它之前的内容没有被O_TRUNC
重新截断,则可能还保留着)。
示例 3:dup
选择最小可用文件描述符
这个例子演示 dup
如何选择最小的可用文件描述符。
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
int fd, new_fd1, new_fd2;
// 1. 打开一个文件
fd = open("test_dup.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
printf("Original file descriptor: %d\n", fd);
// 2. 复制文件描述符 (使用 dup)
new_fd1 = dup(fd);
if (new_fd1 == -1) {
perror("dup 1");
close(fd);
exit(EXIT_FAILURE);
}
printf("First dup() returned new fd: %d\n", new_fd1);
// 3. 再次复制
new_fd2 = dup(fd);
if (new_fd2 == -1) {
perror("dup 2");
close(fd);
close(new_fd1);
exit(EXIT_FAILURE);
}
printf("Second dup() returned new fd: %d\n", new_fd2);
// 4. 写入数据到所有描述符,验证它们指向同一文件
write(fd, "Written via fd\n", 15);
write(new_fd1, "Written via new_fd1\n", 20);
write(new_fd2, "Written via new_fd2\n", 20);
printf("Data written via all three file descriptors.\n");
// 5. 关闭所有描述符
close(fd);
close(new_fd1);
close(new_fd2);
printf("All file descriptors closed.\n");
return 0;
}
代码解释:
- 打开一个文件,得到文件描述符
fd
(假设为 3)。 - 调用
dup(fd)
。因为 0, 1, 2 (stdin, stdout, stderr) 通常已被占用,所以dup
会返回下一个最小的可用描述符,比如 4。 - 再次调用
dup(fd)
。现在 3, 4 已被占用,所以会返回 5。 - 向
fd
,new_fd1
,new_fd2
写入数据,验证它们都写入了同一个文件。 - 关闭所有文件描述符。
总结:
dup
和 dup2
是用于复制文件描述符的强大工具,它们在实现输入/输出重定向、保存和恢复标准流、以及在进程管理(如 fork
后)中非常有用。理解它们的关键在于掌握新旧描述符共享文件表项的特性,以及 dup2
自动关闭目标描述符的行为。