Linux 文件 IO 管理(第三讲:文件系统)

news/2024/9/29 0:57:19/

Linux 文件 IO 管理(第三讲:文件系统)

  • 进程为什么默认要打开文件描述符为 0,1 和 2 的文件呢?
  • 文件系统
    • 物理磁盘
      • 简单认识
      • 存储结构
      • 对磁盘存储进行逻辑抽象
      • 分组 —— 文件系统
        • Block Bitmap
        • inode Table
        • inode Bitmap
        • GDT(Group Descriptor Table)
        • Super Block
        • 格式化
      • 文件系统细节
        • inode number
        • datablocks[N] 数组
        • 编号唾手可得?
        • 理解文件的增删查改
        • 逆向路径解析和如何找到文件自己所在分区

进程为什么默认要打开文件描述符为 0,1 和 2 的文件呢?

我们写的程序,本质上都是 对数据进行处理(计算,存储等等),既如此就肯定有三个问题:

  • 数据从哪里来
  • 数据去哪里
  • 用户要不要看到这个过程

程序变成进程之后数据并不全是硬编码而成的,也有比如 scanfcin 的数据等等,所以是为了更好地让进程获取数据,动态地让用户看到进程的结果,毕竟这些现象都是和人来往的

归结到底,还是人有这个需求罢了,是历史的原因

文件描述符2标准错误文件 为什么也要被打开呢?

其实 标准错误文件 对应的文件也是 显示器文件,咱可以来验证一下:

int main()
{fprintf(stdout, "fprintf hello stdout\n");fprintf(stderr, "fprintf hello stderr\n");return 1;
}

咱们分别往 标准输出和错误 打印数据,但结果就是两条打印结果都在显示器上

所以在刚开始,文件描述符为 12 的两个下标其实指向同一个文件罢了;如果你 重定向,那也只是重定向 文件描述符1 的下标,所以 标准错误 仍然会往显示器打印,如下:

[exercise@localhost redirection]$ ./Test > log.txt
fprintf hello stderr
[exercise@localhost redirection]$ cat log.txt
fprintf hello stdout
[exercise@localhost redirection]$ 

那为什么还要有 2 呢?
程序运行输出的消息无非就是 正确错误 两类

而正常 Debug 的时候,会将 正确 的调试信息往 1 里打印,错误 的调试信息往 2 里打印,未来我们只需要做一次 重定向 就可以 将正确和错误的调试信息分开

./Test 1>ok.log 2>err.log

正确调试信息 都在 ok.log 文件里,错误调试信息 都在 err.log 文件里

要是想把两种信息写在别的文件里,可以这样:

./Test 1>all.log 2>&1

如此就都在 all.log 文件里了

文件系统

之前谈到的都是被打开的文件,但是磁盘上的大量文件里,被打开的只是少量文件,还有大量没有被打开的啊!

没有被打开的文件是在 磁盘 内存放,所以这种文件也被叫做 磁盘文件

可是你要打开某个文件都是要先找到这个文件,也就是在大容量磁盘里寻找此文件,所以必须要有 文件路径 + 文件名 才能在偌大的磁盘空间里找到此文件

没有被打开的文件 无非就是要放在磁盘中存放,还是那句话,存放的意义就是有朝一日可以更好的取走,所以在本质上就是在 研究文件如何存取的问题

物理磁盘

简单认识

计算机只认识二进制是公认的,而 0 ,1 是被规定出来的,其表示形式可能大不相同,可能使用高低电平表示,也有可能使用磁极表示,所以在物理上会有不同的表现

磁盘拆开就发现里面会有圆形反光的结构,叫做 盘片盘片可读可写可擦除 ,一片盘片两面都可以存数据

接续拆,会发现不止一个盘片,而是一摞盘片,是由很多盘片组合而成的结构,盘片越多,容量越大

而每一个盘面都会有一个磁头(一面一个磁头),磁头通常是用于在特定的盘面当中来回寻址

