一、核心基础:文件描述符与文件表项
要理解dup/dup2,必须先明确 Linux 文件操作的三层结构(进程级 + 内核级):
- 进程的文件描述符表:每个进程独有,存储文件描述符(非负整数,如 0/1/2 分别是标准输入 / 输出 / 错误)与内核文件表项的映射关系;
- 内核的文件表项 :存储文件的当前偏移量、访问模式(RDONLY/WRONLY 等)、文件状态标志;
- 内核的 v-node/i-node 表:存储文件的物理属性(路径、权限、磁盘地址等)。
dup/dup2的本质 :复制的是文件描述符到文件表项的映射 ,新、旧文件描述符会共享同一个文件表项(包括文件偏移量、访问模式),这是后续代码解析的关键。
二、dup 函数:自动分配新的文件描述符
1. 函数原型与参数
#include <unistd.h>
int dup(int oldfd);
- 入参
oldfd:已打开的有效文件描述符; - 返回值 :成功返回最小的未被使用的文件描述符 ;失败返回 - 1,设置
errno。
2. 核心规则
- 新描述符是进程中可用的最小非负整数;
- 新、旧描述符共享同一个文件表项,文件偏移量联动(操作一个描述符的偏移量,另一个也会同步变化);
- 复制后的描述符继承原描述符的所有属性(访问模式、文件状态标志等)。
3. 结合dup.c代码解析
、(1)代码解析
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv)
{
char buf[10];
char buf2[10];
if (argc != 2)
{
printf("Usage: %s <file>\n", argv[0]);
return -1;
}
int fd = open(argv[1], O_RDONLY); // 第一次打开文件,分配fd(如3)
int fd2 = open(argv[1], O_RDONLY); // 第二次打开同一文件,分配新fd(如4),独立文件表项
int fd3 = dup(fd);// 复制fd,分配最小可用fd(如5),与fd共享文件表项
printf("fd = %d\n", fd);
printf("fd2 = %d\n", fd2);
printf("fd3 = %d\n", fd3);
if (fd < 0 || fd2 < 0 || fd3 < 0)
{
printf("can not open %s\n", argv[1]);
return -1;
}
read(fd, buf, 1); // 从fd的偏移量0读取1个字符,fd的偏移量变为1(fd3同步变为1)
read(fd2, buf2, 1);// 从fd2的偏移量0读取1个字符,fd2的偏移量变为1(与fd无关)
printf("data get from fd : %c\n", buf[0]); // 输出文件第1个字符
printf("data get from fd2: %c\n", buf2[0]);// 输出文件第1个字符(独立偏移量)
read(fd3, buf, 1); // 从fd3的偏移量1读取1个字符,偏移量变为2
printf("data get from fd3: %c\n", buf[0]); // 输出文件第2个字符(共享偏移量)
return 0;
}
(2)运行结果

核心结论 :dup复制的fd3与原fd共享偏移量,而两次open的fd和fd2偏移量独立。
三、dup2 函数:手动指定目标文件描述符
1. 函数原型与参数
cpp
#include <unistd.h>
int dup2(int oldfd, int newfd);
- 入参
oldfd:已打开的有效文件描述符; - 入参
newfd:手动指定的目标文件描述符 (想要将oldfd复制到的描述符); - 返回值 :成功返回
newfd;失败返回 - 1,设置errno。
2. 核心规则
- 若
newfd已被进程使用,dup2 会先自动关闭newfd,再将其指向oldfd的文件表项(原子操作,避免竞态); - 若
oldfd是无效描述符,dup2失败,newfd不会被关闭; - 若
oldfd == newfd,dup2直接返回newfd,不做任何操作; - 与
dup一致,新、旧描述符共享文件表项(偏移量、属性联动)。
3. 结合dup2.c代码解析
dup2.c的核心是重定向标准输出 (将 stdout 从终端重定向到文件),这是dup2最经典的使用场景,逐行解析关键逻辑:
(1)代码逻辑
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
/*
* ./dup2 1.txt
* argc = 2
* argv[0] = "./dup2"
* argv[1] = "1.txt"
*/
int main(int argc, char **argv)
{
int fd;
if (argc != 2)
{
printf("Usage: %s <file>\n", argv[0]);
return -1;
}
fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0777);
if (fd < 0)
{
printf("can not open file %s\n", argv[1]);
printf("errno = %d\n", errno);
printf("err: %s\n", strerror(errno));
perror("open");
}
else
{
printf("fd = %d\n", fd);
}
dup2(fd, 1); //1是标准输出 stdout的文件描述符,默认指向终端;此操作将fd的文件表项复制到1,关闭原 stdout 的终端映射,让1指向打开的文件,后续所有向stdout的输出(如printf、puts)都会被写入目标文件,而非终端。
printf("hello, world\n"); /* 打印到fd=1的文件 */
return 0;
}
(2)代码结果

四、dup 与 dup2 的核心区别
| 特性 | dup 函数 | dup2 函数 |
|---|---|---|
| 新描述符分配方式 | 自动分配最小可用的描述符 | 手动指定目标描述符 |
| 重定向灵活性 | 低(无法指定目标,仅能复制) | 高(可直接重定向标准描述符 0/1/2) |
| 入参数量 | 1 个(仅原描述符) | 2 个(原描述符 + 目标描述符) |
| 返回值 | 新分配的描述符 | 手动指定的目标描述符 |
| 常用场景 | 简单的文件描述符复制 | 标准输入 / 输出 / 错误重定向、管道通信 |
五、dup/dup2 的典型使用场景
1. 标准 IO 重定向 :如dup2.c的 stdout 重定向,同理可重定向 stdin(0)、stderr(2),实现将程序的输入 / 输出 / 错误写入文件;
2. 管道通信 :父子进程间通过管道通信时,用dup2将管道的读 / 写端重定向到标准 IO,让子进程的默认输入 / 输出指向管道;
3. 多描述符操作同一文件 :用dup复制描述符,让多个描述符共享文件偏移量,实现多逻辑流操作同一文件时的偏移量同步;
4. 守护进程 :将守护进程的标准 IO 重定向到/dev/null,避免守护进程输出占用终端资源。
六、注意事项
1. 文件描述符有效性 :使用dup/dup2前,必须确保oldfd是已打开的有效描述符,否则函数失败并设置errno;
2. 关闭描述符的注意事项 :共享文件表项的描述符,需关闭所有描述符后,内核才会释放对应的文件表项;
3. 原子操作 :dup2的 "关闭新描述符 + 复制映射" 是原子操作 ,避免了手动close(newfd)+dup(oldfd)的竞态条件(手动操作可能在中间被信号打断);
4. 权限继承:复制后的描述符完全继承原描述符的访问模式(如 RDONLY/WRONLY/RDWR),无法单独修改;
5. errno 错误码 :常见错误码包括EBADF(oldfd 无效或 newfd 超出范围)、EMFILE(进程打开的文件描述符达到上限)。