嵌入式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(进程打开的文件描述符达到上限)。

相关推荐
qwy71522925816337 分钟前
1-Docker Engine 安装前置环境配置
运维·docker·容器
菜菜艾2 小时前
基于llama.cpp部署私有大模型
linux·运维·服务器·人工智能·ai·云计算·ai编程
重生的黑客2 小时前
Linux开发工具:条件编译、动静态库与 make/makefile 入门
linux·运维·服务器
minji...2 小时前
Linux 线程同步与互斥(三) 生产者消费者模型,基于阻塞队列的生产者消费者模型的代码实现
linux·运维·服务器·开发语言·网络·c++·算法
.柒宇.2 小时前
nginx入门教程
运维·nginx
w6100104663 小时前
cka-2026-ConfigMap
java·linux·cka·configmap
cc_yy_zh3 小时前
Win10 家庭版找不到Device Guard; 无法处理 VMware Workstation与Device Guard不兼容问题
linux·vmware
航Hang*3 小时前
VMware vSphere 云平台运维与管理基础——第2章(扩展):VMware ESXi 5.5 安装、配置与运维
运维·服务器·github·系统安全·虚拟化
嵌入式吴彦祖3 小时前
Luckfox Pico Ultra W WIFI
linux·嵌入式硬件
SPC的存折3 小时前
MySQL 8组复制完全指南
linux·运维·服务器·数据库·mysql