Unix文件系统

当今的Unix文件系统(Unix File System, UFS)起源于Berkeley Fast File System。和所有的文件系统一样,Unix文件系统是以块(Block)为单位对磁盘进行读写的。一般而言,一个块的大小为512Byte或者4KB。文件系统的所有数据结构都以块为单位存储在硬盘上,一些典型的数据块包括:superblock, inode, data block, directory block and indirection block。

Superblock包含了关于整个文件系统的元信息(metadata),比如文件系统的类型、大小、状态和关于其他文件系统数据结构的信息。Superblock对文件系统是非常重要的,因此Unix文件系统的实现会保存多个Superblock的副本。

inode是Unix文件系统中用于表示文件的抽象数据结构。inode不仅是指抽象了一组硬盘上的数据的"文件”,目录和外部IO设备等也会用inode数据结构来表示。inode包含了一个文件的元信息,比如拥有者、访问权限、文件类型等等。对于一个文件系统里的所有文件,文件系统会维护一个inode列表,这个列表可能会占据一个或者多个磁盘块。

Data block用于存储实际的文件数据。一些文件系统中可能会存在用于存放目录的Directory Block和Indirection Block,但是在Unix文件系统中这些文件块都被视为数据,上层文件系统通过inode对其加以操作,他们唯一的区别是inode里记录的属性有所不同。

Xv6中的文件系统设计思想与Unix大抵相同,但是实现细节多有简化。在底层实现上,Xv6采用与Linux类似的分层实现思路,层层向上逐级封装,以便能支持多种多样的设备和IO方式。Xv6的文件系统包含了磁盘IO层、Log层、Inode层、File层和系统调用层,下面会依次介绍其实现,

Xv6中的磁盘IO

Xv6中的磁盘IO在ide.c中,这是一个基于Programmed IO的面向IDE磁盘的简单实现。一个Xv6中的磁盘读写请求用如下的数据结构表示

struct buf {
int flags;
uint dev;
uint blockno;
struct sleeplock lock;
uint refcnt;
struct buf *prev; // LRU cache list
struct buf *next;
struct buf *qnext; // disk queue
uchar data[BSIZE];
};

其中,对IDE磁盘而言,需要关心的域是flags(DIRTY, VALID),dev(设备),blockno(磁盘块编号)和next(指向队列的下一个成员的指针).

磁盘读写实现的思路是这样的:Xv6会维护一个进程请求磁盘操作的队列(idequeue)。当进程请求磁盘读写时,请求会被加入队列,进程会进入睡眠状态(iderw())。任何时候,队列的开头表示当前正在进行的磁盘读写请求。当一个磁盘读写操作完成时,会触发一个中断,中断处理程序(ideintr())会移除队列开头的请求,唤醒队列开头请求所对应的进程。如果还有后续的请求,就会将其移到队列开头,开始处理下一个磁盘请求。

磁盘请求队列的声明如下,当然对其访问是必须加锁的。

static struct spinlock idelock;
static struct buf *idequeue;

ide.c中函数及其对应功能如下

函数名 功能
idewait() 等待磁盘进入空闲状态
ideinit() 初始化IDE磁盘IO
idestart() 开始一个磁盘读写请求
iderw() 上层文件系统调用的磁盘IO接口
ideintr() 当磁盘请求完成后中断处理程序会调用的函数

操作系统启动时,main()函数会调用ideinit()ide磁盘进行初始化,初始化函数中会初始化ide锁,设定磁盘中断控制,并检查是否存在第二个磁盘。

iderw()函数提供了面向顶层文件系统模块的接口。iderw()既可用于读,也可用于写,只需通过判断buf->flag里的DIRTY位和VALID位就能判断出请求是读还是写。如果请求队列为空,证明当前磁盘不是工作状态,那么就需要调用idestart()函数初始化磁盘请求队列,并设置中断。如果请求是个写请求,那么idestart()中会向磁盘发出写出数据的指令。之后,iderw()会将调用者陷入睡眠状态。

当磁盘读取或者写操作完毕时,会触发中断进入trap.c中的trap()函数,trap()函数会调用ideintr()函数处理磁盘相关的中断。在ideintr()函数中,如果当前请求是读请求,就读取目前已经在磁盘缓冲区中准备好的数据。最后,ideintr()会唤醒正在睡眠等待当前请求的进程,如果队列里还有请求,就调用idestart()来处理新的请求。

Buffer Cache的功能与实现

