【Linux】基础IO(三):文件描述符与重定向

【Linux】基础IO(三):文件描述符与重定向
在这里插入图片描述

✨道路是曲折的,前途是光明的!

📝 专注C/C++、Linux编程与人工智能领域,分享学习笔记!

🌟 感谢各位小伙伴的长期陪伴与支持,欢迎文末添加好友一起交流!

在这里插入图片描述


一、文件描述符

1.1 fd

  1. 文件由进程在运行时打开,一个进程可同时打开多个文件,而系统中存在大量并发运行的进程,这意味着系统任一时刻都可能存在数量庞大的已打开文件。
  2. 为了高效管理这些已打开的文件,操作系统会为每个已打开的文件创建专属的 struct file 结构体,并用双链表将所有该结构体串联起来。如此一来,操作系统对已打开文件的管理,就转化为对这一全局双链表的增、删、查、改等基础操作,大幅简化了文件管理的核心逻辑。(即先描述,在组织)
  3. 但仅靠全局的双链表,无法区分哪些已打开文件归属于特定进程,因此操作系统还需额外建立进程与文件之间的关联关系,以此精准界定每个进程所持有、可操作的文件范围。
那么操作系统是如何建立进程与文件直接的关联关系的呢?

我们知道,当一个程序运行起来时,操作系统会将该程序的代码和数据从硬盘加载到内存,然后为其创建对应的task_struct、mm_struct、页表等相关的数据结构,并通过页表建立虚拟内存和物理内存之间的映射关系。

在进程的核心控制结构体 task_struct 中,存在一个指向 files_struct 结构体的指针;而 files_struct 结构体里包含了关键的指针数组 fd_array —— 这个数组的下标,就是我们实际编程中使用的文件描述符

当进程执行打开 log.txt 文件的操作时,操作系统会按以下逻辑完成文件描述符的绑定:

  1. 首先将磁盘中的 log.txt 文件加载到内存中,并为其创建对应的 struct file 结构体(包含文件的读写位置、权限、关联的磁盘inode等核心信息);
  2. 把这个新创建的 struct file 结构体接入系统管理已打开文件的全局双链表,完成系统层面的文件管理登记;
  3. 将该 struct file 结构体的首地址,填入当前进程 fd_array 数组中下标为3的位置(因0、1、2被标准输入/输出/错误占用),使 fd_array[3] 指针精准指向该文件的 struct file 结构体;
  4. 最后将下标值3作为文件描述符返回给调用进程,进程后续通过这个描述符就能找到对应的 struct file,进而操作 log.txt 文件。
因此,我们只要有某一文件的文件描述符,就可以找到与该文件相关的文件信息,进而对文件进行一系列输入输出操作。

1.2 补充说明

1.2.1 进程创建时默认打开0、1、2文件描述符的底层逻辑

我们常说“进程创建时会默认打开0、1、2”,本质是操作系统为每个新进程完成了硬件设备到文件描述符的绑定:

  1. 0对应标准输入流(关联键盘)、1对应标准输出流(关联显示器)、2对应标准错误流(同样关联显示器)。
  2. 键盘和显示器作为硬件设备,会被操作系统识别并纳入文件管理体系: 进程创建时,操作系统会为键盘、显示器分别创建对应的struct file结构体,将这些结构体接入全局的已打开文件双链表,再把3个结构体的首地址依次填入进程files_structfd_array数组下标0、1、2的位置。
  3. 至此,进程无需手动调用open,就默认拥有了对键盘(0)、显示器(1/2)的操作能力。

1.2.2 磁盘文件与内存文件的核心区别

  • 磁盘文件:存储在磁盘上的静态文件,是文件的“持久化形态”,由两部分构成:
    ① 文件内容:文件中实际存储的业务数据(如文本、二进制流);
    ② 文件属性(元信息):文件的基础描述信息,包括文件名、大小、创建时间、权限、所属用户等。
  • 内存文件:磁盘文件被加载到内存后的动态形态,是操作系统为操作文件创建的“内存映射”。
    磁盘文件与内存文件的关系,类比“程序与进程”:程序是磁盘上的静态代码,运行后成为内存中的进程;磁盘文件是静态存储的文件,被打开/加载后成为内存中的文件。
  • 加载规则:文件加载到内存时采用“按需加载”策略——优先加载文件属性(元信息),仅当需要读取、写入文件内容时,才延后加载具体的文件数据,以此节省内存资源。

