操作系统之文件系统

发布时间:2026/5/21 22:09:05

操作系统之文件系统 引言本文章是基于《操作系统导论》的总结如果大家想深入了解很建议阅读这本书。文件和目录我们的电脑存储数据的地方有很多但是主要的就两个一个是内存一个是磁盘。内存离CPU比较近那么自然而然每一次访问数据肯定更加的方便而磁盘离CPU比较的远自然而然串起门来就不这么方便。但是因为内存有一个很大的缺点一断电里面的数据全部消失就像你和她的聊天记录可能一夜之间一无所有。但是磁盘就不一样无论你断不断电写在磁盘里的数据永远在那里就像别人的女朋友一样所以如果很重要的数据我们都要放在磁盘里面而访问的时候所有的数据都在内存之中。那么这些在磁盘中的数据操作系统需要特别的关心因为每个用户都离不开它们。随着时间的推移存储虚拟化形成了两个关键的抽象。文件文件类型第一个是文件。文件就是一个线性字节数组每个字节都可以读入或者写入。对于文件的命名每个文件都有着它的一个自己的低级名称就是inode某一数字但是作为用户我们通常不知道inode。在大多数操作系统之中操作系统不太了解文件的结构是图片还是文档。而文件系统的任务仅仅是把数据存储到磁盘上确保你下一次请求数据的时候还可以访问到。所以如果一个.c 的文件我们趁别人不注意改成.img文件其实从原理上来说也是可以运行的。文件一般分为两类一类是常规文件这也是我们接触到最多的文件图片可执行文件。。。除了常规文件外UNIX系统还引入了字符特殊文件和块特殊文件。字符特殊文件主要与输入/输出操作相关它们通常用于串行I/O类设备如终端、打印机和网络连接等。这些文件允许用户以字符流 的形式与设备进行交互。而块特殊文件则主要用于磁盘类设备它们允许用户以数据块的形式进行读写 操作从而提高了数据处理的效率。不过在本片文章里面还是主要是以常规文件为主当程序需要从文件中读写数据时该请求会被转发给内核驱动程序(kernel driver)进行处理。文件的访问早期的操作系统文件的访问是顺序访问而在现在我们更多采用的是随机访问文件。以数据库系统为例随机访问文件是不可或缺的。当乘客致电预订航班机票时订票程序需要能够迅速定位到特定的航班记录而不是逐一浏览成千上万条其他航班的记录。为了实现这一目标有两种方法可以指定文件的读取起始点。第一种方法是直接使用read操作从头开始读取文件但这种方法缺乏灵活性无法快速定位到文件中的特定位置。另一种更为高效的方法是使用特殊的seek操作来设置文件的当前读取位置。在执行 seek 操作后程序可以从该位置开始顺序地读取文件。UNIX和Windows操作系统均采用了这种seek加顺序读取的方式来处理随机访问文件从而大大提高了文件访问的效率和灵活性。文件的属性简单来说就是文件的信息。所以大家要注意如果你直接把别人的作业copy过来一定要有所更改不然如果发现你们的文件最后一次更改时间一摸一样内容也一样那可坏事了。目录第二个是目录目录里面存的就是文件而一般的目录都是树形结构的一级目录系统优势就是简洁高效但是缺点也很明显文件数量的增加导致查找特定文件十分的困难。层次目录系统可以用多个目录对于文件进行分类让用户的查找更加的方便。现代的文件系统一般都是这种形式。路径名绝对路径和相对路径前者是从root开始找后者是从当前位置开始找。目录操作这里主要讲两个的区别1、删除目录rmdir如果这个目录是空的那么可以直接删除否则不行。2、删除链接unlinkunlink系统调用用于删除一个目录项即文件或目录的链接。如果该文件只在一个目录中被链接那么 unlink将删除该文件及其所有内容。但是如果该文件在多个目录中被链 接 unlink只会删除指定路径名的链接而不会影响其他路径名的链接。在UNIX中 unlink 也可以用于删除文件。硬连接和软连接软链接就是windows的快捷方式软连接本身就是一个单独的小文件里面存的是源文件的路径点击这个文件会通过路径打开源文件。所以删除软链接的文件是没有用的要删除源文件。下一次卸载软件可不要直接把图标丢进垃圾桶了哟。硬链接相当于给文件起别名自始自终都只有一个文件如果你只删除了一个文件无论是哪一个文件文件依然存在只有当把所有文件删除让引用计数为0的时候才真正删除了文件。文件系统文件系统是纯软件所以我们有必要理解它的工作原理。但是在我们讲述原理的时候会忽略一些东西当然这些东西马上也会被提起。第一个就是文件系统的数据结构。文件系统被存储在磁盘里面。磁盘通常可以划分成多个区域这些区域被称为磁盘分区每个分区都有着自己独立的文件系统并且这些文件系统的类型可以不相同。先不管那么多我们可以想象一下为了构建文件系统这些文件肯定是一个一个块组成的文件里面存的就是数据那么一个很自然的想法就是我们把磁盘分成块块的大小一般是4KB然后这些块里面就存数据。这个就是数据区域。但是存了数据没有用我们真正需要的是找到这些数据所以我们在磁盘里还要有一片区域来记录这些文件在磁盘分区里的位置。所以文件系统必须要记录下每个文件的信息该信息是元数据的关键部分比如文件大小访问权限修改时间等。。。为了存储这些信息文件系统里面有一个inode的数据结构至于什么是inode我们稍后来聊。所以为了存放inode磁盘必须要留一部分空间来存inode。假设有64块我们56块存数据5块存inode那么4*5KB20KB一个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读取inodeI/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的位置父目录一改位置也变了又要更新父父目录一直往上递归全毁了。但是我们有imapinode的信息无论怎么变化inumber是不变的而目录的作用就是找到名称对应的inode根本不记录inode的位置记录inode的位置的是imap而imap一直是被顺序写入的并不影响。垃圾收集LFS的特点就是不断地写管你是什么顺序写入的开销小那就不断地写不止是imap如果你更新一个文件也会重新的写入。但是问题就是我们怎么知道那是旧块的。先解决第一个问题每个数据段头部都有一个地方存储额外信息里面就有数据块对应的inode和块的地址。假设一个数据块的原来地址是A更新了之后数据块的地址是B我只需要拿着这个inode跑到imap那查看一下inode的地址然后再跑到inode那个地方看一下这个inode到底指向哪里如果是地址A和之前一模一样那就说明没啥问题如果指向的是地址B不是地址A那你就是垃圾一边去。那既然知道这个是垃圾了怎么处理呢这里有个很简单的方法就是把LFS定期读一般文件数据把那些垃圾释放掉让那些块变成空闲但同时为了避免外部碎片把那些旧的段重新读到缓存里面积累到一定程度重新写入组成一个新的段然后把原来的段全部释放。高速缓存CPU读取内存的速率其实也不是很快所以为了加快速度高速缓存横空出世。它的容量不大但是效率是内存读取的10倍一般我们会把内存读取的数据放到高速缓存上面并且提前读取一段所以为什么说遍历数组最好一行一行的遍历其实原因就在这里因为每次遍历一行的开头其实高速缓存里面已经存储了这一行的所有数据没必要再读取内存了这个效率就很高。emmmm 最后也到了和操作系统短暂告别的时候了~~ 操作系统的所有内容就到这里结束了。从操作系统内存到操作系统文件系统操作系统的故事就到这里结束了。大家如果有兴趣可以阅读我博客的其他内容。感谢大家的阅读

相关新闻