前言
打开自己的blog一看,居然三个月没更新了...回想一下前几个月,开题 + 实验室杂活貌似也没占非常多的时间,还是自己太懈怠了吧,掉线城和文明6真的是时间刹手(
不过好消息是把15445的所有lab都锤完了,最近一个月应该没啥活干。立个flag,这个月更它个10篇blog,把15445的知识点、lab,以及6.S081想写的东西都写完。今天先做个复健,码一下刚做完的lab8,以及xv6的file system的学习笔记。
坏消息是leetcode一道没动,甚至主力啥语言啥框架还没定下来,开学找实习可能要炸了orz...
这个lab不算难,总代码量也就几十行,绝大多数时间拿来读文件系统相关的代码了(两三天吧),不过收获也挺大,理清楚xv6文件系统的逻辑后,lab一个晚上就搞定了。因此这篇blog我会写的简单一些,大头部分放在file system分析的blog中。
强烈推荐在做这个lab前把《xv6 book》中关于file system的章节全部看完,并详细分析相关的代码。我的这篇关于xv6的file system的学习笔记可能对你有帮助:
TODO:(在码了在码了,但愿今天能码完)
Lab链接:https://pdos.csail.mit.edu/6.828/2019/labs/fs.html
都2021年了还在做2019的lab,这真的好嘛(
Part1 Large Files
xv6选择的文件存储介质是磁盘,通过 struct dinode
来描述一个文件
在磁盘上存储,并用 struct inode
作为其对一个的struct dinode
的拷贝,存储在内存中:
// kernel/fs.h
struct dinode {
short type; // File type
short major; // T_DEVICE only
short minor; // 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
};
// 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];
};
我们只关注其中的addrs
。addrs
记录着文件所在磁盘的盘块号,这个数组的容量为13,前12个地址是直接地址,即该文件的第 0 ~ 11 号盘块,第13个地址是一个一级索引
,用于索引文件的第 12 ~ 12 + 256 号盘块,如下图所示:
这样,一个文件最多可以占用 268 个盘块。
bmap
是xv6中非常重要的api,其返回文件偏移量(bn * BSIZE)所对应的的磁盘盘块号:
// kernel/fs.c
static uint
bmap(struct inode *ip, uint bn)
{
uint addr, *a;
struct buf *bp;
if(bn < NDIRECT){ // offset in in NDIRECT range
if((addr = ip->addrs[bn]) == 0)
ip->addrs[bn] = addr = balloc(ip->dev);
return addr;
}
bn -= NDIRECT;
if(bn < NINDIRECT){
// Load indirect block. If indirect block is not exist, allocate it.
if((addr = ip->addrs[NDIRECT]) == 0)
ip->addrs[NDIRECT] = addr = balloc(ip->dev);
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
if((addr = a[bn]) == 0){
a[bn] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
当xv6删除文件时,要将文件所对应的盘块一一释放。如果这个文件存在一级索引块
,那么除了释放间接索引块的表项所对应的盘块之外,还要将这块一级索引块一并释放:
static void
itrunc(struct inode *ip)
{
int i, j;
struct buf *bp;
uint *a;
for(i = 0; i < NDIRECT; i++){ // free direct block
if(ip->addrs[i]){
bfree(ip->dev, ip->addrs[i]);
ip->addrs[i] = 0;
}
}
if(ip->addrs[NDIRECT]){
bp = bread(ip->dev, ip->addrs[NDIRECT]);
a = (uint*)bp->data;
for(j = 0; j < NINDIRECT; j++){ // free block indexed by indirect-block
if(a[j])
bfree(ip->dev, a[j]);
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT]); // free indirect-block
ip->addrs[NDIRECT] = 0;
}
}
Part1要求我们为xv6的文件系统增添一个二级索引
。一个二级索引
占据一个盘块,共计有256个表项,每个表项都指向一个一级索引块
。由于addr的容量仍然是13,因此需要牺牲一个一级索引项,一个文件的盘块索引项也变成了 11个直接索引
+ 1个一级索引
+ 1个二级索引
,共计可以索引 11 + 256 + 256 *256 = 65803个盘块,由此实现了最大文件大小
的扩展。一个二级索引块
的例子如下:
我们首先要修改一下宏 NINDIRECT,将其值改为11,并修改struct dinode
和struct inode
中addr的定义部分,以及有关文件大小的宏:
// kernel/fs.h
#define NDIRECT 11
#define NINDIRECT (BSIZE / sizeof(uint))
#define NDINDIRECT NINDIRECT * NINDIRECT
#define MAXFILE (NDIRECT + NINDIRECT + NDINDIRECT)
还有一些宏(NBLOCKS等)需要修改,在相应的实验手册中已经指出,因此不再赘述了。
随后需要修改两个api:bmap
和itrunc
。在bmap
中添加计算二级索引的相关代码,以及对应的盘块的分配代码:
static uint
bmap(struct inode *ip, uint bn)
{
uint addr, *a;
struct buf *bp;
// offset in direct-block range
if(bn < NDIRECT){
if((addr = ip->addrs[bn]) == 0)
ip->addrs[bn] = addr = balloc(ip->dev);
return addr;
}
bn -= NDIRECT;
// offset in primary-index range
if(bn < NINDIRECT){
// Load indirect block, allocating if necessary.
if((addr = ip->addrs[NDIRECT]) == 0)
ip->addrs[NDIRECT] = addr = balloc(ip->dev); // allocate a block on disk
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
if((addr = a[bn]) == 0){
a[bn] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
bn -= NINDIRECT;
struct buf *dindbuf, *indbuf; // double-indirect block buffer, indirect block buffer
// offset in secondary-index range
if (bn < NDINDIRECT) {
// get the DIRECT index block. if it's not exist, allocate it.
if ((addr = ip->addrs[NDIRECT + 1]) == 0)
ip->addrs[NDIRECT + 1] = addr = balloc(ip->dev);
dindbuf = bread(ip->dev, addr); // map it into buffer
uint *dind = (uint *)dindbuf->data;
if(dind[bn / NINDIRECT] == 0) { // allocate a indirect block for double-indirect index and log it.
dind[bn / NINDIRECT] = balloc(ip->dev);
log_write(dindbuf);
}
indbuf = bread(ip->dev, dind[bn / NINDIRECT]);
uint *ind = (uint *)indbuf->data;
if (ind[bn % NINDIRECT] == 0) { // allocate file block if it's not exist.
ind[bn % NINDIRECT] = balloc(ip->dev);
log_write(indbuf);
}
brelse(dindbuf);
brelse(indbuf);
return ind[bn % NINDIRECT];
}
// out of range
panic("bmap: out of range");
}
此外,还要修改itrunc
这个方法,添加从二级索引中释放盘块的支持代码:
static void
itrunc(struct inode *ip)
{
int i, j;
struct buf *bp;
uint *a;
// release DIRECT block
for(i = 0; i < NDIRECT; i++){
if(ip->addrs[i]){
bfree(ip->dev, ip->addrs[i]);
ip->addrs[i] = 0;
}
}
// release primary-index block
if(ip->addrs[NDIRECT]){
bp = bread(ip->dev, ip->addrs[NDIRECT]);
a = (uint*)bp->data;
for(j = 0; j < NINDIRECT; j++){
if(a[j])
bfree(ip->dev, a[j]);
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT]);
ip->addrs[NDIRECT] = 0;
}
// release secondary-index block
if (ip->addrs[NDIRECT + 1]) {
struct buf *dinddbuf, *indbuf;
uint *dind, *ind;
dinddbuf = bread(ip->dev, ip->addrs[NDIRECT + 1]);
dind = (uint *)dinddbuf->data;
for (int k = 0; k < NINDIRECT; k++) {
if (dind[k]) {
indbuf = bread(ip->dev, dind[k]);
ind = (uint *)indbuf->data;
for (int l = 0; l < NINDIRECT; l++) {
bfree(ip->dev, ind[l]);
ind[l] = 0;
}
brelse(indbuf);
bfree(ip->dev, dind[k]);
}
}
brelse(dinddbuf);
bfree(ip->dev, ip->addrs[NINDIRECT + 1]);
}
ip->size = 0;
iupdate(ip);
}
这段代码里有两个值得注意的地方:
1)一级索引块、二级索引块刚开始并没有被分配,只有在需要使用的时候才会被分配
2)在xv6中使用了缓冲区
来减少I/O次数,一切对盘块的读写操作都首先在缓冲块
上进行,并且为了维持磁盘中元数据(super block)的一致性,一切写盘块的操作都需要调用log_write
将写操作添加到日志中。在bmap
中涉及到了两种对盘块的写操作:对二级索引块的写操作(向其中添加表项,一个表项对应一个一级索引块)和对一级索引块的写操作(向其中添加表项,一个表项对应一个文件内容盘块)。虽然bmap也会分配指向文件内容的盘块,但对这个盘块的写操作并不是在bmap
中进行的,而是在其他api中进行的,因此无需将这个块录入到日志中。
虽然struct dinode
和struct inode
中也包含了type
、major
、nlink
、size
等成员,但读过相关的代码后你会发现,这些成员都无需修改,尤其是size
这个成员很具有迷惑性,我们分配了盘块后这个成员看起来应该要增加的,但实际上这是一个没被用到的成员。毕竟xv6只是一个教学系统,不应对其苛求太多。这部分的分析都在我的关于file system的blog中有详细的说明。
Part1的相关代码也就这么多了,只要认真读下来file system相关的代码,并认真按照Hint来做,这就算是个白送的实验。不过比较要命的是usertests中的writebig
测试,它会写一个MAXFILE大小的文件(添加大文件支持后,这个值从原来的2K变成了20W),会花费更长的测试时间。
xv6 kernel is booting
virtio disk init 0
init: starting sh
$ bigfile
..................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
wrote 65803 blocks
bigfile done; ok
Part2 symlink
这部分实验要求我们为文件系统增添对符号链接(symbolic link)
(也被称为软链接
)的支持。为此我们首先简要了解一下xv6中的硬链接和软链接的含义。
在xv6中,每个文件拥有唯一的struct dinode
和struct inode
,更好的一种理解方法是,struct inode
是一个文件控制块
,存储着这个文件相关的元数据
,一切对文件的读、写、打开、关闭、删除等操作都需要通过inode
来完成。而很多时候,我们需要一个文件可以出现在多个目录下的支持(例如说多用户操作系统,每个用户都拥有一个自己的目录,它们希望共享某一文件)。如下图所示,这些文件的路径(链接)都导向了同一个inode
:
这种可以直接索引到inode的链接即是硬链接,本质上它是目录文件中的一个条目,根据这个条目,可以(准确的说是必定)获得该文件的inode。在xv6的api中,通过sys_link
创建一个文件的硬链接,其中的核心api是 dirlink
:
// Write a new directory entry (name, inum) into the directory dp.
int
dirlink(struct inode *dp, char *name, uint inum)
{
int off;
struct dirent de;
struct inode *ip;
// Check that name is not present.
if((ip = dirlookup(dp, name, 0)) != 0){
iput(ip);
return -1;
}
// Look for an empty dirent.
for(off = 0; off < dp->size; off += sizeof(de)){
if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlink read");
if(de.inum == 0)
break;
}
strncpy(de.name, name, DIRSIZ);
de.inum = inum;
if(writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlink");
return 0;
}
在sys_link
中调用dirlink
时,传入的参数inum
为目标文件的inode
编号,该编号被添加到目录表项中;这样我们也可以理解为什么调用ll
时,会发现同一个文件的硬链接对应的inode号是相同的了;硬链接如果存在,那么其对应的inode
必须存在。inode
通过维护一个nlink
记录着硬链接的数量。当该inode
新增一个硬链接时,其值便增1;当某个路径下的硬链接被删除时,nlink
要减1,当值减至0时,说明该文件已经无法通过路径访问到,这时才能释放相应的文件。
软链接的定义则不同,我们可以看一下symlink
的man page中的部分内容:
symlink() creates a symbolic link named linkpath which contains the string target.
Symbolic links are interpreted at run time as if the contents of the link had been substituted into the path being followed to find a file or directory.
A symbolic link (also known as a soft link) may point to an existing file or to a nonexistent one; the latter case is known as a dangling link.
软链接并不会增加nlink
的数量,且可以指向一个不存在的路径。软链接也可以指向一个软链接,遇到这种情况时将会递归,直至找到相应的硬链接(或者达到最大递归深度后返回错误)。
在本Part中,要求我们实现以下内容:
1)实现一个新的系统调用 sys_symlink,通过该系统调用可以创建一个软链接;
2)为sys_open提供软链接的打开支持(通过软链接打开文件、O_NOFOLLOW);
只要理解了软链接的作用后,实现这个lab的思路也比较容易了,这个Part实现的思路也很多,我使用的是最简单最容易想到的:
1)将软链接本身也看做是一个文件,即软链接本身也有自己的dinode
和inode
,其文件内容也只是一行字符串,表征着软链接指向的文件的路径。创建软链接时,要为其分配一个inode
2)将软链接的inode
添加到目录条目中,注意要避免命名冲突
uint64
sys_symlink(void)
{
char target[MAXPATH + 1], path[MAXPATH + 1], name[MAXPATH + 1];
memset((void *)target, 0, MAXPATH + 1);
memset((void *)path, 0, MAXPATH + 1);
if (argstr(0, target, MAXPATH) < 0 || argstr(1, path, MAXPATH) < 0) {
return -1;
}
begin_op(ROOTDEV);
// get parent inode
struct inode *iparent, *isym;
if ((iparent = nameiparent((char *)path, (char *)name)) == 0) {
end_op(ROOTDEV);
return -1;
}
// avoid name conflict
// do not hold ilock for iparent
uint off;
if ((isym = dirlookup(iparent, name, &off)) != 0) {
// printf("symlink name conflict with an existing file\n");
// iunlockput(iparent);
iput(iparent);
end_op(ROOTDEV);
return -1;
}
// allocate a dinode for symlink. isym is locked when it func create return
// after this operation, symlink entry is added under corresponding path
if ((isym = create(path, T_SYMLINK, 0, 0)) == 0) {
panic("create inode for symlink error"); // panic is not suitable, but simplify our situations.
}
// fill symlink file content with targetpath
int retval = 0;
uint pathlen = strlen((char *)target);
uint r, total;
r = total = 0;
while (total != pathlen) {
if ((r = writei(isym, 0, (uint64)(target + total), total, pathlen - total)) > 0) {
total += r;
} else {
retval = -1;
break;
}
}
// release
iunlockput(isym);
iput(iparent);
end_op(ROOTDEV);
return retval;
}
这样我们也可以理解为什么在一些文件系统上,同一个文件的软链接和硬链接的inode
可能不同,因为软链接本身也是一个文件,有自己的inode,而硬链接只是一个目录中的条目,该条目索引到了对应的inode。
然后修改sys_open
,当发现打开的path对应的是一个软链接时,调用divesymlink
,获得该软链接指向的文件的inode
。如果使用了O_NOFOLLOW,说明我们希望打开的是软链接这个文件本身,一般fstate会使用这个flag:
uint64
sys_open(void)
{
char path[MAXPATH];
int fd, omode;
struct file *f;
struct inode *ip;
int n;
if((n = argstr(0, path, MAXPATH)) < 0 || argint(1, &omode) < 0)
return -1;
begin_op(ROOTDEV);
if(omode & O_CREATE){
ip = create(path, T_FILE, 0, 0);
if(ip == 0){
end_op(ROOTDEV);
return -1;
}
} else {
if((ip = namei(path)) == 0){
end_op(ROOTDEV);
return -1;
}
ilock(ip);
if(ip->type == T_DIR && omode != O_RDONLY){
iunlockput(ip);
end_op(ROOTDEV);
return -1;
}
}
/**************************
* SYMLINK *
*************************/
if (ip->type == T_SYMLINK && (omode & O_NOFOLLOW) == 0) {
ip = divesymlink(ip);
if (ip == 0) { // link target not exist
end_op(ROOTDEV);
return -1;
}
}
if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
iunlockput(ip);
end_op(ROOTDEV);
return -1;
}
if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
if(f)
fileclose(f);
iunlockput(ip);
end_op(ROOTDEV);
return -1;
}
if(ip->type == T_DEVICE){
f->type = FD_DEVICE;
f->major = ip->major;
f->minor = ip->minor;
} else {
f->type = FD_INODE;
}
f->ip = ip; // ref is here, so this function doesn't call iput
f->off = 0;
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
iunlock(ip);
end_op(ROOTDEV);
return fd;
}
divesymlink
的实现如下,其的参数为软链接文件的inode
,返回软链接最终指向的文件的inode
,如果这个文件不存在则返回0。根据Hint,divesymlink
还要解决软链接之间的环路引用问题,这里我们简单的通过递归深度来判断,当深度大于10时,认为软链接最终指向的文件不存在:
static struct inode *
divesymlink(struct inode *isym)
{
struct inode *ip = isym;
char path[MAXPATH + 1];
uint len;
int deep = 0;
do {
// get linked target
// we don't know how long the file str is, so we expect once read could get fullpath.
len = readi(ip, 0, (uint64)path, 0, MAXPATH);
if (readi(ip, 0, (uint64)(path + len), len, MAXPATH + 1) != 0)
panic("divesymlink : short read");
iunlockput(ip);
if (++deep > 10) { // may cycle link
return 0;
}
if ((ip = namei((char *)path)) == 0) { // link target not exist
return 0;
}
ilock(ip);
} while (ip->type == T_SYMLINK);
return ip;
}
这样Part2也解决了。比较棘手的是api的锁获取和锁释放,以及iput
、iunlockput
、iunlock
这三个api的选择。我在这里简单介绍一下:
1)iput
会使ip->ref
的数量减1(注意与ip->nlink
区分开)。这个值记录着外部所持有struct inode
指针的总数量。当这个值减小到0时,说明没有进程在访问这个文件,此时要将icache
中对应的struct inode
释放掉,但不会释放对应的文件。
2)iunlock
会释放掉struct inode
的锁,这把锁用来实现并发环境下的struct inode
的原子访问,即我们在查看struct inode
中的成员(type、data等)时,必须持有这把锁
3)iunlockput
同时调用上述两个函数;
即如果我们不需要访问struct inode
的成员时,应调用iunlock
释放掉inode
的锁,等需要访问时再拿起这把锁;当我们不再需要使用struct inode
的指针时,要调用iput
,弃用掉这个指针;
这样symlinktest也可以Pass了,对应的usertests也可以全pass:
xv6 kernel is booting
virtio disk init 0
init: starting sh
$ symlinktest
Start: test symlinks
test symlinks: ok
Start: test concurrent symlinks
test concurrent symlinks: ok
$ usertests
usertests starting
test reparent2: OK
test pgbug: OK
test sbrkbugs: usertrap(): unexpected scause 0x000000000000000c pid=3210
sepc=0x00000000000044b2 stval=0x00000000000044b2
usertrap(): unexpected scause 0x000000000000000c pid=3211
sepc=0x00000000000044b2 stval=0x00000000000044b2
OK
test badarg: OK
test reparent: OK
test twochildren: OK
test forkfork: OK
test forkforkfork: OK
test argptest: OK
......
......
......
test opentest: OK
test writetest: OK
test writebig: OK
test createtest: OK
test openiput: OK
test exitiput: OK
test iput: OK
test mem: OK
test pipe1: OK
test preempt: kill... wait... OK
test exitwait: OK
test rmdot: OK
test fourteen: OK
test bigfile: OK
test dirfile: OK
test iref: OK
test forktest: OK
test bigdir: OK
ALL TESTS PASSED
后记
这个Lab不算难,绝大多数时间要花费在读代码上。我这篇xv6 file system的学习笔记可能会比较帮助:
TODO: (在码了在码了,但愿今天能码完):