riscv-xv6单步调试x 文件系统2

1. Inode层

首先这里有两个分配硬盘的空闲block的函数。

static uint
balloc(uint dev)
{
  int b, bi, m;
  struct buf *bp;

  bp = 0;
  for(b = 0; b < sb.size; b += BPB){
    bp = bread(dev, BBLOCK(b, sb));
    for(bi = 0; bi < BPB && b + bi < sb.size; bi++){
      m = 1 << (bi % 8);
      if((bp->data[bi/8] & m) == 0){  // Is block free?
        bp->data[bi/8] |= m;  // Mark block in use.
        log_write(bp);
        brelse(bp);
        bzero(dev, b + bi);
        return b + bi;
      }
    }
    brelse(bp);
  }
  panic("balloc: out of blocks");
}
static void
bfree(int dev, uint b)
{
  struct buf *bp;
  int bi, m;

  bp = bread(dev, BBLOCK(b, sb));
  bi = b % BPB;
  m = 1 << (bi % 8);
  if((bp->data[bi/8] & m) == 0)
    panic("freeing free block");
  bp->data[bi/8] &= ~m;
  log_write(bp);
  brelse(bp);
}

就是遍历bitmap,然后返回或者回收空闲块而已。

  • 这里也可以解释为什么之前说要求bwrite时带buf锁起到简化代码的作用。bread直接返回一个带锁的buf,本来是一个低效的行为,因为它阻止了两个进程同时读一个block。一般来说同时读block是没问题的,但是对于bitmap块就不能允许同时读(否则容易出现一个块分给两个进程的竞争情况)。如果bread不返回一个带锁的buf,这里又需要额外增加对bitmap块的特判,会增加代码的复杂性
  • 注意这里修改block的内容没有发现begin_op请求,因此一定需要先发起begin_op请求后再调用该函数,看后面代码验证

下面是inode层的数据结构

#define T_DIR     1
#define T_FILE    2
#define T_DEVICE  3
// 上面的宏用于type字段,如果type=0表示该dinode尚未分配
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
};
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];
};
struct {
  struct spinlock lock;
  struct inode inode[NINODE];
} icache;

上面三个结构分别对应硬盘上的inode(dinode),缓存到内存中的inode以及管理所有内存中的inode的icache。
inode.major和inode.minor应该是遵循linux的习惯,代表驱动设备号和inode所在的设备号,后面会看到major在设备文件读写中的作用。

static struct inode*
iget(uint dev, uint inum)
{
  struct inode *ip, *empty;

  acquire(&icache.lock);

  // Is the inode already cached?
  empty = 0;
  for(ip = &icache.inode[0]; ip < &icache.inode[NINODE]; ip++){
    if(ip->ref > 0 && ip->dev == dev && ip->inum == inum){
      ip->ref++;
      release(&icache.lock);
      return ip;
    }
    if(empty == 0 && ip->ref == 0)    // Remember empty slot.
      empty = ip;
  }

  // Recycle an inode cache entry.
  if(empty == 0)
    panic("iget: no inodes");

  ip = empty;
  ip->dev = dev;
  ip->inum = inum;
  ip->ref = 1;
  ip->valid = 0;
  release(&icache.lock);

  return ip;
}

iget函数和cache层的bget函数基本上差不多,有两点不同:缓存inode的icache没有组织成双链表;另外,返回的inode没有带锁。

void
ilock(struct inode *ip)
{
  struct buf *bp;
  struct dinode *dip;

  if(ip == 0 || ip->ref < 1)
    panic("ilock");

  acquiresleep(&ip->lock);

  if(ip->valid == 0){
    bp = bread(ip->dev, IBLOCK(ip->inum, sb));
    dip = (struct dinode*)bp->data + ip->inum%IPB;
    ip->type = dip->type;
    ip->major = dip->major;
    ip->minor = dip->minor;
    ip->nlink = dip->nlink;
    ip->size = dip->size;
    memmove(ip->addrs, dip->addrs, sizeof(ip->addrs));
    brelse(bp);
    ip->valid = 1;
    if(ip->type == 0)
      panic("ilock: no type");
  }
}

