dup-dup2系统调用及示例

我们继续学习 Linux 系统编程中的重要函数。这次我们介绍 dup 和 dup2 函数,它们用于复制一个已存在的文件描述符 (file descriptor)。


1. 函数介绍

dup 和 dup2 是 Linux 系统调用,它们的功能是创建一个指向同一文件表项 (open file description) 的新文件描述符

简单来说,当你调用 dup 或 dup2 时,你得到的是一个别名副本,这个新文件描述符和原始文件描述符指向同一个打开的文件,共享文件的:

  • 文件偏移量 (file offset): 通过一个描述符读写会改变文件位置,通过另一个描述符读写会从新的位置开始。
  • 状态标志 (status flags): 如 O_APPENDO_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 进程打开的文件描述符已达上限等)。

6. 相似函数,或关联函数

  • fcntldup(oldfd) 等价于 fcntl(oldfd, F_DUPFD, 0)fcntl 提供了更灵活的复制方式,例如 F_DUPFD_CLOEXEC 可以在复制时设置 FD_CLOEXEC 标志。
  • closedup2 在复制前如果 newfd 已打开,会隐式调用 close(newfd)
  • open: 用于获取最初的文件描述符。
  • readwrite: 对复制后的文件描述符进行操作。

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;
}

代码解释:

  1. 调用 dup(STDOUT_FILENO) 创建原始标准输出(文件描述符 1)的一个副本,并将其保存在 saved_stdout 中。这个副本让我们可以稍后恢复标准输出。
  2. 使用 open 创建或打开一个名为 output_redirection.txt 的文件,用于接收重定向的输出。
  3. 调用 dup2(file_fd, STDOUT_FILENO)。这会:
    • 关闭当前的 STDOUT_FILENO(文件描述符 1)。
    • 将 file_fd 复制一份,并使这个新副本的号码为 1 (即 STDOUT_FILENO)。
    • 现在,文件描述符 1 和 file_fd 都指向 output_redirection.txt 文件。
  4. 执行 printf。因为标准输出已经被重定向,所以这些内容会写入到 output_redirection.txt 文件中。
  5. 调用 dup2(saved_stdout, STDOUT_FILENO) 来恢复标准输出。这会将之前保存的原始终端输出描述符复制回文件描述符 1。
  6. 再次执行 printf。现在标准输出已经恢复,内容会显示在终端上。
  7. 最后,使用 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;
}

代码解释:

  1. 打开两个文件 file1.txt 和 file2.txt,分别得到文件描述符 fd1 和 fd2
  2. 向两个文件写入不同的初始内容。
  3. 调用 dup2(fd1, fd2)。关键点在于:
    • fd2 当前是打开的,指向 file2.txt
    • dup2 会自动关闭 fd2
    • 然后,它将 fd1(指向 file1.txt)复制一份,并使这个副本的号码为 fd2
  4. 现在,fd1 和 fd2 都指向 file1.txt
  5. 向 fd2 写入数据,数据会出现在 file1.txt 中,证明了 fd2 现在确实指向 file1.txt
  6. 关闭 fd1 和 fd2。由于它们指向同一个文件表项,文件只会在最后一个引用关闭时才真正关闭。
  7. 读取 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;
}

代码解释:

  1. 打开一个文件,得到文件描述符 fd(假设为 3)。
  2. 调用 dup(fd)。因为 0, 1, 2 (stdin, stdout, stderr) 通常已被占用,所以 dup 会返回下一个最小的可用描述符,比如 4。
  3. 再次调用 dup(fd)。现在 3, 4 已被占用,所以会返回 5。
  4. 向 fdnew_fd1new_fd2 写入数据,验证它们都写入了同一个文件。
  5. 关闭所有文件描述符。

总结:

dup 和 dup2 是用于复制文件描述符的强大工具,它们在实现输入/输出重定向、保存和恢复标准流、以及在进程管理(如 fork 后)中非常有用。理解它们的关键在于掌握新旧描述符共享文件表项的特性,以及 dup2 自动关闭目标描述符的行为。

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注