1.2.3 文件写入的缓冲区机制

向文件写入数据时,并非直接写入磁盘,而是先写入该文件对应的内存缓冲区;操作系统会通过预设策略(如缓冲区满、定时刷新、手动调用fsync),将缓冲区中的数据批量刷新到磁盘,以此减少磁盘IO次数,提升整体读写效率。


1.3 文件描述符的分配规则

#include<stdio.h>#include<sys/stat.h>#include<sys/types.h>#include<fcntl.h>intmain(){umask(0);int fd1 =open("log1.txt", O_RDONLY | O_CREAT,0666);int fd2 =open("log2.txt", O_RDONLY | O_CREAT,0666);int fd3 =open("log3.txt", O_RDONLY | O_CREAT,0666);int fd4 =open("log4.txt", O_RDONLY | O_CREAT,0666);int fd5 =open("log5.txt", O_RDONLY | O_CREAT,0666);printf("fd1:%d\n", fd1);printf("fd2:%d\n", fd2);printf("fd3:%d\n", fd3);printf("fd4:%d\n", fd4);printf("fd5:%d\n", fd5);return0;}

若我们在打开这五个文件前,先关闭文件描述符为0的文件,此后文件描述符的分配又会是怎样的呢?

close(0);

我们发现第一个打开的文件获取到的文件描述符变成了0,而之后打开文件获取到的文件描述符还是从3开始依次递增的。

我们再试试将文件描述符为0和2的文件都关闭。

注意:1不能关闭,因为他是标准输出,关闭了我们无法从显示器观察现象
close(0);close(2);

可以看到前两个打开的文件获取到的文件描述符是0和2,之后打开文件获取到的文件描述符才是从3开始依次递增的。

结论: 文件描述符是从最小但是没有被使用的fd_array数组下标开始进行分配的。

二、重定向

2.1 重定向的原理

上面我们已经了解了文件描述以及文件描述符的分配规则,接下来我们通过实例来看看重定向的本质。

2.1.1 输出重定向原理

输出重定向的原理就是我们本该输出到一个文件的原理输出到另外一个文件中。

如上述图片,我们让本应该输出到“显示器文件”的数据输出到log.txt文件当中,那么我们可以在打开log.txt文件之前将文件描述符为1的文件关闭。

#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intmain(){close(1);int fd =open("log.txt", O_WRONLY | O_CREAT,0666);if(fd <0){perror("open");return1;}printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");printf("hello Linux\n");fflush(stdout);close(fd);return0;}

我们可以看到原先应该输出到屏幕上的内容输出到了我们的log.txt文件当中。

注意:

  1. printf 函数默认向 stdout(标准输出流)输出数据,stdout 指向 struct FILE 类型的结构体,该结构体中存储的文件描述符固定为1,因此 printf 本质是向文件描述符为1的文件(显示器)输出数据。
  2. 调用 printf 后,数据不会立即写入操作系统内核,而是先存入C语言标准库维护的用户态缓冲区;只有当缓冲区满、遇到换行符(\n)或程序结束时,数据才会批量刷新到操作系统层面。若需让数据即时输出,需通过 fflush(stdout) 手动强制刷新C语言缓冲区,将数据同步到操作系统。

2.1.2 追加重定向原理

我们知道输出重定向的时候会将原有文件里面的内容数据覆盖,而追加重定向是对原有内容数据不进行覆盖,追加再后面输出数据。

如我们想让本应该输出到“显示器文件”的数据追加式输出到log.txt文件当中,那么我们应该先将文件描述符为1的文件关闭,然后再以追加式写入的方式打开log.txt文件,这样一来,我们就将数据追加重定向到了文件log.txt当中。

#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intmain(){close(1);int fd =open("log.txt", O_WRONLY|O_APPEND|O_CREAT,0666);if(fd <0){perror("open");return1;}printf("hello linux!\n");printf("hello linux!\n");printf("hello linux!\n");printf("hello linux!\n");printf("hello linux!\n");fflush(stdout);close(fd);return0;}

