Linux之文件管理与重定向

 文件的管理

最开始说到过, 一个进程是可以打开多个文件的并且可以对这些文件做出不同的操作, 也就是说加载到内存中的文件可能存在多个.

操作系统要不要管理这些打开的文件呢? 

当我们在程序里面打开多个文件时, 操作系统肯定是得对这些文件进行管理的, 而管理的本质就是对数据进行管理, 管理的方法就是先描述再组织, 所以操作系统为了管理被打开的文件就会创建内核数据结构来描述这些文件, 在操作系统中这个结构体就叫做struct file, 在这个结构体里面包含了文件的大部分属性, 进程可以通过这些属性来找到文件并访问文件的内容, 每打开一个文件操作系统就会创建一个file结构体, 然后采用链式结构将这些文件的结构体连接起来, 这样操作系统只要找到一个文件结构体的起始地址就能找到所有被该进程打开的文件的file结构体, 对文件的管理就变为对file结构体的增删查改.

那这里就存在一个问题, 我们之前说文件操作的本质是: 进程和被打开文件的关系, 可是的进程和被打开的文件好像没有任何联系啊, 这就和之前的open函数的返回值文件描述符fd有关了!

fd是什么?

我们通常把fd称为文件描述符,对于fd大家最熟悉的一点就是:通常使用fd来记录open函数的返回值,比如:

通过代码的运行结果我们可以看到open函数的返回值都是整数, 而且随着打开文件的数目增加open函数的返回值也在有规律的增加从3开始依次往后加1.

为什么这里的文件描述符是从3开始的呢?文件描述符是从0开始递增的, 那么0,1,2都去哪了?

当我们运行一个程序的时候操作系统会帮自动帮我们打开三个输入输出流

标准输入流(stdin)对应键盘标准输出流(stdout)对应显示器标准错误流(stderr)也对应显示器,在Linux下它们也是文件, 分别占据了0, 1, 2的位置.

我们在学习C语言文件操作的时候, 我们曾经学过一个文件指针(FILE* pf), 它和文件描述符又有什么联系呢? 

c语言的文件函数中通过FILE*指针来访问具体的文件, 操作系统的文件函数中是通过fd文件描述符来访问具体的文件,:

而c语言的文件函数是基于操作系统的文件函数实现的, FILE是一个结构体, fd是一个整型变量, 所以这里我们就可以推测出结构体FILE中一定存在着一个字段记录着fd的数值, 这三个流分别占用着文件描述符的0 1 2, 所以我们在程序中打开文件的描述符是从3开始, 这里可以通过下面的代码来验证上述的内容:

但是还存在一个问题为什么文件描述符是一个串连续的整数呢?

在操作系统里面不止一个进程要打开文件, 还有很多的进程也要打开各种各样的文件, 所以操作系统中存在着很多被打开的文件, 进程是没有办法从这么多被打开的文件中找到属于本进程的文件, 所以在task_struct里面就会存在一个名为files的指针, 这个指针指向的对象是一个名为files_struct的结构体:

在files_struct结构体里面存在一个数组这个数组fd_array, 数组的元素类型为struct file*, 也就是说这个数组的每个元素都是一个指针, 指针指向的对象是描述文件属性的file结构体, 当我们打开文件时操作系统就会在fd_arry数组里面从上往下查找没有被用到的元素, 找到之后就会就会将file结构体的地址填入该元素里面.

当操作系统将地址填入数组之后就会将该文件的file在数组中对应的下标返回给用户, 所以当我们使用完open函数时就可以得到一个返回值, 我们把这个返回值称为文件描述符也可以叫fd, fd的本质就是数组下标.

所以当我们通过fd对文件执行操作时, 实际上就是进程的PCB通过struct files_struct找到结构体files_struct中的数组fd_array, 再把fd的值作为数组的下标找到记录文件属性的struct file的地址, 然后再根据file结构体找到具体的文件最后执行对应的操作, fd的本质就是一个数组的下标, 所以他是一个连续的整数.

此外, struct file里面也应该有能获得文件缓冲区(在内存上)的成员, 而struct file结构体是在内核中创建的, 专门用来管理被打开的文件, 所以无论读写数据, 都要把数据加载到文件缓冲区中, 而磁盘上的文件加载到内存这一工作是由OS来进行的, 用户不需要担心文件是否被加载到内存中.

