xv6文件系统五:Inode层浅析

系列文章目录

系列第一篇文章:第一章 xv6文件系统磁盘层详解
系列第二篇文章:第二章 xv6文件系统BufferCache层详解
系列第三篇文章:第三章 xv6文件系统总览
系列第四篇文章:第四章 xv6文件系统日志层详解

前言

系列第三篇文章中介绍过,Inode层硬盘中起始于32号block,中止于44号,45号磁盘块为bitmap所属block。Inode是文件系统中最重要的元数据。
Inode用于管理文件系统中的文件和目录,每个文件或目录在文件系统中都会对应一个唯一的 inode。本文也将从xv6出发真正接触到一个文件如何被通过inode的形式存储到磁盘中。
对文件的读写,就是通过读取该文件对应的Inode数据结构读写该文件的数据block以达到修改文件内容的效果。

硬盘中的Inode

xv6系统中按照dinode的形式顺序挨个存储于磁盘中,大小为64B,存放具体磁盘块号从32号磁盘块开始,44号磁盘块结束,这几块被称为Inode磁盘区。
磁盘中的dinode(disk inode)结构如下所示:大小总和为 – (2 + 2 + 2 + 2 + 4 + 13 * 4) = 64

// On-disk inode structure
struct dinode {
  short type;           // 类型--文件或目录或设备
  short major;          // 主设备号
  short minor;          // 从设备号
  short nlink;          // inode的link数目
  uint size;            // 文件的大小
  uint addrs[NDIRECT+1];   // 数据块号组成的数组
};

其中:
type为0,则此Inode未被使用
nlink记录多少个文件指向这个Inode节点。例如:系统调用link(oldpath, newpath)会增加一个Inode节点的nlink数目。
addrs是一个数组,用于存储该Inode所表示的文件的数据存储在哪些块中。
在文件系统中,数据被分成固定大小的块进行存储,各个文件的数据存储于位于bitmap块之后的数据块区域。这些块通过数据块号进行索引。
xv6的InodeaddrsNDIRECT(12)个块号为直接索引,指向本文件的内容所在的数据块号。
addrs数组多的一个最后一个元素为一个一级间接索引,即它指向的数据块中存储的才是直接索引,一个uint大小为4B,一块大小为1024B,故而这个一级间接索引可以为文件索引共1024/4=256个数据块。
总的来说,xv6文件系统最大支持文件大小为(12+256)*BLOCKSIZE=268KB

一、内存中的Inode

由于程序操作的所有数据都必须位于内存,xv6对dinode在内存中的抽象为结构inode,先给出内存中Inode层的数据结构。文件系统中所有对Inode的操作都是操作内存中Inode,它是位于硬盘中dinode的内存镜像/副本。当然,一个硬盘dinode在内存中有且仅有一个副本。

// in-memory copy of an inode
struct inode {
  uint dev;           // 设备号
  uint inum;          // inode 号
  int ref;            // 内存中的inode的refcnt,记录多少个进程的打开文件中含有这个文件
  struct sleeplock lock; // 保护结构体中的其余内容
  int valid;          // inode是否从磁盘中读取出来

  short type;         // 
  short major;//
  short minor;//
  short nlink;//
  uint size;//
  uint addrs[NDIRECT+1];//
};
struct {
  struct spinlock lock;
  struct inode inode[NINODE];
} itable;

注意:
0:ref用于多进程的对同一个文件的访问,当ref为0,且文件的nlink为0时,此时可以删除文件。回收Inode和数据块。若仅仅ref为0,则代表OS没有进程需要使用该Inode,可以放回Inode缓存用于换出。
1:内存中维护一个inode table其中允许所有进程在内存中同时存在NINODE(50)个活跃inode。要操作Inode必须持有该Inode所属的睡眠锁。
2:inode table的自旋锁用于控制inode table的申请和初始化inode表中某一项inode属性等内容。
3:内存中的inode结构的作用还在于作为文件类型的FD_INODE类型,作为Linux系统万物皆文件文件的文件抽象。
注:下文统一用Inode表示内存中的Inode数据结构。硬盘中的Inode用dinode代替

