dup3系统调用及示例

1. 函数介绍

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

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

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

2. 函数原型

1
2
3
4
5
6
#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:

  • EBADF: oldfd 或 newfd 不是有效的文件描述符

  • EINVAL: flags 参数无效,或 oldfd 等于 newfd

  • EMFILE: 进程打开的文件描述符数量达到上限

6. 相似函数

  • dup(): 复制文件描述符到最小可用编号

  • dup2(): 复制文件描述符到指定编号(不支持标志)

  • fcntl(): 更通用的文件描述符控制函数

7. 示例代码

示例 1:基本的 dup3 使用

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
#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&#91;256];
ssize_t bytes_read = read(fd1, buffer, sizeof(buffer) - 1);
if (bytes_read > 0) {
buffer&#91;bytes_read] = '\0';
printf("\n读取到的数据:\n%s", buffer);
}

// 7. 清理资源
close(fd1);
close(fd2);
close(fd3);
unlink("test_dup3.txt");

return 0;
}

示例 2:O_CLOEXEC 标志的重要性

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#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:错误处理演示

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

编译和运行说明

1
2
3
4
5
6
7
8
9
10
# 编译示例
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

重要注意事项

Linux 特定: dup3 是 Linux 特定的系统调用,在其他 Unix 系统上可能不可用

标志支持: 主要优势是支持 O_CLOEXEC 标志,提高程序安全性

原子操作: dup3 是原子操作,避免了 dup2 + fcntl 组合可能的竞态条件

错误处理: 当 oldfd 等于 newfd 时,dup3 返回错误,而 dup2 返回 newfd

兼容性: 如果需要跨平台兼容性,应该使用 dup2 或 fcntl

data-ad-format="auto" data-full-width-responsive="true">