所以我们在应用层进行数据的读写, 本质是将内核缓冲区中的数据进行来回的拷贝!

 此外struct file中还应该有文件的操作方法集, 通过这些方法集, 就可以实现底层不同的调用方法(多态), 后面会详细说.

再来看一段代码:

 

这段代码调用了read函数, 从0号文件也就是stdin中读取字符并打印出来. 这也侧面证明了我们并不需要打开0号文件, 通过0文件标识符即可访问文件, 所以c语言默认打开标准输入其实本质是系统默认打开了0号文件标识符, 打开了标准输入对应的设备文件.

同样的, 1号文件也在进程启动时直接被打开了. 


fd的分配规则

我们在上面提到fd的分配规则是从数组fd_array从上往下依次寻找没有被用到的元素, 因为每运行一个进程操作系统会自动打开三个文件, 所以我们再打开文件时得到的fd就是从3开始依次往后增加.

那既然操作系统会自动给我们打开三个文件而且这三个文件的fd分别是0 1 2, 那我们是不是能够通过 close函数 + 这三个fd 将这三个文件关闭呢?

可以, 并且将这几个文件关闭之后这些文件对应在fd_array上的地址是会被清空的, 也就是说这些fd又可以被新的文件占用.

所以按照fd的分配规则, 当我们关闭其中一个文件再打开一个新的文件时, 新文件会按顺序占用已经关闭文件的下标.

比如说将stdin文件关闭再打开一个文件, 我们就可以发现打开文件的fd为0:

关闭2号也是同理: 

 

那关闭1号是不是呢:

 这里好像就出问题屏幕上没有显示打印结果,要想明白这个问题我们就得聊聊重定向是什么.


重定向

输出重定向

FILE是一个结构体在这个结构体里面有一个字段记录着文件描述符的值, 所以在stdout的结构体里面就会存在一个字段来专门记录stdout的文件描述符(fd = 1), 当我们在程序里面使用printf函数向屏幕上打印内容时, 实际上就是向stdout文件里面打印内容.

printf("fd1:%d\n",fd1);//两者一样
fprintf(stdout,"fd1:%d",fd1);

printf函数是默认向stdout里面打印内容, fprintf函数可以向指定的文件里面打印内容, 当fprintf函数第一个File*类型的参数填入stdout时这两个函数的功能是一样的, 向stdout里面输出内容实际上就是向stdout内部的fd所指向的文件里面打印内容, stdout内部的fd永远都是1, 所以每次使用printf函数向屏幕上打印数据时, 操作系统都会在fd_array中寻找下标为1的元素得到元素里面的地址, 然后往该地址指向的文件里面输出对应的数据, 最后这些数据就会显示在屏幕上面.

 

此时依然什么都没有输出, 但是cat log.txt就会发现:

 

 printf要输出的内容被打印在了log.txt文件中, 我们本来是要向显示器里打印新打开的文件的fd, 现在却输出在了文件里, 这就叫做输出重定向.

为什么呢?

一开始我们将文件描述符为1的文件关闭了, 然后我们又使用open函数打开了一个新文件, 按照fd的分配规则这时新打开的文件log.txt的文件描述符就是1, 也就是fd_array数组中1号下标位置对应的struct file*是log.txt文件的struct file的地址.

之后我们使用printf函数打印一些内容, printf函数默认向stdout文件里面输出数据, 而stdout是一个结构体, 其中保存的文件描述符一直是1, 与其说printf只认显示器/stdout, 不如说printf函数只认文件描述符1, 此时的fd_array[1]不再指向stdout文件, 而指向log.txt, 所以printf输出的数据就不会显示在屏幕上而是log.txt里, 所以要完成输出重定向其实只需要修改fd_array[1]的内容即可. 

换成fprintf效果是一样的: 

 

 为什么需要fflush刷新缓冲区呢? 后面再说.


 追加重定向

把O_TRUNC改为O_APPEND 即追加重定向. 

 输入重定向

本来默认要从键盘stdin中读取内容, 但是现在直接从打开的log.txt中读取, 这就完成了输入重定向.


dup2函数