二、Inode层提供的接口

struct inode* ialloc(uint, short);用于在磁盘的Inode block中找到一个未使用inode标记为使用同时调用iget函数返回该inode的内存副本。
struct inode* idup(struct inode*);用于对该inode节点的ref属性加1
void iinit();初始化Inode
void ilock(struct inode*);获取Inode对应的睡眠锁
void iput(struct inode*);Inode放回队列,当ref和Inode.nlink为0,则回收该文件的Inode和数据块
void iunlock(struct inode*);释放该Inode的睡眠锁
void iunlockput(struct inode*);释放该Inode的睡眠锁并将Inode放回队列
void iupdate(struct inode*);更新该Inode的信息到磁盘中
struct inode* namei(char*);返回该path对应文件的Inode
struct inode* nameiparent(char*, char*);返回该path父目录的inode
int readi(struct inode*, int, uint64, uint, uint);从Inode对应文件指定偏移位置中读取指定大小数据
void stati(struct inode*, struct stat*);将Inode对应的信息写入stat结构体中并返回
int writei(struct inode*, int, uint64, uint, uint);Inode对应的文件的数据块中指定偏移位置写入指定大小数据
void itrunc(struct inode*);删除Inode的数据块数据,同时回收该Inode对应的磁盘中的dinode

本层提供接口众多,本文中不会对每一个接口进行介绍,会介绍Inode层中基本的几个接口。剩余接口会在后面的文章中通过追踪系统调用的方式再引入文件描述符层以及万物皆文件抽象时进行详细介绍。

Inode层接口调用操作一般顺序:
ip = iget(dev, inum)先获得该inode的内存镜像,方便对Inode操作
ilock(ip)申请该Inode的睡眠锁
examine and modify ip->xxx ...可能的iupdate,writei,readi调用对Inode进行操作,数据读写等
iunlock(ip)释放该Inode的睡眠锁
iput(ip)若后续不再操作该文件,将Inode放回队列

接口设计思路

注意到,在iget之后,需要通过单独的ilock获取锁,这里设计的巧处在于:可以使一个系统调用长期引用一个inode并在需要操作使用它的时候上锁,不需要的时候及时解锁。
例如:
1:作为用户我们调用open系统调用打开文件,返回文件描述符,作为用户会在下文对该文件描述符进行读写等操作。
2:而文件系统在经过open系统调用之后,会在通过对该文件Inode的操作之后对其解锁,但不调用iput函数重新放回内存中的Inode表,此时,该inode不会被换出。但别的进程可以获取锁并操作该inode
3:同时该文件会通过文件的抽象封装并记录到进程的ofile属性–>记录进程所有打开的文件,而该文件在进程的打开文件数组的下标就是所谓的文件描述符fd。这个会在后面的文章中详细介绍,其实非常简单。

三、源码解析

Inode层初始化接口–iinit

iinit函数初始化内存中inode表的自旋锁以及各个inode的睡眠锁

void
iinit()
{
  int i = 0;
  
  initlock(&itable.lock, "itable");
  for(i = 0; i < NINODE; i++) {
    initsleeplock(&itable.inode[i].lock, "inode");
  }
}

获取Inode接口函数–ialloc

iget函数为本文件可见的内部函数,从内存inode table中遍历,根据设备号和inode号寻找缓存,没有则从table中找到一个可用项并设置其信息,iget不从硬盘读取该inode数据,不申请该项睡眠锁。

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

  acquire(&itable.lock);

  // 遍历inode table,判断该inode是否存在于table中,是则ref+1,返回该inode
  empty = 0;
  for(ip = &itable.inode[0]; ip < &itable.inode[NINODE]; ip++){
    if(ip->ref > 0 && ip->dev == dev && ip->inum == inum){
      ip->ref++;
      release(&itable.lock);
      return ip;
    }
    if(empty == 0 && ip->ref == 0)    // 记下ref为0的inode项下标
      empty = ip;
  }

  // 回收空入口,设置信息,设置valid为尚未从硬盘读取inode信息
  if(empty == 0)
    panic("iget: no inodes");

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

  return ip;
}

