dup函数和dup2函数
dup函数和dup2函数
一、dup函数:文件描述符复制基础
1. dup 函数核心原理
1.1 函数定义与功能
- 函数原型:
int dup(int oldfd),功能是复制文件描述符,生成一个新的文件描述符,新描述符与原描述符指向同一个文件表项(共享文件状态标志、文件偏移量、v 节点指针等核心资源)。 - 核心特性:新文件描述符是系统当前未被使用的最小可用描述符;复制后两个描述符共享文件指针,操作其中一个会同步改变文件偏移量。
- 返回值:成功返回新的文件描述符,失败返回 - 1 并设置 errno。
#include <unistd.h> // 必须包含的头文件
int dup(int oldfd);
- 参数:
oldfd:要复制的原文件描述符(必须是已打开的有效 fd) - 返回值:
- 成功:返回最小的未被使用的文件描述符(新 fd)
- 失败:返回
-1,并设置errno错误码
1.2 内核层面逻辑
- 原文件描述符(oldfd)指向 PCB 中已打开的文件描述符表项,该表项关联文件表(含文件状态、偏移量等)和 v 节点(对应实际文件)。
- dup 调用后,系统会分配新的文件描述符表项,使其指向同一个文件表,实现两个描述符对同一文件的共享操作。
2. 底层原理

