当前位置:   article > 正文

xv6源码阅读——文件系统_xv6 panic函数在哪里

xv6 panic函数在哪里

说明

  • 阅读的代码是 xv6-riscv 版本的

七层结构

xv6文件系统实现分为七层

文件描述符(File descriptor)
路径名(Pathname)
目录(Directory)
索引结点(Inode)
日志(Logging)
缓冲区高速缓存(Buffer cache)
磁盘(Disk)
  • disk 层从一个虚拟硬盘(vitro hard drive)上读取 block
  • buffer cache 层缓存 block
    保证同一个 block 只能被一个内核进程同时访问
  • logging 层记录一些日志信息,用于出错恢复
  • inode 层是一个独立的文件,唯一的 i-number 和一些存储数据的块
  • directory 层把文件夹封装称为一些特殊的 inode
    内容为一些列目录项,包括文件名和 i-number
  • pathname 层提供一个层次化的树结构,处理了递归包含的情况
  • file descriptor 层把 pipes, devices, files 等都抽象为文件,使用统一的文件系统的接口

磁盘(Disk)

文件系统必须有将索引节点和内容块存储在磁盘上哪些位置的方案。为此,xv6将磁盘划分为几个部分,如图8.2所示。
在这里插入图片描述

  • boot:文件系统不使用块0(它保存引导扇区)

  • super:它包含有关文件系统的元数据(文件系统大小(以块为单位)、数据块数、索引节点数和日志中的块数)。

  • log:保存日志(用于出现crash后进行数据恢复)

  • inodes: 是索引节点,每个块有多个索引节点

  • bit map:位图,跟踪正在使用的数据块,用来保存那些块是被使用或空闲的。

  • data:剩下的就是数据块,是否被占用保存在bitmap中

缓冲区高速缓存(Buffer cache)

设计概述

  • buffe catch 的两个任务

    • 1.同步对磁盘块的访问,以确保磁盘块在内存中只有一个副本,并且一次只有一个内核线程使用该副本
    • 2.缓存常用块,以便不需要从慢速磁盘重新读取它们
  • 对上提供的接口

    • bread() ,获取一个磁盘块,拷贝到缓存中
    • bwrite(),将缓存中的数据写回磁盘
    • brelse(),用完需要释放缓存快
  • 通过给每一个 buffer 分配一个 sleeplock 的方式实现一个磁盘块的拷贝只能被一个内核线程使用

  • Buffer cache中保存磁盘块的缓冲区数量固定,如果文件系统请求还未存放在缓存中的块,Buffer cache必须回收当前保存其他块内容的缓冲区。Buffer cache为新块回收最近使用最少的缓冲区。

    • 通过 virtio_disk_rw(b, 0) 实现

缓冲区高速缓存(代码实现)

Buffer cache是以双链表表示的缓冲区(为了实现LRU替换算法)

  • 每一个buf结构体如下
    • valid 字段表示这个 buffer 是否对应磁盘上的某一个块(1 表示有对应)
    • disk 字段表示是否将 buffer 中的信息写回了磁盘上(1 表示修改了未写回)
    • refcnt 表示引用计数
    • blockno 扇区号
    • dev 不同设备
struct buf {
  int valid;   // has data been read from disk?
  int disk;    // does disk "own" buf?
  uint dev;
  uint blockno;
  struct sleeplock lock;
  uint refcnt;
  struct buf *prev; // LRU cache list
  struct buf *next;
  uchar data[BSIZE];
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

main(kernel/main.c)调用的函数binit使用静态数组buf(kernel/bio.c)中的NBUF个缓冲区初始化列表。对Buffer cache的所有其他访问都通过bcache.head引用链表,而不是buf数组。

struct {
  struct spinlock lock;
  struct buf buf[NBUF];