看到如下图,我们能发现新的数据已经追加到原来文件数据内容的后面。


2.1.2 输入重定向的原理

输入重定向就是,将我们本应该从一个文件读取数据,现在重定向为从另一个文件读取数据。(如原来是从键盘获取,我们可以让他从文件获取)

我们可以在打开log.txt文件之前将文件描述符为0的文件关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是0。

#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intmain(){close(0);int fd =open("log.txt", O_RDONLY | O_CREAT,0666);if(fd <0){perror("open");return1;}char str[40];while(scanf("%s", str)!=EOF){printf("%s\n", str);}close(fd);return0;}
scanf函数是默认从stdin读取数据的,而stdin指向的FILE结构体中存储的文件描述符是0,因此scanf实际上就是向文件描述符为0的文件读取数据。

三、标准输出流与标准错误流的其区别

标准输出流和标准错误流对应的都是显示器,它们有什么区别?
#include<stdio.h>intmain(){printf("hello printf\n");//stdoutperror("perror");//stderrfprintf(stdout,"stdout:hello fprintf\n");//stdoutfprintf(stderr,"stderr:hello fprintf\n");//stderrreturn0;}

这样看起来标准输出流和标准错误流并没有区别,都是向显示器输出数据。

但我们若是将程序运行结果重定向输出到文件log.txt当中,我们会发现log.txt文件当中只有向标准输出流输出的两行字符串,而向标准错误流输出的两行数据并没有重定向到文件当中,而是仍然输出到了显示器上。

因为我们使用重定向时,重定向的是文件描述符是1的标准输出流,而并不会对文件描述符是2的标准错误流进行重定向。

四、dup2

要完成重定向我们只需进行fd_array数组当中元素的拷贝即可。

  • 例如,我们若是将fd_array[3]当中的内容拷贝到fd_array[1]当中,因为C语言当中的stdout就是向文件描述符为1文件输出数据,那么此时我们就将输出重定向到了文件log.txt。

Linux操作系统中,就给我们给了一个实现此功能的函数:dup2

原型如下:

intdup2(int oldfd,int newfd);
  1. 函数功能

dup2 会将 fd_array[oldfd] 的内容拷贝到 fd_array[newfd] 中,若有必要,会先关闭文件描述符为 newfd 的文件。

  1. 函数返回值

dup2 调用成功时返回 newfd,调用失败时返回 -1。

  1. 使用注意事项
  • 若 oldfd 并非有效的文件描述符,dup2 调用失败,且此时文件描述符为 newfd 的文件不会被关闭;
  • 若 oldfd 是有效的文件描述符,且 newfd 与 oldfd 的值相同,则 dup2 不执行任何操作,直接返回 newfd。

我们将打开文件log.txt时获取到的文件描述符fd和1传入dup2函数,那么dup2将会把fd_arrya[fd]的内容拷贝到fd_array[1]中,在代码中我们向stdout输出数据,而stdout是向文件描述符为1的文件输出数据,因此,本应该输出到显示器的数据就会重定向输出到log.txt文件当中。

#include<stdio.h>#include<sys/types.h>#include<sys/stat.h>#include<unistd.h>#include<fcntl.h>intmain(){int fd =open("log.txt", O_WRONLY | O_CREAT,0666);if(fd <0){perror("open");return1;}close(1);dup2(fd,1);printf("我跑到这里来啦!!!\n");fprintf(stdout,"我也跑到这里来啦!!1\n");return0;}

代码运行后,我们即可发现数据被输出到了log.txt文件当中。


✍️ 坚持用清晰易懂的图解+可落地的代码,让每个知识点都简单直观!💡 座右铭:“道路是曲折的,前途是光明的!”

Read more

Java中的反射机制详解:从原理到实践的全面剖析

Java中的反射机制详解:从原理到实践的全面剖析