磁盘的本质是一个机械设备 ,一般磁盘在加电工作的时候,盘片会在类似马达的带动下高速旋转,而磁头会进行左右摆动,由于速度极快,所以磁头和盘片不能紧挨着,不然会造成两个硬件不可逆的损伤,所以 磁头其实是悬浮在盘面上的

存储结构

怎么存就怎么取,这是很正常的想法,所以必须要了解磁盘的内部结构

我们知道磁盘内部有一摞 盘片,每一个 盘片 的正反两面都可以写数据,而每一面都会配备一个 磁头,所以,要想精准找到想要的数据,就要确定数据存放在哪个 磁头

每一个 盘面 又被划分为一圈圈同心圆,这一圈圈就是 磁道,如果从一摞盘片的角度看,相同半径的磁道会构成 柱面,而一旦找到了数据在哪个 磁头 下,就可以确定数据在那一圈 磁道(柱面)

盘面 上一圈圈 磁道 并非磁盘的 最小读写单位,因为人们又将盘面均等的过圆心分开,那么一圈圈磁道就被分为好多 扇区,这 扇区 才是磁盘最小的 读写单位,也就是说不论读取修改与否,都是要将一整个 扇区 的内容送进内存

那么现在要想确定数据的位置,只需要知道 磁头(Header),柱面(Cylinder),扇区(Sector) 的编号即可(CHS定址法

对磁盘存储进行逻辑抽象

其实无论是内存还是外存,我们对其抽象均为 线性结构 ,磁盘内盘面虽然是一圈圈磁道,但拉直依然是直线结构,所以 磁盘整体的抽象结果就是线性的

想象你现在把一圈圈磁道拉直了,变成了直线,那现在唯一可以度量这条直线的就只剩下扇区了,而每一个扇区(sector)的大小均固定,那是不是就可以抽象成为 数组 啊,基本单位就是 扇区 sector disk_array[N] ,而数组会有自己的 下标,那么 在无形之中就相当于为每一个扇区完成编址

那怎么 将这个数组里的扇区下标转换为CHS 地址 呢?其实每一个盘片里的空间都是一样大的,扇区大小数量也一样,非常均等,所以可以通过计算找出 CHS 参数:

假设一块磁盘里,每一个盘面共有 N 个扇区,M 个磁道,那么每个磁道里就有 N / M 个扇区,而 index 是任意扇区在 sector disk_array[N] 里的索引下标

首先要明白,上面的数组是抽象出来的,而真正磁盘的每一个盘面都是从 0 开始标号的,并不是接着上一个盘面编号,不然还抽象什么呀

// num 表示一个磁道有几个扇区
num = N / M;
// 计算位于哪一个盘面,即磁头编号
Header = index / N;
// 计算出所在盘面后,使用 temp 存储 index 编号处于该盘面的下标
temp = index % 1000;
// 利用 temp 直接计算出磁道编号
Cylinder = temp / num;
// 同样 temp 取模得到扇区编号
Sector = temp % num;

这时 index 地址完美映射成为 CHS 地址

上面的工作其实是 磁盘内部直接完成的(比较简单),所以 OS 使用的一直都是抽象出来的虚拟磁盘地址

所以目前为止,文件 = 数组内很多个 sector 的下标内容构成 ,只需要记录下该文件所占的扇区下标,将其送往磁盘,再经过映射,即可完美定位磁盘文件位置

上面的问题是解决了,可是磁盘的一个扇区大小为 512 字节(现在可能是 4KB),OS 觉得太小;如果系统只是需要小小的 4KB 的数据,那 IO 端口就得完成来回 8 次拷贝,效率问题凸显!!!

一般而言,虽然磁盘被访问的 基本单位是 0.5 KB OS 未来和磁盘交互的时候,基本单位是 4 KB(后续博文会说明为什么是 4KB),也就是一次性要拿 8 个 sector ,如此提高 IO 效率问题

那么 OS 就不以 0.5 KB 进行访问,而是 4 KB,这 4 KB 是连续的 8 个扇区 sector ,被称之为

既然 OS 不愿意以扇区为基本单位进行抽象,那就 用块来抽象,那么此时 8 个扇区组成一个基本单位为块,使用下标为连续的块进行编址,那么目前为止 文件 = 数组内很多个 块 的下标内容构成

那现在还怎么使用下标来定位磁盘位置啊?很简单啊,现在的一个块是 8 个扇区,那么 下标值乘以 8 就是这个块开头扇区的原来编号,至此于 OS 而言,未来读取数据可以以块为单位

很显然,块的大小既然是固定的,那么现在只需要知道磁盘的总容量,就可以确定抽象后的每一块磁盘地址,有多少块,每个块的块号,如何转移到对应的多个 CHS 地址之类的全都知道

而块的编号叫做 LBA (Logical Block Address)逻辑块地址 ,也就是 LBA block[N] ,以一个数字来描述磁盘空间的地址,以数组的形式组织起来,妥妥的 先描述,再组织,此后 对磁盘的管理就转变为对数组的管理

如果磁盘空间太大不好管理,那么分区就浮出水面,只要把其中的小分区管理好,其他分区使用一样的方法就能实现管理了啊,分区如何实现呢?只需要记住所有分区的开始和结束的 LBA ,如此分区完成

那么现在 文件就是由很多个 LBA 块组成

分组 —— 文件系统

很显然,分完区还是很大,都是以 100GB 为单位的,所以还需要进行分组,分完组一个组的大小 可能 为 10GB ,相同的问题,只要管理好这 10GB 的小组就能管理好分区,进而就能管理好整个磁盘

而上面的一整套思想被称为 分治思想

我们之前就就说 文件 = 内容 + 属性 ,所以文件在磁盘存储,本质上是存储文件的内容数据 + 文件的属性数据,而 Linux 文件系统特定:文件内容和属性分开存储 ,要想理解这些,就得先理解分组后的一个小组,被称为 磁盘级文件系统 的东西:

在这里插入图片描述

一个 Block group 就是一个分组,也就是 磁盘文件系统Linux ext2文件系统):

  • Block Groupext2 文件系统会根据分区的大小将其划分为数个 Block Group ;每个 Block Group 都有着相同的结构组成
  • 超级块(Super Block):存放文件系统本身的结构信息Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了;记录的信息主要有:
    • blockinode 的总量
    • 未使用的 blockinode 的数量
    • 一个 blockinode 的大小
    • 最近一次挂载的时间
    • 最近一次写入数据的时间
    • 最近一次检验磁盘的时间
    • 等其他文件系统的相关信息
  • GDTGroup Descriptor Table):块组描述符,描述块组属性信息
  • 块位图(Block Bitmap):Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用
  • inode 位图(inode Bitmap):每个 bit 表示一个 inode 是否空闲可用
  • i 节点表:存放文件属性 ,如:文件大小,所有者,最近修改时间等
  • 数据区 Data Block :存放文件内容,是整个分组系统里占据空间最大的区域(九成以上) ,里面都是基本单位大小为 4 KB 的数据块,只存储文件的内容 ,每个块都有其块号

