Linux基础I/O-打开新世界的大门:文件描述符的“分身术”与高级重定向

🔥海棠蚀omo:个人主页

❄️个人专栏:《初识数据结构》《C++:从入门到实践》《Linux:从零基础到实践》

✨追光的人,终会光芒万丈

博主简介:

目录

一.struct_file的初识

二.文件标识符的分配规则

三.理解重定向

3.1dup2函数

3.2关于重定向的思考

3.1.1.如果我们创建子进程,子进程是如何看待父进程打开的文件的?

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

今天我们主要的内容是关于文件标识符的补充知识以及介绍重定向的相关知识,通过这篇的内容我们就能够跟深入的理解进程和文件之间的关系,以及理解我们之前可能不理解的问题。

在讲解后面的内容之前,我们还是先来了解一点补充知识,是关于我们上一篇讲的struct_file,上一篇我们只是提了一嘴,并没有讲解相关知识,下面我们先来了解一部分。

一.struct_file的初识

根据我们上一篇的内容画出了上面这张图,其中左半边的内容上一篇我们已经见过,今天我问大家一个问题:我们现在要向被打开的文件中写内容,具体是往那儿写呢?或者说我们是如何让处于硬盘中的文件的内容发生改变的?

可能有人会说:不就是通过write函数等方式向文件中写入吗?

但是文件是处于硬盘中的,而根据冯诺依曼结构,cpu只能与内存交互,那么write函数是如何将内容写到硬盘中的呢?

所以说在内存中一定存在空间来保存被打开文件的内容和属性,就和进程一样,在内存中有专门的空间来保存进程的代码和数据,进而通过某种方式来将内容更新到处于硬盘的文件中,那这个空间是什么呢?

和进程一样,在struct_file中也有相应的指针只想处于内存中的两段空间,分别存储着文件的属性和内容,而存储文件内容的那段空间叫做文件内核缓冲区

下面我们讨论write函数等是如何做的:

既然write无法直接与硬件进行交互,只有操作系统才能做到,只能与内存进行交互,那么write函数也就会向内存中保存着文件内容的文件内核缓冲区中**" 写入 "** 内容,而我们要写入的内容是用户定义的缓冲区内部保存的数据,也就是"hello world",但是是真的向文件内核缓冲区中写入内容吗?

答案不是的,这里write函数真正做的是将用户定义的缓冲区内部保存的数据拷贝 到文件内核缓冲区中,注意是拷贝,不是写入!!!

也就是write函数的本质就是一个拷贝函数,把数据从用户空间拷贝到对应文件的内核缓冲区中。

那既然write函数是这样,那么我们如果要读文件(read)或者修改文件的内容呢?

答案和write是一样的,write是与文件的内核缓冲区交互,那么上面的操作当然也是这样,读文件也是只能从文件的内核缓冲区中去读取。

总结起来就是我们对文件内容的增,删,查,改操作,前提都必须把文件的内容提前预加载到该文件的内核缓冲区中,进而才能完成前面的操作

那么如何将文件内核缓冲区中的内容更新到硬盘的文件中呢?

这个工作就是由操作系统来完成的,毕竟操作系统是电脑软硬件资源的管理者,但是具体什么时候完成这个更新工作,由操作系统自己决定。

当然这个时间很短哈,毕竟我们在实际操作时,启动进程向文件写入内容后,我们再打开硬盘中的文件可以看到内容已经发生了变化。

二.文件标识符的分配规则

我们在讲文件标识符时大家有没有想过这样一个问题:为什么stdin,stdout和stderr这三个文件占据了数组的0,1,2这三个位置,为什么不是其他位置呢?

这个问题的答案就与文件标识符的分配规则有关,那么这个分配规则具体是什么内容呢?

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

所以说上面的三个文件占据了最小的0,1,2三个位置,并且我们也知道了为什么连着打开的文件,它们的fd从3开始并且是连续的了,就是根据文件标识符的分配规则来的。

那么我们来思考一个问题:那要是把前面三个文件给关了,是不是新打开文件的fd就可能是0,1,2?

与其用语言说,我们不如实操一番看看结果:

看到输出结果我们可能很疑惑,我要输出的内容呢?怎么没输出啊?

