说明

  • 阅读的代码是 xv6-riscv 版本的

七层结构

xv6文件系统实现分为七层

文件描述符(File descriptor)
路径名(Pathname)
目录(Directory)
索引结点(Inode)
日志(Logging)
缓冲区高速缓存(Buffer cache)
磁盘(Disk)
  • disk 层从一个虚拟硬盘(vitro hard drive)上读取 block
  • buffer cache 层缓存 block
    保证同一个 block 只能被一个内核进程同时访问
  • logging 层记录一些日志信息,用于出错恢复
  • inode 层是一个独立的文件,唯一的 i-number 和一些存储数据的块
  • directory 层把文件夹封装称为一些特殊的 inode
    内容为一些列目录项,包括文件名和 i-number
  • pathname 层提供一个层次化的树结构,处理了递归包含的情况
  • file descriptor 层把 pipes, devices, files 等都抽象为文件,使用统一的文件系统的接口

磁盘(Disk)

文件系统必须有将索引节点和内容块存储在磁盘上哪些位置的方案。为此,xv6将磁盘划分为几个部分,如图8.2所示。

  • boot:文件系统不使用块0(它保存引导扇区)

  • super:它包含有关文件系统的元数据(文件系统大小(以块为单位)、数据块数、索引节点数和日志中的块数)。

  • log:保存日志(用于出现crash后进行数据恢复)

  • inodes: 是索引节点,每个块有多个索引节点

  • bit map:位图,跟踪正在使用的数据块,用来保存那些块是被使用或空闲的。

  • data:剩下的就是数据块,是否被占用保存在bitmap中

缓冲区高速缓存(Buffer cache)

设计概述

  • buffe catch 的两个任务

    • 1.同步对磁盘块的访问,以确保磁盘块在内存中只有一个副本,并且一次只有一个内核线程使用该副本
    • 2.缓存常用块,以便不需要从慢速磁盘重新读取它们
  • 对上提供的接口

    • bread() ,获取一个磁盘块,拷贝到缓存中
    • bwrite(),将缓存中的数据写回磁盘
    • brelse(),用完需要释放缓存快
  • 通过给每一个 buffer 分配一个 sleeplock 的方式实现一个磁盘块的拷贝只能被一个内核线程使用

  • Buffer cache中保存磁盘块的缓冲区数量固定,如果文件系统请求还未存放在缓存中的块,Buffer cache必须回收当前保存其他块内容的缓冲区。Buffer cache为新块回收最近使用最少的缓冲区。

    • 通过 virtio_disk_rw(b, 0) 实现

缓冲区高速缓存(代码实现)

Buffer cache是以双链表表示的缓冲区(为了实现LRU替换算法)

  • 每一个buf结构体如下

    • valid 字段表示这个 buffer 是否对应磁盘上的某一个块(1 表示有对应)
    • disk 字段表示是否将 buffer 中的信息写回了磁盘上(1 表示修改了未写回)
    • refcnt 表示引用计数
    • blockno 扇区号
    • dev 不同设备
struct buf {int valid;   // has data been read from disk?int disk;    // does disk "own" buf?uint dev;uint blockno;struct sleeplock lock;uint refcnt;struct buf *prev; // LRU cache liststruct buf *next;uchar data[BSIZE];
};

main(kernel/main.c)调用的函数binit使用静态数组buf(kernel/bio.c)中的NBUF个缓冲区初始化列表。对Buffer cache的所有其他访问都通过bcache.head引用链表,而不是buf数组。

struct {struct spinlock lock;struct buf buf[NBUF];// Linked list of all buffers, through prev/next.// Sorted by how recently the buffer was used.// head.next is most recent, head.prev is least.struct buf head;
} bcache;

binit函数

void
binit(void)
{struct buf *b;initlock(&bcache.lock, "bcache");// Create linked list of buffersbcache.head.prev = &bcache.head;bcache.head.next = &bcache.head;for(b = bcache.buf; b < bcache.buf+NBUF; b++){b->next = bcache.head.next;b->prev = &bcache.head;initsleeplock(&b->lock, "buffer");bcache.head.next->prev = b;bcache.head.next = b;}
}
  • 对buf进行初始化,构建双向循环链表(NBUF个节点)
  • 为bcache初始化一把大锁(bcache),再为每一个节点初始化一把小锁(buffer)

bget函数

bget扫描缓冲区列表,查找具有给定设备和扇区号的缓冲区。

