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

[linux仓库]解剖Linux内核:文件描述符(fd)的‘前世今生’与内核数据结构探秘

🌟 各位看官好,我是egoist2023!

🌍 Linux == Linux is not Unix !

🚀 今天来学习open返回值,文件描述符以及重定向。

👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦!

目录

书接上文

open返回值

文件描述符

0 & 1 & 2

FILE(扩展)

分配规则

关闭fd为0的文件

关闭fd为2的文件

关闭fd为1的文件

​编辑

解决方法 

重定向

dup2系统调用

扩展 

添加重定向功能 

总结


书接上文

在理解了系统调用 open 的基本使用方法与参数标志位传递逻辑后,我们自然会产生一个疑问:当调用 open 成功打开(或创建)一个文件后,操作系统会如何 “告知” 我们操作结果?又会以何种形式,让后续的读写操作能精准定位到这个文件?这就需要从 open 的返回值入手 —— 它正是连接 “打开文件” 这一动作与 “操作文件” 这一过程的关键桥梁,而这个返回值的核心身份,就是文件描述符(File Descriptor, FD)

深入理解文件描述符,不仅能帮我们掌握 Linux 下文件 IO 的底层标识逻辑,还能进一步解释一个更实用的场景:为什么我们在终端中执行 ls > test.txt 时,原本要输出到屏幕的内容会 “转移” 到文件里?这背后的核心机制,正是基于文件描述符的重定向。接下来,我们就从 open 返回值的意义切入,逐步拆解文件描述符的本质、规则,以及重定向的实现原理。

open返回值

int open(const char *pathname, int flags, .../* mode_t mode */ );

我们之前对open接口的三个参数做了依次介绍,而返回值放在本章节进行介绍,是为了和文件描述符联系起来。

int main()
{umask(0);//int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);int fd0 = open("log0.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);printf("fd0: %d\n", fd0);printf("fd1: %d\n", fd1);printf("fd2: %d\n", fd2);printf("fd3: %d\n", fd3);close(fd0);close(fd1);close(fd2);close(fd3);return 0;
}

我们对open的返回值进行打印查看:

fd0:3

fd1:4

fd2:5

fd3:6 

我们看到的这些打印结果 —— 它们就是文件描述符 fd,本质是数组下标。

所以到底什么是文件描述符呢?为什么下标又是从3开始的呢?为什么没有0、1、2呢?

文件描述符

OS 打开文件时,只通过文件描述符 fd 来识别(只认fd,不认文件)。

进程和文件是 1 : n 的关联关系

一个进程能打开多个文件,这就使得 OS 内部必然会存在大量被打开的文件!!!那么需要进行管理吗?如何管理呢?

先描述,再组织!!!

  • 每个进程都有对应的task_struct结构体,而内部含有一个struct files_struct *files指针,指向当前进程的文件管理结构,是进程与文件交互的 “总入口”。
  • fd_array 指针数组:数组下标就是文件描述符(fd) 。
  • 内核的 struct file 链表:管理 “系统中所有打开的文件”:大量被进程打开的文件,通过 struct file 链表组织 。对文件的打开、关闭、读写等操作,转化为链表的增删查改,实现高效管理。

0 & 1 & 2

那么0、1、2分别被谁占着呢?

Linux进程默认情况有3个缺省打开的⽂件描述符:

  • 标准输⼊ 0
  • 标准输出 1
  • 标准错误 2

0,1,2对应的物理设备⼀般是:键盘,显⽰器,显示器。

文件描述符就是从0开始的整数。当我们打开⽂件时,操作系统在内存中要创建相应的数据结构来描述⽬标⽂件。于是就有了file结构体。表⽰⼀个已经打开的⽂件对象。⽽进程执⾏open系统调⽤,所以必须让进程和⽂件关联起来。每个进程都有⼀个指针*files, 指向⼀张表files_struct,数组的每个下标,都指向内核中描述 “已打开文件” 的 struct file 结构体

本质上,⽂件描述符就是该数组的下标。只要拿着⽂件描述符,就可以找到对应的⽂件。

那该如何证明0、1、2确实如我们所说呢?将标准输入、输出、错误进行打印,如果证明确实是0、1、2,不就说明的确如此。

_fileno指的是文件描述符fd

int main()
{printf("stdin->%d\n",stdin->_fileno);   // 0printf("stdout->%d\n",stdout->_fileno); // 1printf("stderr->%d\n",stderr->_fileno); // 2return 0;
}

FILE(扩展)

还记得上一章节我们用 C 语言操作文件时,频繁接触的FILE吗?当时我们只需调用fopenfread这些库函数,就能轻松完成文件的读写,似乎不用关心底层细节。

 

但这里有个关键问题:我们之前说过,OS只认文件描述符(fd)
既然如此,FILE又是什么?
FILE是 C 标准库精心设计的一个结构体。

