MIT 6.S081 2021: Lab file system

i-node

xv6文件系统是使用inode来管理文件,先上一张图来解释一个文件的inode是怎么管理它的磁盘块的:

 

xv6文件系统里定义了2个版本的inode。一个是硬盘上面存储的版本struct dinode,在fs.h里定义;另一个是内存里存储的版本struct inode,起到缓存的作用,在file.h里定义。两个结构都有成员ref和nlink。nlink是inode在文件系统里面的链接数,如果nlink为0,说明inode对应的文件已经被删除;ref是此inode被进程引用的次数,如果ref为0,就说明没有进程使用这个inode了,这时操作系统就不用继续在内存里缓存它,可以将它驱逐到硬盘。操作系统可以使用iupdate()更新硬盘里面的inode,用iget()增加一次ref,iput()减少一次ref。

内存里的inode有一个睡眠锁,读写inode的时候需要使用ilock()和iunlock()加锁。使用readi()和writei()可以读写inode管理的磁盘块,使用namei()可以根据路径搜索inode,使用nameiparent()可以搜索指定文件的父目录的inode。

Large files

这里我们主要关心的是指向数据块的指针。原来的xv6系统里,有12个direct pointer和1个single indirect pointer,一共能容纳268个硬盘块,现在要求改成:前11个指针是direct pointer,第12个指针改成single indirect pointer,第13个指针改成double indirect pointer,这样就扩大文件容量到65803块。

首先修改inode的相关定义:

在fs.h里

#define NDIRECT 11
#define SDIRECT_PTR 11
#define DDIRECT_PTR 12
#define NINDIRECT (BSIZE / sizeof(uint))
#define NDBINDIRECT ((BSIZE / sizeof(uint))*(BSIZE / sizeof(uint)))
#define MAXFILE (NDIRECT + NINDIRECT + NDBINDIRECT)
//#define MAXFILE 2000
​
// 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
};

把NDIRECT改为11;增加一个宏NDBINDIRECT,用来表示double indirect pointer的容量。为了保持inode大小不变,addrs数组的定义要改成NDIRECT+2。务必记得file.h里面的struct inode也需要修改。设置两个宏SDIRECT_PTR和DDIRECT_PTR,指示两个indirect pointer。另外还要修改MAXFILE,操作系统不会为inode安排多于MAXFILE的硬盘块数。

接下来要修改bmap()函数。bmap()负责的是:Return the disk block address of the nth block in inode ip.所以bmap会检查参数bn位于哪个区间,如果位于direct pointer之内就直接访问,如果没有对应数据块的话就调用balloc()分配一个新的硬盘块。如果超出了direct的范围,就把bn减去NDIRECT的偏移量,先访问addrs[11]指向的一级指针块,使用一级指针块里面的指针访问数据块。

同理可以实现二级指针块:

static uint
bmap(struct inode *ip, uint bn)
{
  uint addr, *a, *a1;
  struct buf *bp;
  struct buf *bp1st, *bp2nd;
  //printf("%d init bn\n",bn);
  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;
  }
  bn -= NINDIRECT;
  if(bn<NDBINDIRECT)
  {
    if((addr = ip->addrs[DDIRECT_PTR]) == 0)//先分配二阶指针的一阶节点
      ip->addrs[DDIRECT_PTR] = addr = balloc(ip->dev);
    
    bp1st = bread(ip->dev, addr);//读装满指针的一阶block
    a = (uint*)bp1st->data;
    uint block1st = bn/NINDIRECT;//访问哪个二阶block?
    if((addr = a[block1st]) == 0)
    {
      a[block1st] = addr = balloc(ip->dev);
      log_write(bp1st);
    }
    
    brelse(bp1st);//可以释放一级指针的读写了
​
    bp2nd = bread(ip->dev, addr);//读装满指针的二阶block
    a1 = (uint*)bp2nd->data;
    uint block2nd = bn%NINDIRECT;//访问二阶block的哪个指针
    if((addr = a1[block2nd]) == 0)
    {
      a1[block2nd] = addr = balloc(ip->dev);
      log_write(bp2nd);
    }
    
    brelse(bp2nd);
    return addr;
  }
​
  panic("bmap: out of range");
}