我们先按照上面的思路先分析一下:既然stdout已经被关了,那么我们新打开的文件的fd就应该是1,也就是数组的1号下标中保存保存的指针指向的就是log.txt。

但是printf并不知道啊,它不知道底层发生了什么,还是像往常一样向下标为1指向的文件中写入内容,也就是说本来应该写入到显示器文件中的内容写到了log.txt文件当中,事实是否是这样呢?我们打印log.txt文件的内容就知道了:

答案依旧出乎意料,怎么我们打印log.txt中的内容也什么都没有输出呢?不要急,我们来稍微修改一下:

当我们把close语句给注释掉时,就会发现可以打印出log.txt文件中的内容了,并且还有另一种写法:

通过这种方式也能够打印出log.txt中的内容,不过我们的重点不在这上面,我们的重点是log.txt中的内容。

和我们上面想的一样,当关闭了stdout后,下标为1的位置就空出来了,新打开的log.txt的fd就变为1了,符合我们上面说的文件标识符的分配规则。

当然大家也同样疑惑为什么不用上面的操作,就无法打印出log.txt中的内容,上面的操作涉及到后面的内容,我们先记着这个现象,等到讲到相关知识的时候,我会来填这个坑。

同样的,上面我们是关闭了stdout,我们再来关闭stdin来试试:

这次我就关闭了stdin,那么此时0号下标指向的文件就应该是log.txt,而scanf本来应该从stdin中读取数据就变为从log.txt中读取数据,并通过格式化输入将数据转化为三个int类型的数据。

答案也正如我们所料,我们在log.txt中写入了三个值,scanf成功读取了文件中的内容,我们通过printf打印出的三个数的值确实是10,20,30。

并且通过这种方式,scanf不在阻塞了,因为它不在从键盘中读取数据了,而是从log.txt文件中读取数据,也就是一瞬间的事。

而上面的这种操作分别叫做输出重定向输入重定向!!!

对于输出重定向想必大家都不陌生,在上一篇我们在复习C语言文件接口时就提过,不过当时的操作并不是如现在这样,而是通过" > "来实现的,那么这两者有什么联系呢?

不卖关子," > "的底层实现就是类似于上面的操作,而上面的操作就是我们下面要讲的重定向!!!

三.理解重定向

我们上面简单实现了重定向的思路,但是呢,上面的操作比较粗糙,有没有更加优雅的做法呢?

答案是有的,要完成上面的重定向操作,我们需要借助一个系统调用函数。

3.1dup2函数

这个函数叫做dup2,我们不需要关注上面的dup,我们主要用的是dup2这个函数。

这个函数的参数比较易懂,就是传两个fd,就是传两个文件的文件标识符,但dup2函数的具体操作并不和我们上面一样将对应文件给关闭了,它的操作是将oldfd中的内容覆盖掉newfd中的内容:

大概过程就如上图所示,当3号下标的内容覆盖了1号下标中的内容,那么1号下标中的指针就不再指向标准输出stdout了,而是指向了3号下标所指向的文件。

也就是说,通过dup2函数的操作后,1号下标和3号下标中的指针都指向了myfile这个新打开的文件,之后凡事往1号下标写的内容,都写到了myfile中,不再写到stdout中了,这也就完成了重定向的操作。

那么这个时候有人就会疑惑:那传参的时候是传dup2(3,1)呢还是传dup2(1,3)呢?

要解决这个问题我们就要弄清楚参数中的oldfd和newfd之间的区别,我们来看:

这是对dup2函数的介绍,我来总结oldfd和newfd之间的区别:oldfd指的是"旧的文件描述符",也就是你希望复制的那个已经打开的文件描述符,而newfd指的是"新的文件描述符",也就是你希望newfd复制到哪个描述符数值上

也就是上面的3指的就是oldfd,1指的就是newfd,所以我们在传参时应该是dup2(3,1)。

简单介绍完dup2,我们来实操一下看看:

可以看到,通过dup2函数我们确实完成了输出重定向的操作,那么既然现在1号下标和3号下标都指向了log.txt这个文件,那我们现在既向1号下标中写,又向3号下标中写,看是否都能写到log.txt这个文件中:

通过print函数向1号下标中写入内容,通过write函数向3号下标写入内容,结果也正如我们所料,他们确实都向log.txt文件写入了相应的内容。

不过这里我们同样也看到一种现象:明明打印fd的语句在write之前,为什么log.txt中却是write所写的内容在前呢?

这个现象呢我们同样到后面会讲,这里我们先记着这个现象即可。

3.2关于重定向的思考

当我们有了对重定向的概念和本质理解,我们下面来思考两个问题:

3.1.1.如果我们创建子进程,子进程是如何看待父进程打开的文件的?

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

我们先来解决第一个问题:

我们知道父进程来创建子进程,子进程会拷贝父进程的PCB,而指向files_struct的指针也在父进程的PCB中,也就是子进程同时也会拷贝文件描述符表,父进程的文件描述表中的数据同时也就拷贝过来了。

那么我问大家一个问题:拷贝了文件描述符表中的指针,那么会将父进程打开的文件也拷贝一份吗?

答案肯定是不会的啊,文件是父进程打开的,跟子进程有什么关系?

所以这里的拷贝只是简单的浅拷贝,而经过拷贝后,子进程的文件描述符表中的指针也会指向父进程所打开的文件:

讲到这里我们就能回答一个问题了:我们常说,当一个进程启动的时候会默认打开三个文件,也就是stdin,stdout和stderr,那么为什么会默认打开呢?

我们之前讲过,在linux中创建的进程的父进程都是bash进程,而bash进程如果打开了这三个文件,那么我们创建的进程不就默认打开了这三个文件吗?

我们创建进程的时候这三个文件已经被bash进程给打开了,所以我们创建的进程的文件描述符表中的指针就指向0,1,2这三个已经被打开的文件,也就是默认打开了。

那此时还有一个问题:如果子进程close关闭了父进程的某个文件,那么父进程还能够访问这个文件吗?

答案是可以的,针对这种问题,在每个文件的struct file中有一个变量叫做int ref_count ,我们通过这个变量来实现引用计数的操作!!!

当只有我们所创建的一个进程时,这个值就为1,当我们创建一个子进程时,这个值就会变为2,如果子进程关闭了这个文件,那么这个值就会变为1,只有当这个值变为0的时候,也就是父进程也关闭这个文件,这个文件才算彻底关闭。

虽然子进程执行了close的操作,实际上并不会关闭父进程打开的文件,而是将子进程的文件描述符表的相应位置中的指针置为NULL,这样子进程就无法访问这个文件,但是父进程依旧可以访问这个文件。

说了这么多,我们来用一个例子证明一下:

答案也正如我们所料,虽然子进程通过close关闭了stdout,但父进程依旧可以访问stdout文件。

接下来我们来解决第二个问题:

这句话就是问exec函数的操作,是否会影响重定向,这里我们也直接通过一个例子来证明:

我们通过执行execl函数来实现程序替换,但是在程序替换前,已经完成了重定向的工作,所以当执行ls的命令时,ls原本向stdout中写入的内容转而写到了log.txt中,结果也跟我们说的一样,确实将内容写到了log.txt文件中。

我们将图画出来也可以看到,这二者是互不影响的。

以上就是打开新世界的大门:文件描述符的"分身术"与高级重定向的全部内容。

相关推荐
HLC++37 分钟前
Linux的进程间通信
android·linux·服务器
groundhappy4 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python
通信小小昕5 小时前
Ubuntu 26.04 中文输入法安装
linux·运维·ubuntu
张小姐的猫6 小时前
【Linux】网络编程 —— HTTP协议(上)
linux·运维·服务器·网络·http·单例模式·策略模式
栩栩云生7 小时前
AI 写代码犯的错,早被写进了错题集
linux·安全·ai编程
imc.118 小时前
linux基础IO
linux·运维·服务器
BelongPanda10 小时前
Linux Nginx 纯手动 Let‘s Encrypt 泛域名证书配置教程
linux·nginx
酷可达拉斯10 小时前
Linux操作系统-shell编程(0)
linux·运维·服务器·python·云计算
2301_7779983410 小时前
Linux中断机制:操作系统如何高效运行
linux·运维·服务器
yunwei3710 小时前
eBPF 教程:检查 exec 后真正安装的可执行镜像
linux·开源