「实验记录」MIT 6.S081 Lab9 file system

#Lab9: file system

  • I. Source
  • II. My Code
  • III. Motivation
  • IV. Large file (moderate)
    • i. Motivation
    • ii. Solution
      • S1 - 修改 inode's addrs 结构
      • S2 - 使 bmap() 支持三级地址
      • S3 - itrunc() 释放文件内容
    • iii. Result
  • V. Symbolic links (moderate)
    • i. Motivation
    • ii. Solution
      • S1 - 声明 symlink() 相关流程
      • S2 - sys_symlink() 建立链接
      • S3 - sys_open() 追根溯源
    • iii. Result
  • VI. Reference

I. Source

  1. MIT-6.S081 2020 课程官网
  2. Lab9: file system 实验主页
  3. MIT-6.S081 xv6 book Chapter7 File system 个人笔记
  4. B站 - MIT-6.S081 Lec14: File Systems

II. My Code

  1. Lab9: file system 的 Gitee
  2. xv6-labs-2020 的 Gitee 总目录

III. Motivation

Lab9: file system 主要是想让我们在 xv6 已有的 File system 机制的基础上添加支持大文件和符号链接的功能

在开始实验之前,请研读 xv6-6.S081 的第七章节 File system 及相关代码

IV. Large file (moderate)

i. Motivation

我们根据 inode 结构图知道,目前 xv6 只支持最大为 268 KB 的文件(详情请移步 MIT 6.S081 Chapter7 File system’s #7.10 - Code: Inode Content ),

「实验记录」MIT 6.S081 Lab9 file system_第1张图片

现在抛给我们的问题是,能不能想出一种方法让 xv6 支持更大的文件?

ii. Solution

答案当然是肯定的!我们来看看怎么做?

S1 - 修改 inode’s addrs 结构

在 Lab: Large files 中我们将要修改 inode 中 addrs[] 的组成部分,使其支持多级地址空间。原先的 struct inodekernel/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];
};

其中宏 NDIRECT 为 12 ,addrs[] 里存放的是文件内容所在 block 的编号,有 12 个一级地址(直接指向 data-block ),还有 1 个二级地址(指向了一个 addrs-block ,addrs-block 又指向 data-blocks )

我们要使 xv6 支持更大的文件,唯有变动此处!将原先的 12 个一级地址减少为 11 个,再添加 1 个 二级地址和 1 个三级地址,这样在总数 13 不变的情况下,使 inode 能够索引到更多的 block

为什么不能变更总数?如果总数变了,那么 struct inode 的大小就变了,进而会改变 1 个 block 中持有 16( = 1024/64 ) inode 的局面!这个非常关键,它涉及到查找 inode 时所需的 inum 编号,而且在 xv6 许多代码中已经写死了。故,总数不能变!

好,我们来修改一下结构,

// in-memory copy of an inode
struct inode {
  ...
  uint addrs[NDIRECT+2];
};

且在 kernel/fs.h 中将一级地址的个数减少为 11 ,

#define NDIRECT 11

此时 addrs[] 的结构就变成下图这样,

「实验记录」MIT 6.S081 Lab9 file system_第2张图片

可以很清楚的看到,我们新增了一个三级地址,即是 addrs-addrs-block ,1 个 dindirect 可以容纳 256x256x1024 B = 65,536 KB ,加上 1 个 indirect 可以容纳 256x1024 B = 256 KB ,再加上 11 个 direct(11x1024B = 11KB),文件最大支持 65,803 KB ,即 65 MB 左右

S2 - 使 bmap() 支持三级地址

根据 Lab9: file system 实验主页 我们知道 kernel/fs.c:bmap() 主要是负责为 inode 分配 block 的,看一下声明,

static uint bmap(struct inode *ip, uint bn);

其中 bn 是 block no 的缩写,即 inode 文件的逻辑块号。原先只有二级地址的定义如下,

// Return the disk block address of the nth block in inode ip.
// If there is no such block, bmap allocates one.
static uint
bmap(struct inode *ip, uint bn)
{
  uint addr, *a;
  struct buf *bp;

  if(bn < NDIRECT){
    if((addr = ip->addrs[bn]) == 0)
      ip->addrs[bn] = addr = balloc(ip->dev);
    return addr;
  }
  bn -= NDIRECT;

  if(bn < NINDIRECT){
    // Load indirect block, allocating if necessary.
    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;
  }
  
  panic("bmap: out of range");
}