注意 文件加载到内存就是以块为单位分批加载,没有即使文件的最后一个块没有占完,而这一整个块也都是此文件的,只是内容不是此文件的内容而已

Block Bitmap

这是 块位图理想情况下Data Block 里有多少个块 Block Bitmap 就会申请多少个比特位,也就是说 Data Block 里的每一个数据块都对应 Block Bitmap 里的一个 bit 位,如此在 Data Block 里,数据块的占用状态就能被表示出来

注意 bit 位的位置也和数据块一一对应,不能有差错,未来想要为新文件分配空间,可以直接扫描 Block Bitmap 位图,查出 bit 位为 0 的数据块分配即可

inode Table

这玩意就是所谓的 i 节点表,这里面宏观上,其实也全都是数据块,但里面保存的是 所有文件的所有属性

Linux 中文件的属性是大小固定的集合体,就是将所有可以准确描述文件的属性集合在一起成为一个结构体,那也就是说一个文件的内容可以不一样大,但 它们的属性结构体一定是一样大的,这是每个文件都要有的,只是不同的文件属性值不同罢了 ,此乃 先描述

那么在内核里就一定存在 struct inode 结构体,可以描述任意一个文件,里面包含文件的所有属性 ,在里面存在一个非常重要的字段 int inode_number;inode 编号,因为在 struct inode 结构体内部是没有文件名属性的,但是在内核层面,每一个文件都要有 inode number ,我们通过 inode 号去标识一个文件

