嵌入式linux--文件IO中dup/dup2的使用

一、核心基础:文件描述符与文件表项

要理解dup/dup2,必须先明确 Linux 文件操作的三层结构(进程级 + 内核级):

  1. 进程的文件描述符表:每个进程独有,存储文件描述符(非负整数,如 0/1/2 分别是标准输入 / 输出 / 错误)与内核文件表项的映射关系;
  2. 内核的文件表项 :存储文件的当前偏移量、访问模式(RDONLY/WRONLY 等)、文件状态标志;
  3. 内核的 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共享偏移量,而两次openfdfd2偏移量独立。

三、dup2 函数:手动指定目标文件描述符

1. 函数原型与参数

cpp 复制代码
#include <unistd.h>
int dup2(int oldfd, int newfd);
  • 入参oldfd:已打开的有效文件描述符;
  • 入参newfd :手动指定的目标文件描述符 (想要将oldfd复制到的描述符);
  • 返回值 :成功返回newfd;失败返回 - 1,设置errno

2. 核心规则

  1. newfd已被进程使用,dup2 会先自动关闭newfd ,再将其指向oldfd的文件表项(原子操作,避免竞态);
  2. oldfd是无效描述符,dup2失败,newfd不会被关闭;
  3. oldfd == newfddup2直接返回newfd,不做任何操作;
  4. 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(进程打开的文件描述符达到上限)。

相关推荐
雪碧聊技术2 小时前
前端项目部署到服务器
服务器·nginx·ubuntu·前端项目部署
AC赳赳老秦2 小时前
OpenClaw 系统监控实战指南:构建高效的电脑/服务器状态监控与自动告警系统
服务器·开发语言·人工智能·php·ai-native·deepseek·openclaw
H_老邪2 小时前
新人初识ECS 服务器
运维·服务器
北风toto2 小时前
Jenkins的安装与启动
运维·servlet·jenkins
殷忆枫2 小时前
基于STM32的ML307R连接Onenet平台
服务器·前端·javascript
牛奶咖啡132 小时前
DevOps自动化运维实践_Legacy Boot与UEFI Boot网络启动(PXE)的原理解析
运维·devops·dhcp·pxe·tftp·uefi boot网络启动原理·legacy boot启动原理
Cx330❀2 小时前
Linux System V标准简介
大数据·linux·运维·服务器·人工智能
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 基于springboot的酒店预订小程序自动化订制系统为例,包含答辩的问题和答案
运维·小程序·自动化
CDN3602 小时前
高防服务器避坑:360CDN 高防与其他产品对比
运维·服务器·网络安全