首页 > 其他分享 >MIT 6.S081 Lec13: File system

MIT 6.S081 Lec13: File system

时间:2023-07-25 12:37:34浏览次数:28  
标签:blocks log buffer 写入 system 磁盘 S081 MIT block

Overview

文件系统的设计目标就是组织和存储数据,文件系统一个比较重要功能是持久化,即重启之后,数据不会丢失。xv6 通过把数据存储在 virtio disk 上来实现持久化

文件系统设计的几大挑战:

  • The file system needs on-disk data structures to represent the tree of named directories and files, to record the identities of the blocks that hold each file’s content, and to record which areas of the disk are free.
  • 由于文件系统需要实现持久化,因此必须要实现 crash recovery,即如果发生意外的 crash(例如断电),文件系统在计算机重启之后要能依旧正常工作;
  • 可能有多个进程同时操作文件系统;
  • 由于访问磁盘比访问内存要慢得多得多,因此文件系统需要能支持将部分 popluar 的 blocks 缓存在内存中;

xv6 的文件系统可以说组织为七层,如下图所示:

disk layer 负责读写 virtio hard drive 上的 blocks,buffer cache layer 是 blocks 的 cache,并且保证同一时间,只有一个内核进程可以修改存储在特定块上的数据;logging layer 将对几个特定 block 的更新打包为一次 transaction(就是数据库常说的事务?),从而确保这些 blocks 都是被原子化地更新,即要么一次都更新,要么一次都不更新;inode layer 则是用来表示单独的文件,每个文件都是以具有不重复的 index 的 inode 和保存了这个文件的数据的一些 blocks 来表示;而在 directory layer,每个 directory 都是一种特殊的 inode,包含一系列 direcotry entry,directory entry 则是包含了文件名和 index(对应 indode layer 所说的 index);pathname layer 提供了层级化的路径名,利用递归查找来解析它们;

disk hardware 一般将磁盘划分为 $512$ bytes 的 block 的序列(这个 $512$ bytes 的 block 又称为 sector(扇区))。xv6 使用的 block 的大小一般是 sector 大小的整数倍。

xv6 将磁盘划分为几个功能部分,如下图所示:

4wINxhLbqZXPMBE

Buffer cache layer

buffer cache 的实现代码在 kernel/bio.c, 它主要有两个任务:

  1. 同步进程对磁盘的访问,内存中只有一份磁盘的 block 的拷贝,并且同一时间内只有一个进程可以使用这份拷贝(同时读呢,是否可行?);
  2. 缓存 popluar blocks 到内存中,从而可以直接从内存访问磁盘的 popular blocks,避免再从速度极其慢的磁盘中读取;

这里记 struct buf 的一个实例为 buffer

buffer cache 的主要接口是 breadbwritebread 返回一个存储于内存中的 buffer(会被加锁),我们可以读取或者修改这个 buffer,bwrite 负责将一个修改过的 buffer 写回到磁盘中合适的 block 去,当完成 bwrite 时,内核线程需要调用 brelse 来释放 buffer 的锁(是一个 sleep-lock,可以理解为与自旋锁相对?)

bread 会返回带锁的 buf,因为 bread 调用 bget,而 bget 会加锁,加的是 sleep-lock,相当于 pthread 中的互斥锁(与自旋锁 spin-lock 相对); 使用 sleep-lock 是因为磁盘相关的操作,可能会消耗比较长的时间;

那些 poplular 的 blocks 其实被缓存在一个 buffer 的双向链表里面(这个双向链表是首尾相接的),双向链表的长度是有限的,这就意味着需要有缓存淘汰机制,xv6 中,这里使用了 LRU,head->prev 应该是 least recently used,而 head->next 则是 most recently。这里的头结点 head 应该是虚拟头结点?TODO(zwyyy)

Code: Buffer cache

buffer cache 实际上是 buffer 的双向链表,kernel/main.cmain 函数调用 binit,使用静态数组 buf 中的 NBUFstruct buf 来初始化这个链表,数组只用来初始化,之后访问 buffer 都是通过双向链表都是通过 bcache.head,也就是说这个数组只用来初始化链表!

buffer 中含有两个状态字段,valid 说明这个 buffer 含有磁盘的 block 的拷贝,因此是有效的,disk 字段为 $1$ 说明 buffer 已经写回了 disk。

