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将异常信息与正常数据分离,可以提升系统可维护性,也可以通过重定向和管道实现精细化的输出管理。