如果bn超出了double indirect pointer的范围,就再把bn减去NINDIRECT的偏移量,然后访问addrs[12]指向的一级指针。每个一级指针指向256个二级指针,所以使用bn/NINDIRECT找到待访问的一级指针block1st,通过它访问二级指针块bp2nd。使用bn模NINDIRECT找到指向数据块的二级指针block2nd。

注意函数调用bread()之后的处理方法:bread()读取硬盘块,把硬盘块缓存在内存里一个struct buf类型的结构体里面,返回一个指向它的指针。我们需要的数据在此结构体的data成员中。这个data是一个1024字节的uchar类型的数组,而我们的指针都是4字节的。因此需要把data转换成uint*类型,这样a[block1st]做的指针运算才是以4字节为步长的。

另外注意一下log_write的细节。log_write()上面的注释给出了使用xv6的日志系统的方法,这几个系统调用不能乱用:

log_write() replaces bwrite(); a typical use is:
   bp = bread(...)
   modify bp->data[]
   log_write(bp)
   brelse(bp)

然后修改itrunc(),释放所有数据块。itrunc()里面也给我们举了释放single indirect pointer的例子。直接模仿:

void
itrunc(struct inode *ip)
{
  int i, j;
  struct buf *bp, *bp1, *bp2;
  uint *a, *a1;
​
  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[DDIRECT_PTR])
  {
    bp1 = bread(ip->dev, ip->addrs[DDIRECT_PTR]); //访问一阶指针块
    a=(uint*)bp1->data;
    for(j=0;j<NINDIRECT;j++)
    {
      if(a[j])
      {
        //如果发现了二级指针块
        bp2 = bread(ip->dev, a[j]); //访问它
        a1=(uint*)bp2->data;
        for(int k=0;k<NINDIRECT;k++)
        {
          if(a1[k])//如果这个位置有数据块则释放
            bfree(ip->dev,a1[k]);
        }
        brelse(bp2);
        bfree(ip->dev,a[j]);//释放这个二级指针块
      }
      //释放完了二阶指针块,
    }
    brelse(bp1); //释放一级指针块
    bfree(ip->dev, ip->addrs[DDIRECT_PTR]);
    ip->addrs[DDIRECT_PTR] = 0;
  }
  ip->size = 0;
  iupdate(ip);
}

 

Symbolic links

我们需要实现一个符号链接的功能。符号链接是用专门的特殊文件类型“符号链接文件”来实现的,文件中只有一个表明链接对象路径的字符串。一旦建立了符号链接,删除操作删除的是符号链接文件,其它所有操作都访问符号链接引用的文件。

根据提示来逐步做:

1.首先在user.h和syscall.h等地方加入系统调用,在kernel里加入sys_symlink(),加入T_SYMLINK等定义。

2.实现symlink()调用。

先看一下硬链接link()做了什么:link接受两个参数,把待链接的文件路径放在old里,把新的链接路径放在new里。link()使用namei查询old的inode,返回指针ip,增加ip的nlink数;使用nameiparent()查询new的父目录的inode,然后使用dirlink()把new加入它的父目录之中。

相比link(),symlink()少了增加原来节点的nlink数,多了创建新的inode,写入target字符串的操作。因此再结合create()的操作来实现symlink()。

uint64 sys_symlink(void)
{
    char name[DIRSIZ], target[MAXPATH], path[MAXPATH];
    struct inode *dp, *ip, *sym;
    //ip指向target dp指向path的父目录
    if(argstr(0, target, MAXPATH) < 0 || argstr(1, path, MAXPATH) < 0)
        return -1;
​
    begin_op();
    if((ip = namei(target)) != 0) // 提示要求target指定的文件是可以不存在的
    {
        ilock(ip);
​
        if(ip->type == T_DIR) // 根据提示,不需要处理target是一个目录文件的情况
        {
            iunlockput(ip);
            end_op();
            return -1;
        }
        iunlockput(ip);
    }
​
    if((dp = nameiparent(path, name)) == 0) //获取path的父目录
    {
        end_op();
        return -1;
    }
​
    ilock(dp);
    if((sym = dirlookup(dp, name, 0)) != 0)  //如果有同名文件,则生成失败
    {
        iunlockput(dp);
        end_op();
        return -1;
    }
​
    if((sym = ialloc(dp->dev, T_SYMLINK)) == 0)  //仿照create生成符号链接类型的inode
        panic("create: ialloc");
​
    ilock(sym);
    sym->nlink = 1;
    iupdate(sym);
​
​
    if(dirlink(dp, name,sym->inum) < 0) //把符号链接文件加入到path的父目录中
        panic("create: dirlink");
​
    iupdate(dp);
    //把target字符串写入符号链接文件里面
    if(writei(sym, 0, (uint64)&target, 0, strlen(target)) != strlen(target))
        panic("symlink: writei");
​
    iupdate(sym);//注意调用iupdate把inode信息写入磁盘
​
    iunlockput(dp);
​
    iunlockput(sym);
​
    end_op();
    return 0;
​
}
​