在 Linux 里可以通过 ls -li 指令查看磁盘文件的 inode number

现在我也知道了一个文件的 inode 号,但是我要怎么找到文件内容的位置呢?在结构体里还会存在属性 int datablocks[N] 用于标识该文件占据的所有块号

在 Linux 系统里, struct inode 结构体 大小一般固定为 128 Byte,而现在一个块的大小为 4KB ,那么一个块就能存储 32 个 inode,每一个文件都有一个 inode

那我怎么知道 inode 的使用情况呢?

inode Bitmap

当然啦,和 Block Bitmap 相同的作用:

比特位的位置 表示第几个 inodeinode number);比特位的内容,表示该 inode 是否被占用

有几个 inodeinode Bitmap 里就有几个比特位

GDT(Group Descriptor Table)

块组描述符,顾名思义:描述块组信息

该结构体主要描述当前分组的基本情况(相当具体),如:块大小,共有多少个 inode ,共有多少个 Data Blocks ,有多少个块没有被使用,有多少个 inode 被使用了等等,都会被记录在此结构体中

说白了就是个管理字段,用来管理整个块组的使用情况

Super Block

超级块 :存放文件系统本身的结构信息,这是存放一个分区的基本信息,上面的简介里也提到存放的相关内容

既然是一整个分区的基本内容,那为啥放在 0 号分组里?不应该单独于所有分组进行存放吗?

并不是每一个分组都有 Super Block,一般会根据实际的文件系统,可能存在于 2 ~ 3 个分组里,但是即便有好几个分组里都有,但大家的 Super Block 内容都是一样的

为什么要这么干呢?纯纯浪费空间啊?都是一样的内容有什么好存的呢?

既然是存放一整个分区的使用情况,说明极为重要啊!磁盘是个机械设备,靠磁头和盘片的物理旋转来定位物理空间,而 Super Block 也并不大,可能就是其中的几个扇区,如果因为一些特殊原因把 Super Block 内容刮花,导致数据失真,那后果就是这个分区都会挂掉,这个影响是巨大的!!!

所以虽然没必让整个分区的所有组都有 Super Block ,但咱还是需要选中几个幸运分组来多保存几份 Super Block ,这是出于安全考量,让文件系统更具有健壮性

格式化

现在我们知道了,一块磁盘仅仅分区是不够的,还需要分组;分完组也是不够,还需要在分组后的所有分组里写入上述 Block Group 结构内容来管理数据

那么日后我们在用磁盘的时候,是基于这么一套文件系统之上来新建删除修改等等

而在每一个分区内进行分组,然后写入文件系统的管理数据,这个叫做 格式化!!!

所以啊,格式化的本质: 在磁盘中写入文件系统

文件系统细节

inode number

寻找任何文件只能通过 inode 编号,所以这个必须知道

inode 编号是以分区为单位整体分配的,而不是分组;一个分区内部的任意文件的 inode 编号都不能重复,但两个分区可能会出现重复,所以 inode 不能跨分区访问!!!

inode 被分配的时候,是按照区域进行划分的:在 Super Block 里会记录当前整个分区的 inode 编号范围;在 GDT 里会记录当前整个分组的 inode 编号范围

当拿到一个文件的 inode 编号后,就可以对照分区和分组的范围,确定文件所属的分组位置,找到所属分组后,对照 inode bitmap 合法与否,合法可直接定位 i 节点表找到该文件

当然 Data Block 里的块号也是如此!是基于分区为单位来整体分配的!而且当文件被分配到某个组后,会优先分配该组 Data Block 里的块, 除非此文件非常大,要不然不会跨组存储