bread 可以读取指定 block 对应的 buffer,如果返回的 buffer 的 valid 字段为 $0$,说明 get 是利用 lru 机制选择了一个 victim buffer,因此我们需要调用 virtio_disk_rw(b, 0) 将磁盘的对应 block 读入这个 buffer。

bget 遍历 buffer 链表寻找与 devblockno 对应的 buffer,并为这个 buffer 申请 sleep-lock,返回未解锁的 buffer;如果找不到对应的 buffer,bget 会从 head 开始,反向遍历双向链表,找一个 refcnt 为 $0$ 的 buffer,将这个 buffer 的 valid 字段标记为 $0$,即说明我们要重新从磁盘对应的 sector 读取数据到 buffer。

bget 持有 bache.lock 保证了“寻找对应的 buffer 以及如果不存在,为 block 分配对应的 buffer” 这个过程是原子的。

b->refcnt = 1 保证了这个 buffer 不会被其他进程拿来作为 victim buffer 去存放其他 block 的数据,因此可以先释放 bcache.lock 再申请 b->lock

The sleep-lock protects reads and writes of the block’s buffered content, while the bcache.lock protects information about which blocks are cached.

如果 bread 的调用者通过 bread 拿到了这个 buffer,那么通过 b->lock,它享有了对这个 buffer 的独占权,如果它修改了这个 buffer 的内容,那么再释放 b->lock 之前,它必须调用 bwrite 将 buffer 的被修改的内容写回到 disk。

调用者完成对 buffer 的处理之后,必须调用 brelseb->lock 释放。

Logging layer

file system 设计的一大重要问题就是 crash recovery。这是因为文件系统操作往往涉及向磁盘多次写入,而几次写入之后的 crash 可能导致磁盘上的文件系统处于一个不一致的状态。

For example, suppose a crash occurs during file truncation (setting the length of a file to zero and freeing its content blocks). Depending on the order of the disk writes, the crash may either leave an inode with a reference to a content block that is marked free, or it may leave an allocated but unreferenced content block.

前者当系统重启之后,可能导致一个磁盘 block 被两个文件所对应,这是一个很严重的问题。

xv6 的解决方法是使用 logging(日志)。xv6 的系统调用不会直接向 on-disk file system data structure(例如 buffer 以及磁盘 block?)写入,而是将它所有希望执行的磁盘写入描述放在磁盘的某个 log 中,once the system call has logged all of its writes, it writes a special commit record to the disk indicating that the log contains a complete operation(这里不知道怎么翻译好了)。此时,系统调用才将 disk write 应用到 on-disk file system data structure。当所有的写入操作完成后,系统调用会清除磁盘上的 log。

如果系统崩溃重启,那么 file-system 代码会在运行进程之前,先检查磁盘上的 log。如果 log 被标记为包含完整的操作,那么 file-system recovery code 会将 log 中的写操作应用到对应的 on-disk file system;否则 file-system recovery code 会直接清除 log,不执行写入操作。

如果 file system 恢复时看到 log 被标记为不包含完整操作,那么说明 log 中的写入操作都还没有应用到磁盘上,即磁盘没有发生真正的写入,因此直接清除 log 即可,不需要执行任何写入操作。

log 使得这些写入操作对 crash 来说是原子的,要么都没写入,要么全都写入到磁盘了。

Log design

log 存在于磁盘的 superblock 中,它由一个 header block 和后面的一系列 updated block copies(更新块的副本,又称 logged blocks) 组成。header block 包含一个扇区号(sector index)的数组,每个 sector index 对应一个 logged block,还包含着 log blocks 的数量。header block 中的计数要么为 $0$,说明 log 中不存在 transaction,要么不为 $0$,说明 log 包含一个完整的已、已提交的 transaction,并包含指定数量的 logged blocks。

xv6 在 transaction 提交时,向 header block 写入数据,在将 logged blocks 复制到 file system 之后,将 header block 的计数置为 $0$。

为了允许 file system 的操作被不同进程并发执行,日志系统(logging system)会将多个系统调用的写操作合并成一个 transaction。因此,单次的 commit 可能涉及多个完整的系统调用的写操作,为了避免一次系统调用被划分成两次 transaction,日志系统只会在没有 file-system 系统调用的时候进行 commit。