可以看到,获取inode锁的任务放到了ilock中,ilock同时保证返回的inode中的内容一定是有效的。因此ilock的功能类似于bread函数。

void
iunlock(struct inode *ip)
{
  if(ip == 0 || !holdingsleep(&ip->lock) || ip->ref < 1)
    panic("iunlock");

  releasesleep(&ip->lock);
}
struct inode*
idup(struct inode *ip)
{
  acquire(&icache.lock);
  ip->ref++;
  release(&icache.lock);
  return ip;
}

void
iput(struct inode *ip)
{
  acquire(&icache.lock);

  if(ip->ref == 1 && ip->valid && ip->nlink == 0){
    // inode has no links and no other references: truncate and free.

    // ip->ref == 1 means no other process can have ip locked,
    // so this acquiresleep() won't block (or deadlock).
    acquiresleep(&ip->lock);

    release(&icache.lock);

    itrunc(ip);
    ip->type = 0;
    iupdate(ip);
    ip->valid = 0;

    releasesleep(&ip->lock);

    acquire(&icache.lock);
  }

  ip->ref--;
  release(&icache.lock);
}

iunlock函数是与ilock函数对称,idup函数类似于bpin函数,而iput函数则与brelse函数相似。当iput函数发现自己是最后一个持有该inode指针且该inode的引用链接数为0时,会将该inode删除。

  • 删除的第一步是调用itrunc函数释放该inode对应文件占用的所有块(因此itrunc函数内部是调用bfree,实际上是对bitmap块进行修改,当然也不是实际的写到磁盘,而是调用log_write记上一笔)
  • 删除的第二步是调用调用iupdate函数,即把内存中的inode写回到磁盘中的dinode,当然这也是通过log_write实现。
  • 安全性:任何尝试获取inode前,需要获得icache锁,任何尝试读写inode内容时,需要获得inode锁。因此在获得icache锁之后,没有进程能够通过iget获取inode,而在释放icache锁之后,因为该inode的link数为0,因此其它进程也没办法获取到该inode(全局看一下,只有ialloc, dirlookup, namex这三个函数内部调用了iget方法,由于函数内部没有调用end_op方法,因此直到函数结束之后,对inode的修改都还没写到磁盘上,故ialloc是不会把这个inode分出去的,而dirlookup和namx都是基于目录项来获取inode,由于link数为0,故这两个函数也没办法获取到正在删除的inode)
  • 这里唯一的危险是可能一个进程在持有一个link数为0的inode时,还没有释放这个inode指针(调用iput),系统就崩溃了,这样的话就导致存在一个文件已经占用了内存空间,但是却没有目录项指向它
int
writei(struct inode *ip, int user_src, uint64 src, uint off, uint n)
{
  uint tot, m;
  struct buf *bp;

  if(off > ip->size || off + n < off)
    return -1;
  if(off + n > MAXFILE*BSIZE)
    return -1;

  for(tot=0; tot<n; tot+=m, off+=m, src+=m){
    bp = bread(ip->dev, bmap(ip, off/BSIZE));
    m = min(n - tot, BSIZE - off%BSIZE);
    if(either_copyin(bp->data + (off % BSIZE), user_src, src, m) == -1) {
      brelse(bp);
      n = -1;
      break;
    }
    log_write(bp);
    brelse(bp);
  }
  if(off > ip->size)
    ip->size = off;
    // write the i-node back to disk even if the size didn't change
    // because the loop above might have called bmap() and added a new
    // block to ip->addrs[].
  iupdate(ip);

  return n;
}

inode层提供的最重要的两个函数就是writei和readi了。其中bmap函数的输入为inode指针和inode内的block编号,返回该block实际在硬盘上的扇区号。类似于walk函数,bmap也包括了内部调用balloc,进行block的分配。

  • 总的来说,该函数的作用就是给定inode和偏移量,在inode指向的block中写指定数目的字节。显然,该函数必须在begin_op(), end_op()中调用。
  • readi与writei类似,唯一的区别是readi不需要在begin_op(),end_op()中调用。

2. inode层的总结

  • inode层疑问:inode的refcnt来自哪些方面,有什么作用?

3. directory层

