MIT 6.S081 Lab8 File system 文件系统

134 阅读8分钟

Lab地址:pdos.csail.mit.edu/6.828/2021/…

git代码地址:github.com/cardchoosen…

Lab: file system

Large files

In this assignment you'll increase the maximum size of an xv6 file. Currently xv6 files are limited to 268 blocks, or 268*BSIZE bytes (BSIZE is 1024 in xv6). This limit comes from the fact that an xv6 inode contains 12 "direct" block numbers and one "singly-indirect" block number, which refers to a block that holds up to 256 more block numbers, for a total of 12+256=268 blocks.

在这个作业中,你将增加 xv6 文件的最大大小。目前,xv6 文件被限制在 268 个块,即 268×BSIZE 字节(在 xv6 中,BSIZE 是 1024)。这个限制源于这样一个事实:一个 xv6 索引节点包含 12 个 “直接” 块编号和一个 “单间接” 块编号,后者指向一个块,该块最多可容纳 256 个更多的块编号,总共为 12 + 256 = 268 个块。

类似FAT文件系统,xv6的文件系统中的inode结构体采用了混合索引的方式记录数据所在的具体磁盘块号。每个文件所占用的前12个磁盘块为直接索引,记录在inode中(每个磁盘快1024字节),对于任何小于12KB的文件,都可以直接访问inode得到磁盘块号。称为直接记录盘块。

对于超出12KB部分的文件,会分配一个额外的一级索引表(1024Byte),用于存储这部分数据所在的盘块号。一个盘块号占用4个字节,所以一级索引表可以包含1024 / 4 = 256个盘块号,再加上inode中12个直接索引,一个文件最多可以使用12+256=268个盘块,也就是268KB。

inode结构如下,NDIRECT = 12:

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

显然268KB的文件大小限制对于操作系统来说太严苛了,我们需要扩展dinode结构,增加一个二级索引来扩大能够支持的文件大小。

首先修改struct inode 和struct dinode,这里说明一下inode和dinode的区别。

功能上:

  1. inode通常是内存中的索引节点数据结构,代表了文件系统中的一个文件或目录的元数据,这些元数据包括但不限于文件的大小、文件类型(普通文件、目录、符号链接等)、文件的权限、文件的所有者、文件的时间戳(创建时间、修改时间、访问时间)、指向文件数据块的指针等。
  2. dinode通常是存储在磁盘上的索引节点的数据结构,是磁盘上文件元数据的持久存储形式。它存储了文件的基本信息,这些信息在文件系统创建文件或目录时被写入磁盘,并且在文件系统的生命周期内持续存在。

存储位置上:

  1. inode 主要存储在内存中,是操作系统为了方便文件操作而在内存中维护的数据结构。
  2. dinode 存储在磁盘上,是文件元数据的持久化存储形式。

结构上:

  1. inode 可能包含一些内存管理和性能优化相关的信息,这些信息在文件系统运行时使用,但不会存储在磁盘上,例如缓存的数据、文件的使用状态等。
  2. dinode 通常只包含文件的基本信息,确保在磁盘上以最精简的方式存储文件的重要信息,以节省磁盘空间和提高文件系统的存储效率。

生命周期上:

  1. inode 的生命周期通常是从文件被打开或操作开始,直到文件关闭或系统需要回收内存资源。
  2. dinode 的生命周期与文件的存在周期相关,只要文件存在于文件系统中,其对应的 dinode 就会存储在磁盘上。

将NDIRECT 从12减少为11,MAXFILE宏增大NINDIRECT * NINDIRECT,腾出inode中的空间来存储二级索引的索引表盘块号。

// kernel/fs.h
#define NDIRECT 11
#define NINDIRECT (BSIZE / sizeof(uint))
#define MAXFILE (NDIRECT + NINDIRECT + NINDIRECT * NINDIRECT)

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

修改bmap和itrunc,bmap用于获取inode中第bn(bn为逻辑块号)个块的物理块号。itrunc用于释放inode中所有使用的数据块。

这里说明一下逻辑块号和物理块号的区别,可以把直接索引+一级索引+二级索引所构成的所有索引块,想象成一个块数组,逻辑块号对应的就是数组下标,通过逻辑块号来判断是直接索引或是一级索引,或是二级索引,然后从索引中,取出真正的物理块号。而物理块号对应磁盘块上真正的物理存储区域。inode中的addrs[NDIRECT+2]可以理解成是逻辑块号-物理块号的映射表。

xv6中的代码现在只能获取直接索引和一级索引的磁盘块,itrunc也是,只需要按照它的代码扩展即可,让bmap和itrunc意识到二级索引的存在。

