XV6 RISCV 源码阅读之文件系统
六、文件系统
xv6的文件系统分为七层,由低到高如下所示。
- 磁盘层读取和写入virtio硬盘上的块。
- BufferCache层缓存磁盘块,同步对他们的访问。
- 日志层允许更高层在一次事务中将更新包装到多个块,并在崩溃时自动更新这些块。
- i节点层提供单独的文件,每个文件表示为一个i节点,包含索引号和一些保存数据的块。
- 目录层将每个目录实现为一种特殊的i节点,包含一系列目录项。
- 路径名层提供分层路径名,并且可以递归查找。
- 文件描述符通过文件系统抽象接口简化了对资源的操作。
6.1代码阅读和机制
6.1.1 磁盘层
磁盘驱动的代码在virtio_disk.c中,驱动的磁盘是fs.img,代码中充满了大量的宏。通过一个内存中disk结构来管理磁盘上的分配和撤销。
图中展示了磁盘的物理存放结构。文件块0被保留,用于储存引导扇区,文件系统不使用。块1为超级块,包含文件系统的元数据和一些用于构建操作系统的代码(mkfs)。块2保存日志log,接下来是inode,bitmap(跟踪正在使用的数据块)。其余的块是数据块,要么在位图中标记为空闲,要么保存目录或文件的内容。
5.1.2 BufferCache层
BufferCache有两个任务,一是同步对磁盘块的访问,确保每个磁盘块在内存中只有一个副本,且一次只有一个内核线程使用这个副本;二是缓存常用块,加快访问速度。主要代码在bio.c中。
bread通过调用bget找到某个设备上的特定block,valid为1说明已经缓存,否则需要virtio_disk_rw来读磁盘,最终返回一个可以在内存中读写的上锁的副本。bget在找块时,扫描缓冲区列表,如果已经缓存就直接返回,如果未缓存,就会在内存中分配一个新的buf,bget返回时会获取buf结构的锁,返回锁定的buffer。
bwrite会在b->lock的保护下将块的内容写回磁盘。在bread读入时加的锁,在此处写回时解锁。brelse释放锁时,将buf结构b加入bcache链表的头部,表明它刚刚被使用过,不应该替换。
5.1.3 logging层
日志层是为了解决文件系统操作期间发生崩溃导致读写混乱的问题,xv6系统调用不直接对磁盘上的文件系统进行操作,而是在磁盘上的日志中描述需要进行的磁盘读写,在系统调用写完日志后,就会写入一条commit记录,表明日志包含一个完整的操作。然后,系统调用将写操作复制到磁盘上的文件系统数据结构,完成写入后系统调用将擦除日志。
如果系统在commit之前崩溃,那么便不会被标记为已完成,恢复代码会忽略它;如果在已经提交后崩溃,那么恢复代码会恢复所有的写入操作,可能会从头开始进行一遍写的操作。日志使操作在崩溃时原子化了,不会出现只完成一半操作的情况。
日志驻留在超级块中,由一个头块和一些列logged block组成,头块包含扇区号数组,对应每个logged block,以及日志块的技术,头块中计数为0表示没有事务,为非0则表示日志包含一个完整的commit,将logged block写入磁盘后计数又会归零。如果在事务中途崩溃,会导致日志头块计数为0,提交后崩溃则计数非0。
在log.c中定义了log数据结构和相关代码。
begin_op会等待直到log中没有正在commit的事务并且有充足的blocks,然后讲系统调用数+1并返回。
log_write()会把buffer的blockno保存到log中,在找到一个同一磁盘块的拷贝时,会直接覆盖(反正写进去也要被覆盖)。再将buffer的引用数+1。
end_op()首先减少未完成系统调用的计数,如果所有都已经完成,则提交当前事务。首先write_log将修改的每个块从BufferCache写入日志对应的位置;write_head将头写入磁盘;install_trans从日志中读取块写入文件系统;最后end_op写入计数为0的日志头。必须在下一个事物写入日志块之前发生。
recover_from_log读取日志头,如果有已提交的日志,它也会写入文件系统并将log归零。
5.1.4 inode层
inode可能指磁盘上的数据结构,包含文件大小和数据块编号列表;也可能指内存中的inode,包含磁盘上inode的副本和内核中需要的额外信息。
ondisk的inode内容如下
- //kernel/fs.h
- // On-disk inode structure
- struct dinode {
- short type; // File type
- short major; // Major device number (T_DEVICE only)
- short minor; // Minor device number (T_DEVICE only)
- short nlink; // Number of links to inode in file system
- uint size; // Size of file (bytes)
- uint addrs[NDIRECT+1]; // Data block addresses
- };
nlink为引用数,size为文件大小,addrs为文件所在数据块的地址。
in-memory的inode内容如下
- //kernel/file.h
- // in-memory copy of an inode
- struct inode {
- uint dev; // Device number
- uint inum; // Inode number
- int ref; // Reference count
- struct sleeplock lock; // protects everything below here
- int valid; // inode has been read from disk?
- short type; // copy of disk inode
- short major;
- short minor;
- short nlink;
- uint size;
- uint addrs[NDIRECT+1];
- };
除了上文内容外,还有判断是否应该释放的ref,判断是否从磁盘中读的valid。
函数iget会返回一个inode,如果是新使用的,会把valid置0以表示尚未从磁盘中读入数据,之后会在ilock中上锁并读入数据设置其他变量。
iput通过减少引用计数(ref)释放指向inode的c指针,如果引用已经减少到0,则inode的槽将变为空闲的,可以重新被其他inode使用。
ilock将锁定inode并从磁盘读取,iunlock释放锁,将inode指针的获取和锁定分离有助于在某些情况避免死锁。
inode是直写的,修改已缓存的inode会立即用iupdate写入磁盘。iupdate将对应的数据进行写入,然后拷贝addrs,使用log_write进行一次commit,最后用brelse释放锁,改变最近使用情况(头插法)。
可能出现的情况:nlink=0,ref不为0,这时inode没有被释放,ref被减少到0的时候系统崩溃,这块空间并没有被释放,因此可能磁盘空间耗尽。
5.1.5 目录层
目录的实现和文件相似,但i节点的type是T_DIR,其数据为一系列目录条目,每个条目是一个dirent结构,其中储存了字符串即文件目录的名称,和inode编号。
- //kernel/fs.h
- struct dirent {
- ushort inum;
- char name[DIRSIZ];
- };
dirlookup寻找指定name的文件,返回inode指针。
dirlink按照参数中的name和inum创建一个新的目录,如果有空闲的就写入,如果已经存在就报错。
5.1.6 路径名层
路径名查找通过一系列dirlookup实现。
Namei计算path并返回相应的inode。
nameiparent返回给定文件父节点的inode。
namex是上面两个函数的主体部分,首先根据path起始字符判断是绝对路径还是相对路径,返回查找的起始inode,然后递归查询,直到下一级不存在,最终返回inode。
5.1.7 文件描述符层
经过以上6层抽象以后,xv6以更加简洁的方式提供了文件的接口即进程描述符。每一个进程都有自己的进程描述符表,所有打开的文件保存在全局的文件表中。
- //kernel/file.h
- struct file {
- enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
- int ref; // reference count
- char readable;
- char writable;
- struct pipe *pipe; // FD_PIPE
- struct inode *ip; // FD_INODE and FD_DEVICE
- uint off; // FD_INODE
- short major; // FD_DEVICE
- };
fileinit初始化锁。
filealloc从文件表ftable中找到一个空闲的文件,增加其引用并返回。
filedup增加指定文件的引用数;fileclose则减少文件引用数。
filestat通过stati获取文件元数据。
fileread对pipe、device和inode分别采取不同的方式进行读取。filewrite与其相似。
XV6 RISCV 源码阅读之文件系统相关推荐
- XV6 RISCV 源码阅读报告之 进程调度
xv6阅读之进程调度 四.进程调度 在计算机资源不足以同时运行所有进程时,就需要操作系统考虑如何分配有限的资源,如CPU时间和内存.xv6通过切换每个CPU上的进程实现多路复用.当进程等待设备或管道I ...
- XV6 RISCV源码阅读报告之 锁
二.xv6的锁 为了在多处理器上防止多个CPU操作同一片地址空间互相干扰引起的错误,以及即使是在单个处理器上防止中断处理程序与非中断代码之间互相干扰,xv6使用锁来实现互斥. 2.1代码阅读与分析 x ...
- XV6 RISCV源码阅读报告之中断
一.xv6中断异常 在操作系统运行用户进程时处于用户态,当发生异常.中断或陷入时,需要从用户态进入内核态调用处理机制.陷入内核之前需要保存上下文,而在处理完成,回到用户态时需要恢复上下文.代码包括tr ...
- XV6 RISCV源码阅读之 虚拟内存
五.内存管理 通过进程线程模型,xv6为每个程序提供了独占处理器的错觉,而通过虚拟内存,xv6为程序提供了统一的0-MAXVA地址的虚拟内存,并通过页表进行内存的管理.RISCV页表通过将每个虚拟地址 ...
- XV6 RISC-V 源码阅读报告之进程模型
三.xv6进程模型 进程是对运行程序的抽象,通过CPU调度和虚拟地址等机制,为每个程序提供了独占处理器和内存空间的错觉. 3.1代码阅读 代码主要在proc.h和proc.c 3.1.1proc.h ...
- xv6操作系统源码阅读之init进程
首先看main函数,里面调用了一个userinit函数,这就是启动第一个init进程. // Bootstrap processor starts running C code here. // Al ...
- 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配置文件的 ...
最新文章
- 安装ATi显卡驱动后增加的鼠标右键菜单的清理
- 对话吴恩达:AI火得还不够,997都满足不了我
- Ansible 入门指南 - ansible-playbook 命令
- MySQL数据单个数据太大,导入不进去
- c#编译器对byte类型的一些规则 (转)
- 计算机网络知识点3——数据交换(报文交换、分组交换)
- 【代码示例】 一个简单的Java死锁
- linux忆连软件,linux安装软件方法汇总
- tcpdump 不显示指定ip_wordpress首页不显示指定分类文章的方法
- solr6.5的分词
- FCKEditor报java.lang.NullPointerException
- Docker安装以及docker run hello-world 不能下载镜像报错
- 多系统导航电文下载与分析
- SSH基础:ssh首次连接的公钥认证
- SpringMVC入门案例【三层架构和MVC、SpringMVC的概述和入门程序】(超详细)
- postgresql的下载与安装
- CSAPP实验2:bomblab
- 第4章 需求分析和model设计
- matlab 网状图,Matlab如何画3维网状图
- 文件管理助手函数升级