引言

本文章是基于《操作系统导论》的总结,如果大家想深入了解,很建议阅读这本书。

文件和目录

我们的电脑存储数据的地方有很多,但是主要的就两个,一个是内存,一个是磁盘。

内存离CPU比较近,那么自然而然;每一次访问数据肯定更加的方便,而磁盘离CPU比较的远,自然而然,串起门来就不这么方便。

但是因为内存有一个很大的缺点,一断电,里面的数据全部消失(就像你和她的聊天记录,可能一夜之间一无所有)。但是磁盘就不一样,无论你断不断电,写在磁盘里的数据永远在那里(就像别人的女朋友一样),所以,如果很重要的数据,我们都要放在磁盘里面,而访问的时候,所有的数据都在内存之中。

那么,这些在磁盘中的数据,操作系统需要特别的关心,因为每个用户都离不开它们。

随着时间的推移,存储虚拟化形成了两个关键的抽象。

文件

文件类型

第一个是文件。文件就是一个线性字节数组,每个字节都可以读入或者写入。

对于文件的命名,每个文件都有着它的一个自己的低级名称,就是inode(某一数字),但是作为用户,我们通常不知道inode。

在大多数操作系统之中,操作系统不太了解文件的结构(是图片还是文档)。而文件系统的任务仅仅是把数据存储到磁盘上,确保你下一次请求数据的时候,还可以访问到。所以如果一个.c 的文件,我们趁别人不注意,改成.img文件,其实从原理上来说也是可以运行的。

文件一般分为两类,一类是常规文件,这也是我们接触到最多的文件,图片,可执行文件。。。

除了常规文件外,UNIX系统还引入了字符特殊文件和块特殊文件。字符特殊文件主要与输入/输出
操作相关,它们通常用于串行I/O类设备,如终端、打印机和网络连接等。这些文件允许用户以字符流 的形式与设备进行交互。而块特殊文件则主要用于磁盘类设备,它们允许用户以数据块的形式进行读写 操作,从而提高了数据处理的效率。
不过在本片文章里面,还是主要是以常规文件为主
当程序需要从文件中读写数据时,该请求会被转发给 内核驱动程序(kernel driver) 进行处理。

文件的访问

早期的操作系统,文件的访问是顺序访问,而在现在,我们更多采用的是随机访问文件。

以数据库系统为例,随机访问文件是不可或缺的。当乘客致电预订航班机票时,订票程序需要能够
迅速定位到特定的航班记录,而不是逐一浏览成千上万条其他航班的记录。为了实现这一目标,有两种方法可以指定文件的读取起始点。
第一种方法是直接使用 read 操作从头开始读取文件,但这种方法缺乏灵活性,无法快速定位到文
件中的特定位置。另一种更为高效的方法是使用特殊的 seek 操作来设置文件的当前读取位置。在执行 seek 操作后,程序可以从该位置开始顺序地读取文件。UNIXWindows操作系统均采用了这种 seek加顺序读取的方式来处理随机访问文件,从而大大提高了文件访问的效率和灵活性。

文件的属性

简单来说就是文件的信息。所以,大家要注意!!!!如果你直接把别人的作业copy过来,一定要有所更改,不然如果发现你们的文件最后一次更改时间一摸一样,内容也一样,那可坏事了。

目录

第二个是目录,目录里面存的就是文件,而一般的目录,都是树形结构的

一级目录系统

优势就是简洁高效,但是缺点也很明显,文件数量的增加导致查找特定文件十分的困难。

层次目录系统

可以用多个目录对于文件进行分类,让用户的查找更加的方便。现代的文件系统一般都是这种形式。

路径名

绝对路径和相对路径,前者是从root开始找,后者是从当前位置开始找。

目录操作

这里主要讲两个的区别

1、删除目录(rmdir)

如果这个目录是空的,那么可以直接删除,否则不行。

2、删除链接(unlink)

unlink 系统调用用于删除一个目录项(即文件或目录的链接)。如果该文件只在一个目录中被链接,那么 unlink 将删除该文件及其所有内容。但是,如果该文件在多个目录中被链 接, unlink 只会删除指定路径名的链接,而不会影响其他路径名的链接。在 UNIX 中, unlink 也可以用于删除文件。

