Exercise1 源代码阅读
文件系统部分 buf.h fcntl.h stat.h fs.h file.h ide.c bio.c log.c fs.c file.c sysfile.c exec.c
- buf.h:对xv6中磁盘块数据结构进行定义,块大小为512字节。
1 | // xv6中磁盘块数据结构,块大小512字节 |
- fcntl.h:宏定义操作权限。
1 | #define O_RDONLY 0x000 // 只读 |
- stat.h:声明文件或目录属性数据结构。
1 | #define T_DIR 1 // Directory |
- fs.h / fs.c:声明超级块、dinode、文件和目录数据结构,以及相关的宏定义。
1 | #define ROOTINO 1 // root i-number |
- file.h:声明inode、file数据结构。
1 | struct file { |
- ide.c:磁盘IO的具体实现,xv6维护了一个进程请求磁盘操作的队列(idequeue)。当进程调用void iderw(struct buf *b)请求读写磁盘时,该请求被加入等待队列idequeue,同时进程进入睡眠状态。当一个磁盘读写操作完成时,会触发一个中断,中断处理程序ideintr()会移除队列开头的请求,唤醒队列开头请求所对应的进程。
1 | // idequeue points to the buf now being read/written to the disk. |
- bio.c:Buffer Cache的具体实现。因为读写磁盘操作效率不高,根据时间与空间局部性原理,这里将最近经常访问的磁盘块缓存在内存中。主要接口有struct buf bread(uint dev, uint sector)、void bwrite(struct buf b),bread会首先从缓存中去寻找块是否存在,如果存在直接返回,如果不存在则请求磁盘读操作,读到缓存中后再返回结果。bwrite直接将缓存中的数据写入磁盘。
- log.c:该模块主要是维护文件系统的一致性。引入log模块后,对于上层文件系统的全部磁盘操作都被切分为transaction,每个transaction都会首先将数据和其对应磁盘号写入磁盘上的log区域,且只有在log区域写入成功后,才将log区域的数据写入真正存储的数据块。因此,如果在写log的时候宕机,重启后文件系统视为该log区的写入不存在,如果从log区写到真实区域的时候宕机,则可根据log区域的数据恢复。
- sysfile.c:主要定义了与文件相关的系统调用。主要接口及含义如下:
1 | // Allocate a file descriptor for the given file. |
- exec.c:只有一个exec接口,实质就是传入elf格式的可执行文件,装载到内存并分配内存页,argv是一个指针数组,用于携带参数。
1 | int exec(char *path, char **argv) |
Exercise2 带着问题阅读
- 了解 UNIX 文件系统的主要组成部分:超级块(superblock),i节点(inode),数据块(datablock),目录块(directoryblock),间接块(indirectionblock)。分别解释它们的作用。
boot | super block | dinode | free bitmap blocks | data blocks | log blocks |
---|---|---|---|---|---|
第0块 | 第1块 | superblock.ninodes块 | 位图管理空闲区块 | superblock.nblocks块 | superblock.nlog块 |
- bootloader引导区(第0块):用于存放引导程序,系统启动从这里开始;
- superblock超级块(第1块):记录文件系统的元信息,如文件系统的总块数,数据块块数,i节点数,日志的块数;
- i节点(inode):从第2块开始存放 i 节点,每一块能够存放多个 i 节点;
- bitmap空闲块管理区:用于存放空闲块位图,因为系统需要知道文件系统的使用情况,哪些块已经分配出去了,哪些块还未被分配;
- 数据块 (datablock):数据块存储的是真真实实的文件内容;
- 目录块(directoryblock):文件系统中除了文件外,还有目录,目录本身是一个文件目录(由很多FCB组成),文件目录也需要以文件的形式存储到磁盘上,存储到磁盘上的这个文件叫做目录文件,目录文件就是存储到目录块中的;
- 间接块(indirectionblock):xv6这里应该是指log日志块,这是文件系统执行磁盘IO操作的中间层,主要目的是维护文件系统的一致性。
- 阅读文件ide.c。这是一个简单的ide硬盘驱动程序,对其内容作大致了解。
- xv6 的文件系统分6层实现,从底至顶如下:
System calls | File descriptors |
---|---|
Pathnames | Recursive lookup |
Directories | Directory inodes |
Files | Inodes and block allocator |
Transactions | Logging |
Blocks | Buffer cache |
- 底层通过块缓冲Buffer cache读写IDE 硬盘,它同步了对磁盘的访问,保证同时只有一个内核进程可以修改磁盘块;
- 第二层Loggins向上层提供服务,该层实现了文件系统的一致性,使得更高层的接口可以将对磁盘的更新按会话打包,通过会话的方式来保证这些操作是原子操作(要么都被应用,要么都不被应用);
- 第三层提供无名文件,每一个这样的文件由一个 i 节点和一连串的数据块组成;
- 第四层将目录实现为一种特殊的 i 节点,它的内容是一连串的目录项,每一个目录项包含一个文件名和对应的 i 节点;
- 第五层提供了层次路经名(如/usr/rtm/xv6/fs.c这样的),这一层通过递归的方式来查询路径对应的文件;
- 最后一层将许多 UNIX 的资源(如管道,设备,文件等)抽象为文件系统的接口,极大地简化了程序员的工作。
- 阅读文件buf.h,bio.c。了解 XV6 文件系统中buffer cache层的内容和实现。描述buffer双链表数据结构及其初始化过程。了解 buffer的状态。了解对buffer的各种操作。
- 数据结构bcache维护了一个由struct buf组成的双向链表,同时bcache.lock用户互斥访问;
- 首先系统调用binit()初始化缓存,随即调用initlock初始化bcache.lock,然后循环遍历buf数组,采用头插法逐个链接到bcache.head后;
- 上层文件系统读磁盘时,调用bread(),随即调用bget()检查请求的磁盘块是否在缓存中,如果命中,返回缓存命中结果。如果未命中,转到底层的iderw()函数先将此磁盘块从磁盘加载进缓存中,再返回此磁盘块;
- 上层文件系统写磁盘时,调用bwrite()直接将缓存中的数据写入磁盘。Buffer Cache层不会尝试执行任何延迟写入的操作,何时调用bwrite()写入磁盘是由上层的文件系统控制的;
- 上层文件系统可通过调用brelse()释放一块不再使用的缓冲区。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59// buf.h
struct buf {
int flags;
uint dev;
uint sector;
struct buf *prev; // LRU cache list
struct buf *next;
struct buf *qnext; // disk queue
uchar data[512];
};
// bio.c
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;
void binit(void)
{
struct buf *b;
initlock(&bcache.lock, "bcache");
//PAGEBREAK! 头插法,每次都是插入到bcache.head的后面
// Create linked list of buffers
bcache.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;
b->dev = -1;
bcache.head.next->prev = b;
bcache.head.next = b;
}
}
// Return a B_BUSY buf with the contents of the indicated disk sector.
struct buf* bread(uint dev, uint sector)
{
struct buf *b;
// 优先查找缓存
b = bget(dev, sector);
if(!(b->flags & B_VALID))
iderw(b); // 命中失败时调用下一次接口真真实实读磁盘
return b;
}
// Write b's contents to disk. Must be B_BUSY.
void bwrite(struct buf *b)
{
if((b->flags & B_BUSY) == 0)
panic("bwrite");
b->flags |= B_DIRTY;
iderw(b); // 立即写, 未延迟写
}
阅读文件log.c,了解XV6文件系统中的logging和transaction机制;
日志存在于磁盘末端已知的固定区域。它包含了一个起始块,紧接着一连串的数据块。起始块包含了一个扇区号的数组,每一个对应于日志中的数据块,起始块还包含了日志数据块的计数。xv6 在提交后修改日志的起始块,而不是之前,并且在将日志中的数据块都拷贝到文件系统之后将数据块计数清0。提交之后,清0之前的崩溃就会导致一个非0的计数值。阅读文件fs.h/fs.c。了解XV6文件系统的硬盘布局。
1 | // On-disk inode structure |
- 阅读文件file.h/file.c。了解XV6的“文件”有哪些,以及文件,i节点,设备相关的数据结构。了解XV6对文件的基本操作有哪些。XV6最多支持多少个文件? 每个进程最多能打开多少个文件?
- xv6文件分为管道文件,设备文件和普通文件;
- XV6最多支持同时打开100个文件,也就是分配100个文件句柄;
- 单个进程最多能打开16个文件。
1
2
3// param.h
#define NOFILE 16 // open files per process
#define NFILE 100 // open files per system
- 阅读文件sysfile.c。了解与文件系统相关的系统调用,简述各个系统调用的作用。
参见源代码阅读部分,已经做出了完整解答。
参考文献
[1] xv6中文文档
[2] xv6文件系统博客园
[3] xv6文件系统CSDN
[4] xv6文件系统CSDN
[5] 操作系统-文件系统课件