// fs.c
// Inode content
//
// The content (data) associated with each inode is stored
// in blocks on the disk. The first NDIRECT block numbers
// are listed in ip->addrs[].  The next NINDIRECT blocks are
// listed in block ip->addrs[NDIRECT].

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

  // 对于小于 NDIRECT 的逻辑块号,使用直接映射
  if(bn < NDIRECT){
    // 检查该逻辑块对应的物理块地址是否为 0,如果为 0,则调用 balloc 函数分配一个新的物理块,并更新 inode 的地址表
    if((addr = ip->addrs[bn]) == 0)
      ip->addrs[bn] = addr = balloc(ip->dev);
    return addr;
  }
  // 逻辑块号超出直接映射范围,进入间接映射部分,更新逻辑块号
  bn -= NDIRECT;

  // 对于小于 NINDIRECT 的逻辑块号,使用一级间接映射
  if(bn < NINDIRECT){
    // 检查一级间接块的地址是否为 0,如果为 0,则调用 balloc 函数分配一个新的物理块作为一级间接块,并更新 inode 的地址表
    if((addr = ip->addrs[NDIRECT]) == 0)
      ip->addrs[NDIRECT] = addr = balloc(ip->dev);
    // 读取一级间接块的数据
    bp = bread(ip->dev, addr);
    a = (uint*)bp->data;
    // 检查间接块中存储的物理块地址是否为 0,如果为 0,则调用 balloc 函数分配一个新的物理块,并更新间接块中的地址
    if((addr = a[bn]) == 0){
      a[bn] = addr = balloc(ip->dev);
      log_write(bp);
    }
    // 释放一级间接块的 buf 结构
    brelse(bp);
    return addr;
  }
  // 逻辑块号超出一级间接映射范围,进入二级间接映射部分,更新逻辑块号
  bn -= NINDIRECT;

  // 对于小于 NINDIRECT*NINDIRECT 的逻辑块号,使用二级间接映射
  if(bn < NINDIRECT*NINDIRECT){
    // 检查二级间接块的地址是否为 0,如果为 0,则调用 balloc 函数分配一个新的物理块作为二级间接块,并更新 inode 的地址表
    if((addr = ip->addrs[NDIRECT+1]) == 0)
      ip->addrs[NDIRECT+1] = addr = balloc(ip->dev);
    // 读取二级间接块的数据
    bp = bread(ip->dev, addr);
    a = (uint*)bp->data;
    // 检查二级间接块中存储的一级间接块的地址是否为 0,如果为 0,则调用 balloc 函数分配一个新的物理块作为一级间接块,并更新二级间接块中的地址
    if((addr = a[bn/NINDIRECT]) == 0){
      a[bn/NINDIRECT] = addr = balloc(ip->dev);
      log_write(bp);
    }
    // 释放二级间接块的 buf 结构
    brelse(bp);
    // 计算在一级间接块中的偏移量
    bn %= NINDIRECT;
    // 读取一级间接块的数据
    bp = bread(ip->dev, addr);
    a = (uint*)bp->data;
    // 检查一级间接块中存储的物理块地址是否为 0,如果为 0,则调用 balloc 函数分配一个新的物理块,并更新一级间接块中的地址
    if((addr = a[bn]) == 0){
      a[bn] = addr = balloc(ip->dev);
      log_write(bp);
    }
    // 释放一级间接块的 buf 结构
    brelse(bp);
    return addr;
  }

  // 如果逻辑块号超出二级间接映射范围,程序出现错误,调用 panic 函数
  panic("bmap: out of range");
}

...

// Truncate inode (discard contents).
// Caller must hold ip->lock.
void
itrunc(struct inode *ip)
{
  int i, j;
  struct buf *bp;
  uint *a;

  for(i = 0; i < NDIRECT; i++){
    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++){
      if(a[j])
        bfree(ip->dev, a[j]);
    }
    brelse(bp);
    bfree(ip->dev, ip->addrs[NDIRECT]);
    ip->addrs[NDIRECT] = 0;
  }

  if(ip->addrs[NDIRECT+1]){
    bp = bread(ip->dev, ip->addrs[NDIRECT+1]);
    a = (uint*)bp->data;
    for(j = 0; j < NINDIRECT; j++){
      if(a[j]) {
        struct buf *bp2 = bread(ip->dev, a[j]);
        uint *a2 = (uint*)bp2->data;
        for(int k = 0; k < NINDIRECT; k++){
          if(a2[k])
            bfree(ip->dev, a2[k]);
        }
        brelse(bp2);
        bfree(ip->dev, a[j]);
      }
    }
    brelse(bp);
    bfree(ip->dev, ip->addrs[NDIRECT+1]);
    ip->addrs[NDIRECT + 1] = 0;
  }

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

