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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM