1. Linux下一切皆文件

⾸先,在windows中是文件的东西,它们在linux中也是文件;其次⼀些在windows中不是文件的东西,比如磁盘、显示器、键盘这样硬件设备也被抽象成了文件,你可以使用访问文件的方法访问它们获得信息;甚至将来我们要学习网络编程中的socket(套接字)这样的东西,使用的接口跟文件接口也是⼀致的。

这样做最明显的好处是:

开发者仅需要使用⼀套 API 和开发工具,即可调取 Linux 系统中绝大部分的资源。
举个简单的例子,Linux 中几乎所有读(读文件,读系统状态,读PIPE)的操作都可以用read 函数来进行;几乎所有更改(更改文件,更改系统参数,写 PIPE)的操作都可以⽤ write 函数来进行。

那我们改如何理解呢?

2. 深入理解

在之前的学习中,我们其实也经常提到Linux下一切皆文件:

包括前几篇文章我们讲基础IO,文件描述符、重定向这些东西。我们提到一个进程启动会默认打开标准输入、标准输出、标准错误这三个流。
我们也知道它们底层对应的是键盘、显示器,这些东西是文件吗?
现在我们知道这三个东西其实就是三个文件指针嘛(FILE*)
在这里插入图片描述
FILE内部封装了文件描述符(Linux平台上),它们三个分别占用0,1,2。我们使用诸如read、write这些文件IO的接口也可以访问它们。
所以,从这里也能看出,Linux下它们也被抽象成了文件。
这也在一定程度上证实了Linux下一切文件这一说法。

但要真正理解这一结论其实不是很好搞,会比较抽象一点。那我们这里要怎么讲呢?

切入点——谈谈设备

我们可以从这样一个点进行切入:

我们之前的文章中有给大家介绍过这样一张图——计算机体系结构
在这里插入图片描述
操作系统在中间起到一个承上启下的作用,对下管理软硬件资源,对上给用户提供良好的服务,虽然操作系统给我们提供服务,但是他不相信任何人。
为了保证自己的安全,在开发角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。
所以我们前面学习的各种诸如进程、文件相关的各种操作,都是通过各种系统调用来完成的(即使用语言级别的函数底层也必定封装了系统调用)。
这都是我们之前讲过的内容。
🆗,那么问题来了。
既然操作系统要对各种软硬件资源进行管理,那我们的计算机中有各种各样的硬件资源,比如:磁盘、网卡、键盘、显示器…
那操作系统要不要将他们管理起来呢?
当然!如何管理?
先描述、再组织!

我们可以来模拟一下这个过程:

比如
在这里插入图片描述
我们使用一个struct device结构体来描述各种硬件,比如:
在这里插入图片描述
我们列举这样几个,当然在内核中就是一个个结构体来描述它们。
再组织:
然后我们就可以使用一个双链表把它们都链接起来
在这里插入图片描述
那未来我们的进程可能需要和各种设备进行各种各样的交互,比如我们以读写(IO)为例,那各种不同的设备它们的读写方法也大概率甚至说一定是不同的
在这里插入图片描述
比如键盘,读方法可以读取从键盘输入的数据,但是键盘好像并不需要写方法啊。
所以键盘的写方法就实现成一个空就行了,函数体里面啥也不用写。
但是如果是磁盘的话,就有读也有写。
所以这些计算机外设,有的设备只读,有的设备只写,有的又读又写。
不同设备的各种操作的访问方式一定是不同的,要不然为什么不同的设备有不同的驱动呢?

下一个问题:我们访问这些设备的时候,本质是谁在访问?

其实还是我们的进程(代表了用户)在访问,进程执行了某些特定的代码,底层可能通过某些系统调用,然后访问到某种设备完成相关的操作。
那么内核中描述一个进程——task_struct
在这里插入图片描述
Linux下一切皆文件,所以进程访问这些设备也要通过访问文件的方式进行。
另外我们知道一个进程启动默认打开标准输入、标准输出、标准错误这三个文件,它们就对应键盘、显示器、显示器这三个设备。
🆗,那我们就以键盘、显示器设备为例来讲解
在这里插入图片描述
那打开的这些文件,它们底层可能就对应的是一些设备。
因为不同设备的各种操作方法是不同的(以读写为例),那操作系统为了以一种统一的方式进行管理和访问,会在file结构体中提供类似这样的函数指针
在这里插入图片描述
尽管底层对应的设备可能是不同的,但是站在struct file这一层,无所谓,我们看到的接口都是统一的,一模一样的。
这样我们打开了一个文件之后,它底层对应的是什么设备,就把struct file中的函数指针指向其特定的操作方法
在这里插入图片描述
那这样的话,站在进程的视角,不管它要读写哪些设备,都无需关心底层这些设备的各种方法有多大的不同,只需调用struct file中的对应方法即可。
所以,这就是一切皆文件,是站在进程的视角,看待它可以访问的“一切”——都是文件!
所以,按照上面说的,在struct file中,存在了一组方法集,当然这组方法集不是具体的函数实现,而是一组函数指针!
当对应的文件被打开时,这些函数指针就会指向具体设备的操作方法。

看看内核源码

那如何证明?

打开内核源码,我们能够找到:
首先:
在这里插入图片描述
这是之前讲过的,然后在struct file的定义中,我们能找到这样一个字段
在这里插入图片描述
一个结构体指针——const struct file_operations *f_op;
那我们转到这个结构体的定义
在这里插入图片描述
这里面放的不就是各种文件操作的函数指针嘛!
这里的函数指针指向的是“针对特定文件类型/设备类型”所实现的读写例程。 对于磁盘文件,它指向文件系统模块(负责缓存和日志);对于键盘/串口,它指向字符设备驱动(负责直接操作硬件寄存器)。但它们都通过 struct file 作为桥梁,完美地实现了“一切皆文件”的统一接口。
在这里插入图片描述

思考:

那讲完上面的逻辑链,大家有没有感觉到些许熟悉?
同样的函数指针,在最终调用的时候,可以调到不同的特定的方法。
这种感觉好像有点像我们之前C++里面学习的多态(当然面向对象的编程语言都有多态)啊!
多态表现出的结果是怎样的:继承关系的不同类对象,去调用同一个函数,产生不同的行为(其实是底层调到了不同的函数)。
C++多态的原理是怎样的,简单回顾一下(之前的C++文章中有讲):
其实就是通过对象里的虚函数指针,去找到其对应的虚函数表,那子类对象的虚指针就指向子类的虚函数表(虚表其实是一个函数指针数组),父类对象的虚指针就指向父类的虚函数表,那这样它们就能调到不同的虚函数,进而实现多态(不同对象去完成同一行为时,展现出不同的形态)。
所以,可以认为,上面讲的逻辑,就是C语言实现的多态(因为Linux主要是由C语言写的)

VFS

最后:

这一部分在这里插入图片描述
我们可以把它称为虚拟文件系统
没有这套机制,操作系统就无法做到用 open 打开一切,用 read/write 操作一切。
在这里插入图片描述
当进程调用 read(fd, …) 系统调用时,VFS 根本不关心 fd 背后是什么(真实的一个文件还是对应一个硬件),对应的操作方法由多么不同,它只需要通过 fd 找到 struct file,然后直接调用 file->f_op->read(…) 即可。底层的差异被 file_operations 完美屏蔽了
一切都可以看成是“文件”!
虚拟文件系统(VFS)的本质就是在底层硬件驱动和上层用户进程之间,搭建一层抽象的“中间层”。

回想我们之前提到过的一句话
在这里插入图片描述

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