[xv6]:文件系统

    1. 总览

    xv6的文件系统是一个类Linux文件系统,具体分为7个层次,下面是xv6的文件系统层次图

    • 文件系统层次图
      1

    磁盘结构方面,同样是Linux的简化版本,分为了6个区块

    • 磁盘结构图

      2

      • boot

        存放boot sector, 即启动引导程序, 具体就是mkfs(mkfs.c)

      • super block

        包含了文件系统的元数据, 包括文件系统大小,数据块大小,日志区域大小,inode数量

        • 实际结构

          1
          2
          3
          4
          5
          6
          7
          8
          9
          10
          struct superblock {
          uint magic; // Must be FSMAGIC
          uint size; // Size of file system image (blocks)
          uint nblocks; // Number of data blocks
          uint ninodes; // Number of inodes.
          uint nlog; // Number of log blocks
          uint logstart; // Block number of first log block
          uint inodestart; // Block number of first inode block
          uint bmapstart; // Block number of first free map block
          };
      • bit map

        用于指示哪些块正在被使用, 这里的块是磁盘中的所有块

    2. Buffer cache

    xv6中 buffer cache相关代码位于bio.c文件, buffer cache有两项工作:

    1. 同步访问磁盘块,以确保磁盘块在内存中只有一个buffer缓存,并且一次只有一个内核线程能使用该buffer缓存
    2. 缓存使用较多的块,这样它们就不需要从慢速磁盘中重新读取
    • bio.c

      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
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      #include "types.h"
      #include "param.h"
      #include "spinlock.h"
      #include "sleeplock.h"
      #include "riscv.h"
      #include "defs.h"
      #include "fs.h"
      #include "buf.h"

      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;

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

      // Look through buffer cache for block on device dev.
      // If not found, allocate a buffer.
      // In either case, return locked buffer.
      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");
      }

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

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

      // Release a locked buffer.
      // Move to the head of the most-recently-used list.
      void
      brelse(struct buf *b)
      {
      if(!holdingsleep(&b->lock))
      panic("brelse");

      releasesleep(&b->lock);

      acquire(&bcache.lock);
      b->refcnt--;
      if (b->refcnt == 0) {
      // no one is waiting for it.
      b->next->prev = b->prev;
      b->prev->next = b->next;
      b->next = bcache.head.next;
      b->prev = &bcache.head;
      bcache.head.next->prev = b;
      bcache.head.next = b;
      }

      release(&bcache.lock);
      }

      void
      bpin(struct buf *b) {
      acquire(&bcache.lock);
      b->refcnt++;
      release(&bcache.lock);
      }

      void
      bunpin(struct buf *b) {
      acquire(&bcache.lock);
      b->refcnt--;
      release(&bcache.lock);
      }
    • 代码分析

      • bcache

        xv6使用了一个数据结构bcache用于维护整个缓存,其使用了一把大锁来保证缓存的并发访问的正确性

      • buf

        xv6使用buf结构体来表示单个缓存块

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        struct buf {
        int valid; // 其中的数据是否有效(可以刷回磁盘)
        int disk; // does disk "own" buf?
        uint dev;
        uint blockno; // 缓存块对应的设备文件以及设备上的磁盘块号
        struct sleeplock lock;
        uint refcnt; // 引用计数,表示当前是否有进程正在使用该buufer
        struct buf *prev; // LRU cache list
        struct buf *next;
        uchar data[BSIZE];
        };

        每个缓存块都有一把自己的sleeplock, 如果一个进程想要使用该buffer块, 必须要拿到buffer块的锁,然后将refcnt++

      • 内存布局

        xv6中使用了一个双向链表来维护所有的缓存块,该链表是一个LRU链表,链表头部的buffer块是最新被使用的buffer

        在这里,满足LRU的缓存块的条件就是refcnt == 0且最靠近链表尾部

      • API

        • binit(): 初始化整个缓存区域

        • bget(): 返回某个设备上的某个磁盘块对应的buffer块,如果没有,那么就创建一个buffer块,并不覆盖其中的内容

          该函数只会被bread()调用

        • bread(): 调用bread(), 并且如果没有对应buffer块的话,会使用磁盘块的内容覆盖得到的buffer

        • bwrite(): 覆写一个bufffer

        • brelse(): 每当进程使用完一个buffer块之后,就需要调用breles()将其释放回LRU链表

        注意这里的LRU链表只有增加块而没有减少块

    3. Logging

    xv6中采用了日志系统来进行崩溃恢复

    • 磁盘日志结构

      在xv6的实现中,磁盘中的Log区域由logheader以及很多logged block构成, logged block是更新之后的缓存块的副本

      • logheader

        1
        2
        3
        4
        struct logheader {
        int n; // 当前日志块的数量, 每一个块都是一个完整的事务提交
        int block[LOGSIZE]; // 日志块(扇区号)数组, block[i]表示该block要写入的磁盘的扇区号
        };
      • 全局log结构

        1
        2
        3
        4
        5
        6
        7
        8
        9
        struct log {
        struct spinlock lock;
        int start; // logheader 块在磁盘中对应的blockno
        int size; // log区域的大小
        int outstanding; // how many FS sys calls are executing.
        int committing; // in commit(), please wait.
        int dev; // 该log区域对应的是哪个磁盘设备
        struct logheader lh; // logheader
        };
    • 代码分析

      先从底层函数开始描述,然后逐渐过渡到顶层

      日志记录

      • write_log(): 将所有logged block刷新回磁盘

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        // 将内存Logged block拷贝到磁盘log区域
        static void
        write_log(void)
        {
        int tail;

        // 这里并没有直接调用磁盘接口,而是将logged内容覆盖到了磁盘log块在内存中的缓存
        // 然后再将缓存刷新回磁盘
        for (tail = 0; tail < log.lh.n; tail++) {
        // 遍历所有的Loggded block
        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);
        }
        }
      • write_head(): 将logheader刷新回磁盘

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        static void
        write_head(void)
        {
        struct buf *buf = bread(log.dev, log.start);
        struct logheader *hb = (struct logheader *) (buf->data); // 磁盘logheader对应的内存缓存
        int i;
        hb->n = log.lh.n;
        for (i = 0; i < log.lh.n; i++) {
        hb->block[i] = log.lh.block[i]; // 更改磁盘Logheader内存buffer
        }
        /********** commit point !! ***********/
        // 在bwrite 之前,如果发生crash, 那么并不会发生redo操作
        // 在bwrite 之后,logheadr已经被刷新回磁盘,此时恢复程序就可以从磁盘中获取到
        // 已经commit的事务,然后进行redo(此时恢复程序只知道已经commit,
        // 但是不知道数据究竟有没有被刷回磁盘, 所以可能会发生重复redo)
        bwrite(buf); // 磁盘Logheader刷回磁盘
        brelse(buf);
        }

        注意这里的第16行, 该函数是真正意义上的commit point, bwrite(buf)会将logheader刷新回磁盘,一旦该操作完成,就意味着该事务已经commit

        在该行执行之前,一旦crash, 那么恢复系统将不会在磁盘中对该事务进行redo, 因为根本没有logheader的记录

        在此之后,一旦crash, 那么恢复系统就会对该事务进行redo

        即, 一旦logheader被刷新回磁盘事务就提交成功

      • install_trans(): 应用事务,即将修改数据在log区域中的缓存刷新回磁盘

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        // 将log 区域中缓存的内容真正刷新回磁盘
        // 即应用事务
        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);
        }
        }

      处于简单考虑, xv6中并没有直接将使用virtio_disk_rw()缓存数据刷新回磁盘,而是使用bread() + bwrite()的形式

      • commit(): 提交事务, 然后清除日志

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        static void
        commit()
        {
        if (log.lh.n > 0) {
        write_log(); // Write modified blocks from cache to log
        // 将内存的Logged block写入磁盘的log区域
        write_head(); // Write header to disk -- the real commit
        // 将内存中的logheader写入磁盘当中
        install_trans(0); // Now install writes to home locations
        log.lh.n = 0; // clear log
        write_head(); // Erase the transaction from the log
        }
        }

        该函数是上面函数的整合,先将log刷回磁盘,然后将log中缓存的修改数据刷新回磁盘,最后执行clear log

      • begin_op(): 代表事务开始

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        // 表示事务的开始
        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;
        }
        }
        }
      • end_op(): 代表事务结束

        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
        // 表示事务的结束
        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);
        }
        }

      基本上每一个与文件系统相关的系统调用都会在开始使用begin_op(), 然后在最后调用end_op()

      崩溃恢复

      依旧是从底层函数开始

      • read_head(): 从磁盘中读取logheader到内存的logheader

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

        1
        2
        3
        4
        5
        6
        7
        8
        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
        }

        恢复过程比较简单,首先读取磁盘logheadr,因为此时内存中的logheader是无效的或者说根本就没有,然后再将log中的修改数据刷新回磁盘,进而clear log

      • init_log(): 初始化日志, 在xv6重启时会自动调用

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        void
        initlog(int dev, struct superblock *sb)
        {
        if (sizeof(struct logheader) >= BSIZE)
        panic("initlog: too big logheader");

        initlock(&log.lock, "log");
        log.start = sb->logstart;
        log.size = sb->nlog;
        log.dev = dev;
        recover_from_log();
        }

        只是简单地对recover_from_log()进行了封装而已

    Block Allocator
    下面是xv6 fs 有关磁盘块分配与释放的代码

    • balloc()

      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
      static uint
      balloc(uint dev)
      {
      int b, bi, m;
      struct buf *bp;

      bp = 0;
      // 这里xv6貌似采用了可拓展的方案?
      // 因为xv6的bit map只有一个block, 因此该循环至多也就执行一次
      for(b = 0; b < sb.size; b += BPB){
      // 读出bitmap block
      bp = bread(dev, BBLOCK(b, sb));
      for(bi = 0; bi < BPB && b + bi < sb.size; bi++){
      // bp->[]得到的是8bit的一个字节
      // 因此使用m表示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; // 这里b肯定就是0
      }
      }
      brelse(bp);
      }
      panic("balloc: out of blocks");
      }
    • bfree()

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      /**
      * @brief 释放一个磁盘块
      *
      * @param dev 指定设备
      * @param b 指定block num
      */
      static void
      bfree(int dev, uint b)
      {
      struct buf *bp;
      int bi, m;

      bp = bread(dev, BBLOCK(b, sb));
      bi = b % BPB; // 这里肯定就是bi本身
      m = 1 << (bi % 8);
      if((bp->data[bi/8] & m) == 0)
      panic("freeing free block");
      bp->data[bi/8] &= ~m; // 将bitmap中的该位置0
      log_write(bp);
      brelse(bp);
      }

    4. inode

    xv6中的inode分为磁盘中的dinode以及内存中的inode, 内存中的inode包含了更多内核所需要的额外信息

    • dinode

      1
      2
      3
      4
      5
      6
      7
      8
      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
      };
    • inode

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      struct inode {
      uint dev; // Device number
      uint inum; // Inode number
      int ref; // Reference count
      // 这里的引用计数是指在内存中有多少指针正指向该inode
      // 如果ref == 0, 那么内核救护清除inode在内存中的副本
      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];
      };

      注意这里nlinkref的区别:

      nlink代表有多少目录块正包含该inode, 这些目录块的数据块都存储在磁盘当中

      ref代表内存中引用该inode的指针数目,断电之后这些指针就会消失

    xv6同样使用了一个数据结构来维护内存中的整个inode区域

    • icache

      1
      2
      3
      4
      struct {
      struct spinlock lock;
      struct inode inode[NINODE]; // NINODE == 50
      } icache;

      使用了一把全局锁来保护所有的inode

    下面对inode的相关代码进行分析:

    • iupdate(): 将更新后的内存inode block写回磁盘

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      void
      iupdate(struct inode *ip)
      {
      // 每次更改icache中inode副本,都需要调用iupdate写入到磁盘

      struct buf *bp;
      struct dinode *dip;

      bp = bread(ip->dev, IBLOCK(ip->inum, sb));
      dip = (struct dinode*)bp->data + ip->inum%IPB;

      // ip是新的inode缓存副本,dip是旧的磁盘inode
      dip->type = ip->type;
      dip->major = ip->major;
      dip->minor = ip->minor;
      dip->nlink = ip->nlink;
      dip->size = ip->size;
      memmove(dip->addrs, ip->addrs, sizeof(ip->addrs));
      // 此次更新写入log
      log_write(bp);
      brelse(bp);
      }

      注意,这里并没有真的写回磁盘,而只是将对inode块的修改写入了LOG中

    • itrunc(): 释放一个inode块以及其执行的data

      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
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      void
      itrunc(struct inode *ip)
      {
      int i, j;
      struct buf *bp;
      uint *a;
      // 直接block块
      for(i = 0; i < NDIRECT; i++){
      if(ip->addrs[i]){
      bfree(ip->dev, ip->addrs[i]);
      ip->addrs[i] = 0;
      }
      }
      // 间接block块
      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;
      }
      // 双重间接block
      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] = 0;
      }

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

      这里的代码是lab8中的代码,使用了双重间接指针

    • iget(): 从内存中获取inode的副本,如果没有就为其分配一个

      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
      33
      34
      35
      36
      37
      static struct inode*
      iget(uint dev, uint inum)
      {
      struct inode *ip, *empty;

      acquire(&icache.lock);

      // Is the inode already cached?
      // empty负责记住一个空的inode slot
      empty = 0;
      for(ip = &icache.inode[0]; ip < &icache.inode[NINODE]; ip++){
      // ip->ref > 0 才能表示该inode是有效的数据
      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");

      // 在inode当中分配一个新的inode
      // 但这里其实只是改了一些状态变量而已
      // inode的真正数据还没有修改, 在ilock()中会进行数据的分配
      ip = empty;
      ip->dev = dev;
      ip->inum = inum;
      ip->ref = 1;
      ip->valid = 0;
      release(&icache.lock);

      return ip;
      }
    • iput(): 解除一个inode的引用

      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
      33
      34
      /**
      * @brief 解除一个inode block在内存中的引用
      * 如果ref为0, 那么inode cache就可以将其替换出去了
      * 如果ref为0且nlinks为0, 即已经没有文件正在使用该
      * inode了, 那么此时磁盘上的对应dinode就应该被释放
      * @param 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);
      }

      关于iput()有一些需要注意的点:

      • iput()中只给出了一个条件if(ip->ref == 1 && ip->valid && ip->nlink == 0)

        仅当上面条件满足时才会释放inode的数据块

      • 假设如下情况发生:
        系统删除了所有某个文件的所有目录,此时其nlink == 0, 但是目前内存中仍有进程在读取该文件,即ref != 0, 那么当调用

        iput()时就不会释放该文件的数据(没有itrunc(), ip->valid == 1)

        此时,如果突然crash, 那么当进程重启时,ip->valid == 1, 这也就意味着该inode块正在被使用,但是系统中却没有正在引用该inode 块的进程,此时xv6就永远失去了一些磁盘空间

      • 有两种方式处理这种情况:

        一个简单的方法是,在系统启动的恢复阶段,文件系统扫描磁盘上的整个文件系统,检查是否有文件标记被分配,但是没有任何目录条目包含它,然后如果有,就删除释放这些文件

        第二个方法是不需要扫描整个文件系统,而是在磁盘的某个位置上(例如超级块中),记录硬链接数为0,而指向其的指针数不为0的文件的inode号。如果该文件最后确实被删除释放了,那么就从该记录列表中移出相应的inode号。然后在系统启动的恢复阶段,不扫描整个文件系统,而是简单地扫描该列表,删除释放该列表中的文件即可

    • ialloc(): 分配一个inode

      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
      /**
      * @brief 在指定设备上分配一个inode
      *
      * @param dev 指定设备
      * @param type inode所代表文件的类型
      * @return struct inode* 分配的inode的指针,该inode处于 unlocked 且 referenced的状态
      */
      struct inode*
      ialloc(uint dev, short type)
      {
      int inum;
      struct buf *bp;
      struct dinode *dip;

      // 遍历所有inode, 找到一个空闲的inode,然后将其分配
      for(inum = 1; inum < sb.ninodes; inum++){
      // 读取inode block
      bp = bread(dev, IBLOCK(inum, sb));
      dip = (struct dinode*)bp->data + inum%IPB;
      if(dip->type == 0){ // a free inode
      memset(dip, 0, sizeof(*dip));
      dip->type = type;
      log_write(bp); // mark it allocated on the disk
      brelse(bp);
      return iget(dev, inum);
      }
      brelse(bp);
      }
      panic("ialloc: no inodes");
      }
    • ilock(): 锁住给定的inode

      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
      33
      34
      35
      36
      37
      /**
      * @brief 锁住给定的inode, 必要时从磁盘读取inode
      *
      * @param ip 指定inode
      */
      void
      ilock(struct inode *ip)
      {
      struct buf *bp;
      struct dinode *dip;

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

      acquiresleep(&ip->lock);

      // ip->valid = 0表示inode虽然在内存Inode区域分配了
      // 但是其内容还是无效的,没有与磁盘上的数据同步
      if(ip->valid == 0){
      // 读取对应inode block
      bp = bread(ip->dev, IBLOCK(ip->inum, sb));
      // 读取block中的对应条目
      // bp->data[ip->inum%IPB]
      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); // 释放之前获取的临时inode block缓存
      ip->valid = 1;
      if(ip->type == 0)
      panic("ilock: no type");
      }
      }
    • iunlock(): 解锁给定的inode

      1
      2
      3
      4
      5
      6
      7
      8
      void
      iunlock(struct inode *ip)
      {
      if(ip == 0 || !holdingsleep(&ip->lock) || ip->ref < 1)
      panic("iunlock");

      releasesleep(&ip->lock);
      }

    Inode Content

    • inode 数据块布局

      3

      xv6中的dinode中包含了12个直接块,1个间接块,每个间接块指向256个数据块

    下面是代码分析:

    • bmap(): 返回inode中的第bn个块的磁盘地址

      这里由于采用了间接指针,所以必要时需要递归的访问

      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
      33
      34
      35
      36
      /**
      * @brief 返回inode中的第bn个块的磁盘地址
      *
      * @param ip 给定inode
      * @param bn 第n个块
      * @return uint 数据块地址
      */
      static uint
      bmap(struct inode *ip, uint bn)
      {
      uint addr, *a;
      struct buf *bp;

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

      panic("bmap: out of range");
      }
    • readi(): 将inode数据块的内容拷贝到dst

      开始时先检查读取是否合法,不能读超过文件大小的字节。然后主循环就会不断地读出文件相应的数据块,并把数据块的内容从Buffer Cache拷贝到dst

      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
      33
      /**
      * @brief 从inode当中读取数据
      *
      * @param ip inode pointer
      * @param user_dst 为1表示dst是用户虚拟地址,否则表示dst是内核物理地址
      * @param dst 目的地址
      * @param off 相对于ip->addr的偏移量
      * @param n 拷贝的字节数
      * @return int 实际拷贝的字节数
      */
      int
      readi(struct inode *ip, int user_dst, uint64 dst, uint off, uint n)
      {
      uint tot, m;
      struct buf *bp;

      if(off > ip->size || off + n < off)
      return 0;
      if(off + n > ip->size)
      n = ip->size - off;

      for(tot=0; tot<n; tot+=m, off+=m, dst+=m){
      bp = bread(ip->dev, bmap(ip, off/BSIZE));
      m = min(n - tot, BSIZE - off%BSIZE);
      if(either_copyout(user_dst, dst, bp->data + (off % BSIZE), m) == -1) {
      brelse(bp);
      tot = -1;
      break;
      }
      brelse(bp);
      }
      return tot;
      }
    • writei()

      writei如下,和readi()的架构相似,但有几个不同之处:

      • writei可以超出文件大小,从而增长文件

      • 与readi相反,writei拷贝数据到Buffer Cache中

      • 一旦文件增长,就要更新其inode的大小信息

      值得一提的是,即便文件大小没有增长,也照样调用iupdate将inode写入磁盘,因为在调用bmap时可能分配了新的数据块,从而inode的addrs[ ]会被改变

      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
      33
      34
      35
      36
      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)
      // off可以等于ip->size,这样就相当于增长文件,但off+n即期望文件总大小不能超过规定的
      return -1;

      // copies data into the buffers
      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);
      break;
      }
      // 缓存块被修改,将更新写入日志
      log_write(bp);
      brelse(bp);
      }

      if(n > 0){
      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;
      }
    • stati(): 将inode的元数据拷贝到位于内存中的struct stat,为上层的用户进程提供访问该inode元数据的接口

      1
      2
      3
      4
      5
      6
      7
      8
      9
      void
      stati(struct inode *ip, struct stat *st)
      {
      st->dev = ip->dev;
      st->ino = ip->inum;
      st->type = ip->type;
      st->nlink = ip->nlink;
      st->size = ip->size;
      }
      • stat结构

        1
        2
        3
        4
        5
        6
        7
        8
        struct stat {
        int dev; // File system's disk device
        uint ino; // Inode number
        short type; // Type of file
        short nlink; // Number of links to file
        uint64 size; // Size of file in bytes
        };

    5. directory

    目录的实现机制与文件比较类似,它的inode类型是T_DIR,目录的data块中存储的是dirent结构的序列, dirent结构如下:

    • dirent

      1
      2
      3
      4
      5
      // 目录条目结构
      struct dirent {
      ushort inum; // 目录的inode num
      char name[DIRSIZ]; // 条目名
      };

      即一个简单的{inum, name}对

    inode为0的目录项是空闲的,在进行目录项的遍历是要将其忽略

    下面介绍目录有关API

    • dirlookup()

      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
      33
      34
      35
      /**
      * @brief 在指定目录文件当中寻找一个目录条目
      *
      * @param dp 目录的inode pointer
      * @param name 寻找的条目名称
      * @param[out] poff 寻找到的条目在数据块中的偏移量
      * @return struct inode* 找到的条目所对应的inode的内存副本指针
      */
      struct inode*
      dirlookup(struct inode *dp, char *name, uint *poff)
      {
      uint off, inum;
      struct dirent de;

      if(dp->type != T_DIR)
      panic("dirlookup not DIR");

      // 遍历所有的条目,查找指定文件
      for(off = 0; off < dp->size; off += sizeof(de)){
      if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
      panic("dirlookup read");
      // inode号为0的目录项是空闲的
      if(de.inum == 0)
      continue;
      if(namecmp(name, de.name) == 0){
      // entry matches path element
      if(poff)
      *poff = off;
      inum = de.inum;
      return iget(dp->dev, inum);
      }
      }

      return 0;
      }
    • dirlink

      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
      33
      34
      35
      36
      37
      38
      /**
      * @brief 将一个新的目录条目写入指定目录的data block中
      *
      * @param dp 待写入的目录
      * @param name 条目name
      * @param inum 条目inum
      * @return int 0代表成功, -1代表失败
      */
      int
      dirlink(struct inode *dp, char *name, uint inum)
      {
      int off;
      struct dirent de;
      struct inode *ip;

      // Check that name is not present.
      if((ip = dirlookup(dp, name, 0)) != 0){
      // 如果条目已经存在的话认为是错误
      iput(ip);
      return -1;
      }

      // Look for an empty dirent.
      for(off = 0; off < dp->size; off += sizeof(de)){
      if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
      panic("dirlink read");
      if(de.inum == 0)
      break;
      }

      // 找到一个空闲条目,将给定条目写入
      strncpy(de.name, name, DIRSIZ);
      de.inum = inum;
      if(writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
      panic("dirlink");

      return 0;
      }

    6. path name

    path name本身概念上没有什么可说的,主要是解析路径字符串的API较为繁杂

    • skipelem()

      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
      /**
      * @brief 返回path对应的首元素, 将其(字符串)拷贝到name中
      *
      * @param path 给定路径名
      * @param name[out] path对应的首元素
      * @return char* 首元素后面的路径
      */
      static char*
      skipelem(char *path, char *name)
      {
      // 以a/bb/c为例
      char *s;
      int len;

      while(*path == '/')
      path++;
      if(*path == 0)
      return 0;
      s = path; // s == a
      while(*path != '/' && *path != 0)
      path++;
      len = path - s; // len == 1
      if(len >= DIRSIZ)
      memmove(name, s, DIRSIZ);
      else {
      memmove(name, s, len); // name <- "a"
      name[len] = 0;
      }
      while(*path == '/')
      path++;
      return path; // path == "bb/c"
      }
    • namex(): 工具函数

      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
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      /**
      * @brief 查找并返回一个路径名对应的inode
      *
      * @param path 给定的路径
      * @param nameiparent 如果nameiparent不为0, 那么会返回最后一个元素的父路径的inode, 并且
      * 拷贝最后一个路径元素到name中
      * 如a/b/c, 此时如果nameiparent !=0, 那么name = c, 返回的是a/b的inode
      * @param name[out] 写出的文件名
      * @return struct inode*
      */
      static struct inode*
      namex(char *path, int nameiparent, char *name)
      {
      struct inode *ip, *next;

      // 如果路径名以'/'开头则从根目录开始, 否则从当前工作目录开始
      if(*path == '/')
      ip = iget(ROOTDEV, ROOTINO);
      else
      ip = idup(myproc()->cwd);

      // 以"/a/b", nameiparent == 1为例
      while((path = skipelem(path, name)) != 0){ // 1. path == b, name = a
      ilock(ip); // 2. paht == '0', name = b
      if(ip->type != T_DIR){
      iunlockput(ip);
      return 0;
      }
      if(nameiparent && *path == '0'){
      // Stop one level early. // 2. stop! ip == "/a"对应的inode
      iunlock(ip);
      return ip;
      }
      if((next = dirlookup(ip, name, 0)) == 0){ // 1. next: "/a"的inode
      iunlockput(ip);
      return 0;
      }
      iunlockput(ip);
      ip = next;
      }

      // 正常情况下nameiparent应该在主循环中返回
      // 如果nameiparent运行到这就说明提前退出
      // 由于iget()阶段refcnt++, 所以这里要复原
      if(nameiparent){
      iput(ip);
      return 0;
      }
      return ip;
      }
    • nameiparent()

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      /**
      * @brief 返回path对应的父目录的inode
      *
      * @param path 给定目录
      * @param name[out] 最底层元素
      * @return struct inode*
      */
      struct inode*
      nameiparent(char *path, char *name)
      {
      return namex(path, 1, name);
      }
    • namei()

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      /**
      * @brief 返回path的inode
      *
      * @param path
      * @return struct inode*
      */
      struct inode*
      namei(char *path)
      {
      char name[DIRSIZ];
      return namex(path, 0, name);
      }

    7. file descriptor

    xv6中使用了两张表来维护所有打开的文件,一张是每个进程自身的打开文件表ofile

    • proc

      1
      2
      3
      4
      5
      struct proc{
      // ...
      struct file *ofile[NOFILE]; // Open files, NOFILE = 16
      // ...
      }

      其实就是一个数组, 数组的每个条目是进程打开的文件

    另一张是全局打开文件表ftable

    • ftable

      1
      2
      3
      4
      struct {
      struct spinlock lock;
      struct file file[NFILE]; // NFILE == 100
      } ftable;

      该表中记录了系统中正处于打开状态的所有文件实例, xv6中最多能同时打开100个文件

    并不是每一个文件都有一个与之对应的file结构体,而是每个打开的文件都有一个对应的file结构体, 如果一个文件被打开(open())多次,那么它就会有多个file实例

    • file

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      struct file {
      enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
      int ref; // reference count
      char readable;
      char writable;
      struct pipe *pipe; // FD_PIPE
      struct inode *ip; // FD_INODE and FD_DEVICE
      uint off; // FD_INODE
      short major; // FD_DEVICE
      };

      每个打开的文件在内存中对应的file实例维护了文件的打开状态,包括

      1. 文件类型

        xv6将文件类型分为了4类:

        1. FD_NONE: 已关闭文件
        2. FD_PIPE: 管道文件
        3. FD_INODE: 普通文件
        4. FD_DEVICE: 设备文件
      2. 文件权限(r/w)

      3. 文件偏移量

        由于每个进程打开一个文件后都会产生一个对应的file实例,因此多个进程会有着不同的文件偏移量

    下面介绍相关函数

    • filealloc()

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      /**
      * @brief 分配一个file结构体
      *
      * @return struct file* 分配的file结构体
      */
      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;
      }
    • filedup()

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      /**
      * @brief 创建一个打开文件的副本
      *
      * @param f 给定打开文件
      * @return struct file* 打开文件结构
      */
      struct file*
      filedup(struct file *f)
      {
      acquire(&ftable.lock);
      if(f->ref < 1)
      panic("filedup");
      f->ref++;
      release(&ftable.lock);
      return f;
      }

      这里所做的操作很简单,只是将打开文件的引用计数+1, 然后返回相同的file结构体指针

    • fileclose()

      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
      33
      /**
      * @brief 关闭文件f
      *
      * @param f 被关闭的文件
      */
      void
      fileclose(struct file *f)
      {
      struct file ff;

      acquire(&ftable.lock);
      if(f->ref < 1)
      panic("fileclose");
      // 仅当引用计数为0时才真正地做释放操作
      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();
      }
      }
    • filestat()

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      /**
      * @brief 获取文件的相关状态
      *
      * @param f 待访问文件
      * @param addr struct stat的地址
      * @return int 成功返回0, 否则返回-1
      */
      int
      filestat(struct file *f, uint64 addr)
      {
      struct proc *p = myproc();
      struct stat st;

      if(f->type == FD_INODE || f->type == FD_DEVICE){
      ilock(f->ip);
      stati(f->ip, &st);
      iunlock(f->ip);
      if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
      return -1;
      return 0;
      }
      return -1;
      }

      只允许获取普通文件或者设备文件的状态

    • fileread()

      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
      33
      34
      /**
      * @brief 从文件读取内容
      *
      * @param f 待读取文件
      * @param addr 缓冲区地址
      * @param n 读取字节数
      * @return int 读取的字节数,失败的话返回-1
      */
      int
      fileread(struct file *f, uint64 addr, int n)
      {
      int r = 0;

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

      if(f->type == FD_PIPE){
      r = piperead(f->pipe, addr, n);
      } else if(f->type == FD_DEVICE){
      if(f->major < 0 || f->major >= NDEV || !devsw[f->major].read)
      return -1;
      r = devsw[f->major].read(1, addr, n);
      } else if(f->type == FD_INODE){
      ilock(f->ip);
      // 从文件的当前偏移量开始读取, 由于偏移量off会被更新,因此加锁
      if((r = readi(f->ip, 1, addr, f->off, n)) > 0)
      f->off += r;
      iunlock(f->ip);
      } else {
      panic("fileread");
      }

      return r;
      }
    • fwrite()

      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
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      /**
      * @brief 向文件写内容
      *
      * @param f 待写入文件
      * @param addr 缓冲区地址
      * @param n 写入字节数
      * @return int 写入的字节数
      */
      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 != n1){
      // error from writei
      break;
      }
      i += r;
      }
      ret = (i == n ? n : -1);
      } else {
      panic("filewrite");
      }

      return ret;
      }
    • fdalloc()

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      /**
      * @brief 给一个打开的文件分配一个文件描述符
      *
      * @param f
      * @return int
      */
      static int
      fdalloc(struct file *f)
      {
      int fd;
      struct proc *p = myproc();

      for(fd = 0; fd < NOFILE; fd++){
      if(p->ofile[fd] == 0){
      p->ofile[fd] = f;
      return fd;
      }
      }
      return -1;
      }

      从该函数中我们可以看出,当一个进程打开文件时,分配到的文件描述符永远是当前自己可以获得的最小文件描述符,与打开什么文件没有关系


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!