一次一起 commit 多个 transaction 的方法被称为 group commit,group commit 能通过平摊一次提交的开销到多次操作上来降低所需的磁盘操作数。

xv6 在磁盘上标识出了一段固定大小的空间用来存放 log。因此,系统调用要写入的 blocks 必须能被这块空间容纳。对 writeunlink 系统调用来说,这个限制可能会导致两个问题:

  1. 假设 write 要写一个大文件,那么可能要写入许多 data blocks、bitmap blocks 和 inode blocks,unlink 一个大文件可能要写入许多 bitmap blocks 和 inode blocks。为了解决这一问题,xv6 的 write 会将一次大的写入划分成多个更小的适合 log 的写入操作,由于 xv6 只使用一个 bitmap lock,因此 unlink 不会造成问题;
  2. 除非能确定系统调用的写入能容纳于 log 的剩余空间中,否则系统不会允许系统调用执行;

Code: logging

系统调用中的 log 的使用一般如下图所示:

rJgukCV9DnRGfaE

begin_op wait until the logging system is not currently commiting, and until there is enough unreserved log space to hold the writes from this call. log.outstanding 会统计预订了 log space 的系统调用的数量,被预订的总空间就是 log.outstanding 乘上 MAXOPBLOCKS。递增 log.outstanding 会预订 log space,并且防止在此系统调用期间发生 commit。 xv6 保守地预计每个系统调用都会写入 MAXOPBLOCKS 个不同的 block。

log_write 表现得就像是 bwrite 的代理那样。它将 block 的 sector number 记录在内存中,在 disk 的 log 部分给这个 block 预留的了一个槽位,并调用了 bpin(b) 来保证 buffer b 一定不会被 evict。

调用 bpin(b) 之后,哪怕 buffer 缓存不足时,buffer b 也不会被换出,它修改了 buffer b 的 refcnt,之所以要这样设置,是因为假设 buffer b 中途被换出到磁盘,就违背了“必须 commit 之后再将写入应用到磁盘”的设定了,也就破坏了磁盘写入相对于 crash 的原子性。

void log_write(struct buf *b) {
    int i;

    acquire(&log.lock);
    if (log.lh.n >= LOGSIZE || log.lh.n >= log.size - 1)
        panic("too big a transaction");
    if (log.outstanding < 1)
        panic("log_write outside of trans");

    for (i = 0; i < log.lh.n; i++) {
        if (log.lh.block[i] == b->blockno) // log absorption
            break;
    }
    log.lh.block[i] = b->blockno;
    if (i == log.lh.n) { // Add new block to log?
        bpin(b);
        log.lh.n++;
    }
    release(&log.lock);
}

在 commit 之前,block 必须要缓存在 buffer 上, 这是因为 buffer 唯一地记录着我们对这个 block 的修改;只有在 commit 之后,才可以将 buffer 写入磁盘上的位置;同一 transaction 的假设要读取这个 block,那么它必须能看到对这个 buffer 的修改。log_write 会注意到一个 transaction 中,多次写入同一个 block 的情况,如果是写入同一个 block,那么就不会添加新 block 到 log 中,而是在 log 中为该 block 分配相同的槽位。这种优化通常被称为 absorption(合并),这样的话,从 buffer 写入到 log 的槽位只会发生一次,即使写入 buffer 发生了很多次。

end_op 首先会减少未完成的系统调用的计数(说明有系统调用完成了),如果计数被递减到了 $0$,它就会通过调用 commit() 来提交当前的 transaction。这个过程分为四个阶段:

  1. write_log 将每个在 transaction 被修改的 block 的 buffer 复制到 log 对应的槽位中(应该是复制到了 log 的 buffer 的对应的槽位中)
  2. write_head 将 header block 从 buffer 写到磁盘中,而这就是 commit 的关键节点:
    • 当完成 header block 的从 buffer 写入到磁盘后,如果发生了 crash,那么恢复程序会从 log 中执行该次事务的写操作,将数据从 log 的 buffer 写入到磁盘;
  3. install_trans 会从 log 中读取每个 block,然后写入到文件系统(磁盘);
  4. end_op 将 log header 的计数重置为 $0$(说明 log 中不存在 transaction),这会在下一个 transaction 要写入 logged blocks 之前完成,这保证了 crash 恢复时,不会将前一个 transaction 的 header 与下一个 transaction 的 logged blocks 混在一起恢复,即保证了一次恢复不会涉及两个 transaction。

