我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 dup 和 dup2 函数,它们用于复制一个已存在的文件描述符 (file descriptor)。
1. 函数介绍 dup 和 dup2 是 Linux 系统调用,它们的功能是创建一个指向同一文件表项 (open file description) 的新文件描述符。
data-ad-format="fluid"
data-ad-layout-key="-7k+ex-4a-9w+4a">
简单来说,当你调用 dup 或 dup2 时,你得到的是一个别名或副本,这个新文件描述符和原始文件描述符指向同一个打开的文件,共享文件的:
文件偏移量 (file offset): 通过一个描述符读写会改变文件位置,通过另一个描述符读写会从新的位置开始。
状态标志 (status flags): 如 O_APPEND, O_NONBLOCK 等。
文件锁 (file locks): 通过任何一个描述符获取的锁,对另一个描述符也有效。
它们最常见的用途是重定向标准输入、标准输出或标准错误。例如,将一个程序的输出重定向到文件,而不是终端。
你可以把文件描述符想象成一个指向文件的“把手”。dup 就像是给这个“把手”又做了一个一模一样的复制品。你拿着任何一个“把手”都能操作同一个文件,而且它们的状态是同步的。
2. 函数原型 1 2 3 4 5 6 7 8 #include <unistd.h> // 必需 // 复制文件描述符 (返回新的最小可用 fd) int dup(int oldfd); // 复制文件描述符到指定的新 fd int dup2(int oldfd, int newfd);
3. 功能 dup(int 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: 请求的新文件描述符号码。
5. 返回值 成功时:
失败时:
返回 -1,并设置全局变量 errno 来指示具体的错误原因(例如 EBADF oldfd 或 newfd 无效,EMFILE 进程打开的文件描述符已达上限等)。
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 来保存原始标准输出,然后将标准输出重定向到一个文件,最后再恢复标准输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 #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 在目标文件描述符已打开时自动关闭它的行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 #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)。关键点在于:
现在,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 如何选择最小的可用文件描述符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #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 自动关闭目标描述符的行为。