上面的代码都是通过人为的使用close函数关闭指定文件描述符来实现重定向, 但是这种方法使用起来还是很麻烦, 所以操作系统提供了一个函数接口来专门实现重定向, 这个函数叫做dup2.

该函数的参数如下;

调用接口:int dup2(int oldfd, int newfd);

头文件:unistd.h

参数:oldfd为需要转移位置的文件描述符, newfd表示oldfd需要转移到的文件描述符的位置.

功能:将oldfd的内容覆盖到newfd处(不是交换)

所以重定向的原理就是文件描述符表中数组下标里的内容进行拷贝.

如果执行dup2(3,1):

执行完dup2函数之后就会将下标为oldfd的数据拷贝到下标为newfd的元素里面去, 也就是将fd_array[3]赋值给 fd_array[1], 此时有两个指针指向log.txt, 如果close(3), 但是log.txt还需要需用, log.txt结构体会不会直接被释放了呢? 这里用到了引用计数, 有几个指针指向这个文件, 引用计数就是几, close的时候会先--引用计数, 如果引用计数为0文件才会被释放.

 所以上面的重定向可以用dup2再简化一遍:

输出重定向:

 追加重定向, 只需要把O_TRUNC改成O_APPEND:

输入重定向: 


指令和输入/出重定向( > <)有什么关系呢?(myshell的修改) 

对于这几个重定向, 我们以前是怎么用的?

首先大部分的指令在执行的时候本质都是bash的子进程, 对于这行命令行字符串, 首先会去做识别, 检查>,>>,<, 然后判断出这个指令需要有重定向功能, 前半部分是指令, 后半部分是要重定向的目标文件, 所以进程在替换之前可以先进行重定向操作, 未来echo指令在执行时就可以把本应该打印在显示器的内容写入文件中,

 所以可以对之前的myshell进行修改:

先添加几个宏用于标识重定向:

再定义两个全局变量表示重定向状态和重定向的文件名:

 

要判断重定向是需要对指令进行处理, 所以在获取命令字符串后进行重定向判断.

从后向前开始寻找>或<, 找到的是<输入重定向的话, 当前位置替换为'\0', filename指向下一个位置,这样就将usercommand和filename分割开, 但是filename此时指向的可能是空格, 需要跳过空格, 可以写一个宏来实现.

程序替换部分也需要进行修改,  在程序替换前先用dup2进行重定向:

可以看到 输出重定向 追加重定向 输入重定向都可以实现了

问题: 我们的重定向是在程序替换之前进行的, 程序替换后的进程会不会影响曾经进行的重定向呢? 为什么? 

不会, 从运行结果也可以看出并没有影响,因为程序替换只是在内存中替换了代码和数据以及修改了对应的页表映射,重定向是修改了进程PCB中对应的数据, 而程序替换不会创建新进程, 也不会对当前PCB有影响, 所以并没有影响.


标准错误流 

上面的一系列操作都是与标准输出(stdout)和标准输入(stdin)有关, 那标准错误(stderr)呢? 

先看一段代码:

可以看到输出重定向后hello stdout重定向到了log.txt文件中, 而hello stderr打印在了屏幕上, 因为这里输出重定向是把1号文件(stdout)替换为log.txt, 和 (stderr)没有关系, 那如果我想把两行内容全都输出到log.txt中呢?

 另一种重定向方式:

在之前的命令后加一个 2>&1 ,意思是把文件标识符表中fd=1中的内容放入fd=2中(理解成把2重定向为1):

所以./mytest4 > log.txt的完整写法应该是 ./mytest4 1 > log.txt:

程序运行时往往会产生一些常规消息错误消息, 如果我们想把常规消息错误消息分别输出到不同的文件中, 就可以这样:

 这也就是标准错误存在的意义.

之前用过的printf就是向标准输出打印, perror就是向标准错误打印, 将正常消息和错误消息分开存储, 排查错误时只需查看错误日志即可.


如何理解linux下一切皆文件 ?

Linux下一切皆文件是指, Linux系统中的一切东西全都可以通过文件的方式进行访问或者管理. 反过来说, 任何被挂在系统中的东西, 即使它们的本质并不是文件, 也会被OS以文件的眼光来呈现.

