当前位置: 首页 > news >正文

Linux系统:详解文件描述符与重定向原理以及相关接口(open,read,write,dup2)

本节重点 

  • 从狭义与广义角度理解文件
  • 理解文件描述符
  • 掌握open,write,read系统调用
  • 理解重定向的概念与原理
  • 掌握重定向的指令操作
  • stdout与stderr的比较
  • 为什么存在stderr?

一、理解“文件”

1.1 狭义角度

在狭义层面,Linux文件是磁盘或存储设备上连续或分散的数据块集合,具有明确的元数据(如文件名、权限、所有者等),通过文件系统进行管理。其核心特征包括:

1.1.1 数据存储载体

  • 文本文件(如.txt.conf):存储人类可读字符。
  • 二进制文件(如.exe.o):编译后的程序或库文件,需特定程序解析。
  • 设备文件(如/dev/sda/dev/null):通过文件接口与硬件或内核交互(如/dev/null丢弃所有写入数据)。

1.1.2 元数据

每个文件由inode(索引节点)描述,包含:

  • 文件类型(普通文件、目录、符号链接等)
  • 权限(rwx)与所有者(UID/GID)
  • 时间戳(创建、修改、访问时间)
  • 实际数据块的磁盘地址(通过直接/间接指针)。

1.2 广义角度

在广义层面,Linux将几乎所有系统资源抽象为文件,通过统一的文件操作接口(open、write、read等)访问,形成“一切皆文件”的设计哲学。

1.3 系统角度

用户对文件的操作本质是进程对文件的操作,文件的管理者是操作系统,对文件的操作是通过文件相关的系统调用接口来实现的。

二、回顾C语言文件接口

https://blog.csdn.net/yue_2899799318/article/details/146305837?fromshare=blogdetail&sharetype=blogdetail&sharerId=146305837&sharerefer=PC&sharesource=yue_2899799318&sharefrom=from_link

三、文件相关系统调用

3.1、open

在Linux系统中系统调用open是文件操作的核心接口,它用来打开或创建文件并返回文件描述符,后续可通过文件描述符对文件进行读写等操作。

函数原型:

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

两参数模式:用于打开已经存在的文件,pathname是文件路径,flags是打开方式

三参数模式:创建并打开新文件时使用,pathname是文件路径,flags是打开方式,mode用来设置新文件创建时的权限。

参数解析 :

pathname:

要打开或创建的文件路径,可以是绝对路径也可以是相对路径

flags:

必选标志:(只能选其一)

  • O_RDONLY,只读打开
  • O_WRONLY,只写打开
  • O_RDWR,读写打开

可选标志:(可组合使用)

  • O_CREAT:若文件不存在则创建,需配合mode参数。
  • O_NOFOLLOW:不跟随符号链接。
  • O_DIRECTORY:要求路径必须是目录,否则失败。
  • O_CLOEXEC:执行exec时自动关闭文件描述符。
  • O_SYNC:同步写入,确保数据写入物理设备。
  • O_NONBLOCK:非阻塞模式打开,适用于设备文件或管道。
  • O_APPEND:追加写入,每次写操作从文件末尾开始。
  • O_TRUNC:若文件存在且以写模式打开,则将其长度截断为0。
  • O_EXCL:与O_CREAT一起使用时,若文件已存在则返回错误,确保原子性创建。

mode:

使用mode参数时说明进程想要创建并打开一个新文件,此时mode表示创建文件时初始化文件权限。具体如下:

注意:mode参数只有O_CREAT参数被指定时有效,用来设置新文件的权限

常用权限宏(定义在<sys/stat.h>中):

  • S_IRUSR(用户读权限)、S_IWUSR(用户写权限)、S_IXUSR(用户执行权限)。
  • S_IRGRP(组读权限)、S_IWGRP(组写权限)、S_IXGRP(组执行权限)。
  • S_IROTH(其他用户读权限)、S_IWOTH(其他用户写权限)、S_IXOTH(其他用户执行权限)。

实际上由于文件掩码的存在,文件实际的权限=mode&~umask