在文件系统中,Buffer Cache担任了一个磁盘与内存文件系统交互的中间层。由于对磁盘的读取是非常缓慢的,因此将最近经常访问的磁盘块缓存在内存里是很有益处的。

Xv6中Buffer Cache的实现在bio.c中,Buffer Cache的数据结构如下(rev11版本)

struct {
struct spinlock lock;
struct buf buf[NBUF];
// Linked list of all buffers, through prev/next.
// head.next is most recently used.
struct buf head;
} bcache;

此数据结构在固定长度的数组上维护了一个由struct buf组成的双向链表,并且用一个锁来保护对Buffer Cache链表结构的访问。值得注意的是,对链表结构的访问和对一个struct buf结构的访问需要的是不同的锁。

在缓存初始化时,系统调用binit()对缓存进行初始化。binit()函数对缓存内每个元素初始化了睡眠锁,并从后往前连接成一个双向链表。一开始的时候,缓存内所有的块都是空的。

上层文件系统使用bread()bwrite()对缓存中的磁盘块进行读写。关于缓存的全部操作是在bread()bwrite()中自动完成的,不需要上层文件系统的参与。

bread()会首先调用bget()函数,bget()函数会检查请求的磁盘块是否在缓存中。如果在缓存中,那么直接返回缓存中对应的磁盘块即可。如果不在缓存中,那么需要先使用最底层的iderw()函数先将此磁盘块从磁盘加载进缓存中,再返回此磁盘块。

bget()函数的实现有一些Tricky。搜索缓存块的代码非常直接,但是在其中必须仔细考虑多进程同时访问磁盘块时的同步机制。在Xv6 rev7版本中由于没有实现睡眠锁,为了避免等待的缓冲区在等待的过程中改变了内容,必须在从锁中醒来时重新扫描磁盘缓冲区寻找合适的磁盘块,但是在rev11版本中由于实现了睡眠锁,在找到对应的缓存块时,只需释放对Buffer Cache的锁并拿到与当前缓存块有关的睡眠锁即可。

bwrite()函数直接将缓存中的数据写入磁盘。Buffer Cache层不会尝试执行任何延迟写入的操作,何时调用bwrite()写入磁盘是由上层的文件系统控制的。

上层文件系统调用brelse()函数来释放一块不再使用的冲区。brelse()函数中主要涉及的是对双向链表的操作,在此不再赘述。

Log层的功能与实现

在文件系统中添加Log层是为了能够使得文件系统能够处理诸如系统断电之类的异常情况,避免磁盘上的文件系统出现Inconsistency。Log层的实现思路是这样的,对于上层文件系统的全部磁盘操作,将其分割为一个个transaction,每个transaction都会首先将数据和其对应磁盘号写入磁盘上的Log区域,并且只有在Log区域写入全部完成后,再将Log区域的数据写入真正存储的数据区域。通过这种设计,如果在写入Log的时候断电,那么文件系统会当做这些写入不存在,如果在写入真正区域的时候断电,那么Log区域的数据可以用于恢复文件系统。如此,就可以避免文件系统中文件的损坏。

在Xv6 rev7的文件系统实现中,不允许多个进程并发地向Log层执行transaction,然而rev11的实现有所不同,允许多个进程并发地向Log层执行transaction。以下对实现细节的讨论基于rev11版本。

上层文件系统在使用log层时,必须首先调用begin_op()函数。begin_op()函数会记录一个新的transaction信息。在使用完log层后,上层系统必须调用end_op()函数。只有当没有transaction在执行时,log才会执行真正的磁盘写入。真正的磁盘写入操作在commit()函数中,可以看到commit()函数只有在end_op()结束,log.outstanding==0时才会被调用(以及开机的时刻)。commit()函数会先调用write_log()函数将缓存里的磁盘块写到磁盘上的Log区域里,并将Log Header写入到磁盘区域。只有当磁盘里存在Log Header的区域数据更新了,这一次Log更新才算完成。在Log区域更新后,commit()函数调用install_trans()完成真正的磁盘写入步骤,在这之后调用write_head()函数清空当前的Log数据。

XV6 文件系统的硬盘布局

在Xv6操作系统的硬盘中,依次存放了如下几个硬盘块。对这些硬盘块的索引是直接使用一个整数来进行的,

[boot block | super block | log | inode blocks | free bit map | data blocks]

第一个硬盘块boot block会在开机的时候被加载进内存,磁盘块编号是0。第二个superblock占据了一个硬盘块,编号是1,在Xv6中的声明如下