datablocks[N] 数组

其实里面就是指向该文件所使用的数据块

这是将文件属性和文件内容存储位置相关联的属性,而这 N 一般为 15

如果是 15 ,那只能映射到 15 个 data block 数据块,那一个文件最大 15 * 4KB = 60 KB 吗?肯定说不通

其实这 15 个空间里面,前 12 个是 直接映射 的,也就是可以直接指向文件存储的数据块编号;后 2 个则是 间接映射 的,可以通过这 2 个来寻找 2 个数据块单元,但这数据块单元里全都是此文件的存储地址,从而完成扩容;而最后一个存的就厉害了,它指向一个数据块单元,但是这个数据块单元里又指向其他的数据块单元,其他的数据块单元才指向真正的文件存储单元

这就很大了,可以直接映射也可以间接映射(3级甚至是4级)

那如果文件容量大于分组空间?当然是可以的,因为只要文件愿意,依然可以让 datablocks[N] 数组指向其他的分组,所以是 支持跨组访问 的!但 非常不建议这么做,因为文件较大,可能不是存在相邻的组中导致同一个文件零碎存储,跨度大,磁盘寻址时间较长,导致效率过低

在这里插入图片描述

编号唾手可得?

我们使用文件可是使用的文件名,但 OS 找文件却是用的 inode number ,很反直觉啊!而且 inode 文件属性里还不包括文件名!怎么回事?

首先用户在电脑里所处位置一定是目录,那目录是文件吗?肯定是!那就有它自己的属性和内容,所以目录也会有自己的 inode ,和普通文件有着相同的属性字段,只是属性的值不一样罢了

属性可以理解,那目录的内容呢?放什么?目录的内容其实放的是:目录名和 inode number 的关系映射

所以每次打开查看一个目录的内容时,都是通过文件名和其编号的映射关系,才找到文件的 inode number

/ 目录是 系统规定 的,是一定可以找到的,所以每次找文件,都是会对文件路径进行逆向路径解析,但这操作是 OS 自己做的,只是 Linux 会为用户缓存常用的路径,不至于每次都要逆向到 / 目录

所以现在就可以解释:

  • 在同一个目录下为什么不能创建同名文件
  • 目录的 r 权限(查看),本质上是是否允许我们 读取 目录的内容(文件名和 inode 号的映射关系)
  • 目录的 w 权限(新建删除),本质上是是否允许我们向目录进行 修改写入 (文件名和 inode 号的映射关系)
理解文件的增删查改

新建文件:在特定的分区中申请一个 inodeSuper Block 也会记录下最近的 inode 编号的分配,确定好分组的编号后再进入此分组中查找 inode bitmap ,寻找为 0 的 bit 位,计算出 inode 编号,并在 inode table 的对应位置填写属性;再去查找 Block Bitmap 寻找空间分配给该文件,并将数据块的地址和属性进行映射;最后将文件内容进行保存,并将 inode 编号返回和文件名进行映射完成新建

查找 就不谈了,有文件名和 inode 编号就很简单; 修改 也是,只是分为修改属性或内容罢了

删除文件:需要在 inode Bitmap 里找到要删的文件的位置,由 1 置为 0 ;再找到 inode 属性里的 datablocks[N] 数组,将对应在 Block Bitmap 的数据块地址由 1 置为 0 ,此时就完成了

所以啊如果一个文件被误删了,只要还没有被覆盖,是可以恢复出来的!

逆向路径解析和如何找到文件自己所在分区

在云服务器上,一般都只有一个盘,查看:

ls /dev/vda

v 代表虚拟,而 /dev/vda1 则是虚拟出来的一个分区,在 Linux 上要访问一个分区是要将一个分区进行挂载的

挂载 意思是说:将磁盘分区和文件系统的一个目录进行关联,未来我们进入一个分区其实是进入指定的一个目录

指令 df -h

在这里插入图片描述

上图红框就是将 /dev/vda1/ 目录挂载