有了inode层的函数,这一层就很简单了。新的数据结构是目录项,每个目录inode对应的

struct dirent {
  ushort inum;  // 该目录项对应的inode
  char name[DIRSIZ];   // 该目录项的名称
};

目录层主要的函数就是用来解析目录,添加目录项这样。

static struct inode*
namex(char *path, int nameiparent, char *name)

int
dirlink(struct inode *dp, char *name, uint inum)

struct inode*
dirlookup(struct inode *dp, char *name, uint *poff)
  • dirlookup的作用是根据目录inode参数dp,和目录项名字name,返回该目录项指向的inode,当然也是不带锁的。
  • dirlink的作用在目录inode中添加一个新的名字为name的inode项,指向的inode编号为inum(因此调用前需要发起写请求)。
  • namex用于路径解析,给定文件路径,依据nameiparent的值返回该路径对应的文件或者目录的inode,或者返回其父目录对应的inode。返回的inode也是不带锁的。

值得注意的是,得益于cache层和inode层的设计,在目录层就不需要过多地考虑加锁,解锁的问题了。对于inode和block的使用,只需要遵循以下的原则

struct inode* i = iget(dev, inum);
//做一些其它的事情
ilock(i);  // 需要读或者写inode了
//读写操作
iunlock(i) // 或者iunlockput(i)

struct buf* b = bread(dev, num);
// 读写操作
brelse(b);

因为有refcnt的存在,保证了手上的inode不会被删除(虽然内容可能被修改),在希望读取的时候,调用ilock保证内容的有效性,同时加锁避免竞争。

4. file descriptor层

最后我们来看下前面的函数是如何为顶层调用服务的。
首先还是数据结构。

struct file {
#ifdef LAB_NET
  enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE, FD_SOCK } type;
#else
  enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
#endif
  int ref; // reference count
  char readable;
  char writable;
  struct pipe *pipe; // FD_PIPE
  struct inode *ip;  // FD_INODE and FD_DEVICE
#ifdef LAB_NET
  struct sock *sock; // FD_SOCK
#endif
  uint off;          // FD_INODE
  short major;       // FD_DEVICE
};

struct {
  struct spinlock lock;
  struct file file[NFILE];
} ftable;

readable, writable刻画该文件的读写权限,off表示文件读写偏移。
ftable就管理所有打开的文件,文件打开上限为NFILE。

struct file*
filealloc(void)
{
  struct file *f;

  acquire(&ftable.lock);
  for(f = ftable.file; f < ftable.file + NFILE; f++){
    if(f->ref == 0){
      f->ref = 1;
      release(&ftable.lock);
      return f;
    }
  }
  release(&ftable.lock);
  return 0;
}

// Increment ref count for file f.
struct file*
filedup(struct file *f)
{
  acquire(&ftable.lock);
  if(f->ref < 1)
    panic("filedup");
  f->ref++;
  release(&ftable.lock);
  return f;
}
void
fileclose(struct file *f)
{
  struct file ff;

  acquire(&ftable.lock);
  if(f->ref < 1)
    panic("fileclose");
  if(--f->ref > 0){
    release(&ftable.lock);
    return;
  }
  ff = *f;
  f->ref = 0;
  f->type = FD_NONE;
  release(&ftable.lock);

  if(ff.type == FD_PIPE){
    pipeclose(ff.pipe, ff.writable);
  } else if(ff.type == FD_INODE || ff.type == FD_DEVICE){
    begin_op();
    iput(ff.ip);
    end_op();
  }
}

这三个函数与ftable的管理相关,和inode层,cache层那些都差不多了。filealloc分出一个未使用的file,filedup让该file的引用计数加1,fileclose让该file的引用计数减少1。

接下来看下上层系统调用的实现