Make clean重置xv6的文件映像fs.img,然后make qemu验证

init: starting sh
$ bigfile
..................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
wrote 65803 blocks
bigfile done; ok

Symbolic links

Symbolic links即符号链接,在linux中,符号链接使用如下:

ln -s /path/to/target /path/to/symlink

target是目标文件或目录的路径,symlink是要创建的符号链接的路径,之后当用户或程序访问符号链接时,会自动重定向到它所指向的文件或目录,在这里的表现就是,访问symlink实际在访问target。

符号链接也被称为软链接soft link,是一种特殊的文件类型。符号链接可以跨越不同的文件系统,因为它只存储了一个路径,而不是像硬链接那样直接指向文件的inode。

这里继续说明符号链接和硬链接的区别:

硬链接直接指向文件的 inode,多个硬链接指向同一个文件实际上是多个目录项指向同一个 inode,它们共享文件的数据和属性,并且它们的地位是等同的,删除其中一个硬链接不会影响文件的存在,只有当所有硬链接和原始文件都被删除,文件的数据才会被删除,硬链接只能链接到文件,不能链接到目录,并且不能跨越文件系统,因为不同文件系统有不同的 inode 空间。

符号链接只是存储了一个指向目标文件或目录的路径,它有自己独立的 inode,删除符号链接不会影响目标文件或目录的存在,只删除了这个指向的关系。符号链接可以指向文件或目录,并且可以跨越文件系统,因为它不依赖于目标文件的 inode,而是依赖于路径。

这里需要我们为xv6添加symlink系统调用,关于添加系统调用的过程在Lab2中有详细描述,这里只给出过程。

user/user.h中注册symlink

// user.h
int symlink(char *target, char *path);

user/usys.pl中添加系统调用存根

// usys.pl
entry("symlink");

在kernel/syscall.h中定义系统调用号

// syscall.h
#define SYS_symlink 22

在kernel/syscall.c中使用extern定义系统调用函数,并将其调用号-调用函数指针的映射关系加入syscalls中

// syscall.c
extern uint64 sys_symlink(void);

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

添加需要使用的宏定义

// kernel/fcntl.h
#define O_NOFOLLOW 0x800
// kernel/stat.h
#define T_SYMLINK 4   // Symbolic link

在sysfile.c中实现sys_symlink系统调用,新建一个inode结构体ip,获取目标path和链接path,将目标path写入inode第0个块中。

// kernel/sysfile.c
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();

  ip = create(path, T_SYMLINK, 0, 0);
  if (ip == 0)
  {
    end_op();
    return -1;
  }

  if (writei(ip, 0, (uint64)target, 0, strlen(target)) < 0)
  {
    end_op();
    return -1;
  }

  iunlockput(ip);
  end_op();
  return 0;
}

修改sys_open,使其在遇到符号链接时,可以递归处理符号链接,且在符号链接出现环路时报错退出,这里的判断方式比较简单,当层级超过10时,直接退出。

// kernel/sysfile.c
uint64
sys_open(void)
{
  ...

  if (omode & O_CREATE)
  {
    ...
  }
  else
  {
    int symlink_depth = 0;
    while (1)
    {
      if ((ip = namei(path)) == 0)
      {
        end_op();
        return -1;
      }
      ilock(ip);
      if (ip->type == T_SYMLINK && (omode & O_NOFOLLOW) == 0)
      {
        if (++symlink_depth > 10)
        {
          iunlockput(ip);
          end_op();
          return -1;
        }
        if (readi(ip, 0, (uint64)path, 0, MAXPATH) < 0)
        {
          iunlockput(ip);
          end_op();
          return -1;
        }
        iunlockput(ip);
      }
      else
      {
        break;
      }
    }

    if (ip->type == T_DIR && omode != O_RDONLY)
    {
      iunlockput(ip);
      end_op();
      return -1;
    }
  }

  ...
  
  iunlock(ip);
  end_op();
  
  return fd;
}

Makefile中添加symlinktest

UPROGS=\
    ...
    $U/_symlinktest\

重新编译验证

$ symlinktest
Start: test symlinks
test symlinks: ok
Start: test concurrent symlinks
test concurrent symlinks: ok

实验完成,make grade验证(验证过程可能会有些长,耐心):

$ make qemu-gdb
running bigfile: OK (141.0s) 
== Test running symlinktest == 
$ make qemu-gdb
(1.3s) 
== Test   symlinktest: symlinks == 
  symlinktest: symlinks: OK 
== Test   symlinktest: concurrent symlinks == 
  symlinktest: concurrent symlinks: OK 
== Test usertests == 
$ make qemu-gdb
usertests: OK (231.8s) 
== Test time == 
time: OK 
Score: 100/100