返回值:

成功时:返回文件描述符(非负整数)

失败时:返回-1,并设置全局变量errno指示错误类型

代码演示:

#include<stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include<string.h>
#include<errno.h>
int main()
{umask(0);int ret=open("./text.txt",O_WRONLY|O_CREAT,0666);if(ret==-1){perror("open fail!\n");printf("%s\n",strerror(errno));return errno;}printf("文件描述符为%d\n",ret);return 0;
}

3.2、write

在Linux系统中,write系统调用用来向文件描述符所指定的文件中写入数据。

函数原型:

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

参数解析:

fd:文件描述符,通过open等系统调用获取,标识要读取的文件,管道,套接字等

buf:用户空间缓冲区指针,存储待写入的数据。

count:请求写入的字节数

返回值:

成功时:返回实际写入的字节数,可能会小于count。

失败时:返回-1,并设置全局变量errno指示错误类型。

代码示例:

向指定文件中写入字符串并读取打印

#include<stdio.h>
#include <sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include <fcntl.h>
#include<string.h>
#include<errno.h>
int main()
{//读写方式打开方便我们将写入数据后打印出来int ret=open("./text.txt",O_RDWR);if(ret==-1){printf("open fail! %s\n",strerror(errno));return 1;}//打开成功:char buff[]={"jinnzhiqi yuejianhua"};int n=write(ret,buff,sizeof(buff));if(n==-1){printf("write fail! %s\n",strerror(errno));return 2;}printf("写入数据成功!\n"); lseek(ret,0,SEEK_SET);char buff1[1024];int sz=read(ret,buff1,sizeof(buff1)-1);buff1[sz]='\0';printf("%s\n",buff1);return 0;
}

3.3、read

在Linux系统中,系统调用read表示从文件描述符所指定的文件中读取数据。

函数原型:

#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

参数解析 :

fd:文件描述符,通过open等系统调用获取,标识要读取的文件,管道,套接字等。

buf:用户空间缓冲区指针,用来存储读取到的数据。

count:请求读取的最大字节数。

返回值:

成功时:返回实际读取到的字节数。

  • 若返回值小于cout,说明数据不足read已经读到文件末尾
  • 若返回值等于0,表示已经读到文件末尾或连接失败

失败时:返回-1,并设置全局变量errno指示错误类型。 

代码示例:

从指定文件中读取字符串:

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<errno.h>
int main()
{int ret=open("./text.txt",O_RDONLY);if(ret==-1){printf("open fail! %s\n",strerror(errno));return 1;}//打开成功:char buff[1024];int n=read(ret,buff,sizeof(buff)-1);if(n==-1){printf("read fail! %s\n",strerror(errno));return 2;}//读取成功:buff[n]='\0';printf("%s\n",buff);return 0;
}

  

四、文件描述符

在Linux系统中,文件描述符(File Descriptor,简称FD)是操作系统内核为每个进程维护的一个非负整数标识符,用于抽象地引用进程已打开的文件、套接字(Socket)、管道(Pipe)、设备文件等I/O资源。它是进程与内核交互时管理I/O操作的核心机制。

4.1 核心概念

文件描述符是一个索引值,指向进程打开文件表(Open File Table)中的条目,而非直接指向文件本身。每个描述符对应一个内核维护的struct file结构体,记录文件的元数据(如偏移量、权限、引用计数等)。

4.2 理解文件描述符

与进程管理类似,Linux系统对已经打开的文件也采取“先描述再组织”的管理方法。当用户(进程)打开磁盘上的文件时,系统在系统层面会创建一个struct file结构体用来描述所打开的文件并存储相关文件信息。

在系统层面,当有多个文件被打开时,为了更高效地管理各个已打开的文件,系统会将每个struct file结构体用双链表的方式链接起来,此时对文件的管理就成了对该双链表的增删查改。

我们知道,Linux系统天然支持多进程,当多个进程打开多个文件时,一方面系统会给每个打开的文件创建struct file结构体并链入到全局链表中,另一方面,每个进程PCB中都会管理和维护一张文件描述符表(本质是以struct file* 为元素的指针数组)用来指明当前进程打开了多少个文件。