uint64
sys_link(void)
{
  char name[DIRSIZ], new[MAXPATH], old[MAXPATH];
  struct inode *dp, *ip;

  if(argstr(0, old, MAXPATH) < 0 || argstr(1, new, MAXPATH) < 0)
    return -1;

  begin_op();
  if((ip = namei(old)) == 0){
    end_op();
    return -1;
  }

  ilock(ip);
  if(ip->type == T_DIR){
    iunlockput(ip);
    end_op();
    return -1;
  }

  ip->nlink++;
  iupdate(ip);
  iunlock(ip);

  if((dp = nameiparent(new, name)) == 0)
    goto bad;
  ilock(dp);
  if(dp->dev != ip->dev || dirlink(dp, name, ip->inum) < 0){
    iunlockput(dp);
    goto bad;
  }
  iunlockput(dp);
  iput(ip);

  end_op();

  return 0;

bad:
  ilock(ip);
  ip->nlink--;
  iupdate(ip);
  iunlockput(ip);
  end_op();
  return -1;
}

可以看到,所有可能对文件系统的修改都用begin_op, end_op包起来了。整个实现也很直接,通过old路径找到inode,然后引用计数+1,再找到新路径的父目录,加上一个指向该inode的目录项即可。

  • 要先释放了ip锁,再尝试获取dp锁,否则会有死锁的危险(看下面的unlink代码)。