标签:blocks,log,buffer,写入,system,磁盘,S081,MIT,block
From: https://www.cnblogs.com/zwyyy456/p/17579571.html

相关文章

  • [踩坑] WSL2 Ubuntu20.04启用systemctl
    一、安装fontconfigdaemonizesudoaptinstall-yfontconfigdaemonize二、修改/etc/profile#在末尾添加以下内容SYSTEMD_PID=$(ps-ef|grep'/lib/systemd/systemd--system-unit=basic.target$'|grep-vunshare|awk'{print$2}')if[-z"$SYST......
  • Unity UGUI的EventSystem(事件系统)组件的介绍及使用
    UnityUGUI的EventSystem(事件系统)组件的介绍及使用1.什么是EventSystem组件?EventSystem是UnityUGUI中的一个重要组件,用于处理用户输入事件,如点击、拖拽、滚动等。它负责将用户输入事件传递给合适的UI元素,并触发相应的事件回调函数。2.EventSystem组件的工作原理EventSystem......
  • MIT 6.5840 Raft Implementation(2B, Log Replication)
    Raft实现思路+细节(2B)任务分解2B中最主要的任务就是进行日志的复制。Raft是一个强领导人的系统,这意味着所有的日志添加都是由领导人发起的,与之相类似的,还有很多其他的结论(它们都是比较显然的),读者可以自行证明。我们可以这样地分解复制日志的过程我们首先需要完善Raft结构......
  • Mit 6.824 学习记录
    MapReduce实验干嘛实现一个分布式的MapReduce,由两部分组成,master和worker。一个master,多个worker。在本机运行,worker和master用rpc通信。每个worker向master索要任务,从一个或多个文件读取任务的输入,执行任务,并将任务的输出写入一个或更多文件。如果超时(10s)将工作......
  • Mac环境下,在 VS Code下执行Run Code打印Operation not permitted
    步骤1。打开系统设置;步骤2。选择隐私与安全性;步骤3。选择完全磁盘访问权限;步骤4。添加VisualStudioCode,输入完管理员密码后重启VSCode。......
  • System.NullReferenceException[转] 解决方法总和
    “System.NullReferenceException:未将对象引用设置到对象的实例”问题可能原因如下:1、ViewState对象为Null。2、DateSet空。3、sql语句或Datebase的原因导致DataReader空。4、声明字符串变量时未赋空值就应用变量。5、未用new初始化对象。6、Session对象为空。7、对控件赋文本......
  • TortoiseGit合并commit的一条记录到另一个分支
    背景:git仓库有2个分支,分别是master和develop两个分支。多人协作的代码分支为develop,正式发布的分支为master,现要指定develop分支下的commit合并到master分支中。 第一、把本地代码切换到master分支如果没有克隆master分支,则先克隆,克隆完成后在master下操作下述步骤;如果没有Git......
  • MIT 6.5840 Raft Implementation(2A, Leader Election)
    Raft实现思路+细节2A任务分解总体来说,2A中主要的任务就是选出领导人,在选出领导人的时候,我们要遵循下图。在2A中,由于并没有出现日志复制,所以我们只需要考察两者的任期是否相等,以及接收者在本轮任期中有没有投票即可。因而我们可以这样地给出2A中的实现内容:完善GetState()......
  • No suitable Java Virtual Machine could be found on your system. The version
    Java虚拟机简介与安装什么是Java虚拟机?Java虚拟机(JavaVirtualMachine,简称JVM)是一种能够运行Java字节码的虚拟机。它是Java语言的核心,提供了跨平台的特性,使得一次编写的Java代码可以在不同的操作系统上运行。JVM有两个主要的任务:将Java源代码编译成字节码。在各个操作系统上......
  • appsmith使用第三方库进行http请求
    安装使用exportdefault{ debugMeter:async()=>{ letrID=Number(Select1.selectedOptionValue); letmetricName=Input6.text; letmeterAsset=Input7.text; letbusAddr=Input8.text; letmeterAddr=Number(Input9.text); letregisterAddr=......