硬连接和软连接

软链接就是windows的快捷方式,软连接本身就是一个单独的小文件,里面存的是源文件的路径,点击这个文件,会通过路径打开源文件。

所以删除软链接的文件是没有用的,要删除源文件。下一次卸载软件可不要直接把图标丢进垃圾桶了哟。

硬链接相当于给文件起别名(自始自终都只有一个文件),如果你只删除了一个文件,无论是哪一个文件,文件依然存在,只有当把所有文件删除,让引用计数为0的时候,才真正删除了文件。

文件系统

文件系统是纯软件,所以我们有必要理解它的工作原理。但是在我们讲述原理的时候,会忽略一些东西,当然,这些东西马上也会被提起。

第一个就是文件系统的数据结构。

文件系统被存储在磁盘里面。磁盘通常可以划分成多个区域,这些区域被称为磁盘分区,每个分区都有着自己独立的文件系统,并且这些文件系统的类型可以不相同。

先不管那么多,我们可以想象一下,为了构建文件系统,这些文件肯定是一个一个块组成的,文件里面存的就是数据,那么,一个很自然的想法就是,我们把磁盘分成块,块的大小一般是4KB,然后这些块里面就存数据。这个就是数据区域。

但是存了数据没有用,我们真正需要的是找到这些数据,所以我们在磁盘里,还要有一片区域来记录这些文件在磁盘分区里的位置。所以文件系统必须要记录下每个文件的信息,该信息是元数据的关键部分,比如文件大小,访问权限,修改时间等。。。为了存储这些信息,文件系统里面有一个inode的数据结构,至于什么是inode,我们稍后来聊。所以为了存放inode,磁盘必须要留一部分空间,来存inode。假设有64块,我们56块存数据,5块存inode,那么4*5KB=20KB,一个inode假设256字节,20KB/256B = 80个inode,说明在这64块里面,我们可以建立80个文件。但是,信息是有了,那我们总不能说每一次要访问信息了,每次都是遍历整个inode,看看哪些是有空位的,哪些是有人的。所以我们还需要用位图的方式记录inode和数据区的空闲情况,这里假设各分配一个块去存储inode和数据区的位图,也就是4KB * 8 = 32 KB,可以记录32KB的对象是否分配。最后,整个文件系统还有一块,这里存放的是超级块,超级块包含了这个分区文件系统的信息,比如有多少个inode和数据块。

因此,在挂载文件系统(让操作系统可以访问)的时候,操作系统首先读取的是超级块,初始化各种参数,然后将该卷添加到文件系统树中,当卷中的文件被访问到的时候,操作系统就知道在磁盘的哪里去寻找。

inode数据结构

inode里面存的就是文件的信息,这个不是我们的重点。假设我们读取32号inode,那么先计算inode区域的偏移量(32 * 4 KB),再加上inode在磁盘上的位置,就得到了32inode的位置。

我们把所有关于文件的数据称为元数据,在设计inode的时候,最重要的就是如何引用数据块鹅位置。一个最简单的方法就是inode里面有一个或者多个指针,每个指针指向一个数据块。假设我们有12个指针,那么文件的最大大小就是(12 * 4 KB = 48 KB)。

当我们看到这个数的时候,就知道问题出现了,文件太小了,我要是搞个大项目,怎么办。所以我们推出了多级索引

多级索引

这是一个很巧妙的想法,我们为了扩大文件的大小,我们inode块里面不再存指向含用户数据的块的指针,而是存指向一个包含更多数据块的指针,当然,这些块是在数据区,只不过存的是指针罢了。假设只分配一个块存更多的指针,一个块是4KB,一个指针的大小是4字节,那么一个块就可以增加1024个指针,文件就可以增加(12 + 1024 )* 4 KB = 4144KB字节。如果还要更多,可以分配更多的块存指针。

目录组织

目录只是“存储名->inode”映射特定类型的文件

目录存在哪里,这是一个好问题。通常目录是一个特殊的文件(dir),但至始至终,目录也是一个文件,所以目录有一个inode,也在inode表中的某处,所以我们的磁盘结构不需要改变。