代码会根据 Figure 7.3 - The representation of a file on disk 的树形结构,定位到逻辑块号为 bn 的位置并尝试分配 disk 空间,成功则返回 disk block 地址空间

所以我们再添加一个三级目录,照猫画虎又有何难?如下,

static uint
bmap(struct inode *ip, uint bn)
{
  uint addr, *a;
  struct buf *bp;

  if(bn < NDIRECT){
    if((addr = ip->addrs[bn]) == 0)
      ip->addrs[bn] = addr = balloc(ip->dev);
    return addr;
  }
  /** 说明 bn > NDIRECT ,应该计算 block 在一级 indirect 目录中的逻辑编号 */
  bn -= NDIRECT;

  if(bn < NINDIRECT){
    // Load indirect block, allocating if necessary.
    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;
  }
  /** 说明 bn > NINDIRECT ,应该计算 block 在二级 indirect 目录中的逻辑编号 */
  bn -= NINDIRECT;

  if(bn >= NDINDIRECT) 
    panic("bmap: out of range");

  /** 把二级 indirect 目录调至 Buffer cache 中 */
  if((addr = ip->addrs[NDIRECT+1]) == 0)
    ip->addrs[NDIRECT+1] = addr = balloc(ip->dev);
  bp = bread(ip->dev, addr);
  a = (uint*)bp->data;  /** 定位到二级 indirect 目录 */
  uint i = bn/NINDIRECT;  /** 定位到二级 indirect 目录中的第 i 条 entry */
  bn %= NINDIRECT;  /** 第 bn 块 block */

  /** 把三级的 indirect 目录调至 Buffer cache 中 */
  if((addr = a[i]) == 0) {
    a[i] = addr = balloc(ip->dev);
    log_write(bp);
  }
  brelse(bp);

  bp = bread(ip->dev, addr);
  a = (uint*)bp->data;  /** 定位到三级 indirect 目录 */
  if((addr = a[bn]) == 0) { /** 第 bn 块 block */
    a[bn] = addr = balloc(ip->dev);
    log_write(bp);
  }
  brelse(bp);
  return addr;
}

这段代码,我用了一些涉及 C 编码的技巧,感兴趣的话可以移步 C 编码准则 。因为扩展之后的逻辑结构图已经诠释的很明了啦,就不再讲解代码了

其中,宏 NDINDIRECTkernel/fs.h 中有所定义,

#define NDINDIRECT (NINDIRECT * NINDIRECT)

因为追加了 1 个三级地址,所以文件最大 size 也会发生变化,

#define MAXFILE (NDIRECT + NINDIRECT + NDINDIRECT)

S3 - itrunc() 释放文件内容

我们通过修改 inode’s addrs 结构和 kernel/fs.c:bmap() 实现文件大小扩充功能,但这仅仅是分配环节,我们很自然地想到,有分配必然要有释放,不然就会造成空间泄露

释放的事,在 Lab: Large file 中交由 kernel/fs.c:itrunc() 来完成,与 bmap() 类似,还是根据 inode’s addrs 的树形结构,清空并归还各级地址空间目录,

// Truncate inode (discard contents).
// Caller must hold ip->lock.
/** 清空 inode 所包含的文件内容 */
void
itrunc(struct inode *ip)
{
  int i, j;
  struct buf *bp;
  uint *a;

  /** 清空一级 indirect 目录 */
  for(i = 0; i < NDIRECT; i++){
    if(ip->addrs[i]){
      bfree(ip->dev, ip->addrs[i]);
      ip->addrs[i] = 0;
    }
  }

  /** 清空二级 indirect 目录 */
  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;
  }

  /** 清空三级 indirect 目录 */
  if(!ip->addrs[NDIRECT+1])
    goto truncDone;

  bp = bread(ip->dev, ip->addrs[NDIRECT+1]);
  a = (uint*)bp->data;  /** 先定位到二级 indirect 目录 */
  for(int i=0; i<NINDIRECT; i++) {  /** 遍历二级目录中的每个非空三级 indirect 目录 */
    if(!a[i])
      continue;
    
    struct buf *bp2 = bread(ip->dev, *(a+i));
    uint *a2 = (uint*)bp2->data;  /** 定位到非空的三级 indirect 目录 */
    for(int j=0; j<NINDIRECT; j++) {
      if(a2[j]) bfree(ip->dev, a2[j]);    
    }
    a2[j] = 0;
    brelse(bp2);
  }
  brelse(bp);
  bfree(ip->dev, ip->addrs[NDIRECT+1]);
  ip->addrs[NDIRECT+1] = 0;