比如: 我们经常谈到的进程, 磁盘, 显示器等, 实际上都不是文件, 但是用户可以以文件系统的规范去访问它, 修改属性.

 一个文件站在方法的角度上, 最重要的是读写方法, 键盘,显示器,磁盘,网卡等硬件的读写方法肯定是不一样的, 键盘只有读没有写, 显示器只有写没有读, 磁盘读写方法都有, 那如何把不一样的东西看成一样的呢?

在软件层往上来看, 我们不需要关注底层硬件层的差异, 而只需要关注文件就可以了, 他们的读写方法都是一样的. 操作系统给我们虚拟化的一层软件层叫做VFS虚拟文件系统, 正是因为有这层的存在, 所以再往上看认为一切皆文件.

所以不管我们打开什么设备, 进程只需要和对应的struct file关联起来, 就可以用上层的指向底层读写方法的函数指针, 直接完成读写操作, 这也就是多态.


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/286942.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【MySQL】存储过程、存储函数、触发器

目录 存储过程介绍技术背景存储过程的作用与优势存储过程跟自定义函数很像。它们的区别是&#xff1a; 存储过程的缺点存储过程的特性基本存储过程使用1.创建语法语法说明&#xff1a;使用案例1.创建获取新闻类别数量的存储过程2.创建获取指定新闻类别ID下新闻数量的存储过程 2…

【微服务-网关】SpringCloud GateWay核心技术

在前面的文章中我们介绍了微服务网关的基础知识&#xff0c;了解了什么是网关&#xff0c;网关有什么作用&#xff0c;以及市面上有哪些成熟的网关产品&#xff0c;最后了解了网关的配置技巧。通过上篇文章&#xff0c;大家应该可以在微服务架构中完成网关的基本配置。 但是&am…

2022 年甘肃省职业院校技能大赛 高职组 网络系统管理竞赛 网络构建模块试题

2022 年甘肃省职业院校技能大赛 高职组网络系统管理竞赛 网络构建模块试题 目 录 考试说明… 3 任务描述… 3 任务清单… 3 &#xff08;一&#xff09;基础配置… 3 &#xff08;二&#xff09;有线网络配置… 4 &#xff08;三&#xff09;无线网络配置… 6 &#xff08;四&a…

老大语录六 谈产品规划

老大语录六 谈产品规划 产品经理一个重要的职责就是做产品规划。它是考验产品经理对市场和需求的阅读能力,是决定一个产品在市场上跟竞争对手分出高下的必要条件,是最能体现产品经理水平的工作。 然而我们往往在执行过程中在这一步上做的并不是特别好。虽然很多企业都会容易建…

2024 年 5 款适用于 Linux 的参考文献管理软件

时间是宝贵的&#xff0c;因此如果某款软件能让您摆脱必须执行的日常繁琐任务&#xff0c;那么它就会派上用场。 参考文献管理工具就是此类软件的典型代表&#xff0c;只需点击几下就能自动格式化引文。学生、教育工作者、作家、科学家和研究人员一定会发现它们非常有用。 在…

NFTScan | 03.18~03.24 NFT 市场热点汇总

欢迎来到由 NFT 基础设施 NFTScan 出品的 NFT 生态热点事件每周汇总。 周期&#xff1a;2024.03.18~ 2024.03.24 NFT Hot News 01/ NFT 系列 NodeMonkes 地板价已超越 BAYC 3 月 18 日&#xff0c;据数据显示&#xff0c;NFT 系列 NodeMonkes 地板价已超越 Bored Ape Yacht …

9.2024

使用冒泡排序给{10 ,1,35,61,89,36,55}排序 代码&#xff1a; public class 第九题 {public static void main(String[] args) {int a[]{10,1,35,61,89,36,55};for (int i0;i<a.length-1;i){for (int j0;j<a.length-1;j){if (a[j]>a[j1]){int temp0;tempa[j];a[j]a[…

光致发光荧光量子产率测试光纤光谱仪

光致发光荧光量子产率测试系统是一种用于测量材料发光效率的高精度设备&#xff0c;它通过光致发光方法来确定样品的发射效率。光致发光荧光量子产率测试系统不仅提供了一种高效、可靠的测量手段&#xff0c;而且对于提升科学研究和工业应用中的发光材料性能具有重要作用。通过…

nacos集群搭建实战