uint64
sys_unlink(void)
{
  struct inode *ip, *dp;
  struct dirent de;
  char name[DIRSIZ], path[MAXPATH];
  uint off;

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

  begin_op();
  if((dp = nameiparent(path, name)) == 0){
    end_op();
    return -1;
  }

  ilock(dp);

  // Cannot unlink "." or "..".
  if(namecmp(name, ".") == 0 || namecmp(name, "..") == 0)
    goto bad;

  if((ip = dirlookup(dp, name, &off)) == 0)
    goto bad;
  ilock(ip);

  if(ip->nlink < 1)
    panic("unlink: nlink < 1");
  if(ip->type == T_DIR && !isdirempty(ip)){
    iunlockput(ip);
    goto bad;
  }

  memset(&de, 0, sizeof(de));
  if(writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
    panic("unlink: writei");
  if(ip->type == T_DIR){
    dp->nlink--;
    iupdate(dp);
  }
  iunlockput(dp);

  ip->nlink--;
  iupdate(ip);
  iunlockput(ip);

  end_op();

  return 0;

bad:
  iunlockput(dp);
  end_op();
  return -1;
}

这里unlink同时持有了两把锁,先获取了待删除项的父目录的inode锁,然后再获取了待删除项的锁。

  • 设想如果两个进程,一个进程调用link在同一个目录中试图创造文件A的副本,另一个进程调用unlink试图删除文件A,如果link函数在不释放ip锁的时候获取了dp锁,那么就成了先获取文件A的锁,再获取文件A的父目录锁。此时unlink正好相反,先获取A的父目录的锁,再获取文件A的锁,这里可能造成死锁
  • 那能不能unlink在获取文件A锁的时候释放父目录的锁呢?再设想两个进程同时调用unlink试图删除文件A,如果unlink的获取文件A锁的时候释放了父目录锁,那么一个进程M获得了文件A的锁,另一个进程N可能会卡在获取文件A锁的位置。当M最终退出时,进程获取到文件A锁,却发现A的link数为0,触发panic!。相反,如果M在获取文件A锁时没有释放A的父目录的锁,这样N会卡在获取父目录锁上。当M最终退出时,已经删除了指向A的目录项,N进程调用dirlookup时将会失败,从而返回
  • 在获取多个inode锁时,只要遵循先获取父目录锁,再获取子目录锁的原则就不会死锁。
  • 另外,link和unlink在使用时并没有区分设备文件和普通文件。

接下来是与文件创建相关的函数。

uint64
sys_mkdir(void)
{
  char path[MAXPATH];
  struct inode *ip;

  begin_op();
  if(argstr(0, path, MAXPATH) < 0 || (ip = create(path, T_DIR, 0, 0)) == 0){
    end_op();
    return -1;
  }
  iunlockput(ip);
  end_op();
  return 0;
}

uint64
sys_mknod(void)
{
  struct inode *ip;
  char path[MAXPATH];
  int major, minor;

  begin_op();
  if((argstr(0, path, MAXPATH)) < 0 ||
     argint(1, &major) < 0 ||
     argint(2, &minor) < 0 ||
     (ip = create(path, T_DEVICE, major, minor)) == 0){
    end_op();
    return -1;
  }
  iunlockput(ip);
  end_op();
  return 0;
}

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

sys_mkdir, sys_mknod, sys_open这三个函数分别用来创建目录文件,设备文件,普通文件。分别内部调用create,传入的type参数对应相应的文件类型。可以看到,xv6允许用只读方式打开目录,也可以用open打开设备文件。另外,在sys_open中只释放了inode锁,并没有调用iput,这是因为file结构持续地持有inode指针,不能够让对应的inode缓存被另外的inode占用。这也意味着一个打开的文件将持续地贡献一个inode的引用计数,直到调用fileclose

5. 读写操作与设备文件

在file.c和file.h中记录了xv6的驱动

#define CONSOLE 1 //xv6只实现了读取键盘输入和写显示器的驱动

struct devsw {
  int (*read)(int, uint64, int);
  int (*write)(int, uint64, int);
};
struct devsw devsw[NDEV];
//其中NDEV=10,即xv6最多能有10种不同的设备驱动程序

在console.c中,将键盘输入的读入和写到显示器绑定到devsw上了

void
consoleinit(void)
{
  initlock(&cons.lock, "cons");

  uartinit();

  // connect read and write system calls
  // to consoleread and consolewrite.
  devsw[CONSOLE].read = consoleread;  //读键盘输入
  devsw[CONSOLE].write = consolewrite;  // 写到显示器
}

接下来看init.c中的一段代码:

  if(open("console", O_RDWR) < 0){
    mknod("console", CONSOLE, 0);
    open("console", O_RDWR);
  }
  dup(0);  // stdout
  dup(0);  // stderr

这里会创建设备文件console,把CONSOLE这个major号绑定到了console文件上。然后打开console。最后看下读写的函数

int
filewrite(struct file *f, uint64 addr, int n)
{
  int r, ret = 0;

  if(f->writable == 0)
    return -1;

  if(f->type == FD_PIPE){
    ret = pipewrite(f->pipe, addr, n);
  } else if(f->type == FD_DEVICE){
    if(f->major < 0 || f->major >= NDEV || !devsw[f->major].write)
      return -1;
    ret = devsw[f->major].write(1, addr, n);
  } else if(f->type == FD_INODE){
    // write a few blocks at a time to avoid exceeding
    // the maximum log transaction size, including
    // i-node, indirect block, allocation blocks,
    // and 2 blocks of slop for non-aligned writes.
    // this really belongs lower down, since writei()
    // might be writing a device like the console.
    int max = ((MAXOPBLOCKS-1-1-2) / 2) * BSIZE;
    int i = 0;
    while(i < n){
      int n1 = n - i;
      if(n1 > max)
        n1 = max;

      begin_op();
      ilock(f->ip);
      if ((r = writei(f->ip, 1, addr + i, f->off, n1)) > 0)
        f->off += r;
      iunlock(f->ip);
      end_op();

      if(r < 0)
        break;
      if(r != n1)
        panic("short filewrite");
      i += r;
    }
    ret = (i == n ? n : -1);
  } else {
    panic("filewrite");
  }

  return ret;
}
  • 可以看到,如果file的类型是一个驱动,那么就会调用相应的函数进行读写。这里可以大概明白unix设备文件的含义了,一个设备文件总是绑定了一个驱动函数,打开设备文件后的读写,就相当于调用相应的驱动程序
  • MAXOPBLOCK相当于一个规定,保证一次写请求需要修改的block数目不超过MAXOPBLOCK,用在这里防止一次计算一次writei最多能写多少字节(虽然并没有看懂这是怎么算的)

6. 总结

  • 关于inode中引用计数的来历:最常见的一种就是需要调用iget临时读取一个inode,然后很快就释放。另外有可能来自于一个打开的文件,这可能会长时间贡献一个引用计数。还有一种来自于进程的工作目录。查看proc定义,p->cwd也有一个指向inode的指针。比如fork函数的代码就有这么一行
np->cwd = idup(p->cwd);
  • 关于整个xv6的文件系统的感受,可以说文件系统是整个xv6最复杂的部分了,个人感觉尤其体现在锁的运用上,各个地方的锁如何使用,如何避免竞争,如何避免死锁等等问题还是挺麻烦的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值