static struct buf*
bget(uint dev, uint blockno)
{struct buf *b;acquire(&bcache.lock);// Is the block already cached?for(b = bcache.head.next; b != &bcache.head; b = b->next){if(b->dev == dev && b->blockno == blockno){b->refcnt++;release(&bcache.lock);acquiresleep(&b->lock);return b;}}// Not cached.// Recycle the least recently used (LRU) unused buffer.for(b = bcache.head.prev; b != &bcache.head; b = b->prev){if(b->refcnt == 0) {b->dev = dev;b->blockno = blockno;b->valid = 0;b->refcnt = 1;release(&bcache.lock);acquiresleep(&b->lock);return b;}}panic("bget: no buffers");
}
  • 若在缓冲区找到了,返回锁定的buf

    • refcnt引用计数要增加1。
  • 若没有找到,它再次扫描缓冲区列表,查找未在使用中的缓冲区(b->refcnt = 0):任何这样的缓冲区都可以使用。
    • 给dev和blockno初始化。
    • valid=0,表示没有对应磁盘上的某一块。
    • 引用计数初始化为1。

bread函数

// Return a locked buf with the contents of the indicated block.
struct buf*
bread(uint dev, uint blockno)
{struct buf *b;b = bget(dev, blockno);if(!b->valid) {virtio_disk_rw(b, 0);b->valid = 1;}return b;
}
  • 调用bget函数从bcatch获取指定buf
  • 若b->valid==0,表示没有对应磁盘上的某一块
    • 调用 virtio_disk_rw(b, 0);从磁盘中将其读入内存(buffer 中)

bwrite函数
该函数作用,将内存(buffer中)的数据写回到磁盘

// Write b's contents to disk.  Must be locked.
void
bwrite(struct buf *b)
{if(!holdingsleep(&b->lock))panic("bwrite");virtio_disk_rw(b, 1);
}

日志(Logging)

设计概述

  • 目的

    解决了文件系统操作期间的崩溃问题

  • 解决方法

    • xv6系统调用不会直接写入磁盘上的文件系统数据结构。相反,它会在磁盘上的log(日志)中放置它希望进行的所有磁盘写入的描述。
    • 一旦系统调用记录了它的所有写入操作,它就会向磁盘写入一条特殊的commit(提交)记录,表明日志包含一个完整的操作。
    • 当完成commit后,再将写操作复制到磁盘当中,然后擦除磁盘上的日志
    • 如果系统崩溃并重新启动
      • 日志标记为包含完整操作,则恢复代码会将写操作复制到磁盘文件系统中它们所属的位置。
      • 如果日志没有标记为包含完整操作,则恢复代码将忽略该日志。
      • 擦除记录
  • log设计

    • 日志驻留在超级块中指定的已知固定位置。它由一个头块(header block)和一系列更新块的副本(logged block)组成。

      • 头块包含一个扇区号数组(每个logged block对应一个扇区号)以及日志块的计数

        • 磁盘上的头块中的计数或者为零,表示日志中没有事务;
        • 非零,表示日志包含一个完整的已提交事务 ;
struct logheader {int n;int block[LOGSIZE];
};

日志(代码实现)

下面看一下log结构体

struct log {struct spinlock lock;int start;int size;int outstanding; // 有多少FS系统调用正在执行调用正在执行int committing;  // 是否在commit当中int dev;struct logheader lh;
};

在系统调用中一个典型的日志使用就像这样:

 begin_op();...bp = bread(...);bp->data[...] = ...;log_write(bp);...end_op();

下面我们就上面过程,进行逐一分析

begin_op函数

  • begin_op等待直到日志系统当前未处于提交中,并且直到有足够的未被占用的日志空间来保存此调用的写入。
