xv6源码阅读——文件系统
说明
- 阅读的代码是 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对应一个扇区号)以及日志块的计数
- 磁盘上的头块中的计数或者为零,表示日志中没有事务;
- 非零,表示日志包含一个完整的已提交事务 ;
- 头块包含一个扇区号数组(每个logged block对应一个扇区号)以及日志块的计数
- 日志驻留在超级块中指定的已知固定位置。它由一个头块(header 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源码阅读——文件系统相关推荐
- xv6源码阅读——中断与异常
目录 说明 陷入机制概述 Traps from user space 调用逻辑 ecall uservec usertrap usertrapret userret sret Traps from k ...
- SpringMVC源码阅读:过滤器
SpringMVC源码阅读:过滤器 目录 1.前言 2.源码分析 3.自定义过滤器 3.1 自定义过滤器继承OncePerRequestFilter 3.2 自定义过滤器实现Filter接口 4.过滤 ...
- Mybatis源码阅读(一):Mybatis初始化1.1 解析properties、settings
*************************************优雅的分割线 ********************************** 分享一波:程序员赚外快-必看的巅峰干货 如 ...
- 封装成jar包_通用源码阅读指导mybatis源码详解:io包
io包 io包即输入/输出包,负责完成 MyBatis中与输入/输出相关的操作. 说到输入/输出,首先想到的就是对磁盘文件的读写.在 MyBatis的工作中,与磁盘文件的交互主要是对 xml配置文件的 ...
- [Linux] USB-Storage驱动 源码阅读笔记(一)
USB-Storage驱动 源码阅读笔记--从USB子系统开始 最近在研究U盘的驱动,遇到很多难以理解的问题,虽然之前也参考过一些很不错的书籍如:<USB那些事>,但最终还是觉得下载一份最 ...
- Octopus 源码阅读(一)
Octopus 源码阅读--fs部分 开源代码 bitmap.cpp bitmap中的代码基本上没啥好说的,比较清楚.不过不解的是为什么在初始化的时候要统计freecount,理论上buffer不是应 ...
- XV6源码解读:安装与编译
系列文章目录 第一章:XV6源码解读:安装与编译 文章目录 系列文章目录 一.Xv6介绍 二.编译 1. 从`make qemu`开始 2. 编译`kernel`生成可执行文件 2.1 链接器脚本:` ...
- Android源码阅读---init进程
Android源码阅读-init进程 文章目录 Android源码阅读---init进程 1. 编译命令和进程入口 1. init 进程编译命令 2. main函数流程 2. 主函数处理流程 1. 创 ...
- redis源码阅读-持久化之RDB
持久化介绍: redis的持久化有两种方式: rdb :可以在指定的时间间隔内生成数据集的时间点快照(point-in-time snapshot) aof : 记录redis执行的所有写操作命令 根 ...
最新文章
- HttpClient(4.5.x)正确的使用姿势
- 记坑 ----- Arrays.sort()
- Spring velocity 中文乱码 解决方案
- session 拦截器中获取不到session值_拦截器实现登陆验证
- dfs.datanode.directoryscan.throttle.limit.ms.per.
- 单例模式(Singleton mode)实战讲解
- PDF虚拟打印机怎么虚拟打印
- iOS 审核被拒绝问题汇总
- Linux | Shell脚本从入门到实战
- VIM-Plug安装插件失败,更换源
- 保研之路——复旦计算机学院预推免
- 这是一篇系统的追热点方法论
- 自动驾驶过冬,需要点燃“降本增效”的炉火
- 入冬的寒冷让人更精神
- 百度搜索稳定性问题分析的故事
- VOT-toolkit Python 版本使用教程--官方样例版
- 国密sm2 js加密后台解密,sm3 js、后台加密,sm4 后台加密
- 最新Android 9.0 Pie,你想知道的都在这了
- .NET类比学JAVA之访问SqlServer数据库
- 【Ajax】了解Ajax与jQuery中的Ajax