这里需要注意的就是ilock()问题,使用inode记得上锁,用完inode一定要记得释放睡眠锁和减少ref值。

3.修改open()系统调用,识别T_SYMLINK类型;加入O_NOFOLLOW,如果使用O_NOFOLLOW则打开符号链接文件本身;保证文件系 统是一个有向无环图,不能出现环形链接。

首先要理解open()的作用。为了理解open(),我们首先要了解Linux的三级文件活动目录。首先,每个进程的PCB的user结构中有自己的FDT,FDT是一个名为u_ofile的数组,这个数组记录了该进程打开的文件,数组下标就是所谓的文件描述符。整个内核有一张struct file类型的系统文件表SFT。整个核心还有一张活动inode表,用作硬盘里inode的缓冲。

三张表的关系如下图:

 

 

我们再来看xv6。在file.c中有一个ftable结构体,里面的file数组存储了所有打开的文件。open调用的filealloc()其实就是在这个file数组里找一个ref数为0的项,返回指向这个项的指针;struct proc里有一个struct file*类型的ofile数组,fdalloc()在ofile数组里找到一个值为0的项,把这项填入一个指针,把下标返回,这样就得到了文件描述符。

不难发现,在xv6里ftable对应SFT、ofile对应FDT、前面的buffer cache对应活动inode表。所以open()“打开一个文件”,就是在SFT(ftable.file)里找到一个空的struct file数组项,向这项里写入需要的数据,再把这项的指针存入该进程的ofile数组,返回这个指针在ofile里的下标,把这个下标作为文件描述符返回。

理解了open()的作用就可以写代码了。我们的目的就是通过符号链接找到目标文件的inode:

uint64
sys_open(void)
{
  char path[MAXPATH];
  char sympath[MAXPATH];
  int fd, omode;
  struct file *f;
  struct inode *ip;         //稍后要传递给f->ip
  struct inode* symip=0;    //这里要设置一个symip用来防止锁的混乱
  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 {
      //printf("here2\n");
    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;
    }
  }
    //判断一下是不是符号链接;如果加了O_NOFOLLOW,则按照普通文件处理
  if(!(omode & O_NOFOLLOW) && ip->type == T_SYMLINK)
  {
    int i=0;
    //循环向下搜索,搜索到非符号链接文件为止;或者搜索深度达到10为止
    while(ip->type==T_SYMLINK)
    {
      //printf("here\n");
      if(readi(ip,0,(uint64)&sympath,0,MAXPATH)==-1)    //读取文件里的字符串,放在sympath里
      {
        iunlockput(ip);
        end_op();
        return -1;
      }
      iunlockput(ip);   //读取的ip可以释放了,记得ref减去1
      if((symip = namei(sympath)) == 0) //根据sympath搜索相应inode,如果搜索失败则退出
      {
        end_op();
        return -1;
      }
      i++;
      if(i==10)
      {
        end_op();
        return -1;
      }
      ip=symip; 
      ilock(ip);        //给刚刚获取的inode加锁
    }
  }
​
​
  if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
    iunlockput(ip);
    end_op();
    return -1;
  }
    //printf("here3\n");
​
  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);
  end_op();
​
  return fd;
}
​
posted @ 2021-11-19 22:48  LunaCancer  阅读(743)  评论(0编辑  收藏  举报