之前我们学习的都是进程与被打开文件的关系,但是那些没有被打开的文件呢,它们需不需要被管理呢 ❓❓❓
答案肯定是需要的,对于一台计算机来说,磁盘上大部分的文件是未被打开的,而这些文件也需要被静态管理起来,方便我们随时打开!操作系统对未打开文件与打开文件的管理,称为文件系统。 接下来我们就来学习未打开文件的管理!
我们大部分人日常中都没见到过磁盘的真实面目,所以如果要想了解文件系统,我们势必要先了解磁盘的简单物理结构,方便我们理解操作系统层面!
磁盘分为硬盘与软盘,在历史的长流中,软盘渐渐地被淘汰,因为软盘是容量比较小,寿命比较短,现在内存动不动就是 TB
,因此软盘就渐渐地退出历史舞台。
这就导致现在我们就很少看见磁盘,磁盘是我们计算机中唯一一个机械结构,相对于其他存储设备来说较慢。但是价格低廉、存储量大,成为了企业存储设备的首选。
现在笔记本电脑中更多是选择 SSD
(固态硬盘),相比传统的机械硬盘 HDD
的性能优势主要表现在:读写速度快、防震抗摔性、低功耗、无噪音、工作温度范围大、轻便。固态硬盘由于其做工精细,因此价格也更贵。
但是相对于企业,磁盘依旧是主流。SSD
也并不是完美,首先来说价格相对于磁盘是更高,最重要的是 SSD
有读写的限制,如果写多了就会出现 SSD
被击穿的问题,就会造成数据的丢失。但是这都不是绝对的,磁盘和 SSD
都各有所长,很多时候也会选择混盘,磁盘和固态硬盘混用。
磁盘的物理结构如下:主要由永磁铁、磁头、主轴、盘片以及下面的电路板组成,其中盘片为一摞,每一个盘片都分为上下两面,这两面都可以用来存储数据,每一面都配有一个磁头。
磁头摆动和盘片旋转是通过马达控制的,我们可以通过硬件电路组成伺服系统给磁盘发二进制指令,然后让磁盘定位去寻址某个特定的区域,然后从磁盘上读取数据。磁盘和磁头都有两面,两面都是可以进行读写数据的。
磁头和盘片是没有接触的!他们的距离就像一架飞机隔着1米的距离在飞行。而且磁盘是不能有一点灰尘的,不然的话极有可能发生碰撞。所以磁盘必须得防止抖动,一旦磁头和盘面接触就会使得盘面刮花,丢失数据。
硬件上保存二进制,是跟对应的设备有关,寄存器和内存是通过触发器电脉冲,对硬件设备进行带电或者失电,通过电路的有无来代表二进制(实际上为了稳定,一般都是判断相邻两个磁性是否相异或者相同来标识二进制数的)。每一个寄存器触发器存储单元,每一个单元都是硬件电路,他们都可以进行存放电的。
在不同的设备,表示二进制的方式也是不同的,在网络里面通过信号的有无,通过信号的疏密来表示0和1。
这里可以理解磁头通过带放电,对磁盘某一个位置进行N/S极互换,就完成了0/1的写入,如果需要写入512比特位,就相当于触电513次。-- 磁化技术
总结的注意事项:
SSD
,而不再使用磁盘,因为磁盘的磁头与盘面距离非常近,所以为了避免磁盘与盘面接触而刮花盘面导致数据丢失,磁盘不能抖动;但是笔记本通常要进行移动,很可能会发生上述故障;同时,SSD
的读写速度要高于磁盘。SSD
存在造价贵、读写次数有限等缺点。 我们看到盘面是非常光滑的,但是在放大下,磁道和磁道之间是有间隙的。在微观下,一个扇区可存储大量的电子。实际上我们并不是把一圈的空间都用完,所以这里还使用了一些直白线划分,被圆白线和直白线划分出来的区域叫做扇区。所以当盘片在旋转、磁头摆动就可以找到这个盘面的任何一个扇区进行读写。
track
):磁盘的表面即盘面由一些磁性物质组成,可以用这些磁性物质来记录二进制数据;同时,盘面被划分为一个个同心圆,这些同心圆被称为磁道 (一个同心圆就是一个磁道),相邻磁道之间是有间隙的,我们的数据就存在磁道上。
sector
):从圆心向外放射,与磁道围成的一小块区域称为扇区,一个磁道会被划分为许多个扇区,每个扇区就是一个 “磁盘块”,扇区磁盘寻址的基本单位,即数据进行 IO
的单位,大小一般为 512
byte。(注:每个扇区的大小是固定的,所以从圆心往外,扇区的数据存储密度会随着扇区面积的增大而减小)
cylinder
):磁盘中所有盘面的同一个磁道被称为一个柱面,可以说,柱面和磁道是等价的。
盘面是有两面的,且两面都是同心圆,根据配置不同,有些磁盘可能还有多组盘片,我们可以从上至下的分为不同的盘面,也叫做是第几个盘面。
通过这些信息我们也可以得到 存储容量 = 磁头数 × 磁道数(柱面数) × 每道扇区数 × 每扇区字节数
磁盘在进行 IO
时,首先需要进行寻址,而寻址的过程如下:
cylinder
)head
)sector
) 上述过程在物理上的表现方式如下:启动主轴后,所有的盘片以同样的方式进行高速旋转,同时所有的磁头也共同从圆心到半径左右摆动,当定位到柱面后,磁头停止摆动,盘片继续旋转,当盘片对应扇区旋转到磁头下方后,对应盘面的磁头向扇区中写入/读取数据。
所以,在磁盘中定位任意一个/多个扇区,采用的基本硬件定位方式是 柱面、磁头、扇区定位,即 CHS
定位法(CHS
是早期在 IBM PC
架构上面用来进行磁盘寻址的办法)。
TB
,里面保存着大量的用户数据,为了用户数据安全,企业需要要对不用/损坏的磁盘进行消磁。常见的磁盘消磁方法有两种:一、加热,其缺点是销毁成本高,销毁的磁盘不能回收造成浪费,并且不能保证所有盘的数据全部消磁。二、向磁盘中写入垃圾数据或将磁盘格式化,其缺点是某些磁盘厂商有能力恢复之前的数据。所以一般大公司都会和磁盘厂商进行协商,要求厂商提供磁盘深度清理的定制协议功能,比如企业通过向磁盘输入协议密文来对磁盘进行深度清理。(注:一般大型公司都有自己固定的磁盘供应厂商,该企业的大部分磁盘都由该厂商提供,但是企业选择磁盘是不会全部都选一家公司的盘的,一是防止杀熟,二是防止磁盘批量化故障。)
4kb
,在操作系统看来,内存就是一个数组,每一个元素是 4kb
,之前在谈进程地址空间时也说过它叫做 页框,4kb
是页帧,所以操作系统申请内存时是按 4kb
为单位进行分配和加载的,语言层面上并不关心底层是怎么做的。磁盘存储也有基本单位,一个基本单位是一个扇区,它是磁盘读取的最小单元,大部分磁盘的一个扇区是 512byte
,你会发现虽然这里好像越靠近圆心,扇区越小,其实它们都是 512byte
,原因是越靠近圆心的虽然扇区越小,但是比特位也相对比外圈更密集。内存和磁盘之间也是有交互的,它们之间的交互我们称为 output
、 input
,也叫做 IO
,一般内存和磁盘之间 IO
交互时,不是纯硬件级别的交互,而是要通过文件系统完成,也就是通过操作系统。这里用户和内存之间交互的基本单元大小是 1byte
,一般内存和磁盘之间交互时的基本单元大小是 4kb
,所以文件系统在往磁盘读数据时,要读取 8
个扇区,这就是数据由磁盘加载到内存的过程。
比如说我们想查看 linux
中文件的 IO
交互单位是多少,我们可以用 stat
指令查看:
4kb
为基本单位进行 IO
时,有时 4kb
数据并不能完全被利用,但这并不代表着浪费。根据局部性原理,当计算机访问某些数据时,它附近的数据也有非常大的概率被访问到,加载 4kb
有助于提高 IO
效率,同时增大缓存命中率。本质上就是一种数据预加载,以空间换时间的做法。
block
不是越大越好,要根据业务的文件大小进行选择,一般默认 4kB
4KB
大小的空间 (页框);磁盘中的文件尤其是可执行文件是按照 4KB
大小划分好的块**(页帧)**,磁盘向内存拷贝数据,实质也是页帧将数据拷贝到页框中。
一般像这样的机械硬盘,物理上是圆状,操作系统很难去管理它,因为操作系统如果不对它进行抽象化处理,那么操作系统中的代码可能就是 read
(盘面,磁道,扇区),操作系统需要知道这三个参数的话,那么一定要在操作系统读取磁盘的代码中以硬编码的形式写到操作系统中。如果有一天,你给自己的电脑加了一块固态硬盘,你要对固态硬盘进行读操作,就不能再用以前的方法了,因为固态硬盘与机械硬盘的结构不一样,它没有盘面、磁道、扇区,所以操作系统中曾经设计好的代码就得修改。很显然,这样的设计导致它们之间出现了强耦合,这是很不合理的。
所以我们需要对磁盘抽象化处理,将圆状结构的磁盘空间抽象成线性结构的磁盘空间,很多人就纳闷了,这里举两个例子方便理解:
int arr[3][4]
二维数组就是把线性的数据结构抽象成了好理解的有行有列的结构。 同样的,也能把磁盘抽象成线性结构。把磁盘上的磁道抽象成线性形状,比如磁盘的所有磁道被我们抽象成了一条 500GB
的线性空间,我们可以把它看作一个很大的数组 —— 扇区 array[NUM]
,其中每一个元素是 512byte
,操作系统要申请 4kb
,那就给数组的 8
个元素。
所以将磁盘抽象后,操作系统就摆脱盘面、磁道、扇区的束缚了,操作系统只关心你想访问的哪个下标,这里的地址我们称为 逻辑区块地址(Logical Block Address
, LBA
),这里抽象出来的数组下标是和机械硬盘中盘面、磁道、扇区构成映射关系的,这里的映射关系是由对应的机械磁盘驱动维护的,操作系统想往 2
下标处写数据,最终 2
下标一定是能对应到具体磁盘中某个扇区上。如果要往固态硬盘中写数据,也是把它抽象成线性的数组,它也有自己的固态硬盘驱动维护数组下标和固态硬盘之间的映射关系。至此,通过抽象的方法,就完成了操作系统和磁盘之间的解耦。所以最终操作系统对磁盘的管理,转换成了对数组的管理。
LBA
地址转 CHS
定位如下图橙色算式所示:
总结,操作系统为什么要对 CHS
进行逻辑抽象呢 ?
LBA
地址进行定位寻找,只需要改变 LBA
与磁盘扇区的映射关系即可。 访问一个扇区是 512B
,如果将磁盘的访问的基本单位设置 512B
,对于 IO
访问来说效率太低,一般是对 8
个扇区进行同时访问,OS
内的文件系统定制的进行多个扇区的读取 4KB
为基本单位,即使是指向读取/修改 1bit
,也必须将 4KB
加载到内存,进行读取或者修改,如果必要,再写回磁盘。
4KB
大小的空间:页框4KB
大小划分好的块:页帧 注:文件系统以 4KB
作为数据 IO
的单位,那么当读取的数据小于 4KB
时,我们仍然需要读取 4KB
数据,那么就有人可能担心空间浪费的问题,其实,计算机中的局部性原理已经很好的解决了这个问题。
不管是拷贝数据还是修改数据,那么最开始都是需要将数据管理起来,这里我们通过分治的思想,一级一级的到向下一层减少管理空间,我们将最底层管理,对于上层而言也就是重复的工作了。所以理解文件管理我们就开始从这 5GB
开始(自定义5GB)。
下面我们详细的来讨论一下文件管理中的内容!
Linux
的文件【属性】和【内容】是分批储存的,如下图所示:
我们在 linux
中可以使用 ls -li
打印出文件的基本信息,其中最关键的就是这个 inode
了!
一个文件属性的,文件的大小,权限,等等信息在都在 inode
中。
除此之外,为了看到更多文件的信息,可以使用我们上面也有讲过的 stat
指令:
我们在 windows
下也有接触过分区的概念,其实就是我们的 C
、D
、E
盘等等,它们将系统硬盘分为不同的分区分别管理,为什么要这样子呢?这都是因为 分而治之 的思想!
企业中的较小的磁盘有 500GB
,较大的磁盘有几个 TB
,但是文件系统的 IO
单位只有 4KB
,这就存在磁盘太大不方便管理的问题,所以我们需要对磁盘进行区域划分;但是磁盘分区之后仍然比较大,所以我们需要继续对分区进行分组,如下:
上图为 linux
磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的 block
。一个 block
的大小是由格式化的时候确定的,并且不可以更改。例如 mke2fs
的 -b
选项可以设定 block
大小为 1024
、2048
或 4096
字节。而上图中启动块(Boot Block
)的大小是确定的,
注意: 每个分区的第一部分数据是 Boot Block
启动块,后面才是各个分组,它与计算机开机相关,我们不用关心。
现在,我们只需要管理好一个分组,然后管理模式复制到其他分组就可以管理好一个分区;再将一个分区的管理模式复制到其他分区就可以管理好整个磁盘了。其中,操作系统对一个分区的管理就被称为文件系统。
上面这种管理的方法被称为分治,分治的管理方法在我们生活中其实非常常见,比如国家管理划分为省、市、区、乡镇、村等等。这里我们把分配的过程叫做格式化过程,所谓的 格式化在计算机中本质就是写入文件系统,也就是说我们要把文件系统写入某个分区中,这个文件系统的核心包括数据 + 方法,数据就类似这个省有多少人口、粮食等,方法就类似这个省有生育政策、耕种政策等。同样文件系统包含的数据就是文件系统的类型等,方法就是操作各种文件的方法。
当然不同的分区当然可以使用不同的文件系统,Linux
下就使用五六种不同的文件系统,Linux
可以支持多种文件系统,包括 Ext2
、Ext3
、fs
、usb-fs
、sysfs
、proc
。这就好比,各个省份需要因地制宜的分配不同的团队。我们今天谈的都是 Ext
系列的文件系统,另外也不谈其它的文件系统如何,我们就认为磁盘上不同分区的文件系统是一样的。
其中我们先说一下这个每个分区的启动块 Boot Block
,它的大小就是 1kb
,由PC标准规定,用来存储磁盘分区的信息和启动信息,任何文件系统都不能使用该块。(如果这个块损坏,整个文件系统也就启动不起来了)。操作系统的开机,加电,启动相关的信息都是在这个块中的。
但是由于开关机都是时间很短的,甚至我们进入一家公司上班到离职的时候,可能服务器还没关机,所以我们没必要去学系统的开关机工作,性价比非常的低,有兴趣的话可以了解!
存放文件系统本身的结构信息。记录的信息主要有:block
和 inode
的总量、未使用的 block
和 inode
的数量、一个 block
和 inode
的大小、最近一次挂载的时间、最近一次写入数据的时间、最近一次检验磁盘的时间等其他文件系统的相关信息。
Super Block
的信息被破坏,可以说整个文件系统结构就被破坏了。— 保存整个文件系统的信息!
正常情况下,保存的整个文件系统的信息不应该和 Boot Block
一样放在分区的最开始吗,为什么会在多个分组中存在呢 ❓❓❓❓❓ 这里是为了 备份,当文件受损时,就可以将其他分组的 Super Block
拷贝到当前分组,文件系统就得到恢复。
🐰 注: Super Block
不是每个分组里面必备的数据,有些分组里面并没有 Super Block
,可能 10
个分组中只有两三个有 Super Block
。
在讲接下来几个管理方法之前,我们先来讲一些知识,以便后面我们理解那些管理方法:
我们之前说过 文件 = 内容 + 属性,而其中在 linux
中文件属性和文件内容是分批存储的❗❗
其中 文件属性用 inode
(information node
) 来存储,inode
的大小是固定的,并且 inode
为了区分彼此,每个 inode
都有自己的 ID
(可能存在硬链接所以可能会有相同的编号,这个后面会讲);除此之外,inode
中保存的是文件的几乎所有的属性,为什么说是几乎呢,因为 inode
不包括文件名 ❗❗
而 文件内容用 data block
(数据块) 来存储,其中 data block
和 inode
不一样,data block
会随着应用类型的变化,大小会不断的变化❗❗
文件的节点表,存放文件中几乎所有的属性,如文件大小、所有者、最近修改时间等。
唯独文件名不在 inode
表中存储!
另外也保存了分组内部所有的可用(已使用+未使用)的 inode
。如果 inode
表中有 100
个 inode
,每个 inode
的大小是 128
字节或 256
字节(根据文件系统的不同 Inode
大小不同),inode
表总大小就是 100*128
或 100*256
字节。
当我们创建一个文件时,找的就是 inode Table
,将文件属性如文件大小,所有者,最近修改时间等存放在这个表中!
🐇 注:inode
为了区分彼此,每个 inode
都有自己的 ID
,可以在 ls
指令中通过 i
选项查看。
存放文件内容,保存分组内部所有文件的数据块,大小随文件大小变化而变化。
用于快速查找 inode
的空闲位置。用 0
表示某位没有被使用,用 1
表示某位 inode
已经被使用。
用于快速查找 block
的空闲位置。用 0
表示某位没有被使用,用 1
表示某位数据块已经被使用。
存放对应分组的宏观属性信息,Super Block
描述的是整个块组相关的信息,而 GDT
描述的是一组的信息,每一个块组都必需要有一个 GDT
。
在知道如何查找对应文件之前,我们应该得先知道 inode
编号是如何形成的,因为我们查找的时候肯定和 inode
编号有关系:
每个分区都有自己的编号,例如:
Block group 0
到Block group n
的编号可能是1000~10000
,每个区都有自己的分组,分组会通过比特位图进行增加值,例如:起始编号为1000
,当新建一个inode
的时候,比特位图就加1
,所以编号为1001
,你从尾部的1
也可以得知在inode table
中,它是第一个建立的inode
。
🚗 注意: 同一分区中的 inode
是连续的,并且不能重复,但是不同分区的 inode
是不关联的,所以是可以重复的!
知道了这些信息后,我们如何通过 inode
编号查找对应文件呢,首先我们先来讲一下如何查找属性,我们通过 inode
编号就能找到对应的块,然后去对应的 inode Bitmap
中查看该 inode
编号的比特位是否为 1
,是的话说明该文件是存在的,则利用到 inode Table
中去查找对应的文件属性!
而对于文件内容,其实在 inode Table
中还保存了一个叫做 blocks[]
数组存放着该分区内的 Data blocks
中对应文件块的编号,并且每次该数组都是按照类似文件描述符一样的分配规则按顺序找空位存放编号,通过编号到 Block Bitmap
中查看该文件块是否存在,是的话就去 Data blocks
对应编号位置,如果有多个的话,会将它们拼接起来,就是我们要的对应的文件了!
一般来说这个 block[]
数组的大小为 15
个,那么问题就来了,15
个编号怎么够放那么大的数据空间呢,一个 data blocks
的文件块顶多每个分配 4kB
呀,这存放的编号也太少了吧 ❓❓❓
其实不是这样子的,谁规定说文件块内就不能存放其它文件块的编号呢,对不对,而一般对于 block[]
来说,它的前 10
个元素放的文件块编号对应的内容都是真的文件数据,而后几个元素放的文件块编号中指向的文件块其实存放的是其它文件块的编号,这样子一来,是不是就像多叉树一样,拓展出了许多分支,另外还存在二级甚至三级的拓展,也就是说分支产生分支,这样子分配的规则就不会说出现文件块编号太少的问题!如下图所示:
前面我们说到 inode
存储的文件属性中是不包含文件名的,说是这么说,但是我们想一下平时我们在 linux
中查找文件的时候,都是通过文件名啊,哪有什么通过 inode
编号,那也太麻烦了,但是又说 inode
不包含文件名,这不是冲突吗 ❓❓❓
其实这和目录有关系,以前我们都说过,linux
下一切皆文件,那么目录也是文件啊,并且我们知道每个文件都是存在于一个目录下的,根目录也是目录,但是目录到底存放的是什么东西呢 ❓❓❓
目录的内容是下级目录或者普通文件,所以 目录下存放的其实是文件名和 inode
之间的映射关系,也就是说通过文件名我们可以映射到对应的 inode
编号去完成对应的工作,那么既然目录可以存放文件名和 inode
的映射关系,那么自然 inode
也就没必要存储文件名这个属性了,就可以节省空间,让这件事情交给目录去解决!所以现在就能理解为什么大多数操作系统下同一个目录中不允许存在同名文件。
所以当我们在某一个目录下使用文件名查找文件时,操作系统会读取目录文件的 data blocks
里面的数据(时刻记得目录也是文件❗),找到文件名对应的 inode
编号,找不到就提示文件不存在。而当我们在目录下新建文件/文件夹时,操作系统会向目录 data blocks
里面写入新文件与 inode
的映射关系。这也是为什么在目录下读取文件信息需要 r
权限,而在目录下新建文件需要 w
权限的原因❗❗❗
首先在 inode Bitmap
里面查找为 0
的比特位编号,将该比特位置 1
,然后将文件的所有属性填写到 inode Table
对应编号下标的空间中;再在 block Bitmap
中查找一个或者多个为 0
的比特位编号,将其置为 1
,然后将文件的所有内容填写到 data blocks
对应编号下标的空间中;最后再修改 super block
、GDT
等位置中的数据。同时,需要将新文件文件名与 inode
的映射关系写入到目录的 data blocks
中。
总结下来就是以下几点:
i
节点,内核再把文件信息记录到其中。
300
、500
、800
,将内核缓冲区的第一块数据复制到 300
,下一块复制到 500
,以此类推。
300
、500
、800
存放。内核在 inode
上的磁盘分布区记录了上述块列表。
abc
,那么 linux
如何在当前的目录中记录这个文件?内核将入口添加到目录文件。文件名和 inode
之间的对应关系将文件名和文件的内容及属性连接起来。
像我们日常生活中建房子一样,我们需要去打地基、硬装还有软装等等过程,而我们如果不使用这个房间的话,则直接在这个房间上面写一个拆字,这个房间就等于是没用了。
对于文件系统中的新建文件和删除文件也是一样,新建文件需要做很多的工作,而删除文件不需要做很多工作!
在任何文件系统中,需要删除文件只需要将 inode Bitmap
、Block Bitmap
中文件对应的比特位由 1
置 0
,这个文件就相当于被删除了。
当然想要恢复文件的话,只需要找到被删除文件的 inode
编号,将 inode Bitmap
中的比特位由 0
置 1
,找到该文件在 inode Table
中的位置,根据其中的映射关系找到文件的数据块,并把 Block Bitmap
由 0
置 1
即可恢复文件。注意文件被误删之后不要做任何非恢复操作,防止原文件属性和内容被覆盖。
Windows
下就算把回收站的内容删除也是能恢复的,Linux
下,如果要恢复删除的文件是有一些恢复工具的,但有可能在恢复过程中,创建各种临时文件,可能就会把想恢复的文件的信息覆盖掉,你想自己恢复删除的文件,就需要更深入的了解文件系统原理。
删除文件实际上只是删除文件的目录项,文件的数据以及
inode
并不会立即被删除,因此若进程已经打开文件,文件被删除时,并不会影响进程的操作,因为进程已经具备文件的描述信息(可以编写代码进行尝试,在文件打开后,外界删除文件,然后看进程中是否还可以继续写入或读取数据)