文章目录 * 摘要 * 第一章 反射机制概述 * 1.1 什么是反射? * 1.2 反射的江湖地位:为何需要它? * 1.3 反射的优缺点 * 第二章 反射的基石:Class类与类加载 * 2.1 万物皆对象:Class对象 * 2.2 获取Class对象的三种方式 * 2.3 类加载的幕后故事 * 第三章 解剖类:反射的核心API * 3.1 操作构造方法(Constructor):创建对象 * 3.2 操作字段(Field):访问与修改属性 * 3.3 操作方法(Method):动态调用 * 第四章 深入进阶:反射的高级特性 * 4.1

By Ne0inhk
AI大模型驱动的软件开发革命:从代码生成到自愈系统的全流程重构

AI大模型驱动的软件开发革命:从代码生成到自愈系统的全流程重构

目录 * 引言:软件开发范式转移的临界点 * 技术演进:从辅助工具到开发中枢 * 需求分析阶段:智能需求工程师 * 设计阶段:AI架构师登场 * 编码阶段:从Copilot到AutoCode * 测试阶段:智能测试工程师 * 部署与运维:自愈式系统 * 行业应用场景深度解析 * 医疗领域:智能陪诊系统 * 金融领域:智能合规助手 * 技术挑战与解决方案 * 数据隐私保护 * 模型可解释性 * 未来趋势:AI原生开发范式 * 开发工具链重构 * 开发者角色转型 * 产业链影响 * 总结与展望 引言:软件开发范式转移的临界点 在GitHub Copilot用户突破1.5亿的2025年,AI大模型已渗透到软件开发的每个环节。根据微软Build大会披露的数据,某金融企业通过AI开发平台将新功能上线周期从6个月压缩至6周,人力成本降低40%。这场变革不仅体现在效率提升上,更重塑了软件开发的底层逻辑。本文将结合2025年最新实践案例,深度解析AI大模型如何重构软件开发全生命周期。 技术演进:从辅助工具到

By Ne0inhk
AI赋能原则7解读思考:AI时代构建可组合的能力比单点专业更重要

AI赋能原则7解读思考:AI时代构建可组合的能力比单点专业更重要

目录 一、能力组合:战略思维的新范式 二、技术角度:模块化与接口化的能力设计 (一)模块化:能力拆分与重组 (二)接口化:能力间的“沟通语言” (三)思维方式升级 三、可组合能力:适应性与未来竞争力 (一)适应性:能力模块的灵活重组 (二)扩展性:能力的迭代升级 (三)协同性:超越单一模块的能力上限 (四)构建能力平台而非岗位架构 四、深层次启示 (一)个人层面:从专业深度到能力组合的战略升级 (二)组织层面:构建灵活的能力架构 (三)社会层面:教育与培训体系的转型 五、总结 感谢您的阅读! 在AI浪潮的冲击下,过去几十年“单点专业”的价值正在被重塑。霍夫曼明确指出:未来的竞争力不在于你掌握多少孤立的技能,

By Ne0inhk
一文读懂OpenRouter:全球AI模型的“超级接口”,很多免费模型

一文读懂OpenRouter:全球AI模型的“超级接口”,很多免费模型

在人工智能技术百花齐放的今天,开发者面临着一个“幸福的烦恼”:市面上有GPT-4、Claude、Gemini、Kimi、GLM等众多顶尖大模型,但每个平台都需要单独注册、管理API密钥、对接不同接口文档,极大地增加了开发成本与技术门槛。 OpenRouter的出现,正是为了解决这一痛点。它不仅是一个AI模型聚合平台,更被业界视为全球AI模型竞争的“风向标”。 1. 什么是OpenRouter? OpenRouter是一个开源的AI模型聚合平台,它像一个“超级接口”或“路由器”,将全球超过300个主流AI模型(来自400多个提供商)整合在一起,为开发者提供统一的API接口。 其核心价值在于: * 统一API接口:开发者只需使用一套API密钥,即可调用包括OpenAI、Anthropic、Google、以及中国头部厂商(如MiniMax、月之暗面、智谱AI)在内的所有模型,无需为每个模型单独适配接口。 * 智能路由与成本优化:平台支持智能路由,可自动匹配性价比最高的模型,或根据开发者需求手动切换。其采用纯按量付费模式,无月费或最低消费,价格通常与官方持平甚至更低。 * 零

By Ne0inhk