truncDone:
  ip->size = 0;
  iupdate(ip);
}

至此,已完成 Large file 功能的添加工作

iii. Result

手动进入 qemu

make qemu
$bigfile
$usertests

V. Symbolic links (moderate)

i. Motivation

Lab: Symbolic links 是想让我们给新文件打上符号链接,通过链接指向已有的 pathname 文件,而不是又新建一个与 pathname 文件一模一样的文件,类似于 Windows 的快捷方式

符号链接( Symbolic links or soft links)不同于硬链接( hard links ),硬链接只能指向同一磁盘的文件,而 Lab: Symbolic links 支持跨磁盘操作

ii. Solution

S1 - 声明 symlink() 相关流程

打符号链接这种操作,通常是以系统调用的方式实现的,所以我们需要实现一个名为 symlink() 的系统调用,供 Caller 使用。声明在 user/user.h 中,

int symlink(const char*, const char*);

根据 Lab9: file system 实验主页 提示,symlink() 接受两个参数,前者为 target ,意为符号链接要指向文件路径名为 target 的 inode ;后者为 path ,也就是新符号链接的路径名。在 xv6 中文件路径名存放在 inode 文件地址空间的起始位置,即第一块 block

并且在 user/usys.pl 中追加 symlink entry ,使 xv6 能够找到 symlink() 系统调用,

entry("symlink");

以及在 kernel/sysfile.c 中定义暂时为空的 sys_symlink() 系统调用实体,

uint64
sys_symlink(void)
{
  return 0;
}

因为系统调用实体都是如此,返回值为 uint64 ,参数为空,我们要遵循此规则,才能实现在 kernel/syscall.csyscalls[] 序列化。 Lab9: file system 实验主页 原话,

Implement the symlink(target, path) system call to create a new symbolic link at path that refers to target. Note that target does not need to exist for the system call to succeed. You will need to choose somewhere to store the target path of a symbolic link, for example, in the inode’s data blocks. symlink should return an integer representing success (0) or failure (-1) similar to link and unlink.

之后,在 kernel/syscall.h 中定义宏 SYS_symlink

#define SYS_symlink 22

kernel/syscall.c 中追加 extern 声明,并将 sys_symlink() 加入序列中,

extern uint64 sys_symlink(void);

static uint64 (*syscalls[])(void) = {
...
[SYS_symlink] sys_symlink,
};

最后,还需在 kernel/stat.h 中定义宏 T_SYMLINK

#define T_SYMLINK 4   /** 符号链接标记位 */

来标记 inode 是否为符号链接;在 kernel/fcntl.h 中定义宏 O_NOFOLLOW

#define O_NOFOLLOW 0x800

供系统调用 open() 使用,注意其值要与它值避开(具体为二进制的 OR 操作),不能重叠,它值为,

#define O_RDONLY  0x000
#define O_WRONLY  0x001
#define O_RDWR    0x002
#define O_CREATE  0x200
#define O_TRUNC   0x400

Lab9: file system 实验主页 原话,

Add a new flag to kernel/fcntl.h, (O_NOFOLLOW), that can be used with the open system call. Note that flags passed to open are combined using a bitwise OR operator, so your new flag should not overlap with any existing flags.

关于宏 O_NOFOLLOW ,我的理解是,当进程打开文件时,若 omode 是 O_NOFOLLOW ,则忽略符号链接,即是不去追踪符号链接;反之,则根据符号链接,追根溯源。Lab9: file system 实验主页 原话,

When a process specifies O_NOFOLLOW in the flags to open, open should open the symlink (and not follow the symbolic link).

此后会在 sys_open() 中有所体现的。在 Makefile 中找到 UPROGS 选项,在其后追加,