注意:iget函数不会将该Inode从硬盘中读取出来。会设置valid为0,后面的ilock函数会判断该valid位,即若只是获取该inode但并不使用,不会从硬盘中读取浪费时间。

ialloc函数在磁盘寻找空闲inode,初始化其设备号和设备类型,并返回其在内存中对应的Inode指针

struct inode*
ialloc(uint dev, short type)
{
  int inum;
  struct buf *bp;
  struct dinode *dip;
  // 搜索Inode层的每一块,按顺序找空Inode
  for(inum = 1; inum < sb.ninodes; inum++){
    bp = bread(dev, IBLOCK(inum, sb));
    // 指针应当偏移的大小为:i号Inode位于当前block的哪一个inode位置*Inode的大小
    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);   // 写该Inode类型,标记为已使用
      brelse(bp);
      return iget(dev, inum);
    }
    brelse(bp);
  }
  printf("ialloc: no inodes\n");
  return 0;
}

注意:
IBLOCK其内容为#define IBLOCK(i, sb) ((i) / IPB + sb.inodestart),用于计算当前Inode--i号所对应的Inode位于磁盘的哪一块中。其中,IPB宏具体内容为#define IPB (BSIZE / sizeof(struct dinode)),它表示一个块可以存储多少Inode
另外,dip = (struct dinode*)bp->data + inum%IPB;恰当的强制转换block->data类型为dinode,并按照dinode大小向后偏移inum % IPB次,这个次数也就是iInode位于当前block的第几个dinode位置。

更新该Inode的信息到磁盘中–iupdate函数

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

  bp = bread(ip->dev, IBLOCK(ip->inum, sb));
  dip = (struct dinode*)bp->data + ip->inum%IPB;
  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_write(bp);
  brelse(bp);
}

注意:该函数使用了宏IBLOCK和上文一样,先通过该宏找到该Inode位于哪个块,并按照日志层的思路修改块内容并写回。
日志层解析中说过:日志层将块读写的操作中log_write函数替换掉bwrite(),不再赘述。

获取给定inode的睡眠锁函数–ilock

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

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

  acquiresleep(&ip->lock);
  // 尚未从硬盘中读取出来,读取硬盘中的dinode内容,存储其内容到内存数据结构中。
  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");
  }
}

注意:其中关于IBLOCK宏等内容已经介绍过,不再赘述。

释放给定inode睡眠锁函数–iunlock

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

  releasesleep(&ip->lock);
}

回收给定inode函数–iput

iput特殊的点在于需要检查文件是否已经需要被彻底删除回收空间

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

  if(ip->ref == 1 && ip->valid && ip->nlink == 0){
    // inode nlink属性和ref都为0时,则需要彻底删除文件,回收硬盘inode和data block。
    acquiresleep(&ip->lock);

    release(&itable.lock);
    // 回收data block
    itrunc(ip);
    // 回收inode
    ip->type = 0;
    iupdate(ip);
    ip->valid = 0;

    releasesleep(&ip->lock);

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

注意:硬盘中dinode 有效可用的标志即为type域为0,表示尚未使用。所以回收inode只需要设置type为0,并调用iupdate写回该inode所属block即可。

回收data block函数–itrunc

inode中12个数据块直接索引,一个间接索引。回收数据块只需要设置bitmap中代表该块的bit为0则表示该块可被重用,同时设置inode的addrs数组中该位置为0表示该索引没有指向任何数据内容。

void
itrunc(struct inode *ip)
{
  int i, j;
  struct buf *bp;
  uint *a;

  for(i = 0; i < NDIRECT; i++){
      // 在bitmap上释放该直接索引的data block
    if(ip->addrs[i]){
      bfree(ip->dev, ip->addrs[i]);
      ip->addrs[i] = 0;
    }
  }
//在bitmap上释放间接索引的data 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;
  }

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

工具函数bfree–释放数据块

参数b表示block number

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

  bp = bread(dev, BBLOCK(b, sb));
  bi = b % BPB;//计算位于本bitmap的第几个bit
  // bi%8表示位于该字节数据的第几位,左移表示字节数据m中该位为1
  m = 1 << (bi % 8);
  if((bp->data[bi/8] & m) == 0)
    panic("freeing free block");
  // 该位置0,bi/8表示位于bitmap数据中的哪一个字节。
  bp->data[bi/8] &= ~m;
  //写回磁盘
  log_write(bp);
  brelse(bp);
}