void
begin_op(void)
{acquire(&log.lock);while(1){if(log.committing){sleep(&log, &log.lock);} else if(log.lh.n + (log.outstanding+1)*MAXOPBLOCKS > LOGSIZE){// this op might exhaust log space; wait for commit.sleep(&log, &log.lock);} else {log.outstanding += 1;release(&log.lock);break;}}
}
  • 先判断是否处于提交中
  • 在判断是否有足够空间
    (xv6 认为每个系统调用只会使用 MAXOPBLOCKS(10) 个 block)
  • outstanding+1,然后break

log_write函数

  • log_write(kernel/log.c:214)充当bwrite的代理。它将块的扇区号记录在内存中,在磁盘上的日志中预定一个槽位,并调用bpin将缓存固定在block cache中,以防止block cache将其逐出。
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 absorptionbreak;}log.lh.block[i] = b->blockno;if (i == log.lh.n) {  // Add new block to log?bpin(b);log.lh.n++;}release(&log.lock);
}
  • for循环,进行优化,log_write会注意到在单个事务中多次写入一个块的情况,并在日志中这种优化通常称为合并(absorption)。
  • 调用 bpin(b)对b的引用计数++,防止被替换出去。

end_op函数

  • end_op(kernel/log.c:146)首先减少未完成系统调用的计数。如果计数现在为零,则通过调用commit()提交当前事务。
// called at the end of each FS system call.
// commits if this was the last outstanding operation.
void
end_op(void)
{int do_commit = 0;acquire(&log.lock);log.outstanding -= 1;if(log.committing)panic("log.committing");if(log.outstanding == 0){do_commit = 1;log.committing = 1;} else {// begin_op() may be waiting for log space,// and decrementing log.outstanding has decreased// the amount of reserved space.wakeup(&log);}release(&log.lock);if(do_commit){// call commit w/o holding locks, since not allowed// to sleep with locks.commit();acquire(&log.lock);log.committing = 0;wakeup(&log);release(&log.lock);}
}
  • 首先将计数 log.outstanding -1(系统调用数 -1)
  • 如果此时计数变成 0,则调用 commit() 进行 commit

commit函数

commit有四个步骤,代码如下

static void
commit()
{if (log.lh.n > 0) {write_log();     // Write modified blocks from cache to logwrite_head();    // Write header to disk -- the real commitinstall_trans(0); // Now install writes to home locationslog.lh.n = 0;write_head();    // Erase the transaction from the log}
}

write_log()函数

将事务中修改的每个块从缓冲区缓存复制到磁盘上日志槽位中。

// Copy modified blocks from cache to log.
static void
write_log(void)
{int tail;for (tail = 0; tail < log.lh.n; tail++) {struct buf *to = bread(log.dev, log.start+tail+1); // log blockstruct buf *from = bread(log.dev, log.lh.block[tail]); // cache blockmemmove(to->data, from->data, BSIZE);bwrite(to);  // write the logbrelse(from);brelse(to);}
}
  • write_log():把具体的内容写入 log 对应的磁盘块里
    通过调用 bread() 读取磁盘块、memmove() 内存复制、bwrite() 写入磁盘完成
    需要写入的磁盘区域 (\to) 对应的 log 块
  • 注意这里的 log 数组中记录的磁盘块写入对应下标+1的 log 块中,因为第一块留给了 log 的 head

write_head()函数
将头块写入磁盘:这是提交点,写入后的崩溃将导致从日志恢复重演事务的写入操作。

static void
write_head(void)
{struct buf *buf = bread(log.dev, log.start);struct logheader *hb = (struct logheader *) (buf->data);int i;hb->n = log.lh.n;for (i = 0; i < log.lh.n; i++) {hb->block[i] = log.lh.block[i];}bwrite(buf);brelse(buf);
}

write_head():把 log 块的头信息写入磁盘
log 块区域的第一块是 log 的 header 块

install_trans函数

// Copy committed blocks from log to their home location
static void
install_trans(int recovering)
{int tail;for (tail = 0; tail < log.lh.n; tail++) {struct buf *lbuf = bread(log.dev, log.start+tail+1); // read log blockstruct buf *dbuf = bread(log.dev, log.lh.block[tail]); // read dstmemmove(dbuf->data, lbuf->data, BSIZE);  // copy block to dstbwrite(dbuf);  // write dst to diskif(recovering == 0)bunpin(dbuf);brelse(lbuf);brelse(dbuf);}
}
  • install_trans():执行原来要求的写操作

recover_from_log()函数

static void
recover_from_log(void)
{read_head();install_trans(1); // if committed, copy from log to disklog.lh.n = 0;write_head(); // clear the log
}
  • 在启动时会调用这段代码进行恢复

    • 先将磁盘中的日志头读入到内存中的日志头
    • 调用install_trans函数,如果commited,将日志中保存的操作拷贝到磁盘
    • 将内存中的日志头操作数置为0
    • 将内存中的日志头重新写会磁盘

用于crash恢复

块分配器(bitmap)

  • xv6的块分配器在磁盘上维护一个空闲位图,每一位代表一个块。0表示对应的块是空闲的;1表示它正在使用中。

  • 程序mkfs设置对应于引导扇区、超级块、日志块、inode块和位图块的比特位。

  • balloc() 函数从磁盘中找一个空闲的磁盘块并返回(bitmap 中标记为 0)

  • bfree() 函数释放一个指定的磁盘块

索引节点(Inode)

两种含义

  • 磁盘上的数据结构(on-disk),包含文件的大小和一个数据存放位置的列表
  • 内存中的数据结构(in-memory),包括磁盘上数据结构的一个拷贝以及一些内核需要用到的内容

on-disk

  • 磁盘上的 inode 被放置在 inode blocks 中,大小都是相同的,给定一个 n(i-number)就能找到第 n 个 inode 的位置
  • 数据结构如下
    • type:file、directory、special files(devices)、0(表示空闲)
    • nlink:引用数(用于判断数据块是否该被释放)
    • size:文件所占据的字节数
    • addrs:文件所在数据块的地址
// On-disk inode structure
struct dinode {short type;           // File typeshort major;          // Major device number (T_DEVICE only)short minor;          // Minor device number (T_DEVICE only)short nlink;          // Number of links to inode in file systemuint size;            // Size of file (bytes)uint addrs[NDIRECT+1];   // Data block addresses
};

in-memory

struct inode(kernel/file.h:17)是磁盘上struct dinode的内存副本。只有当有C指针引用某个inode时,内核才会在内存中存储该inode。
结构如下:

  • ref:引用计数,如果引用计数降至零,内核将从内存中丢弃该inode。

    • iget和iput函数,修改引用计数
    • 指向inode的指针可以来自文件描述符、当前工作目录和如exec的瞬态内核代码。
  • dev:设备号
  • inum:inode编号
  • valid:表示是否已从磁盘读取
// in-memory copy of an inode
struct inode {uint dev;           // Device numberuint inum;          // Inode numberint ref;            // Reference countstruct sleeplock lock; // protects everything below hereint valid;          // inode has been read from disk?short type;         // copy of disk inodeshort major;short minor;short nlink;uint size;uint addrs[NDIRECT+1];
};
  • 有 4 种锁或类锁的机制

    • icache.lock 保证一个 inode 只会在缓存中出现一次,保证引用计数 ref 的正确
    • 每一个 inode 都有一个 sleeplock,用于保证其内部字段的独占访问
    • ref 不为 0 的时候这个 entry 不会被回收
    • nlink 字段表示这个文件的引用数(他不为 0,xv6 就不会释放 entry)
  • iget() 返回一个 inode

    • 如果是新使用的,valid = 0,之后使用的时候会从磁盘读入(ilock() 实现读入),这块实现和磁盘缓存十分的相似
  • ilock() 是加锁的,iget() 不加锁

  • inode cache 是 write-through(直写的),调用 iupdate() 直接反应到磁盘上

索引节点(代码实现)

分配一个新的 inode 结点(新建文件等)的流程如下

  • 调用 ialloc()

    • 从标号为 1的 inode 的开始查找,找到一个空闲的 inode 之后,调用 iget() 返回
    • 根节点也是从 1 开始的(#define ROOTINO 1)
  • iget()
    • 先看看有没有缓存,若有缓存,则引用数 ref +1,直接返回
    • 如果没有找到,则将第一个空闲的 inode 返回
  • ilock()
    • 想要对 inode 进行读写操作的时候,需要先对 inode 获取锁(调用 ilock)
    • 同时 ilock 会检查这个内容有没有从磁盘冲读进内存,如果没有则读入
  • iunlock()
    • 释放锁
  • iput()
    • 引用数 ref -1
    • 如果减完之后计数为 0,同时 valid=1,nlink=0 的话,释放数据块
  • 可能会出现这样一种情况,nlink=0,ref 不为 0,此时 inode 没有被释放
    • 当 ref 被减为 0 的瞬间,系统崩溃了
    • 重启的时候我们知道 nlink=0 的文件是没有用的,但是我们在磁盘上这块空间并未被释放
    • xv6 没有针对这个问题进行补救,因此可能出现磁盘空间耗尽的情况

目录层(代码实现)

目录的内部实现很像文件。

// kernel/fs.h
struct dirent {ushort inum;char name[DIRSIZ];
};
  • name 最长为 DIRSIZ=14 个字符
  • dirlookup()
    在一个目录中查找指定 name 的文件,返回 inode 指针
  • dirlink()
    按照给定的 name,新建一个新的文件夹
    遍历 entry,找到一个空闲的返回
    如果已经存在,则报错

xv6源码阅读——文件系统相关推荐

  1. xv6源码阅读——中断与异常

    目录 说明 陷入机制概述 Traps from user space 调用逻辑 ecall uservec usertrap usertrapret userret sret Traps from k ...

  2. SpringMVC源码阅读:过滤器

    SpringMVC源码阅读:过滤器 目录 1.前言 2.源码分析 3.自定义过滤器 3.1 自定义过滤器继承OncePerRequestFilter 3.2 自定义过滤器实现Filter接口 4.过滤 ...

  3. Mybatis源码阅读(一):Mybatis初始化1.1 解析properties、settings

    *************************************优雅的分割线 ********************************** 分享一波:程序员赚外快-必看的巅峰干货 如 ...

  4. 封装成jar包_通用源码阅读指导mybatis源码详解:io包

    io包 io包即输入/输出包,负责完成 MyBatis中与输入/输出相关的操作. 说到输入/输出,首先想到的就是对磁盘文件的读写.在 MyBatis的工作中,与磁盘文件的交互主要是对 xml配置文件的 ...

  5. [Linux] USB-Storage驱动 源码阅读笔记(一)

    USB-Storage驱动 源码阅读笔记--从USB子系统开始 最近在研究U盘的驱动,遇到很多难以理解的问题,虽然之前也参考过一些很不错的书籍如:<USB那些事>,但最终还是觉得下载一份最 ...

  6. Octopus 源码阅读(一)

    Octopus 源码阅读--fs部分 开源代码 bitmap.cpp bitmap中的代码基本上没啥好说的,比较清楚.不过不解的是为什么在初始化的时候要统计freecount,理论上buffer不是应 ...

  7. XV6源码解读:安装与编译

    系列文章目录 第一章:XV6源码解读:安装与编译 文章目录 系列文章目录 一.Xv6介绍 二.编译 1. 从`make qemu`开始 2. 编译`kernel`生成可执行文件 2.1 链接器脚本:` ...

  8. Android源码阅读---init进程

    Android源码阅读-init进程 文章目录 Android源码阅读---init进程 1. 编译命令和进程入口 1. init 进程编译命令 2. main函数流程 2. 主函数处理流程 1. 创 ...

  9. redis源码阅读-持久化之RDB

    持久化介绍: redis的持久化有两种方式: rdb :可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot) aof : 记录redis执行的所有写操作命令 根 ...

最新文章

  1. HttpClient(4.5.x)正确的使用姿势
  2. 记坑 ----- Arrays.sort()
  3. Spring velocity 中文乱码 解决方案
  4. session 拦截器中获取不到session值_拦截器实现登陆验证
  5. dfs.datanode.directoryscan.throttle.limit.ms.per.
  6. 单例模式(Singleton mode)实战讲解
  7. PDF虚拟打印机怎么虚拟打印
  8. iOS 审核被拒绝问题汇总
  9. Linux | Shell脚本从入门到实战
  10. VIM-Plug安装插件失败,更换源
  11. 保研之路——复旦计算机学院预推免
  12. 这是一篇系统的追热点方法论
  13. 自动驾驶过冬,需要点燃“降本增效”的炉火
  14. 入冬的寒冷让人更精神
  15. 百度搜索稳定性问题分析的故事
  16. VOT-toolkit Python 版本使用教程--官方样例版
  17. 国密sm2 js加密后台解密,sm3 js、后台加密,sm4 后台加密
  18. 最新Android 9.0 Pie,你想知道的都在这了
  19. .NET类比学JAVA之访问SqlServer数据库
  20. 【Ajax】了解Ajax与jQuery中的Ajax

热门文章

  1. DS树结构转换(先序转双亲)
  2. 机器学习模型调优方法(过拟合、欠拟合、泛化误差、集成学习)
  3. c语言创建文件、文件夹、判断文件内容是否为空
  4. 让小Yi摄像头启用RTSP
  5. PLC中X和Y代表什么
  6. python实现Pyecharts实现动态地图(Map、Geo)
  7. 事业单位编制分为几种?
  8. MySQL8从入门到精通\\数据库和数据表的基本操作
  9. wma转换成mp3格式怎么转?
  10. 2018 年了,你还是只会 npm install 吗