UPROGS=\
	$U/_cat\
	...
	$U/_zombie\
	$U/_symlinktest\

完成 symlinktest 的测试单元编译工作

S2 - sys_symlink() 建立链接

Caller 调用 symlink() ,想要根据 target 和 path 建立符号链接。但 symlink() 属于最上层的系统调用,想要建立链接的话,symlink() 还需将任务继续传递,下沉至 kernel/sysfile.c:sys_symlink() ,由它来完成具体的建立链接事宜,且看定义,

uint64
sys_symlink(void)
{
  struct inode *ip;
  char target[MAXPATH], path[MAXPATH];

  if(argstr(0, target, MAXPATH)<0 || argstr(1, path, MAXPATH)<0)
    return -1;

  begin_op();

  /** 尝试分配一个名为 path 的 inode 作为符号链接节点 */
  if((ip=create(path, T_SYMLINK, 0, 0)) == 0) { 
    /** 调用 create 之后,仍持有 ip->lock ,因为 create 并没有释放 */
    end_op();
    return -1;
  }

  /** 将 target 文件路径名写入 ip 的第一块 block 中 */
  if(writei(ip, 0, (uint64)target, 0, strlen(target)) < 0) {  
    /** 调用 writei 之前需要持有 ip->lock */
    iunlockput(ip);
    end_op();
    return -1;
  }

  /** 所以 writei 之后需要释放 ip->lock */ 
  iunlockput(ip); /** iunlockput = iunlock + iput */

  end_op();
  return 0;
}

sys_symlink() 中定义了 targetpath 字符串,用来接收从 symlink() 中传来的 target 和 path(系统调用是通过寄存器传值的,具体请移步 MIT 6.S081 Lab2 system call )

知道 target 和 path 文件路径名之后,调用 create() 尝试分配一个名为 path 的 inode 作为符号链接节点,在这里我们姑且这么浅显的理解 create() 即可。有个注意点,就是调用 create() 之后,仍持有锁,因为 create 并没有释放!

若符号链接节点创建失败,则结束 File system 系统调用。调用 File system 系统调用时,需要以 begin_op() 开始,以 end_op() 收尾,这是 xv6’s File system 规定的,主要是为了支持 Logging: Crash Recovery 机制,在此就不展开讲解,详情请移步 MIT 6.S081 Chapter7 File system’s #7.6 - Code: logging

符号链接节点创建之后,就调用 writei() 尝试将 target 文件路径名写入 ip 的第一块 block 中,在完成之后放锁即可。需要注意的是,调用 writei() 之前需要持有锁

至此,就已然为 target 和 path 建立了符号链接

S3 - sys_open() 追根溯源

在 S1 - sys_symlink() 建立链接 章节中我们根据 target 和 path 建立了符号链接,即是可以通过调用 symlink() 在 xv6’s File system 中为两个本来没有关联的 inode 建立联系,而且这个联系建立后是实实在在存在的

有联系,我们就要能够使用这层关系,如何使用呢?就是本小节的主要内容。一般,使用这层关系是通过 open() 系统调用打开某文件,打开文件的模式有多种(定义 O_NOFOLLOW 时在 S0 - 声明 symlink() 相关流程 中提起过),若此 inode 为符号链接性且模式支持追根溯源,则我们就寻找该 inode 符号链接的根,且看 kernel/sysfile.c:sys_oepn() 定义,

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();

  if(omode & O_CREATE){
    ip = create(path, T_FILE, 0, 0);
    if(ip == 0){
      end_op();
      return -1;
    }
  } else {
    if((ip = namei(path)) == 0){
      end_op();
      return -1;
    }
    ilock(ip);
    if(ip->type == T_DIR && omode != O_RDONLY){
      iunlockput(ip);
      end_op();
      return -1;
    }
  }

  if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
    iunlockput(ip);   
    end_op();
    return -1;
  }

  /** 符号链接业务流程 */
  if(ip->type==T_SYMLINK && (omode & O_NOFOLLOW)==0) {
    /** 尝试从 ip 处追根溯源 */
    if((ip=symlinkroot(ip)) == 0) { /** 若溯源失败,则在 symlinkroot 中放锁,这是因为代码封装的局限性必须要做出的牺牲 */
      end_op();
      return -1;
    }
  }

  if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
    if(f)
      fileclose(f);
    iunlockput(ip);
    end_op();
    return -1;
  }

  if(ip->type == T_DEVICE){
    f->type = FD_DEVICE;
    f->major = ip->major;
  } else {
    f->type = FD_INODE;
    f->off = 0;
  }
  f->ip = ip;
  f->readable = !(omode & O_WRONLY);
  f->writable = (omode & O_WRONLY) || (omode & O_RDWR);

  if((omode & O_TRUNC) && ip->type == T_FILE){
    itrunc(ip);
  }

  iunlock(ip);  /** 对于成功 open 的 inode ,在最后予以统一放锁  */
  end_op();

  return fd;
}

