dup3系统调用及示例

1. 函数介绍

dup3 是 Linux 系统调用,是 dup2 的扩展版本。它用于将一个已存在的文件描述符复制到指定的目标文件描述符,类似于 dup2,但提供了额外的标志参数来控制复制行为。

这个函数的主要优势是可以设置文件描述符标志,最常用的是 O_CLOEXEC 标志,该标志使得复制的文件描述符在执行 exec 系列函数时自动关闭,避免了文件描述符泄漏到新程序中。


2. 函数原型

#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>

int dup3(int oldfd, int newfd, int flags);

3. 功能

  • 将文件描述符 oldfd 复制到指定的文件描述符 newfd
  • 如果 newfd 已经打开,会先将其关闭
  • 可以设置额外的文件描述符标志
  • 如果 oldfd 等于 newfd,则返回错误(与 dup2 不同)

4. 参数

  • int oldfd: 要被复制的原始文件描述符
  • int newfd: 目标文件描述符编号
  • int flags: 控制标志,可以是以下值的按位或组合:
    • O_CLOEXEC: 设置执行时关闭标志(FD_CLOEXEC)
    • 0: 不设置任何特殊标志(等同于 dup2 的行为)

5. 返回值

  • 成功时: 返回 newfd
  • 失败时: 返回 -1,并设置 errno
    • EBADFoldfd 或 newfd 不是有效的文件描述符
    • EINVALflags 参数无效,或 oldfd 等于 newfd
    • EMFILE: 进程打开的文件描述符数量达到上限

6. 相似函数

  • dup(): 复制文件描述符到最小可用编号
  • dup2(): 复制文件描述符到指定编号(不支持标志)
  • fcntl(): 更通用的文件描述符控制函数

7. 示例代码

示例 1:基本的 dup3 使用

#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main() {
    int fd1, fd2, fd3;
    
    printf("=== Dup3 基本使用演示 ===\n");
    
    // 1. 打开测试文件
    fd1 = open("test_dup3.txt", O_CREAT | O_RDWR | O_TRUNC, 0644);
    if (fd1 == -1) {
        perror("打开文件失败");
        exit(EXIT_FAILURE);
    }
    printf("打开文件获得描述符: %d\n", fd1);
    
    // 2. 使用 dup3 复制文件描述符(无特殊标志)
    fd2 = dup3(fd1, 10, 0);
    if (fd2 == -1) {
        perror("dup3 失败");
        close(fd1);
        exit(EXIT_FAILURE);
    }
    printf("使用 dup3(%d, 10, 0) 复制,获得描述符: %d\n", fd1, fd2);
    
    // 3. 使用 dup3 复制并设置 O_CLOEXEC 标志
    fd3 = dup3(fd1, 15, O_CLOEXEC);
    if (fd3 == -1) {
        perror("dup3 带 O_CLOEXEC 失败");
        close(fd1);
        close(fd2);
        exit(EXIT_FAILURE);
    }
    printf("使用 dup3(%d, 15, O_CLOEXEC) 复制,获得描述符: %d\n", fd1, fd3);
    
    // 4. 验证 O_CLOEXEC 标志是否设置
    int flags = fcntl(fd3, F_GETFD);
    if (flags != -1) {
        if (flags & FD_CLOEXEC) {
            printf("描述符 %d 已设置 FD_CLOEXEC 标志\n", fd3);
        } else {
            printf("描述符 %d 未设置 FD_CLOEXEC 标志\n", fd3);
        }
    }
    
    // 5. 验证文件描述符共享性
    const char *message1 = "通过 fd1 写入\n";
    const char *message2 = "通过 fd2 写入\n";
    const char *message3 = "通过 fd3 写入\n";
    
    write(fd1, message1, strlen(message1));
    write(fd2, message2, strlen(message2));
    write(fd3, message3, strlen(message3));
    
    // 6. 读取验证
    lseek(fd1, 0, SEEK_SET);
    char buffer[256];
    ssize_t bytes_read = read(fd1, buffer, sizeof(buffer) - 1);
    if (bytes_read > 0) {
        buffer[bytes_read] = '\0';
        printf("\n读取到的数据:\n%s", buffer);
    }
    
    // 7. 清理资源
    close(fd1);
    close(fd2);
    close(fd3);
    unlink("test_dup3.txt");
    
    return 0;
}

示例 2:O_CLOEXEC 标志的重要性

#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>