从进程虚拟地址空间逐层拆解,理解dup的本质:
-
进程虚拟地址空间划分:
- 用户区(0~3GB):存储环境变量、命令行参数、堆、动态库加载区、代码段 (.text)、初始化 / 未初始化全局变量段 (.data/.bss),以及 0~4KB 受保护的不可访问区域。
- 内核区(3~4GB):存储进程控制块
PCB(task_struct结构体),其中核心资源是文件描述符表。
-
文件描述符表规则:
- 表中存储已打开的文件描述符,默认前 3 项为标准流:
0(STDIN_FILENO,标准输入)、1(STDOUT_FILENO,标准输出)、2(STDERR_FILENO,标准错误)。 - 新打开文件时,分配规则是最小且未被使用的文件描述符(因此第一个打开的文件通常是 3)。
- 表中存储已打开的文件描述符,默认前 3 项为标准流:
-
dup执行逻辑:调用
dup(oldfd)时,内核在文件描述符表中找到最小的未被使用的文件描述符作为新 fd(newfd),让newfd和oldfd指向同一个文件表项(即同一个打开的文件)。
- 内核维护引用计数(类似硬链接):初始打开文件时计数为 1,
dup后计数变为 2;close其中一个 fd,计数减 1;只有计数为 0 时,文件才会被真正关闭。
- 内核维护引用计数(类似硬链接):初始打开文件时计数为 1,
3. 验证新旧 fd 指向同一文件的方法核心验证逻辑:
用原 fd(
oldfd)对文件执行写操作,再用新 fd(newfd)对文件执行读操作;如果newfd能读到oldfd写入的内容,就证明两个 fd 指向同一个文件。
4. 代码案例:dup.c 测试dup函数
(1)初始版本(无lseek,读不到数据)
// 测试dup函数复制文件描述符
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
// 打开文件
int fd = open(argv[1], O_RDWR);
if(fd < 0)
{
perror("open error");
return -1;
}
// 调用dup函数复制fd
int newfd = dup(fd);
printf("newfd:[%d], fd:[%d]\n", newfd, fd);
// 使用fd对文件进行写操作
write(fd, "hello world", strlen("hello world"));
// 使用newfd读文件
char buf[64];
memset(buf, 0x00, sizeof(buf));
int n = read(newfd, buf, sizeof(buf));
printf("read over: n == [%d], buf == [%s]\n", n, buf);
// 关闭文件
close(fd);
close(newfd);
return 0;
}
-
编译运行:
gcc dup.c && ./a.out test.log -
运行结果:
newfd:[4], fd:[3] read over: n == [1], buf == [] -
问题分析:
buf读不到数据,核心原因是文件指针(文件偏移量)共享:fd执行write后,文件偏移量移动到了文件末尾;此时用newfd``read时,偏移量在末尾,因此读不到有效数据。
初始代码编写与测试
- 头文件与程序框架
- 引入必要头文件:
#include <stdio.h>、#include <stdlib.h>、#include <string.h>、#include <unistd.h>、#include <fcntl.h>(课程中虽未逐行敲,但明确需包含系统调用相关头文件)。 - 主函数参数:
int main(int argc, char *argv[]),通过命令行传参指定操作文件(课程中默认操作test.log)。
- 引入必要头文件:
- 文件打开与描述符复制
- 打开文件:
int fd = open("test.log", O_RDWR);,以读写模式打开已存在的文件,需添加打开失败判断perror("open error");(强调异常处理不可省略)。 - 复制文件描述符:
int new_fd = dup(fd);,打印原描述符fd和新描述符new_fd,课程中验证新描述符通常为4(标准输入 0、输出 1、错误 2 已被占用,3 为原 fd,4 为系统分配的最小可用值)。
- 打开文件:
- 文件读写操作与初始问题
- 写操作:
write(fd, "hello world", strlen("hello world"));,通过原描述符向文件写入内容。 - 读操作:定义缓冲区
char buf[64] = {0};(初始化避免脏数据),通过int n = read(new_fd, buf, sizeof(buf));用新描述符读取文件,打印读取长度n和内容buf。 - 关闭文件:
close(fd);、close(new_fd);,强调两个描述符均需关闭(虽程序退出时系统会自动回收,但规范操作需手动关闭)。
- 写操作:
- 初始测试结果与问题分析
- 运行程序后发现读取失败(n=0 或仅读取到回车 n=1),核心原因:
write操作会改变文件偏移量,写入完成后文件指针指向文件末尾,此时用 new_fd 读取,已无数据可读。
- 运行程序后发现读取失败(n=0 或仅读取到回车 n=1),核心原因:
(2)问题解决方案:lseek 函数调整文件指针(添加lseek重置偏移量)
-
问题定位:两个描述符共享同一文件偏移量,写入后指针在文件尾,读取需将指针移回文件开头。
-
解决方案:在
write操作后调用lseek函数:lseek(fd, 0, SEEK_SET);- 参数说明:第一个参数为文件描述符(fd/new_fd 均可,因共享文件表),第二个参数为偏移量(0 表示无偏移),第三个参数
SEEK_SET表示从文件起始位置偏移。
- 参数说明:第一个参数为文件描述符(fd/new_fd 均可,因共享文件表),第二个参数为偏移量(0 表示无偏移),第三个参数
-
修正后测试结果:重新读取可获取文件中
hello world内容(若文件原无内容,需注意写入覆盖问题),读取长度为 12 字节(与hello world长度一致),验证了 dup 函数复制后两描述符共享同一文件的核心特性。
// 测试dup函数复制文件描述符
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
// 打开文件
int fd = open(argv[1], O_RDWR);
if(fd < 0)
{
perror("open error");
return -1;
}
// 调用dup函数复制fd
int newfd = dup(fd);
printf("newfd:[%d], fd:[%d]\n", newfd, fd);
// 使用fd对文件进行写操作
write(fd, "hello world", strlen("hello world"));
// 调用lseek函数移动文件指针到文件开头
lseek(fd, 0, SEEK_SET);
// 使用newfd读文件
char buf[64];
memset(buf, 0x00, sizeof(buf));
int n = read(newfd, buf, sizeof(buf));
printf("read over: n == [%d], buf == [%s]\n", n, buf);
// 关闭文件
close(fd);
close(newfd);
return 0;
}
-
编译运行:
gcc dup.c && ./a.out test.log -
运行结果:
newfd:[4], fd:[3] read over: n == [12], buf == [hello world] -
结果说明:
lseek将文件偏移量重置到文件开头,因此newfdread时能读到fd写入的hello world,验证了两个 fd 指向同一个文件。- 补充:如果
test.log原本有内容,会被O_RDWR打开后write覆盖(open默认偏移量在文件开头)。
- 补充:如果
(3)关键细节补充
- 文件覆盖与追加
- 若打开文件时仅用
O_RDWR,写入操作会从文件起始位置覆盖原内容;若想保留原内容,需添加O_APPEND标志,以追加模式写入。 - 课程中举例:若文件原内容有回车,直接写入会覆盖部分内容;若用
O_APPEND,写入内容会追加到文件末尾。
- 若打开文件时仅用
- 缓冲区与读取限制
- 读取时
read的第三个参数sizeof(buf)是最大读取字节数(课程中 buf 为 64 字节,最多读 64 字节),实际读取长度由文件剩余内容量决定。
- 读取时
二、dup2 函数
1.dup2 函数基础说明
1.1 函数核心定位
dup2是 Linux 系统下的文件描述符复制函数,功能与dup一致,但灵活性、可控性远高于dup,是文件重定向、多描述符操作的核心 API。
1.2 函数原型
int dup2(int oldfd, int newfd);
成功时返回新的文件描述符
newfd;失败时返回 - 1,并设置errno错误码。
1.3 参数说明
| 参数 | 含义 |
|---|---|
oldfd |
原有的、需要被复制的有效文件描述符(必须对应一个已打开的文件) |
newfd |
用户手动指定的、复制后生成的新文件描述符(支持已占用 / 未占用两种状态) |
1.4 返回值与执行逻辑
- 执行成功:
- 若
newfd原本已指向一个打开的文件:内核会隐式调用close(newfd)关闭原文件,再让newfd指向oldfd所指向的同一个文件 - 若
newfd原本未被占用(无对应打开文件):直接让newfd指向oldfd所指向的同一个文件 - 最终
oldfd和newfd指向同一个打开的文件,共享文件偏移量、状态标志、inode 信息,文件引用计数 + 1(变为 2)
- 若
- 执行失败:返回
-1,并设置errno错误码(如oldfd无效、newfd非法等)
2.dup2 函数底层原理(文件描述符表视角)
通过文件描述符表示意图,直观拆解了函数执行前后的内核变化:
2.1 执行前初始状态
- 进程的文件描述符表中,默认打开
0(标准输入)、1(标准输出)、2(标准错误) - 假设
oldfd=3,指向文件tmp1.log;newfd=4,指向另一个文件tmp2.log,两个描述符独立指向不同文件
2.2 执行dup2(3, 4)后的状态
- 内核自动关闭
newfd=4原本指向的tmp2.log(引用计数减 1,计数为 0 则真正释放文件) - 让
newfd=4指向oldfd=3所指向的tmp1.log - 最终
3和4两个描述符同时指向tmp1.log,文件引用计数变为 2 - 后续对
3和4的读写操作完全等价,共享文件偏移量:用4写入后,用3可直接读到写入的内容
2.3 引用计数机制(与dup完全一致)
- 调用
dup2后,目标文件的引用计数 + 1(从 1 变为 2) - 调用
close(oldfd)或close(newfd)时,引用计数 - 1 - 只有当引用计数减为 0 时,内核才会真正关闭文件、释放资源
2.4 函数基础信息
- 函数原型:
int dup2(int oldfd, int newfd); - 头文件:
#include <unistd.h> - 核心功能:复制一个已存在的文件描述符
oldfd到指定的文件描述符newfd,让newfd成为oldfd的副本,指向同一个打开的文件。
2.5 关键特性(重点强调)
- 自动关闭旧文件:如果
newfd已经对应一个打开的文件,系统会先关闭newfd原本指向的文件,再执行复制操作。 - 共享文件表项:复制成功后,
newfd和oldfd指向内核中同一个文件表项,共享文件偏移量、文件状态标志、文件权限等;操作其中一个 fd(如write/lseek)会直接影响另一个。 - 独立生命周期:两个 fd 是独立的,关闭其中一个不会影响另一个(仅文件表项的引用计数减 1,引用计数为 0 时才真正关闭文件)。
- 与
dup()的区别:dup(oldfd):自动分配当前最小的可用 fd 作为新 fd,无法手动指定;dup2(oldfd, newfd):手动指定新的 fd,这是dup2的核心优势,是实现重定向的关键。
3.dup2 与 dup 函数的核心差异
| 特性 | dup函数 |
dup2函数 |
|---|---|---|
| 函数原型 | int dup(int oldfd); |
int dup2(int oldfd, int newfd); |
| 新描述符分配 | 内核自动分配当前最小的可用文件描述符,用户无法指定 | 用户手动指定新描述符newfd,可控性极强 |
| 灵活性 | 低,只能被动接收内核分配的描述符 | 高,可按需精准控制描述符编号 |
| 典型场景 | 简单复制文件描述符 | 标准输入 / 输出重定向、管道通信等需要指定描述符的场景 |
4.dup2 函数验证思路与完整代码实现
4.1 验证逻辑
验证目标:证明dup2执行后,oldfd和newfd指向同一个文件
验证步骤:
- 打开两个不同的文件,分别得到
oldfd(对应tmp1.log)和newfd(对应tmp2.log) - 调用
dup2(oldfd, newfd):newfd原指向的tmp2.log被自动关闭,newfd转而指向tmp1.log - 使用
newfd向文件写入数据 - 使用
oldfd从文件读取数据 - 验证结果:若
oldfd成功读取到newfd写入的内容,证明两个描述符指向同一个文件
关键注意:读写必须使用不同的文件描述符,若使用同一个描述符,无法验证复制效果
4.2 符合课程思路的完整代码案例
// 测试dup2函数复制文件描述符
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
// 1. 打开第一个文件(argv[1]对应tmp1.log),O_CREAT:文件不存在则创建,0755为文件权限
int oldfd = open(argv[1], O_RDWR | O_CREAT, 0755);
if(oldfd < 0)
{
perror("open error"); // 打印系统调用错误信息,方便调试
return -1;
}
// 2. 打开第二个文件(argv[2]对应tmp2.log)
int newfd = open(argv[2], O_RDWR | O_CREAT, 0755);
if(newfd < 0)
{
perror("open error");
return -1;
}
// 3. 调用dup2复制文件描述符:将oldfd复制到newfd
// 此时newfd会先关闭原本指向的tmp2.log,然后指向oldfd对应的tmp1.log
dup2(oldfd, newfd);
printf("newfd:[%d], oldfd:[%d]\n", newfd, oldfd);
// 4. 用newfd写入内容(newfd指向tmp1.log,内容写入tmp1.log)
write(newfd, "hello world", strlen("hello world"));
// 5. 移动文件指针到开头(两个fd共享指针,用newfd/oldfd操作都生效)
lseek(newfd, 0, SEEK_SET);
// 6. 用oldfd读取内容(oldfd也指向tmp1.log,能读到刚才写入的内容)
char buf[64];
memset(buf, 0x00, sizeof(buf));
int n = read(oldfd, buf, sizeof(buf));
printf("read over: n == [%d], buf == [%s]\n", n, buf);
// 7. 关闭文件描述符,释放资源
close(oldfd);
close(newfd);
return 0;
}
代码逻辑拆解(对应老师讲解)
| 步骤 | 操作 | 核心原理 |
|---|---|---|
| 1-2 | 打开tmp1.log和tmp2.log,得到oldfd(一般为 3,0/1/2 是标准流)、newfd(一般为 4) |
两个 fd 初始分别指向两个独立的文件 |
| 3 | 调用dup2(oldfd, newfd) |
newfd关闭原本的tmp2.log,指向oldfd对应的tmp1.log,两个 fd 现在指向同一个文件 |
| 4 | write(newfd, "hello world") |
newfd指向tmp1.log,内容写入tmp1.log,文件指针偏移到字符串末尾 |
| 5 | lseek(newfd, 0, SEEK_SET) |
移动文件指针到开头(两个 fd 共享指针,操作任意一个都生效) |
| 6 | read(oldfd, buf, ...) |
oldfd也指向tmp1.log,成功读取到hello world,验证两个 fd 指向同一个文件 |
| 7 | 关闭两个 fd | 释放资源,文件表项引用计数减 1,最终关闭文件 |
编译运行与结果验证
-
编译技巧:演示简化编译方法,直接执行
make dup2,即使无 Makefile,make 会自动执行cc dup2.c -o dup2完成编译。 -
运行命令:
./dup2 tmp1.log tmp2.log -
终端输出:
newfd:[4], oldfd:[3] read over: n == [11], buf == [hello world] -
文件验证:
tmp1.log:包含内容hello world(被newfd写入)tmp2.log:为空(newfd被dup2后关闭了原本的tmp2.log,无写入操作)
-
验证:「两个文件哪个空、哪个有内容」,结果显示
tmp2.log空、tmp1.log有内容,完美验证了dup2的原理。
4.3 代码执行效果说明
- 执行后,
tmp2.log不会被写入内容(dup2自动关闭了它),所有写入内容都保存到tmp1.log oldfd成功读取到newfd写入的内容,验证了两个描述符指向同一个文件- 若查看文件,
tmp1.log会包含写入的字符串,tmp2.log为空(或保持原有内容)
4.4 核心知识点总结
dup2的本质是让指定的 newfd 指向 oldfd 对应的文件,核心是文件表项的共享,而非文件内容的复制。dup2是实现 I/O 重定向的核心系统调用,是 Linux shell 重定向、管道等功能的底层基础。open()添加O_CREAT标志可自动创建不存在的文件,避免运行错误;权限0755是八进制,代表文件所有者读写执行、组和其他用户读执行。lseek()用于移动文件指针,write后指针在末尾,必须移到开头才能读取到内容。
4.5 课程延伸知识点补充
- 若
oldfd == newfd,dup2会直接返回newfd,不做任何操作(不会关闭文件) dup2是原子操作,内核保证整个复制 + 关闭过程不会被中断,避免多线程竞态- 重定向实战:
dup2(fd, 1)后,终端所有输出(如printf、cout)都会写入fd对应的文件,实现日志持久化
5.dup2 函数实现重定向操作
5.1 核心目标
本节的核心是讲解如何使用dup2系统调用实现标准输出的文件重定向,让原本输出到终端(屏幕)的内容,写入到指定的普通文件中,效果等价于 Shell 中的>重定向操作(如ls -l > test.log)。
5.2 前置概念:Shell 中的文件重定向
先通过 Shell 命令做类比,帮助理解重定向的效果:
- 正常执行
ls -l:命令的输出会直接打印到终端屏幕。 - 执行
ls -l > test.log:>是 Shell 的文件重定向符号,此时ls -l的输出不再显示在终端,而是全部写入到test.log文件中。 - 本课程的目标:用 C 语言的
dup2函数,手动实现这个>重定向的效果。
5.3 dup2函数核心用法与参数逻辑
(1)函数原型(补充)
int dup2(int oldfd, int newfd);
- 作用:复制文件描述符,将
newfd修改为指向oldfd所指向的文件,实现文件描述符的重定向。
(2)参数方向(重点)
用cp a b命令做类比,帮大家区分参数顺序:
cp a b:最终b文件的内容追随a文件的内容,a是源,b是目标。dup2(oldfd, newfd):newfd(右参数)追随oldfd(左参数),即newfd会被修改为指向oldfd对应的文件,原newfd的指向会被内核自动关闭。
(3)本案例的参数含义
案例中核心语句:dup2(fd, STDOUT_FILENO);
fd:open打开的目标文件(如hello.log)的文件描述符(默认是 3,因为 0、1、2 被标准输入 / 输出 / 错误占用)。STDOUT_FILENO:标准输出的文件描述符,值为1,原本指向终端设备文件/dev/tty(对应屏幕输出)。- 执行效果:
STDOUT_FILENO(1 号描述符)不再指向终端,而是指向fd对应的hello.log文件,实现标准输出重定向。
5.4 完整代码案例
修正后的完整代码
// 测试dup2函数复制文件描述符,实现文件重定向
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // dup2、close、STDOUT_FILENO等系统调用头文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h> // open函数头文件
int main(int argc, char *argv[])
{
// 1. 打开目标文件(不存在则创建,用于重定向输出)
// argv[1]:从命令行传入的目标文件名(如hello.log)
// O_RDWR:读写方式打开;O_CREAT:文件不存在则创建;0755:文件权限(rwxr-xr-x)
int fd = open(argv[1], O_RDWR | O_CREAT, 0755);
if(fd < 0) // open失败的错误处理
{
perror("open error");
return -1;
}
// 2. 核心:调用dup2实现文件重定向
// 作用:将标准输出(STDOUT_FILENO=1)重定向到fd指向的文件
dup2(fd, STDOUT_FILENO);
// 3. 此时printf的输出不再到终端,而是写入fd对应的目标文件
printf("nihao hello world");
// 4. 关闭文件描述符(资源回收)
close(fd); // 关闭open打开的文件描述符
// 老师补充:close(STDOUT_FILENO)不手动关闭也无影响,进程退出时内核会自动回收
// 但规范编程建议手动关闭,释放资源
close(STDOUT_FILENO);
return 0;
}
代码运行效果
- 编译:
make dup2_1(通过 Makefile 编译代码生成可执行文件) - 执行:
./dup2_1 hello.log - 结果:终端不会打印
nihao hello world,内容被写入到hello.log文件中,验证重定向成功。
5.5 重定向原理图解
通过进程文件描述符表的变化,拆解了重定向的底层逻辑:
(1)初始状态(执行open前)
进程的文件描述符表默认分配 3 个标准描述符:
| 文件描述符 | 名称 | 指向的文件 | 作用 |
|---|---|---|---|
| 0 | STDIN_FILENO |
/dev/tty |
标准输入(对应键盘) |
| 1 | STDOUT_FILENO |
/dev/tty |
标准输出(对应屏幕) |
| 2 | STDERR_FILENO |
/dev/tty |
标准错误(错误信息输出) |
(2)执行open("hello.log")后
内核分配新的文件描述符3,指向hello.log普通文件,此时描述符表新增:
| 文件描述符 | 指向的文件 |
|---|---|
| 3 | hello.log |
(3)执行dup2(fd, STDOUT_FILENO)后
- 1 号描述符(
STDOUT_FILENO)原本指向/dev/tty的连接被内核自动关闭; - 1 号描述符现在指向
fd(3 号)对应的hello.log文件; - 此时,所有标准输出(如
printf)的内容,都会通过 1 号描述符写入hello.log,不再输出到终端。
(4)核心思想:Linux 一切皆文件
强调:Linux 中所有设备、资源都被抽象为文件,终端(屏幕、键盘)也是文件(/dev/tty),文件描述符是进程访问文件的索引。dup2的本质就是修改文件描述符的指向,从而改变数据的流向。
5.6 课程重点与注意事项
- 参数顺序绝对不能搞反:
dup2(oldfd, newfd),newfd(右)追随oldfd(左),类比cp a b,b追随a。 - 重定向的本质:修改 1 号标准输出描述符的指向,从终端
/dev/tty改为目标文件。 - 资源回收:
close(fd)和close(STDOUT_FILENO),说明:不手动关闭,进程退出时内核会自动回收,但规范编程必须手动关闭。 - 原理要求:必须掌握文件描述符表的变化逻辑,理解重定向的底层实现,而不是只记代码。
- 拓展性:该方法不仅可以重定向标准输出,也可以重定向标准输入、标准错误,实现更灵活的 IO 流向控制。
5.7 课程总结
本课程从 Shell 重定向的直观效果入手,类比讲解了dup2函数的参数逻辑,通过完整代码实现了标准输出到文件的重定向,再通过文件描述符表的原理图拆解了底层原理,最终验证了运行效果。核心是让学习者理解:文件描述符是进程访问文件的句柄,dup2通过修改句柄的指向,实现 IO 流的重定向,同时强化了 Linux “一切皆文件” 的核心思想。
参考资料:黑马程序员

浙公网安备 33010602011771号