集群结构图 初始化数据库 Nacos默认数据存储在内嵌数据库Derby中&#xff0c;不属于生产可用的数据库。官方推荐的使用mysql数据库&#xff0c;推荐使用数据库集群或者高可用数据库。 首先新建一个数据库&#xff0c;命名为nacos&#xff0c;而后导入下面的SQL&#xff08;直…

日常刷题之77-组合

题目 给定两个整数 n 和 k&#xff0c;返回范围 [1, n] 中所有可能的 k 个数的组合。 你可以按 任何顺序 返回答案 提示&#xff1a;假设 n5,k3 就是需要组合出来&#xff0c;长度3且内容数据是在[1,n]这个区间内的所有可能得组合 同时一个组合里面内个数字只能出现一次&#…

日增500粉的全自动引流神器

全自动引流&#xff0c;采集曝光一体每天截流500&#xff0c;这是每个企业主和网红的终极梦想。今日&#xff0c;我将分享一套高效而可行的引流策略&#xff0c;帮助你实现这个目标。 我们需要理解&#xff0c;全自动引流并不意味着无人参与。相反&#xff0c;它仍需要你的参与…

【王道训练营】第6题 输入一个整型数,判断是否是对称数,如果是,输出yes,否则输出no

文章目录 我的代码改正代码其他代码 我的代码 没有完成 #include<stdio.h> int main(){int a;int b;int c0;//位数int d0;//比较几次scanf("%d",&a);while(b!0){bb/10;c;}dc/2;//比较几次int ffor(int i0 ;i<d;i){int ec;//位数fa - a / (((e-i-1)*10…

算法笔记~—位运算

目录 常见位运算&#xff1a; 1、基础位运算 2、对于一个数n。确定、修改这个数n二进制x位。 3、提取&#xff08;确定&#xff09;一个数n最右侧的1&#xff08;bit&#xff09;与干掉最右侧的1&#xff08;bit&#xff09; 4、异或运算律 5、位运算的优先级&#xff1a…

Go --- Go语言垃圾处理

概念 垃圾回收&#xff08;GC-Garbage Collection&#xff09;暂停程序业务逻辑SWT&#xff08;stop the world&#xff09;程序根节点&#xff1a;程序中被直接或间接引用的对象集合&#xff0c;能通过他们找出所有可以被访问到的对象&#xff0c;所以Go程序的根节点通常包括…

虚拟机Linux-openEuler硬盘空间扩容

虚拟机Linux-openEuler硬盘空间扩容 1、需求场景 我们在使用虚拟机时&#xff0c;可能会出现磁盘空间不够用导致各种bug出现的情况。 首先&#xff0c;我们要扩展虚拟机的可用磁盘空间。如图所示&#xff0c;我的原本硬盘大小为8G&#xff0c;我们扩展到30GB 2、打开虚拟机…

【git分支管理策略】如何高效的管理好代码版本

目录 1.分支管理策略 2.我用的分支管理策略 3.一些常见问题 1.分支管理策略 分支管理策略就是一些经过实践后总结出来的可靠的分支管理的办法&#xff0c;让分支之间能科学合理、高效的进行协作&#xff0c;帮助我们在整个开发流程中合理的管理好代码版本。 目前有两套Git…

【GameFramework框架内置模块】16、配置(Setting)

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址QQ群&#xff1a;398291828 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 【GameFramework框架】系列教程目录&#xff1a;…

2024年软件测试,“我“从初级到高级进阶,不再走弯路...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 现在2024年&#…

54、Qt/对话框、事件机制相关学习20240325

一、完善对话框&#xff0c;点击登录按钮&#xff0c;如果账号和密码匹配&#xff0c;则弹出信息对话框&#xff0c;给出提示”登录成功“&#xff0c;提供一个Ok按钮&#xff0c;用户点击Ok后&#xff0c;关闭登录界面&#xff0c;跳转到其他界面 如果账号和密码不匹配&#…

python(django)之单一接口管理功能后台开发

1、创建数据模型 在apitest/models.py下加入以下代码 class Apis(models.Model):Product models.ForeignKey(product.Product, on_deletemodels.CASCADE, nullTrue)# 关联产品IDapiname models.CharField(接口名称, max_length100)apiurl models.CharField(接口地址, max_…