空闲空间管理

文件系统必须要记录哪些inode和数据块是空闲的,哪些不是,这样可以更快速的分配文件。我们一般解决办法有两个:一个是位图,一个是链表。

在分配空间的时候,为了减少磁盘寻道时间,尽量把一个文件安排在一起。

数据的写入

写入数据一直都是一件非常非常糟糕的事情,首先文件必须打开,其次要更新,最后要关闭,并且运气不好的话,还要分配新的块。

看上去这个过程没啥特别的。别急,我们慢慢来。

假设我们需要一个新的块

首先我们要I/O读取数据位图,(用来更新标记新分配的块被使用),然后I/O写入位图(将新的状态存入磁盘),然后I/O读取inode,I/O写入inode,最后I/O写入数据本身。

前后有5次磁盘I/O,饿啊~

那么如何降低成本呢。这些操作似乎不可以避免,那么我们就尽量减少这些操作的次数,所以我们引入了缓存缓冲,这两个老熟人也不必介绍了。

不过呢,有些人就是不喜欢弯弯绕绕的,所以系统也提供了一个函数fsync(),这个可以避免缓冲直接I/O。

局部性和快速文件系统

快速文件系统是一个非常古老的文件系统,但是我们很有必要了解一下,方便我们之后的学习。

之前性能不好主要是:数据到处都是,相关的文件没安在一起,寻道浪费时间;有碎片。

为了解决这个问题快速文件系统(FFS)保证了相关文件放在一起。其操作就是分块组,一个块组的内容寻道基本不会耗时。大文件除外,大文件分开放,因为如果放在了一起,那么就妨碍了相关文件放置在该块里面,而分开放可以腾出更多的空间。但是有人可能觉得分开放那不是性能低了很多,其实有这种想法不错,但是大文件其实主要时间都是在读取和写入,所以寻道的时间可以忽略了。

FFS的其他几件事情

引入子块(512K)和缓冲区形成配合,避免碎片

优化扇区布局,保证发出一个请求块0并读取完之后,如果发出第二个请求,那么下一个扇区就是块1。所以磁盘的块号是跳跃的(根据转速)

日志

这一方面我们仅仅简单的提及一下。

假设我们要更新磁盘的结构A和结构B,但是如果刚更新完A,啪一下,断电了,那完了,B还没更新,那电脑怎么知道开机之后要继续更新B呢。

所以我们就引入了日志,每一次在更新磁盘的时候,现在磁盘的一个众所周知的地方写下一点小注释,描述我要做的事情,然后断电之后,要恢复内容或者继续覆写,通过注释可以想起往日的事迹。

这也就是日志的主要任务了!

日志结构文件系统(LFS)

这个就比较关键了。当时面临了很多的问题:

1、内存大小不断地增加,所以缓存可以有更多的数据了。所以磁盘更多的开销就放在了写入操作,因为读操作的小助手缓存已经很强大了。所以文件系统的性能很大程度上取决于写入的性能。

2、随机I/O的性能远远低于顺序I/O的性能

3、虽然相同的文件放在了一起,但是会导致短寻道和旋转延迟,性能低(10次1毫米的短寻道,其总耗时远大于1次10厘米的长寻道,在碎片化的小文件读取中,每个文件都要重新等一圈)

所以我们解决方案首先第一个,让所有的写入变成顺序写入。不知道大家还记不记得文章前面我们的inode存放在哪里,存放在文件系统的开头那一部分,位置是固定的,也就是我们每一次写入的时候,要寻道找inode,又要寻道找数据区,很麻烦。

所以我们这一次写入数据的时候把inode一起写进去,这个inode就指向的是对应的块。

我们把一次性写入的所有数据称为段。

从图中我们也可以发现,如果一次写入占用了两个块,那么对应的inode就会存两个块的位置,即使可能这两个块根本不是同一个文件,也没有关系,因为inode不是负责这一部分的,它只负责找到块的位置。

不过可能这个图片看上去inode和块的大小一样,但是实际上inode要小很多,一般都是128B。