所以本质上,每个进程都有自己的文件描述符表(指针数组),文件描述符就是数组下标。

4.3 文件描述符的分配机制

4.3.1 分配流程

查找最小可用FD:

当进程调用open等系统调用时,内核会从进程的文件描述符表(File Descriptor Table)中搜索一个最小的未被占用的整数作为新描述符。

初始化描述符条目:

内核将该FD将一个内核维护的文件对象(struct file)进行关联,记录文件操作指针、偏移量、权限标志等信息。

4.3.2 关键数据结构

进程级文件描述符表:

每个进程都管理或维护一个独立的FD表,由用户态的int fd索引到内核态的struct file对象。

系统级打开文件表:

所有进程共享的全局表,存储struct file的引用计数,inode指针等,避免重复加载文件元数据。

4.4 分配规则的核心逻辑

在Linux系统中文件描述符总是默认从低到高顺序分配,也就是说内核默认优先分配最小的可用的FD,例如,到当前进程打开了FD:0、1、2则下一个分配的文件描述符就是3。

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>int main()
{// 分别打印三个标准流的文件描述符printf("stdin: %d\n", stdin->_fileno);printf("stdout: %d\n", stdout->_fileno);printf("stderr: %d\n", stderr->_fileno);umask(0);int n = open("./text.txt", O_RDONLY |O_CREAT,0666);printf("open: %d\n",n);return 0;
}

 

五、 重定向

5.1 概念

在Linux系统中,文件重定向是用于控制程序输入/输出(I/O)流向的核心机制,允许用户将命令的标准输入(stdin)、标准输出(stdout)或标准错误(stderr重新定向到文件、设备或其他进程,而非默认的终端(键盘/屏幕)

5.2 重定向的类型与语法

5.2.1 输出重定向

>:覆盖目标文件(若文件已存在则清空)

$ echo "Hello" > output.txt  # 将"Hello"写入output.txt(覆盖原有内容)

>>:追加内容到目标文件

$ echo "World" >> output.txt # 在output.txt末尾追加"World"

5.2.2 输入重定向

<:从文件读取并输入(替代键盘输入)

$ wc -l < input.txt  # 统计input.txt的行数(等价于wc -l input.txt)

5.2.3 错误重定向

2>:将标准错误输出到文件(覆盖)

$ ls /nonexistent 2> error.log  # 将错误信息写入error.log

2>>:将标准错误追加到文件

$ ls /nonexistent1 /nonexistent2 2>> error.log  # 追加多个错误

5.3 底层原理(dup2系统调用)

5.3.1 dup2

dup2是Linux系统中的一个核心系统调用,用于复制文件描述符。其核心作用是将一个现有的文件描述符(oldfd)复制到指定的目标文件描述符(newfd),使newfd指向与oldfd相同的文件表项。这一机制是文件重定向、进程间通信(如管道)等操作的基础

函数原型:

#include <unistd.h>
int dup2(int oldfd, int newfd);

 参数解析:

oldfd:需要复制的源文件描述符

newfd:目标文件描述符,若newfd已被占用,dup2会先关闭它

返回值:

成功时:返回newfd

失败时:返回-1,并设置全局变量errno指明错误原因

特殊情况:

  • 若newfd与oldfd相同,则dup2会直接返回newfd不会关闭它
  • 如果oldfd无效则dup2会直接返回-1,并设置errno为EBADF

代码演示:

 输出重定向:

#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{umask(0);int fd=open("./text.txt",O_CREAT|O_WRONLY,0666);if(fd<0){perror("open fail!\n");return 1;}int newfd=dup2(fd,1);if(newfd<0){perror("dup2 fail!\n");return 2;}printf("hello world!\n");printf("hello world!\n");printf("hello world!\n");printf("hello world!\n");return 0;
}

5.4 stdout与stderr

5.4.1 重定向