这个函数用了特殊的宏,在解释之前需要提醒:bitmap块中的第i bit位表示位于本bitmap后的第i块是否可用。
宏解释:
BBLOCK宏用于返回b这个块号在哪个bitmap块中,返回该bitmap的块号。#define BBLOCK(b, sb) ((b)/BPB + sb.bmapstart),其中,super blockbmapstart属性记录bitmap块起始块号,BPB用于表示一个block多少bits#define BPB (BSIZE*8)BSIZE为块的大小,#define BSIZE 1024
其中,bi=块号对BPB取余即代表该块位于bitmap的具体哪一个bit位。BPB也可表示一个bitmap中可以记录多少块。
其余内容注释皆有解释,若仍有疑问,欢迎私信。

总结

本文对Inode层做了简单解析。由于提供的接口众多,若只是简单通过源码解释该接口的功能,给出源码一通注释,难免让读者丧失继续研究文件系统的兴趣。
下文应该是作为文件系统的最后一篇,会通过系统调用的视角,逐层向下,更好引出Inode层提供的接口的作用及其具体实现以及文件描述符的妙处,更会深刻体会到Linux系统中万物皆文件的思想。

文章粗糙,希望读者有所收获。若有错处,欢迎批评指正。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在ext4文件系统中,可以通过调整inode表的大小来增加inode节点的数量。inode表是一个固定大小的数据结构,用于存储inode节点的元数据信息。在创建ext4文件系统时,可以通过指定inode表的大小来设置inode节点的数量。如果需要增加inode节点的数量,可以使用以下步骤: 1. 使用`dumpe2fs`命令获取文件系统的信息,包括inode表的大小和块大小等等: ``` $ sudo dumpe2fs /dev/sda1 | grep "Block size:\|Inode size:\|Inodes per group:\|Block count:\|Inode count:" ``` 输出结果中的`Inode count`表示当前文件系统中inode节点的数量。 2. 计算需要增加的inode节点数量,以及需要增加的inode表的大小。假设需要增加100000个inode节点,可以使用以下公式计算需要增加的inode表的大小: ``` new_inode_table_size = (100000 * inode_size) / block_size ``` 其中,`inode_size`为每个inode节点的大小,可以从`Inode size`中获取;`block_size`为块大小,可以从`Block size`中获取。 3. 使用`resize2fs`命令调整文件系统的大小和inode表的大小: ``` $ sudo resize2fs /dev/sda1 <new_size> ``` 其中,`<new_size>`为调整后的文件系统大小。如果只需要增加inode表的大小,可以将`<new_size>`设置为当前文件系统的大小。 4. 再次使用`dumpe2fs`命令检查文件系统的信息,确保inode节点数量已经增加。 需要注意的是,增加inode节点会占用更多的磁盘空间,因此应该根据实际需要进行调整。同时,增加inode节点并不会改变文件系统的性能特征,因为inode表的大小和块大小都是固定的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值