struct superblock {
uint size; // Size of file system image (blocks)
uint nblocks; // Number of data blocks
uint ninodes; // Number of inodes.
uint nlog; // Number of log blocks
uint logstart; // Block number of first log block
uint inodestart; // Block number of first inode block
uint bmapstart; // Block number of first free map block
};

Superblock中存储了文件系统有关的元信息。操作系统必须先读入Super Block才知道剩下的log块,inode块,bitmap块和datablock块的大小和位置。在Superblock之后顺序存储了多个log块、多个inode块、多个bitmap块。磁盘剩余的部分存储了data block块。

XV6中的文件

Xv6中的文件(包括目录)全部用inode数据结构加以表示,所有文件的inode都会被存储在磁盘上。系统和进程需要使用某个inode时,这个inode会被加载到inode缓存里。存储在内存里的inode会比存储在磁盘上的inode多一些运行时信息。内存里的inode数据结构声明如下。

// 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];
};

其中,inode.type指明了这个文件的类型。Xv6中,这个类型可以是普通文件,目录,或者是特殊文件。

内核会在内存中维护一个inode缓存,缓存的数据结构声明如下

struct {
struct spinlock lock;
struct inode inode[NINODE];
} icache;

对于Inode节点的基本操作如下

函数名 功能
iinit() 读取Superblock,初始化inode相关的锁
ialloc() 在磁盘上分配一个inode
iupdate() 将内存里的一个inode写入磁盘
iget() 获取指定inode,更新缓存
iput() 对内存内一个Inode引用减1,引用为0则释放inode
ilock() 获取指定inode的锁
iunlock() 释放指定inode的锁
readi() 往inode读数据
writei() 往inode写数据
bmap() 返回inode的第n个数据块的磁盘地址

一个Inode有12(NDIRECT)个直接映射的磁盘块,有128个间接映射的磁盘块,这些合计起来,Xv6系统支持的最大文件大小为140*512B=70KB。

Xv6系统中的文件描述符

Unix系统一个著名的设计哲学就是"Everything is a file",这句话更准确地说是"Everything is a file descriptor"。上文所提的inode数据结构用于抽象文件系统中的文件和目录,而文件描述符除了抽象文件之外,还能抽象包含Pipe、Socket之类的其他IO,成为了一种通用的I/O接口。

Xv6中,一个文件的数据结构表示如下

struct file {
enum { FD_NONE, FD_PIPE, FD_INODE } type;
int ref; // reference count
char readable;
char writable;
struct pipe *pipe;
struct inode *ip;
uint off;
};

从中可见,一个file数据结构既可以表示一个inode,也可以表示一个pipe。多个file数据结构可以抽象同一个inode,但是Offset可以不同。

系统所有的打开文件都在全局文件描述符表ftable中,ftable数据结构的声明如下

struct {
struct spinlock lock;
struct file file[NFILE];
} ftable;

从中可以看出Xv6最多支持同时打开100(NFILE)个文件,从struct proc中可以看出Xv6中每个进程最多同时可以打开16(NOFILE)个文件。

对File数据结构的基本操作包括filealloc(), filedup(), fileclose(), fileread(), filewrite()filestat()。命名风格与Unix提供的接口一致,因此从名字很容易就能看出其基本功能。

对于Inode类型的file而言,上述操作的实现依赖于inode的诸如iread()iwrite()等基本操作。

Xv6中文件相关的系统调用

利用上一层的实现,大多数系统调用的实现都是比较直接的。Xv6中支持的文件相关系统调用列表如下

名称 功能
sys_link() 为已有的inode创建一个新的名字
sys_unlink() 为已有的inode移除一个名字,可能会移除这个inode
sys_open() 打开一个指定的文件描述符
sys_mkdir() 创建一个新目录
sys_mknod() 创建一个新文件
sys_chdir() 改变进程当前目录
sys_fstat() 改变文件统计信息
sys_read() 读文件描述符
sys_write() 写文件描述符
sys_dup() 增加文件描述符的引用

绝大多数系统调用的语义都与Unix标准相同。