标准输出流(stdout)与标准错误流(stderror)都是进程启动时默认打开的I/O流,属于Unix/Linux系统的标准文件描述符(0=stdin, 1=stdout, 2=stderr)。

若为显式重定向,两者均会输出到当前终端(如命令行界面)。

#include<iostream>
#include<cstdio>int main()
{    std::cout<<"hello cout"<<std::endl;std::cerr<<"hello error"<<std::endl;fprintf(stderr,"hello error\n");return 0;
}

 

 若进行重定向:

./code 1> text.txt //或者./code > text.txt

此时会发现stdout的内容会写入文件,而strerr的内容会仍然显式在终端

 如果想让stderr的内容也重定向到文件text.txt中可以使用以下指令:

//将stdout重定向到text.txt后再追加stderr中的内容
./code 1> text.txt 2>>text.txt
./code 1> text.txt 2>&1

维度标准输出(stdout)标准错误(stderr)
设计目的输出程序的正常结果(如计算结果、用户提示)。输出程序的错误信息(如语法错误、运行时异常)。
默认行为与标准输入(stdin)关联,通常输出到终端或文件。与标准输入/输出独立,默认也输出到终端,但可重定向。
缓冲机制通常是行缓冲(遇到换行符或缓冲区满时刷新)。无缓冲立即刷新,确保错误信息及时显示。
重定向方式使用 > 或 1> 重定向到文件(如 command > file)。使用 2> 或 &> 重定向到文件(如 command 2> error.log)。
文件描述符默认文件描述符为 1默认文件描述符为 2
典型内容程序运行后的正常输出(如 echo "Hello")。程序异常时的警告或错误(如 ls /nonexistent)。

 5.4.2 为什么要存在stderr?

stderror是工程化设计的必然选择:

  • 错误隔离:将异常信息与正常数据分离,提升系统可维护性。
  • 实时响应:无缓冲机制确保关键错误即时暴露。
  • 灵活控制:通过重定向和管道实现精细化的输出管理。

如果没有stderr导致无论是正常信息还是异常信息都会通过stdout来进行输出,就会导致严重错误:

  • 用户可能因错误信息被截断或延迟而困惑,甚至无法感知程序失败。
  • 监控脚本无法区分正常数据与错误,导致误报或漏报

为了区分两者我们必须花费大量时间来过滤信息,这样做低效且会增加代码复杂度。

而通过系统级机制stderr将异常信息与正常数据分离,可以提升系统可维护性,也可以通过重定向和管道实现精细化的输出管理。

相关文章:

  • 分布式理论:常见分布式协议的概览与解析
  • 51c大模型~合集123
  • C++ 复习
  • AI驱动文字冒险游戏
  • 第 12 届蓝桥杯 C++ 青少组中 / 高级组省赛 2021 年真题
  • 0基础 | STM32 | STM32F103C8T6开发板 | 项目开发
  • #以梦为楫,共航中医传承新程
  • 芯片中的pad、strap和probe
  • Proxmox VE 8.4 显卡直通完整指南:NVIDIA 2080 Ti 实战
  • 深度学习与 PyTorch 基础
  • WindowsPE文件格式入门10.TLS表
  • Day108 | 灵神 | 合并两个有序链表
  • Matlab自学笔记
  • 网工_UDP协议
  • JavaWeb学习打卡-Day7-正向代理、反向代理、Nginx
  • C++--入门基础
  • JVM 如何使用性能分析工具定位代码中的性能问题?
  • 基于bert的情感分析程序
  • 【安装指南】DevC++的安装和使用(超级详细)
  • 【Linux】Linux奇技淫巧
  • 陈芋汐世界杯总决赛卫冕夺冠,全红婵无缘三大赛“全满贯”
  • 高速变道致连环车祸,白车“骑”隔离栏压住另一车,交警回应
  • 国铁集团:5月1日全国铁路预计发送旅客2250万人次
  • 超越梅罗,这样一个亚马尔折射巴萨的容错率
  • 人民日报评论员:焕发风雨无阻、奋勇前行的精气神
  • 央行4月开展12000亿元买断式逆回购操作