void demonstrate_cloexec_flag() {
    printf("=== O_CLOEXEC 标志演示 ===\n");
    
    // 创建测试文件
    int fd = open("cloexec_test.txt", O_CREAT | O_RDWR | O_TRUNC, 0644);
    if (fd == -1) {
        perror("创建测试文件失败");
        return;
    }
    
    write(fd, "测试数据", 8);
    
    // 使用 dup3 设置 O_CLOEXEC 标志
    int fd_with_cloexec = dup3(fd, 10, O_CLOEXEC);
    if (fd_with_cloexec == -1) {
        perror("dup3 设置 O_CLOEXEC 失败");
        close(fd);
        return;
    }
    
    // 使用 dup2 不设置 O_CLOEXEC 标志
    int fd_without_cloexec = dup2(fd, 15);
    if (fd_without_cloexec == -1) {
        perror("dup2 失败");
        close(fd);
        close(fd_with_cloexec);
        return;
    }
    
    printf("创建了两个描述符:\n");
    printf("  %d: 带 O_CLOEXEC 标志\n", fd_with_cloexec);
    printf("  %d: 不带 O_CLOEXEC 标志\n", fd_without_cloexec);
    
    // 创建子进程执行新程序
    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        printf("子进程 PID: %d\n", getpid());
        
        // 检查文件描述符是否仍然打开
        if (fcntl(fd_with_cloexec, F_GETFD) == -1) {
            printf("带 O_CLOEXEC 的描述符 %d 已自动关闭\n", fd_with_cloexec);
        } else {
            printf("带 O_CLOEXEC 的描述符 %d 仍然打开\n", fd_with_cloexec);
        }
        
        if (fcntl(fd_without_cloexec, F_GETFD) == -1) {
            printf("不带 O_CLOEXEC 的描述符 %d 已关闭\n", fd_without_cloexec);
        } else {
            printf("不带 O_CLOEXEC 的描述符 %d 仍然打开\n", fd_without_cloexec);
        }
        
        // 执行新程序(这里用 ls 作为示例)
        execl("/bin/ls", "ls", "-l", "cloexec_test.txt", NULL);
        perror("execl 失败");
        exit(EXIT_FAILURE);
    } else if (pid > 0) {
        // 父进程
        wait(NULL);
        printf("父进程继续执行\n");
    } else {
        perror("fork 失败");
    }
    
    // 清理
    close(fd);
    close(fd_with_cloexec);
    close(fd_without_cloexec);
    unlink("cloexec_test.txt");
}

int main() {
    printf("Dup3 函数演示\n\n");
    
    // 基本使用演示
    system("gcc -o basic_dup3 basic_dup3.c");
    system("./basic_dup3");
    
    printf("\n");
    
    // O_CLOEXEC 标志演示
    demonstrate_cloexec_flag();
    
    printf("\n=== 总结 ===\n");
    printf("dup3 的优势:\n");
    printf("1. 支持设置 O_CLOEXEC 标志,防止文件描述符泄漏\n");
    printf("2. 原子性操作,避免了 dup2 + fcntl 的竞态条件\n");
    printf("3. 当 oldfd == newfd 时返回错误,行为更明确\n\n");
    
    printf("使用建议:\n");
    printf("- 优先使用 dup3 而不是 dup2\n");
    printf("- 在可能执行 exec 的场景中使用 O_CLOEXEC\n");
    printf("- 注意检查返回值和错误处理\n");
    
    return 0;
}

示例 3:错误处理演示

#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

void demonstrate_dup3_errors() {
    printf("=== Dup3 错误处理演示 ===\n");
    
    // 1. 无效的文件描述符
    printf("1. 使用无效的文件描述符:\n");
    int result = dup3(999, 10, 0);
    if (result == -1) {
        printf("   错误: %s\n", strerror(errno));
        if (errno == EBADF) {
            printf("   说明: 文件描述符 999 无效\n");
        }
    }
    
    // 2. 无效的标志
    printf("\n2. 使用无效的标志:\n");
    int fd = open("/dev/null", O_RDWR);
    if (fd != -1) {
        result = dup3(fd, 10, 0x1000); // 无效标志
        if (result == -1) {
            printf("   错误: %s\n", strerror(errno));
            if (errno == EINVAL) {
                printf("   说明: 标志参数无效\n");
            }
        }
        close(fd);
    }
    
    // 3. oldfd 等于 newfd
    printf("\n3. oldfd 等于 newfd:\n");
    fd = open("/dev/null", O_RDWR);
    if (fd != -1) {
        result = dup3(fd, fd, 0);
        if (result == -1) {
            printf("   错误: %s\n", strerror(errno));
            if (errno == EINVAL) {
                printf("   说明: dup3 不允许 oldfd 等于 newfd\n");
                printf("   对比: dup2 在这种情况下会返回 newfd\n");
            }
        } else {
            printf("   意外成功: %d\n", result);
        }
        close(fd);
    }
    
    // 4. 文件描述符数量达到上限
    printf("\n4. 文件描述符数量达到上限的模拟:\n");
    printf("   这种情况很难模拟,但会返回 EMFILE 错误\n");
}

int main() {
    demonstrate_dup3_errors();
    
    printf("\n=== Dup 系列函数对比 ===\n");
    printf("函数     | 目标描述符 | 支持标志 | oldfd==newfd 行为\n");
    printf("---------|------------|----------|------------------\n");
    printf("dup      | 自动分配   | 否       | N/A\n");
    printf("dup2     | 指定       | 否       | 返回 newfd\n");
    printf("dup3     | 指定       | 是       | 返回错误\n");
    
    return 0;
}

编译和运行说明

# 编译示例
gcc -o dup3_basic dup3_basic.c
gcc -o dup3_cloexec dup3_cloexec.c
gcc -o dup3_errors dup3_errors.c

# 运行示例
./dup3_basic
./dup3_cloexec
./dup3_errors

重要注意事项

  1. Linux 特定dup3 是 Linux 特定的系统调用,在其他 Unix 系统上可能不可用
  2. 标志支持: 主要优势是支持 O_CLOEXEC 标志,提高程序安全性
  3. 原子操作dup3 是原子操作,避免了 dup2 + fcntl 组合可能的竞态条件
  4. 错误处理: 当 oldfd 等于 newfd 时,dup3 返回错误,而 dup2 返回 newfd
  5. 兼容性: 如果需要跨平台兼容性,应该使用 dup2 或 fcntl
此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

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