  // Linked list of all buffers, through prev/next.
  // Sorted by how recently the buffer was used.
  // head.next is most recent, head.prev is least.
  struct buf head;
} bcache;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

binit函数

void
binit(void)
{
  struct buf *b;

  initlock(&bcache.lock, "bcache");

  // Create linked list of buffers
  bcache.head.prev = &bcache.head;
  bcache.head.next = &bcache.head;
  for(b = bcache.buf; b < bcache.buf+NBUF; b++){
    b->next = bcache.head.next;
    b->prev = &bcache.head;
    initsleeplock(&b->lock, "buffer");
    bcache.head.next->prev = b;
    bcache.head.next = b;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 对buf进行初始化,构建双向循环链表(NBUF个节点)
  • 为bcache初始化一把大锁(bcache),再为每一个节点初始化一把小锁(buffer)

bget函数

bget扫描缓冲区列表,查找具有给定设备和扇区号的缓冲区。

static struct buf*
bget(uint dev, uint blockno)
{
  struct buf *b;

  acquire(&bcache.lock);

  // Is the block already cached?
  for(b = bcache.head.next; b != &bcache.head; b = b->next){
    if(b->dev == dev && b->blockno == blockno){
      b->refcnt++;
      release(&bcache.lock);
      acquiresleep(&b->lock);
      return b;
    }
  }

  // Not cached.
  // Recycle the least recently used (LRU) unused buffer.
  for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
    if(b->refcnt == 0) {
      b->dev = dev;
      b->blockno = blockno;
      b->valid = 0;
      b->refcnt = 1;
      release(&bcache.lock);
      acquiresleep(&b->lock);
      return b;
    }
  }
  panic("bget: no buffers");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 若在缓冲区找到了,返回锁定的buf
    • refcnt引用计数要增加1。
  • 若没有找到,它再次扫描缓冲区列表,查找未在使用中的缓冲区(b->refcnt = 0):任何这样的缓冲区都可以使用。
    • 给dev和blockno初始化。
    • valid=0,表示没有对应磁盘上的某一块。
    • 引用计数初始化为1。

bread函数

// Return a locked buf with the contents of the indicated block.
struct buf*
bread(uint dev, uint blockno)
{
  struct buf *b;

  b = bget(dev, blockno);
  if(!b->valid) {
    virtio_disk_rw(b, 0);
    b->valid = 1;
  }
  return b;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 调用bget函数从bcatch获取指定buf
  • 若b->valid==0,表示没有对应磁盘上的某一块
    • 调用 virtio_disk_rw(b, 0);从磁盘中将其读入内存(buffer 中)

bwrite函数
该函数作用,将内存(buffer中)的数据写回到磁盘

// Write b's contents to disk.  Must be locked.
void
bwrite(struct buf *b)
{
  if(!holdingsleep(&b->lock))
    panic("bwrite");
  virtio_disk_rw(b, 1);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

日志(Logging)

设计概述

  • 目的

    解决了文件系统操作期间的崩溃问题

  • 解决方法

    • xv6系统调用不会直接写入磁盘上的文件系统数据结构。相反,它会在磁盘上的log(日志)中放置它希望进行的所有磁盘写入的描述。
    • 一旦系统调用记录了它的所有写入操作,它就会向磁盘写入一条特殊的commit(提交)记录,表明日志包含一个完整的操作。
    • 当完成commit后,再将写操作复制到磁盘当中,然后擦除磁盘上的日志
    • 如果系统崩溃并重新启动
      • 日志标记为包含完整操作,则恢复代码会将写操作复制到磁盘文件系统中它们所属的位置。
      • 如果日志没有标记为包含完整操作,则恢复代码将忽略该日志。
      • 擦除记录
  • log设计

    • 日志驻留在超级块中指定的已知固定位置。它由一个头块(header block)和一系列更新块的副本(logged block)组成。
      • 头块包含一个扇区号数组(每个logged block对应一个扇区号)以及日志块的计数
        • 磁盘上的头块中的计数或者为零,表示日志中没有事务;
        • 非零,表示日志包含一个完整的已提交事务 ;
struct logheader {
 int n;
 int block[LOGSIZE];
};
  • 1
  • 2
  • 3
  • 4

日志(代码实现)

下面看一下log结构体

struct log {
  struct spinlock lock;
  int start;
  int size;
  int outstanding; // 有多少FS系统调用正在执行调用正在执行
  int committing;  // 是否在commit当中
  int dev;
  struct logheader lh;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

在系统调用中一个典型的日志使用就像这样:

 begin_op();
 ...
 bp = bread(...);
 bp->data[...] = ...;
 log_write(bp);
 ...
 end_op();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

下面我们就上面过程,进行逐一分析

begin_op函数

  • begin_op等待直到日志系统当前未处于提交中,并且直到有足够的未被占用的日志空间来保存此调用的写入。
void
begin_op(void)
{
  acquire(&log.lock);
  while(1){
    if(log.committing){
      sleep(&log, &log.lock);
    } else if(log.lh.n + (log.outstanding+1)*MAXOPBLOCKS > LOGSIZE){
      // this op might exhaust log space; wait for commit.
      sleep(&log, &log.lock);
    } else {
      log.outstanding += 1;
      release(&log.lock);
      break;
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 先判断是否处于提交中
  • 在判断是否有足够空间
    (xv6 认为每个系统调用只会使用 MAXOPBLOCKS(10) 个 block)
  • outstanding+1,然后break

log_write函数

  • log_write(kernel/log.c:214)充当bwrite的代理。它将块的扇区号记录在内存中,在磁盘上的日志中预定一个槽位,并调用bpin将缓存固定在block cache中,以防止block cache将其逐出。
void
log_write(struct buf *b)
{
  int i;

  acquire(&log.lock);
  if (log.lh.n >= LOGSIZE || log.lh.n >= log.size - 1)
    panic("too big a transaction");
  if (log.outstanding < 1)
    panic("log_write outside of trans");

  for (i = 0; i < log.lh.n; i++) {
    if (log.lh.block[i] == b->blockno)   // log absorption
      break;
  }
  log.lh.block[i] = b->blockno;
  if (i == log.lh.n) {  // Add new block to log?
    bpin(b);
    log.lh.n++;
  }
  release(&log.lock);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • for循环,进行优化,log_write会注意到在单个事务中多次写入一个块的情况,并在日志中这种优化通常称为合并(absorption)。
  • 调用 bpin(b)对b的引用计数++,防止被替换出去。

end_op函数

  • end_op(kernel/log.c:146)首先减少未完成系统调用的计数。如果计数现在为零,则通过调用commit()提交当前事务。
// called at the end of each FS system call.
// commits if this was the last outstanding operation.
void
end_op(void)
{
  int do_commit = 0;

  acquire(&log.lock);
  log.outstanding -= 1;
  if(log.committing)
    panic("log.committing");
  if(log.outstanding == 0){
    do_commit = 1;
    log.committing = 1;
  } else {
    // begin_op() may be waiting for log space,
    // and decrementing log.outstanding has decreased
    // the amount of reserved space.
    wakeup(&log);
  }
  release(&log.lock);

  if(do_commit){
    // call commit w/o holding locks, since not allowed
    // to sleep with locks.
    commit();
    acquire(&log.lock);
    log.committing = 0;
    wakeup(&log);
    release(&log.lock);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 首先将计数 log.outstanding -1(系统调用数 -1)
  • 如果此时计数变成 0,则调用 commit() 进行 commit

commit函数

commit有四个步骤,代码如下

static void
commit()
{
  if (log.lh.n > 0) {
    write_log();     // Write modified blocks from cache to log
    write_head();    // Write header to disk -- the real commit
    install_trans(0); // Now install writes to home locations
    log.lh.n = 0;
    write_head();    // Erase the transaction from the log
  }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

write_log()函数

将事务中修改的每个块从缓冲区缓存复制到磁盘上日志槽位中。

// Copy modified blocks from cache to log.
static void
write_log(void)
{
  int tail;

  for (tail = 0; tail < log.lh.n; tail++) {
    struct buf *to = bread(log.dev, log.start+tail+1); // log block
    struct buf *from = bread(log.dev, log.lh.block[tail]); // cache block
    memmove(to->data, from->data, BSIZE);
    bwrite(to);  // write the log
    brelse(from);
    brelse(to);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • write_log():把具体的内容写入 log 对应的磁盘块里
    通过调用 bread() 读取磁盘块、memmove() 内存复制、bwrite() 写入磁盘完成
    需要写入的磁盘区域 (\to) 对应的 log 块
  • 注意这里的 log 数组中记录的磁盘块写入对应下标+1的 log 块中,因为第一块留给了 log 的 head

write_head()函数
将头块写入磁盘:这是提交点,写入后的崩溃将导致从日志恢复重演事务的写入操作。

static void
write_head(void)
{
  struct buf *buf = bread(log.dev, log.start);
  struct logheader *hb = (struct logheader *) (buf->data);
  int i;
  hb->n = log.lh.n;
  for (i = 0; i < log.lh.n; i++) {
    hb->block[i] = log.lh.block[i];
  }
  bwrite(buf);
  brelse(buf);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

write_head():把 log 块的头信息写入磁盘
log 块区域的第一块是 log 的 header 块

install_trans函数

// Copy committed blocks from log to their home location
static void
install_trans(int recovering)
{
  int tail;

  for (tail = 0; tail < log.lh.n; tail++) {
    struct buf *lbuf = bread(log.dev, log.start+tail+1); // read log block
    struct buf *dbuf = bread(log.dev, log.lh.block[tail]); // read dst
    memmove(dbuf->data, lbuf->data, BSIZE);  // copy block to dst
    bwrite(dbuf);  // write dst to disk
    if(recovering == 0)
      bunpin(dbuf);
    brelse(lbuf);
    brelse(dbuf);
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • install_trans():执行原来要求的写操作

recover_from_log()函数

static void
recover_from_log(void)
{
  read_head();
  install_trans(1); // if committed, copy from log to disk
  log.lh.n = 0;
  write_head(); // clear the log
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 在启动时会调用这段代码进行恢复
    • 先将磁盘中的日志头读入到内存中的日志头
    • 调用install_trans函数,如果commited,将日志中保存的操作拷贝到磁盘
    • 将内存中的日志头操作数置为0
    • 将内存中的日志头重新写会磁盘

用于crash恢复

块分配器(bitmap)

  • xv6的块分配器在磁盘上维护一个空闲位图,每一位代表一个块。0表示对应的块是空闲的;1表示它正在使用中。

  • 程序mkfs设置对应于引导扇区、超级块、日志块、inode块和位图块的比特位。

  • balloc() 函数从磁盘中找一个空闲的磁盘块并返回(bitmap 中标记为 0)

  • bfree() 函数释放一个指定的磁盘块

索引节点(Inode)

两种含义

  • 磁盘上的数据结构(on-disk),包含文件的大小和一个数据存放位置的列表
  • 内存中的数据结构(in-memory),包括磁盘上数据结构的一个拷贝以及一些内核需要用到的内容

on-disk

  • 磁盘上的 inode 被放置在 inode blocks 中,大小都是相同的,给定一个 n(i-number)就能找到第 n 个 inode 的位置
  • 数据结构如下
    • type:file、directory、special files(devices)、0(表示空闲)
    • nlink:引用数(用于判断数据块是否该被释放)
    • size:文件所占据的字节数
    • addrs:文件所在数据块的地址
// 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
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

in-memory

struct inode(kernel/file.h:17)是磁盘上struct dinode的内存副本。只有当有C指针引用某个inode时,内核才会在内存中存储该inode。
结构如下:

  • ref:引用计数,如果引用计数降至零,内核将从内存中丢弃该inode。
    • iget和iput函数,修改引用计数
    • 指向inode的指针可以来自文件描述符、当前工作目录和如exec的瞬态内核代码。
  • dev:设备号
  • inum:inode编号
  • valid:表示是否已从磁盘读取
// 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];
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 有 4 种锁或类锁的机制

    • icache.lock 保证一个 inode 只会在缓存中出现一次,保证引用计数 ref 的正确
    • 每一个 inode 都有一个 sleeplock,用于保证其内部字段的独占访问
    • ref 不为 0 的时候这个 entry 不会被回收
    • nlink 字段表示这个文件的引用数(他不为 0,xv6 就不会释放 entry)
  • iget() 返回一个 inode

    • 如果是新使用的,valid = 0,之后使用的时候会从磁盘读入(ilock() 实现读入),这块实现和磁盘缓存十分的相似
  • ilock() 是加锁的,iget() 不加锁

  • inode cache 是 write-through(直写的),调用 iupdate() 直接反应到磁盘上

索引节点(代码实现)

分配一个新的 inode 结点(新建文件等)的流程如下

  • 调用 ialloc()
    • 从标号为 1的 inode 的开始查找,找到一个空闲的 inode 之后,调用 iget() 返回
    • 根节点也是从 1 开始的(#define ROOTINO 1)
  • iget()
    • 先看看有没有缓存,若有缓存,则引用数 ref +1,直接返回
    • 如果没有找到,则将第一个空闲的 inode 返回
  • ilock()
    • 想要对 inode 进行读写操作的时候,需要先对 inode 获取锁(调用 ilock)
    • 同时 ilock 会检查这个内容有没有从磁盘冲读进内存,如果没有则读入
  • iunlock()
    • 释放锁
  • iput()
    • 引用数 ref -1
    • 如果减完之后计数为 0,同时 valid=1,nlink=0 的话,释放数据块
  • 可能会出现这样一种情况,nlink=0,ref 不为 0,此时 inode 没有被释放
    • 当 ref 被减为 0 的瞬间,系统崩溃了
    • 重启的时候我们知道 nlink=0 的文件是没有用的,但是我们在磁盘上这块空间并未被释放
    • xv6 没有针对这个问题进行补救,因此可能出现磁盘空间耗尽的情况

在这里插入图片描述

目录层(代码实现)

目录的内部实现很像文件。

// kernel/fs.h
struct dirent {
    ushort inum;
    char name[DIRSIZ];
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • name 最长为 DIRSIZ=14 个字符
  • dirlookup()
    在一个目录中查找指定 name 的文件,返回 inode 指针
  • dirlink()
    按照给定的 name,新建一个新的文件夹
    遍历 entry,找到一个空闲的返回
    如果已经存在,则报错
本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/391595
推荐阅读
相关标签
  

闽ICP备14008679号