根据我们前面所说OS只认fd,因此我们可以推测FILE结构体里一定封装一个整数,且这个整数一定是fd!!!(确实如此)

struct _IO_FILE
{int _flags;		/* High-order word is _IO_MAGIC; rest is flags. *//* The following pointers correspond to the C++ streambuf protocol. */char *_IO_read_ptr;	/* Current read pointer */char *_IO_read_end;	/* End of get area. */char *_IO_read_base;	/* Start of putback+get area. */char *_IO_write_base;	/* Start of put area. */char *_IO_write_ptr;	/* Current put pointer. */char *_IO_write_end;	/* End of put area. */char *_IO_buf_base;	/* Start of reserve area. */char *_IO_buf_end;	/* End of reserve area. *//* The following fields are used to support backing up and undo. */char *_IO_save_base; /* Pointer to start of non-current get area. */char *_IO_backup_base;  /* Pointer to first valid character of backup area */char *_IO_save_end; /* Pointer to end of non-current get area. */struct _IO_marker *_markers;struct _IO_FILE *_chain;int _fileno;int _flags2;__off_t _old_offset; /* This used to be _offset but it's too small.  *//* 1+column number of pbase(); 0 is unknown. */unsigned short _cur_column;signed char _vtable_offset;char _shortbuf[1];_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

再看 C++ 中的stdinstdoutstderr(对应流对象cincoutcerr):它们虽然是以类的形式存在,但本质上与结构体并无本质鸿沟 —— 类不过是在结构体基础上增加了成员函数和运算符重载的扩展形式。而我们完全可以肯定,这些类的内部一定藏着一个核心成员:文件描述符(fd)。

而无论是通过 C 的FILE结构体,还是 C++ 的流类操作文件,有一个底层逻辑始终不变:任何对文件内容的增、删、查、改,都必须先经过内核缓冲区 —— 操作系统会先将文件数据预加载到这块内核空间的缓冲区中,后续的读写操作实际是与缓冲区交互,而非直接操作磁盘。这样既能减少对硬件的直接访问(降低开销),也能通过缓冲区的合并、延迟写入等机制提升整体 I/O 效率,这是所有文件操作绕不开的底层环节。

分配规则

文件描述符的分配规则:给新打开的文件分配fd,从文件描述符数组中寻找:最小的,没有被使用的下标,作为该文件的fd

关闭fd为0的文件

关闭fd为2的文件

关闭fd为1的文件

int main()
{close(1);int fd = open("log.txt",O_CREAT | O_WRONLY | O_TRUNC,0666);if(fd<0){perror("fd");return 1;}printf("hello file,fd:%d\n",fd); // stdout -> 1close(fd);return 0;
}

我们会发现显示器上不打印内容了?我写入的信息跑哪里去了呢?? 

这也验证了我们前面所说,fd为1的文件默认为stdout。此时把1关闭了,给新打开的文件分配到了1的位置,因此显示器上并打印显示我们想要的内容。

打印 log.txt里的内容看看:

不对啊,怎么还是没有看到我们想要的内容呢?为什么会看不到啊?!

printf默认是往1位置进行打印,即写入。此时我们的fd=1位置已经被换成log.txt文件了,而printf在上层并不知道fd=1的位置被替换了,还是傻傻地往该位置进行写入,即打印到文件里。

同样地,scanf默认往fd为0的位置上读取,此时0位置也被狸猫换太子了,导致是往log.txt文件读取内容。

解决方法 

第一种:往文件描述符1写入内容后,1位置又被我们关闭了,那么不关闭1位置是否就能看到我们想要的内容了呢?

第二种:fflush功能是立即清空指定流的缓冲区,并将其中的数据强制写入到关联的目标设备中。 

重定向

上述关闭fd为1的文件的代码跟重定向似乎是很类似的啊?会不会重定向的底层就是这样实现的呢?实际上重定向功能实现不是这样做的,而是通过dup2函数

dup2系统调用

扩展 

有了重定向的概念和本质理解,那么如果创建子进程,子进程是如何看待父进程打开的文件的?

当父进程通过 fork 创建子进程时,子进程对父进程已打开文件的 “继承”,本质是对内核文件对象的 “指针共享”

子进程会完整拷贝父进程的 file_struct(进程文件描述符表的容器),但其中每个文件描述符(fd)对应的指针,都指向同一个内核级 struct file 对象(该对象存储了文件偏移、打开模式、引用计数 ref_count 等核心元信息)。

如果我们做exec程序替换,不会创建新进程,会影响我们历史打开的文件吗?? 不会!!! 

添加重定向功能 

//支持重定向功能
#define NONE_REDIR 0
#define OUPUT_REDIR 1
#define APPEND_REDIR 2
#define INPUT_REDIR 3std::string filename;
int redir_type = NONE_REDIR;//初始化化数据
void InitGlobal()
{gargc = 0;memset(gargv,0,sizeof(gargv));filename.clear();redir_type = NONE_REDIR;
}//3.对命令进行解析,支持重定向功能
void CheckRedir(char cmd[])
{char* start = cmd;char* end = cmd + strlen(cmd) - 1;while(start<=end){//1.> >> 输出或追加if(*start=='>'){if(*(start+1)=='>'){//>> 追加*start='\0';redir_type = APPEND_REDIR;start+=2;//去掉空格TrimSpace(start);filename=start;break;}else{//> 输出*start = '\0';redir_type = OUPUT_REDIR;start++;//去掉空格TrimSpace(start);filename=start;break;}}//2. < 输入else if(*start == '<'){*start='\0';redir_type = INPUT_REDIR;start++;TrimSpace(start);filename=start;break;}else{start++;}}}//5.执行命令,让子进程来执行!!!
void ForkAndExec()
{pid_t id = fork();if(id<0){perror("fork"); //将错误码转为错误信息return;}else if(id == 0) //子进程{//支持重定向功能if(redir_type == OUPUT_REDIR){//输出 >int output = open(filename.c_str(),O_CREAT | O_TRUNC | O_WRONLY,0666);dup2(output,1);}else if(redir_type == APPEND_REDIR){//追加 >>int appendfd = open(filename.c_str(),O_CREAT | O_APPEND | O_WRONLY);dup2(appendfd,1);}else if(redir_type == INPUT_REDIR){//输入 <int input = open(filename.c_str(),O_RDONLY);dup2(input,0);}else{//什么都不做}execvp(gargv[0],gargv);exit(0);}else{//父进程//等待子进程int status = 0;pid_t rid = waitpid(id,&status,0);if(rid > 0){lastcode = WEXITSTATUS(status);}}
}   

总结

本文深入探讨Linux文件描述符(FD)机制,从open系统调用返回值切入,揭示FD作为数组下标的本质特性。通过分析进程task_struct中的files_struct结构,阐明0/1/2分别对应标准输入/输出/错误的分配规则,并验证了FILE结构体与FD的封装关系。重点讲解了FD分配规则、重定向实现原理(通过dup2系统调用)及父子进程间的FD继承机制。最后演示了在Shell中实现重定向功能的具体代码实现,包括输出重定向(>)、追加重定向(>>)和输入重定向(<)的处理逻辑。全文贯通理论讲解与实践验证,完整呈现了Linux文件IO的核心机制。

http://www.xdnf.cn/news/1408861.html

相关文章:

  • 编写一个用scala写的spark程序从本地读取数据,写到本地
  • 【ArcGIS微课1000例】0150:如何根据地名获取经纬度坐标
  • openssl使用SM2进行数据加密和数据解密
  • 科普:requirements.txt 和 environment.yml
  • Labview使用modbus或S7与PLC通信
  • Machine Learning HW3 report:图像分类(Hongyi Lee)
  • 《深入剖析Kafka分布式消息队列架构奥秘》之Springboot集成Kafka
  • 中级统计师-统计实务-第四章 专业统计
  • 嵌入式ARM程序高级调试技能:20.qemu arm ARM Linux 上 addr2line 的实际应用示例
  • 【重学MySQL】九十五、Linux 下 MySQL 大小写规则设置详解
  • CF每日3题(1500-1600)
  • 阿里云创建自己的博客,部署wordpress
  • 基于Matlab元胞自动机的强场电离过程模拟与ADK模型分析
  • Scikit-learn Python机器学习 - 数据集的划分
  • 网格图--Day03--网格图DFS--2658. 网格图中鱼的最大数目,1034. 边界着色,1020. 飞地的数量
  • Cartographer中的gflag与lua文件
  • 【开题答辩全过程】以 基于Java的城市公交查询系统设计与实现为例,包含答辩的问题和答案
  • 记录测试环境hertzbeat压测cpu高,oom问题排查。jvm,mat,visulavm
  • 浏览器和 node 操作文件的 api 以及区别
  • GEE 实战:Landsat 5 月度 NDVI 数据插值填补(以 8 月为例)_后附完整代码
  • Python:如何批量下载CLMS NDVI V3数据集?
  • PyQt5 K线图实现与性能优化详解
  • 神州数码之FTP/TFTP 升级 篇
  • 深入解析Linux系统中的/etc/hosts文件
  • 在Windows的wsl中如何以root登录Ubuntu
  • OpenStack 02:使用 DevStack 单节点一体化部署
  • Kafka面试精讲 Day 3:Producer生产者原理与配置
  • Java提供高效后端支撑,Vue呈现直观交互界面,共同打造的MES管理系统,含完整可运行源码,实现生产计划、执行、追溯一站式管理,提升制造执行效率
  • isp图像处理--bayer Binning
  • isp 图像处理--DPC坏点矫正