如代码的符号链接业务流程处所示,尝试从 inode 处追根溯源。在这里我将溯源的具体流程封装起来,提炼为 symlinkroot() 函数,我觉得很直白,就是去找 root ,且看定义,

static struct inode*
symlinkroot(struct inode *ip)
{
  uint visted[SYMLINKDEPTH];
  char path[MAXPATH];
     
  /** for-loop 之前 ip 一定持有 lock */
  for(int i=0; i<SYMLINKDEPTH; i++) {
    visted[i] = ip->inum;

    /** 在调用 readi 之前需要持有 ip->lock */
    if(readi(ip, 0, (uint64)path, 0, MAXPATH) <= 0) 
      goto rootFail;

    /** 寻找 symlink 的上级 inode */
    iunlockput(ip);   /** 调用 namei 时别带 lock ,否则会 deadlock , 因为 namei 可能会操纵 ip */ 
    if((ip=namei(path)) == 0) 
      return 0;
      
    for(int tail=i; tail>=0; tail--) {
      if(ip->inum == visted[tail]) 
        return 0;
    }
    
    /** 没成环 */
    ilock(ip);
    if(ip->type != T_SYMLINK)  /** 持有 lock 返回上层 */
      return ip;
  }  

rootFail:
  iunlockput(ip);
  return 0;
}

在进入 symlinkroot() 之前,inode 是持有锁的,从 sys_open() 中就可以看出(一定是持有锁的,否则不会在 if 中 iunlockput() 的),所以在 symlinkroot() 的 for-loop 之前,inode 也一定持有锁

追根溯源的业务逻辑较为直白,就是从第一个符号链接 inode 开始,一路尾随,直至揪出真正存放所需文件的 inode 。其间,我们不能尾随的太深,因为我们不能保证 symlink() 建立的符号链接没有成环的情况。这是 Lab9: file system 实验主页 要求的,原话是这样的,

If the linked file is also a symbolic link, you must recursively follow it until a non-link file is reached. If the links form a cycle, you must return an error code. You may approximate this by returning an error code if the depth of links reaches some threshold (e.g., 10).

在这里,我采用了一种较为巧妙的算法,即是每访问完一个 inode 后就将其编号记在小抄本中,待访问下一个 inode 时先扫描一遍小抄本,查看一下是否已访问过该 inode 。若有,则说明链接已成环;反之,则无

我将成环阈值设为 10 ,在 kernel/fs.h 中定义,

#define SYMLINKDEPTH 10

在 for-loop 每一次循环中,调用 readi() 读取该 inode 符号链接所指向的 inode 的文件路径名 path ,因为我们在 S1 - sys_symlink() 建立链接 时是将文件路径名 path 写到 inode 的地址空间的起始位置的,即地址为 0 处,所以我们要读取 path 时也要从地址 0 处开始读!

知道链接指向的下一个 inode 的 path 之后,调用 namei() 去找寻名为 path 的 inode ,这里暂且将 namei() 理解成它能够根据文件路径名定位到 inode ,即可

定位到 inode 之后,我们就在 visted[] 小抄本中确认之前是否访问过该 inode 。之后,就是循环往复,直至找到为止

至此,我们就完成了追根溯源的相关工作,在 open() 符号链接文件之后,也能像 Windows 的快捷方式一般,正常的打开文件了!

iii. Result

手动进入 qemu

make qemu
$symlinktest
$usertests

VI. Reference

  1. CSDN - [MIT 6.S081] Lab 9: file system

你可能感兴趣的:(xv6-labs-2020,linux,risc-v)