深入理解Linux0.11内核之文件系统一

写在前面:

很久很久没更新了,最近会持续更新Linux0.11内核相关的文章,全程用官方书籍+内核源码+intel手册论证。

首先,为什么要学习Linux0.11呢?大多数人认为外面根本没人使用了呀.....  现在的centos6 7 8、ubuntu 14 16 20不都是内核2.6、内核3.1或者以上版本吗?

我的答案是:知其先后,则近道矣。0.11确实没人使用了。但是麻雀虽小五脏俱全,其中进程管理、内存内存、文件管理、设备驱动等等模块都是存在的,但是其中算法可能比较欠缺(毕竟1991年,linus一个人完成的)。并且在高版本还保留了很多0.11的思想。

正文

讲文件系统,那么肯定是离不开磁盘和内存,无非就是读写(output、input),读数据、写数据。所以我们先来了解硬件层面的磁盘设计。

选图来自CSAPP

由一个主轴来控制旋转方向,旋转的时候有一个读写头来从盘片上的扇区读数据(读写头图上没有...先假设有把...)。而一个盘片又分为正反两面。一面又由一圈一圈的磁道组成,磁道中有等量的扇区。但是在磁盘设计中,每圈的磁道中的扇区是同等大小的,所以越外层的间隙越大,越内层间隙越小。然后其中存储的最小单元就是扇区了,一个扇区可以存储512byte数据。

三明治架构

 

由三明治架构图我们得知,OS是与硬件打交道的。所以对于OS层面,我们需要抽象出一个磁盘思想出来,然后通过OS中硬件驱动,最终完成硬件打交道。对于磁盘的硬件层面上面已经介绍过了,最小存储单元是一个扇区也就是512byte大小。但是最终OS并没有按照硬件的思想原封不同的抽象出一个磁盘出来,而是使用了MINIX文件系统1.0的思想。由下图可见。

 把硬盘给抽象成一个个1024byte大小的盘块(内存也是抽象成4096大小的一页,所以思想是不变的)。而对于任何抽象思想来说,元数据(描述真实数据的数据)的出现是大大的优化了CRUD的时间。大部分时间只需要去操作元数据进行修改,并不需要去操作真实数据(往往真实数据都非常的庞大,所以比较耗时)

  • 引导块:用于存放OS的代码,启动的时候ROM BIOS会去这里位置去找代码启动。
  • 超级块:用于描述i节点位图和逻辑位图的信息,还有磁盘对于内存中的一些其他的元数据信息
  • i节点位图:位图由N个01组成,所以用01代表i节点是否被使用。
  • 逻辑块位图:位图由N个01组成,所以用01代表数据块(真实数据存储区)是否被使用。
  • i节点:用来描述数据块的元数据信息的块。
  • 数据块:真实存放数据的块。

以上的抽象思想,那么肯定是在OS中有对应的结构体的实现的。源码在0.11内核中fs.h头文件中。具体的结构体内容就不一一介绍了。后续会介绍sys_write等等关于文件的系统调用的时候自然会用到这些结构体,到时候再一一介绍。

介绍完OS对磁盘的抽象以后,我们需要思考一个问题:磁盘的速度?内存的速度?在intel的CPU设计中只能一级一级的去访问(CPU内部只能访问缓存,缓存只能访问内存,内存只能访问硬盘,不能跨级访问)?而每次需要对磁盘进行读写,我们都需要从磁盘把数据读到内存中才能去读写操作。所以为了解决磁盘和内存的速度差,我们需要引入缓存思想。所以在内存中开辟了一段空间用来作为磁盘的高速缓存。那么设计一个缓存,我们需要考虑什么因素?局部性?

  1. 空间局部性:当从磁盘中获取到数据时,我们连带此数据附近的数据一起加入到缓存中,下次可能访问的数据就是此数据附近的数据,这样我们就减少了一次进入到磁盘中读数据的时间。
  2. 时间局部性:对于已经缓存中的数据可能被多次使用。这样就能直接访问内存,不需要再去访问磁盘获取到数据。

对于这里我们考虑到读操作是很需要满足这两点的,而写操作是直接写在内存中,到时候通过某种手段和硬件驱动给落到磁盘中(后面文章会讲这种手段)。

所以接下来我们对内存中的磁盘高速缓存做介绍。

图片选自赵炯博士<<Linux完全注释>>

 

这里把内存划分成一个1024大小的缓存块,目的是为了跟硬盘的数据块统一大小,减少交互次数。其次头部也是缓存块的元数据信息(都是基本操作了...不管那个框架那个源码都是这样),称之为缓存头buffer_head。中间还存在一小部分不使用的内存空间,因为已经没办法组成一个缓存块和缓存头了。

那么现在要思考一个问题,磁盘的数据块怎么和内存高速缓存做映射呢?因为对于读和写来说是在高速缓存中完成,最后落盘到磁盘中,所以磁盘和高速缓存之间需要有映射。

这里是通过磁盘的元数据信息做一个HASH运算(因为O1的时间复杂度)得出落在HASH表的那个节点,因为HASH运算不是唯一性,所以会维护一个链表,同一计算结果就维护成一个链表。所以在Linux0.11中维护了一张HASH表,而缓存头在Linux0.11中的实现如下:

struct buffer_head {
    char * b_data;            /* pointer to data block (1024 bytes) */
    unsigned long b_blocknr;    /* block number */
    unsigned short b_dev;        /* device (0 = free) */
    unsigned char b_uptodate;
    unsigned char b_dirt;        /* 0-clean,1-dirty */
    unsigned char b_count;        /* users using this block */
    unsigned char b_lock;        /* 0 - ok, 1 -locked */
    struct task_struct * b_wait;
    struct buffer_head * b_prev;        // hash表的上一个指针    
    struct buffer_head * b_next;        // hash表的下一个指针
    struct buffer_head * b_prev_free;    // 空闲链表的上一个指针
    struct buffer_head * b_next_free;    // 空闲链表的下一个指针
};

可以很清楚看到缓存头结构体中设计HASH表中节点链表的前后指针。那么也可以清楚的看到还有空闲链表的前后指针。那么空闲链表的作用是什么呢?

此时我们需要思考,硬盘大小?高速缓存的大小?很明显这两的大小并不一致,甚至大小差距特别的大。所以缓存头的数量是有限的。也就是当磁盘的元数据信息做HASH运算后得到的HASH表节点链表中不存在对应的缓存头。所以就需要来一个空闲链表。用来存储当前所有的缓存头。当磁盘的元数据信息做HASH运算过后没有的到HASH表中的数据,就从空闲链表中获取到最理想的缓存头来做缓存数据。其次这个从空闲链表得到缓存头会加入到HASH表中(目的是为了下次加速,因为HASH的时间复杂是O1)。这段话只是描述一个大概,后续会在sys_write系统调用中仔细讲解。HASH表和空闲队列的结构如下图所示。

图片选自赵炯博士<<Linux完全注释>>

 

总结:

本文章是对文件系统的设计做一个初步的认识,后续会对文件系统中系统调用做一个详细讲解。

最后,如果本帖对您有一定的帮助,希望能点赞+关注+收藏!您的支持是给我最大的动力,后续会一直更新各种框架的使用和框架的源码解读~!

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员李哈

创作不易,希望能给与支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值