XV6操作系统代码阅读心得(五):文件系统的更多相关文章

  1. XV6操作系统代码阅读心得(一):启动加载、中断与系统调用

    XV6操作系统是MIT 6.828课程中使用的教学操作系统,是在现代硬件上对Unix V6系统的重写.XV6总共只有一万多行,非常适合初学者用于学习和实践操作系统相关知识. MIT 6.828的课程网 ...

  2. XV6操作系统代码阅读心得(二):进程

    1. 进程的基本概念 从抽象的意义来说,进程是指一个正在运行的程序的实例,而线程是一个CPU指令执行流的最小单位.进程是操作系统资源分配的最小单位,线程是操作系统中调度的最小单位.从实现的角度上讲,X ...

  3. XV6操作系统代码阅读心得(四):虚拟内存

    本文将会详细介绍Xv6操作系统中虚拟内存的初始化过程. 基本概念 32位X86体系结构采用二级页表来管理虚拟内存.之所以使用二级页表, 是为了节省页表所占用的内存,因为没有内存映射的二级页表可以不用分 ...

  4. XV6操作系统代码阅读心得(三):锁

    锁是操作系统中实现进程同步的重要机制. 基本概念 临界区(Critical Section)是指对共享数据进行访问与操作的代码区域.所谓共享数据,就是可能有多个代码执行流并发地执行,并在执行中可能会同 ...

  5. 【原】SDWebImage源码阅读(五)

    [原]SDWebImage源码阅读(五) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 前面的代码并没有特意去讲SDWebImage的缓存机制,主要是想单独开一章节专门讲 ...

  6. 使用 /proc 文件系统来访问 linux操作系统 内核的内容 && 虚拟文件系统vfs及proc详解

    http://blog.163.com/he_junwei/blog/static/19793764620152743325659/ http://www.01yun.com/other/201304 ...

  7. XV6操作系统接口

    操作系统接口 操作系统的工作是(1)将计算机的资源在多个程序间共享,并且给程序提供一系列比硬件本身更有用的服务.(2)管理并抽象底层硬件,举例来说,一个文字处理软件(比如 word)不用去关心自己使用 ...

  8. 【原】AFNetworking源码阅读(五)

    [原]AFNetworking源码阅读(五) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 上一篇中提及到了Multipart Request的构建方法- [AFHTTP ...

  9. [置顶] Linux协议栈代码阅读笔记(一)

    Linux协议栈代码阅读笔记(一) (基于linux-2.6.21.7) (一)用户态通过诸如下面的C库函数访问协议栈服务 int socket(int domain, int type, int p ...

随机推荐

  1. Spring MVC中的ResponseEntity和ResponseBody的区别

    1.ResponseEntity的优先级高于@ResponseBody. 在不是ResponseEntity的情况下才去检查有没有@ResponseBody注解. 如果响应类型是ResponseEnt ...

  2. Ubuntu安装TTF字体

    sudo mkdir /usr/share/fonts/ttf sudo cp ~/ttf/* /usr/share/fonts/ttf cd /usr/share/fonts/ttf sudo ch ...

  3. ssh_crm项目

    1.代码 https://pan.baidu.com/s/1hudAhA8  密码:c7xu 2.总结 https://pan.baidu.com/s/1o9ArFf0 密码:hteu 3.资料 ht ...

  4. 自定义组装json对象

    组装json对象 public string strTree(DataTable dt, string type, string state) { string strjosn = "&qu ...

  5. D. Cow and Snacks 并查集

    D. Cow and Snacks 题意:有n种小吃,m个人,每个人有两种喜欢的小吃,当一个人遇到两种自己都喜欢的小吃,可以都吃掉,问在最优的吃小吃顺序下,不能吃到自己喜欢的小吃的人数最少是多少? 题 ...

  6. C#中SqlDataAdapter的使用小结---转载

    C#中SqlDataAdapter的使用小结 转载 叁木-Neil 最后发布于2018-06-07 21:29:39 阅读数 8275 收藏 展开 SqlDataAdapter对象 一.特点介绍1.表 ...

  7. QQ企业通---登录模块设计--知识点

    Rectangle 成员  存储一组整数,共四个,表示一个矩形的位置和大小.对于更高级的区域函数,请使用 Region 对象. Form与Control的区别? 窗体也是控件,继承Control类.可 ...

  8. Laradock 如何通过 ssh 方式连接到 workspace

    用 docker-compose exec workspace bash 方式可以进入容器,但是还是在 xshell 终端连接比较方便.   在网上也没找到方法,其实可以通过密钥的方式连接.记录一下仅 ...

  9. Hyper-V网卡ping不通本机

    可能的解决方案: 添加网卡时选择"旧版"

  10. Jquery实现功能---购物车

    //需求,勾选选项时,总价格要跟着变,点击添加数量,总价格也要跟着变,全部要动态变化 //代码如下 <!DOCTYPE html> <html> <head> &l ...