系列文章目录
系列第一篇文章:第一章 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的Inode
中addrs
前NDIRECT(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
次,这个次数也就是i
号Inode
位于当前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 block
的bmapstart
属性记录bitmap
块起始块号,BPB
用于表示一个block
多少bits
,#define BPB (BSIZE*8)
,BSIZE
为块的大小,#define BSIZE 1024
。
其中,bi
=块号对BPB
取余即代表该块位于bitmap的具体哪一个bit
位。BPB
也可表示一个bitmap
中可以记录多少块。
其余内容注释皆有解释,若仍有疑问,欢迎私信。
总结
本文对Inode
层做了简单解析。由于提供的接口众多,若只是简单通过源码解释该接口的功能,给出源码一通注释,难免让读者丧失继续研究文件系统的兴趣。
下文应该是作为文件系统的最后一篇,会通过系统调用的视角,逐层向下,更好引出Inode层提供的接口的作用及其具体实现以及文件描述符的妙处,更会深刻体会到Linux系统中万物皆文件的思想。
文章粗糙,希望读者有所收获。若有错处,欢迎批评指正。