不过怎么找到inode呢?

图里面已经告诉我们答案了,imap。

imap记录了最新版本的inode的位置,至于为什么是最新的。当然是每一次更新数据的时候都会更新imap的数据啊~   亲~

遗憾的是,imap需要保持持久的访问,必须要写入磁盘。

当然了, 亲~ ,imap可以写在一个固定的位置,但是这个位置每次更新的时候都要寻道,所以性能并不高。

所以, 亲~ , 我们的imap也是随着数据一起写入到磁盘的,它的位置也一直在变动,但是至少保证了一个点,顺序写入。

那既然是顺序写入,那这个磁盘上会有很多的imap,确实,但是一旦imap被重新写入,也就是更新,那么原来的imap就没有作用了,我们不会再读取原来的imap。

检查点区域

说了这么多,我知道你心中的疑惑依然存在,就是imap也在动,如果数据更新了,inode也在动,那我怎么找到我想要的数据呢?LFS在磁盘上只有这么一个固定的位置,称为检查点区域(CR)。检查点包含了指向最新的inode映射片段的指针(即地址)。

所以我们的读取数据的过程很明显了:

通过CR找到imap,把imap缓存到内存之中,通过imap找到inode,通过inode找到数据区。

目录

那我们怎么找到目录呢?

目录也是文件,也有inode号,既然如此,那也很简单了,目录的inode号也存在在imap里面。

okok,说了这么多,我们模拟一个比较复杂的场景,我们需要找到目录下面的foo文件

先找到imap,存到内存里,然后通过目录的特殊标记(dir)找到目录的inode,然后进入目录的地址,目录里面存的是名称->inode,所以我们找到foo对应的inode(数字),然后回到imap,找到对应inode的地址,最后通过inode找到foo文件。

其实在不知不觉中,imap还帮了我们一个大忙,对于一个不会原地更改的文件系统,而是把新数据写入到新的位置,如果更改一个inode而且还没有imap,我们就要更改父目录,因为父目录记录了inode的位置,父目录一改,位置也变了,又要更新父父目录,一直往上递归,全毁了。

但是我们有imap,inode的信息无论怎么变化,inumber是不变的,而目录的作用就是找到名称对应的inode,根本不记录inode的位置,记录inode的位置的是imap,而imap一直是被顺序写入的,并不影响。

垃圾收集

LFS的特点就是不断地写,管你是什么,顺序写入的开销小,那就不断地写,不止是imap,如果你更新一个文件,也会重新的写入。但是问题就是我们怎么知道那是旧块的。

先解决第一个问题,每个数据段头部都有一个地方存储额外信息,里面就有数据块对应的inode和块的地址。假设一个数据块的原来地址是A,更新了之后数据块的地址是B,我只需要拿着这个inode跑到imap那查看一下inode的地址,然后再跑到inode那个地方看一下这个inode到底指向哪里,如果是地址A,和之前一模一样,那就说明没啥问题,如果指向的是地址B,不是地址A,那你就是垃圾,一边去。

那既然知道这个是垃圾了,怎么处理呢?

这里有个很简单的方法,就是把LFS定期读一般文件数据,把那些垃圾释放掉,让那些块变成空闲,但同时为了避免外部碎片,把那些旧的段重新读到缓存里面,积累到一定程度,重新写入,组成一个新的段,然后把原来的段全部释放。

高速缓存

CPU读取内存的速率其实也不是很快,所以为了加快速度,高速缓存横空出世。它的容量不大,但是效率是内存读取的10倍,一般我们会把内存读取的数据放到高速缓存上面,并且提前读取一段,所以为什么说遍历数组最好一行一行的遍历,其实原因就在这里,因为每次遍历一行的开头,其实高速缓存里面已经存储了这一行的所有数据,没必要再读取内存了,这个效率就很高。

emmmm ,最后,也到了和操作系统短暂告别的时候了~~  操作系统的所有内容就到这里结束了。

从操作系统内存到操作系统文件系统,操作系统的故事就到这里结束了。大家如果有兴趣可以阅读我博客的其他内容。

感谢大家的阅读!!!!

Logo

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

更多推荐