挂载有什么作用呢?就相当于将此分区和目录进行绑定,然后进入该目录,就是在该分区进行文件操作

而不管怎么样,任何文件在被访问之前,一定有目录,只要有目录,对比目录的字符串前缀来确定自己究竟在哪个分区

所以,目录本身除了可以定位文件,还能确定分区

那么找到一个文件就简单了,现在进程提供一个文件的路径,那么路径的末尾就是文件名,需要根据文件名寻找它自己的 inode number ,如何找?需要再上一级目录的文件内容里寻找嘛,那如何获取上一级目录的文件内容?路径里由上级目录名对吧?然后获取它的 inode 编号才能读取需要的文件 inode 编号对吧?那这个目录的 inode 编号又怎么获取?

显然是不是要一直 逆向解析路径,然后一路回退至 / ,此时再返回回来找文件名和 inode 编号的映射即可

其实每一个文件的寻找过程都是这样的,只是会将常用路径进行缓存,所以会效率会比较高

怎么缓存路径?是不是要用数据结构来描述,再将其以树状结构组织起来?没错,这个数据结构在 Linux 里叫做 struct dentry用于缓存路径


http://www.ppmy.cn/news/1531159.html

相关文章

MySql Explain优化命令使用

MySql Explain优化命令使用 truncate table student // 自增id 从 0 开始 delete from student // 自增id 会保留 , 108 区别: 1:自增id 2:delete 可以恢复 truncate 无法恢复 前言 EXPLAIN 是一个用于获取 SQL 语句执行计划的…

SpringCloud 2023 Gateway的Predicate配置详解、自定义Route Predicate Factory

目录 1. Predicate Factories介绍2. 常用的内置Route Predicate使用2.1 配置语法说明2.2 配置使用 3. 自定义Route Predicate Factory3.1 实现步骤:3.2 实现代码如下:3.3 application.yml配置3.4 测试 1. Predicate Factories介绍 Spring Cloud Gateway…

智能PPT行业赋能用户画像

智能PPT市场在巨大的需求前景下,已吸引一批不同类型的玩家投入参与竞争。从参与玩家类型来看,不乏各类与PPT创作有关的上下游企业逐步向智能PPT赛道转型进入,也包括顺应生成式AI技术热潮所推出的创业企业玩家。当前,智能PPT赛道发…

打造同城O2O平台:外卖跑腿APP的架构与功能设计详解

今天,小编将于大家共同讨论外卖跑腿APP的架构设计及其核心功能,旨在为开发者提供一份详尽的参考。 一、外卖跑腿APP的架构设计 1.整体架构概述 通常包括前端、后端和数据库。 2.前端设计 用户端提供直观的界面,方便用户下单、查询订单状态…

error -- unsupported GNU version gcc later than 10 are not supported;(gcc、g++)

服务器跑dit时编译flash-atten以及pytorch的cuda版本检查出错,分别报错题目以及如下: 想了下是系统找不到编译器 subprocess.CalledProcessError: Command [which, c] returned non-zero exit status 1. 备案,以后有人要用12我还得换回来 …

Lab1 Xv6 and Unix utilities

Lab1 Xv6 and Unix utilities 目的是为了熟悉xv6和一些它的系统调用函数 Boot xv6(easy) 1.环境 环境我是用的vscode配置的wsl,系统是ubuntu 20.04。用虚拟机、云服务器都感觉差不多。 网上看到Ubuntu 22.04 版本不适用于20年的课程,在根据20年课程…

vscode【实用插件】Project Manager 项目管理

安装 在 vscode 插件市场的搜索 Project Manager点 安装 安装成功后,vscode 左侧栏会出现 使用 将项目添加到项目列表中 用 vscode 打开项目,点保存即可 将项目移出项目列表 切换项目 单击项目列表中的项目,即可切换到目标项目 新窗口打开…

MySQL数据库进阶知识(五)《锁》

学习目标: 一周掌握数据库锁相关知识 学习内容: 一. 概述 介绍 锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的计算资源(CPU、RAM、I/O)的争用以外,数据也是一种供许多用户共…