xv6文件系统
在Linux中,一切皆文件,对于普通文件、目录、块/字符设备,网络设备套接字等,都是文件,linux对于这些文件提供了一套统一的接口来进行操作。
xv6将文件系统的设计分为7层:磁盘 -> 缓冲区 -> 日志 -> inode -> 目录 -> 路径 -> 文件描述符。(如下图)
关于文件,在内存中维护了三种数据结构:文件描述符、文件结构体和位于内存中的inode,操作文件在内存中的缓存以及为其维护的结构,再写入磁盘进行同步(这就是关于文件的系统调用干的事情)。
文件描述符
对于每个进程,都有一个打开文件描述符的指针数组,指向文件表项(文件结构体),用文件描述符来索引这个文件表项,进而得到对应的文件结构体。
// Per-process state
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
// lab3 add
pagetable_t ptb_k; // kernel page table
};
struct file *ofile[NOFILE]
为每个进程独有的指针数组,NOFILE
为16,表示每个进程最多同时打开NOFILE个文件
file(文件结构体)
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
};
包含文件类型、引用计数、读写权限、对应的inode,文件偏移量等
inode
// 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;
#ifdef SOL_FS
#else
uint addrs[NDIRECT+1];
#endif
};
包含设备编号、inode编号、引用计数、睡眠锁、以及addrs[NDIRECT+1],用于索引磁盘块
dinode
// 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
};
文件系统7层结构
xv6将文件系统的设计分为7层:磁盘 -> 缓冲区 -> 日志 -> inode -> 目录 -> 路径 -> 文件描述符。现在从底层向上层进行介绍
磁盘
主要函数位于kernel/virtio_disk.c
中
对于磁盘,扇区是磁盘读写的基本单位,通常为512字节。
对于操作系统,磁盘读写的单位是块,一般块的大小等于一个或多个扇区的大小。在xv6中,一个block对应两个sector。
对于机械硬盘和固态硬盘,两者的工作方式完全不一样,但操作系统对于硬件的抽象屏蔽的这些差异,从上往下看磁盘驱动的接口,大部分的磁盘看起来都一样,提供block编号,然后在驱动中写设备的控制寄存器,设备就会完成相应的工作。
在xv6中,从文件系统的角度来看磁盘还是很直观的。因为对于磁盘就是读写block或者sector,我们可以将磁盘看作是一个巨大的block的数组,数组从0开始,一直增长到磁盘的最后。
而文件系统的工作就是将所有的数据结构以一种能够在重启之后重新构建文件系统的方式,存放在磁盘上。虽然有不同的方式,但是XV6使用了一种非常简单,但是还挺常见的布局结构。
-
block0要么没有用,要么被用作boot sector来启动操作系统。
-
block1通常被称为super block,它描述了文件系统。它包含磁盘上有多少个block共同构成了文件系统这样的信息。
-
在XV6中,log从block2开始,到block32结束。实际上log的大小可能不同,这里在super block中会定义log就是30个block。
-
block32到block45之间,XV6存储了inode。我之前说过多个inode会打包存在一个block中,一个inode是64字节。
-
bitmap block,它只占据一个block。它记录了数据block是否空闲。
-
之后就全是数据block了,数据block存储了文件的内容和目录的内容。
bitmap block,inode blocks和log blocks被统称为metadata block。它们虽然不存储实际的数据,但是它们存储了能帮助文件系统完成工作的元数据。
对于磁盘块层面的程序位于kernel/virtio_disk.c
中,比较复杂,涉及到驱动程序,后续有时间再进行研究。
超级块
超级块的定义位于kernel/fs.h中
// Disk layout:
// [ boot block | super block | log | inode blocks |
// free bit map | data blocks]
//
// mkfs computes the super block and builds an initial file system. The
// super block describes the disk layout:
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. inode块数量
uint nlog; // Number of log blocks log块数量
uint logstart; // Block number of first log block 第一个日志块块号
uint inodestart; // Block number of first inode block 第一个inode块块号
uint bmapstart; // Block number of first free map block 第一个位图块块号
};
超级块中记录了数据块、Inode块、log块、等信息,这些信息记录了文件系统的元数据,位于block1
缓冲区cache
磁盘操作是很慢的,SSD通常是0.1到1毫秒的访问时间,而HDD通常是10毫秒量级完成读写一个disk block,所以一般都会将一部分内存作为磁盘的缓存,xv6专门在内存中分配一片区域作为磁盘缓存,这片缓存的最小单位是块。xv6中每个缓存块存储一个磁盘块,并采用链表法对这些数据进行管理。
定义
xv6使用struct buf来定义缓存块:
// kernel/buf.h
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; //引用该块的个数
// LRU cache list
struct buf *prev; //该buf的前一个buf
struct buf *next; //该buf的后一个buf
uchar data[BSIZE]; // 缓存的数据
};
初始化、申请、释放
xv6使用双向链表来组织管理缓存块,所以每个buf都有prev和next,分别指向前一个和后一个buf
在kernel/bio.c中声明了全局变量bcache,包含一把自旋锁用于保护bcache中的buf(防止竞争),buf数组(长度为NBUF = 30,磁盘块缓存为30个Buf),还有一个双向链表,使用**LRU机制(最近最少使用算法)**进行维护,head的下一个是最近使用的,head的上一个buf是最久没有使用的。
- 注意:每个struct buf都有一个睡眠锁,因为磁盘操作是很慢的,使用睡眠锁,在必要时刻直接让进程休眠让出CPU,从而提升CPU的利用率。
- 对于bcache也有一个自旋锁,因为bcache属于公共资源,任何时刻应该保证只有一个进程访问。
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;
初始化bcache函数为binit:首先初始化自旋锁(保护bcache的锁),然后遍历bcache.buf数组,使用双向链表维护buf。
// kernel/bio.c
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;
}
}
根据初始化顺序,得到的双向链表如图:
申请buf函数为bget,首先在bcache 的双向链表中寻找对应的Buf,如果命中了,就直接返回这个buf。如果没有命中就根据LRU算法,从链表中找到最近最久没有使用的且引用计数为0的块,进行替换。如果还是没有,就陷入Panic。
// Look through buffer cache for block on device dev.
// If not found, allocate a buffer.
// In either case, return locked buffer.
// 译:在bcache 的双向链表中寻找对应的Buf,如果命中了,就直接返回这个buf,如果没有命中就根据LRU算法,从链表中找到最近最久没有使用的且引用计数为0的块,进行替换。如果还是没有,就Panic
static struct buf*
bget(uint dev, uint blockno)
{
struct buf *b;
acquire(&bcache.lock);
// Is the block already cached? 判断是否cached
for(b = bcache.head.next; b != &bcache.head; b = b->next){ // 从前往后进行扫描
if(b->dev == dev && b->blockno == blockno){ // 命中了
b->refcnt++;//引用次数加一
release(&bcache.lock);//释放bcache的锁
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){ //寻找最近最久没有使用的且引用计数为0的块进行替换,注意是从后往前扫描
if(b->refcnt == 0) { //引用次数为0,可以替换
b->dev = dev;
b->blockno = blockno;
b->valid = 0; //表示此时缓存块内的数据无效,因为只是将设备和块号分配给这个缓存块,但数据还没有传到这个缓存块,需要后续磁盘请求后更新数据
b->refcnt = 1; // 初始化引用次数为1
release(&bcache.lock);
acquiresleep(&b->lock);
return b;
}
}
panic("bget: no buffers");
}
释放缓存块的函数为brelease函数:
// 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) { // 此时引用计数为0,没有进程引用该缓存块,那就真正的释放该缓存块,把这个块移动到LRU双向链表的头部
// 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);
}
读写
缓冲区层面的读函数为bread,再低一层的是磁盘的读写函数virtio_disk_rw
。
void virtio_disk_rw(struct buf *b, int write)
virtio_disk_rw
负责将数据读到缓存块或者将数据写到磁盘中去,根据write为0还是1来区分读/写,比较复杂,涉及到具体的硬件,后续再进一步学习
// 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); // 获取buf
if(!b->valid) { // 如果该块的valid字段为0,说明这个buf是刚申请的,之前并不在cache中,这个buf里面的数据需要更新
virtio_disk_rw(b, 0); // 从磁盘中读取数据
b->valid = 1; //更新valid字段为1
}
return b;
}
缓冲区层面的写函数为bwrite,再低一层的是磁盘的读写函数virtio_disk_rw
。
// 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);
}
日志区log
在文件系统设计中通常要考虑错误恢复,文件系统涉及到对磁盘的多次写操作,如果在写的过程中系统发生崩溃,就会使得磁盘上的文件系统处于错误的状态。
日志就是用于解决系统崩溃导致的错误问题。
- 它可以确保文件系统的系统调用是原子性的。比如你调用create/write系统调用,这些系统调用的效果是要么完全出现,要么完全不出现,这样就避免了一个系统调用只有部分写磁盘操作出现在磁盘上。
- 其次,它支持快速恢复(Fast Recovery)。在重启之后,我们不需要做大量的工作来修复文件系统,只需要非常小的工作量。
在xv6的日志系统中,文件操作的系统调用并不会直接对磁盘进行操作,而是把对磁盘写操作包装成一个日志写在磁盘的日志区,当该系统调用执行完成后,提交一个记录到磁盘中。
当系统崩溃发生在提交之前,磁盘上的日志文件不会被标记为已完成,恢复系统的代码会忽略它,磁盘的状态就像是没有操作过。如果是提交之后发生系统崩溃,那么恢复程序会恢复所有的写操作。也就是说,这种设计使得磁盘操作对于系统崩溃是原子操作:**在恢复之后,要么所有的写操作都完成了,要么一个写操作都没完成。**不会出现写了一半的错误情况。
xv6对于日志区的程序都在kernel/log.c
中
1、日志定义
xv6使用一些数据结构来定义日志区
// kernel/log.c
#define MAXOPBLOCKS 10 // max # of blocks any FS op writes
#define LOGSIZE (MAXOPBLOCKS*3) // max data blocks in on-disk log
// Contents of the header block, used for both the on-disk header block
// and to keep track in memory of logged block# before commit.
struct logheader {
int n; // 日志的大小
int block[LOGSIZE]; // 日志的位置关系信息
};
struct log {
struct spinlock lock;
int start; //日志区第一块块号
int size; // 日志区的大小
int outstanding; // how many FS sys calls are executing.有多少文件系统相关的系统调用正在执行
int committing; // in commit(), please wait. 正在提交标志
int dev;
struct logheader lh; // 日志头
};
struct log log;
日志头logheader结构体,记录了当前日志使用的大小和对应关系,对于磁盘的操作,是先向日志区写入,然后再写入数据区,lh.block数组存放的就是映射关系,block[3] = 1024表示日志块3记录的数据在1024号磁盘块中。
这个结构体在内存当中,用于记录日志信息,同时它也是公共资源,使用一把锁进行保护。
2、一些函数
2.1、日志区初始化
void
initlog(int dev, struct superblock *sb)
{
if (sizeof(struct logheader) >= BSIZE)
panic("initlog: too big logheader");
initlock(&log.lock, "log"); //初始化log的锁
log.start = sb->logstart; //从超级块读取信息:日志区起始位置和长度
log.size = sb->nlog;
log.dev = dev;
recover_from_log(); // 从日志中恢复,用于保证文件系统的一致性
}
2.2、从日志中恢复
static void
recover_from_log(void)
{
read_head(); // 从磁盘中将日志头读出
install_trans(); // if committed, copy from log to disk ,install操作,将日志区的写操作更新到数据区
log.lh.n = 0; // 清空日志记录
write_head(); // clear the log 同步日志头信息到磁盘
}
2.3、读日志头
// Read the log header from disk into the in-memory log header
static void
read_head(void)
{
struct buf *buf = bread(log.dev, log.start); // 从日志区的第一个块读出日志头,记录在缓冲区buf中
struct logheader *lh = (struct logheader *) (buf->data); // 从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); // 在使用完bread后要及时使用brelse
}
2.4、写日志头
// Write in-memory log header to disk.
// This is the true point at which the
// current transaction commits.
// 将内存中的日志头写入磁盘中,这里是真正的提交点
static void
write_head(void)
{
struct buf *buf = bread(log.dev, log.start); // 从日志区的第一个块读出日志头,放入缓冲区buf中
struct logheader *hb = (struct logheader *) (buf->data); // 从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); // 在使用完bread后要及时使用brelse
}
2.5、数据区到日志区
// 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);
}
}
这个函数将执行了写操作的数据块(更新了数据的块)的数据拷贝到日志块。读取日志区的时候tail+1是为了跳过日志头。
2.6、日志区到数据区
// Copy committed blocks from log to their home location
static void install_trans(void)
{
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,把数据块的数据写到磁盘当中
bunpin(dbuf);
brelse(lbuf);
brelse(dbuf);
}
}
将日志区中的数据复制到对应的数据区中,然后再把数据区中的数据同步到磁盘当中。
注意:bread函数调用了bget函数(缓存命中则直接得到buf,未命中则需要从磁盘中读取),这里读到的数据都是从缓存区(cache)中读取的,因此日志区/数据区的数据都是都是从内存中得到的。
3、日志操作
典型日志操作的框架:
begin_op();
...
bp = bread(...); // 读取磁盘数据到缓存块
bp->data[...] = ...; // 修改缓存块的内容
log_write(bp); // 将修改过的缓存块写到日志区,也就是提交操作
...
end_op(); // 日志结束,提交,日志区同步到数据区
begin_op和end_op是一对操作,表明对文件系统操作的开始和结束,在中间进行数据块的读写,然后提交到日志,最后再进行安装
3.1、begin_op
// called at the start of each FS system call. 在文件系统调用的开始调用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; // 文件系统调用+1
release(&log.lock);
break;
}
}
}
在文件系统相关的调用开始的地方调用begin_op函数,首先将等待日志处于未提交状态,然后判断是否有足够的空间允许此次文件系统调用(日志空间已经使用的块数 + 当前并发的系统调用个数 * 每个系统调用可能占用的最大块数MAXOPBLOCKS <= 日志块总个数LOGSIZE)。如果足够则将log.outstanding+1,表示并发的文件系统调用+1.
3.2、log_write
// Caller has modified b->data and is done with the buffer.
// Record the block number and pin in the cache by increasing refcnt.
// commit()/write_log() will do the disk write.
//
// log_write() replaces bwrite(); a typical use is:
// bp = bread(...)
// modify bp->data[]
// log_write(bp)
// brelse(bp)
void
log_write(struct buf *b)
{
int i;
if (log.lh.n >= LOGSIZE || log.lh.n >= log.size - 1) // 当前已使用的日志块不能超过规定大小
panic("too big a transaction");
if (log.outstanding < 1) // 系统调用次数小于1
panic("log_write outside of trans");
acquire(&log.lock);
for (i = 0; i < log.lh.n; i++) {
if (log.lh.block[i] == b->blockno) // 如果在日志块中已经找到了之前已经写入日志的块,那么就进行吸收,对于相同块的写操作在日志区只会占用一个位置
break;
}
log.lh.block[i] = b->blockno; // 可能是吸收,也可能此时i == log.lh.n,也就是新增一个块到日志区
if (i == log.lh.n) { // Add new block to log?
bpin(b); // 增加b的引用次数
log.lh.n++; // 日志块的数量加1
}
release(&log.lock);
}
log_write用于代替bwrite,也就是对于缓存区的数据,先把写操作写入日志区而不是直接写到磁盘。
典型应用框架为:
bp = bread(...)
modify bp->data[]
log_write(bp)
brelse(bp)
首先进行一些合法性判断,然后先在日志区中寻找,看之前有没有相同的要执行写操作的缓存块,如果没有找到,就在日志区新增一个块,同时把这个缓存块的引用次数加一。
3.3、end_op
// 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); // log.lh.n + (log.outstanding+1)*MAXOPBLOCKS > LOGSIZE, log.lh.n增大,outstanding减小,可能会有空间允许其他进程写日志
}
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); // 唤醒其他休眠在log上的进程
release(&log.lock);
}
}
end_op用于文件系统调用的末尾,表明文件系统调用的结束,它将文件系统调用次数减一,如果此时文件系统调用次数为0,就说明此时没有文件系统调用正在执行,可以进行提交操作:do_commit = 1, log.committing = 1。在后续进行提交:调用commit函数,在提交完成后,日志空间清空,有足够的空间可以使用,因此可以唤醒其他休眠在log上的进程。
3.4、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(); // Now install writes to home locations 日志中的数据 -》 磁盘
log.lh.n = 0;
write_head(); // Erase the transaction from the log 日志头 -》 磁盘
}
}
提交日志操作,首先判断是否有日志要提交(log.lh.n > 0),如果有,则先根据内存中的日志头将内存中的数据拷贝到日志块(write_log)。
然后将内存中的日志头写入磁盘当中(write_head,这是真正的提交点,这一部完成之前表明没有提交,之后表示已提交)
经过提交点后,再将日志区中的数据拷贝到磁盘当中(install_trans)
然后把日志头记录的n(需要提交的日志数量)清空,再把日志头同步到磁盘当中,这表明完成了一次日志操作。
3.5、recover_from_log
static void
recover_from_log(void)
{
read_head(); // 磁盘 -》 日志头
install_trans(); // if committed, copy from log to disk 日志中的数据 -》 磁盘
log.lh.n = 0;
write_head(); // clear the log // 日志头 -》 磁盘
}
recover_from_log相比commit多了read_head(从磁盘中读取日志头),因为commit的时候,日志头已经在内存当中了,剩下的就是差不多的操作。
当系统崩溃发生在提交之前(write_head),磁盘上的日志文件不会被标记为已完成(主要是根据日志头中的n,代表日志区的块数),恢复系统的代码会忽略它,磁盘的状态就像是没有操作过。
如果是提交之后发生系统崩溃,那么恢复程序会恢复所有的写操作(install_trans)。
也就是说,这种设计使得磁盘操作对于系统崩溃是原子操作:**在恢复之后,要么所有的写操作都完成了,要么一个写操作都没完成。**不会出现写了一半的错误情况,这就保证了磁盘文件系统的一致性。
inode
根据超级块结构体中和inode有关的字段:ninodes 和 inodestart 可以确定inode块的范围。
inode相关程序位于kernel/fs.c
中
inode是文件的代表,和文件一一对应。
位图
数据块的分配和释放由位图来管理
// Bitmap bits per block, 每个位图块有多少位,也就是说每个位图块能描述BPB个数据块的使用情况
#define BPB (BSIZE*8)
// Block of free map containing bit for block b:数据块b对应在哪个位图块上
#define BBLOCK(b, sb) ((b)/BPB + sb.bmapstart)
// Allocate a zeroed disk 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)); // 读取块b对应的位图块,在放入缓存中
for(bi = 0; bi < BPB && b + bi < sb.size; bi++){ // 从小到大枚举每个位
m = 1 << (bi % 8); //mask
if((bp->data[bi/8] & m) == 0){ // Is block free? 与运算,为0则说明该块空闲
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");
}
// Free a disk block. 释放一个数据块,清空对应的位图
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); // mask
if((bp->data[bi/8] & m) == 0) // 如果这个块没有被使用,但是调用bfree,陷入Panic
panic("freeing free block");
bp->data[bi/8] &= ~m; // 清空使用标志
log_write(bp); // 更新到日志
brelse(bp);
}
对于数据块的分配,就是在位图中寻找空闲位(位图中对应位为0),然后将其置1,就代表分配出去了。释放就是类似的逆运算。
inode和dinode
inode 分为在磁盘上的dinode和内存中的inode
// On-disk inode structure 磁盘上的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,NDIRECT一级指针指向数据块,最后一个是间接的二级指针,指向一个inode表格,其中的addrs指向最低一级的数据块(有点像多级页表)
};
// in-memory copy of an inode 内存中的inode
struct inode {
uint dev; // Device number 设备号
uint inum; // Inode number inode编号
int ref; // Reference count 引用计数
struct sleeplock lock; // protects everything below here 休眠锁
int valid; // inode has been read from disk? 是否已经从磁盘中读取数据,如果是valid,说明此时inode刚分配,里面的数据无效
short type; // copy of disk inode
short major;
short minor;
short nlink;
uint size;
#ifdef SOL_FS
#else
uint addrs[NDIRECT+1];
#endif
};
struct {
struct spinlock lock;
struct inode inode[NINODE]; // #define NINODE 50 // maximum number of active i-nodes
} icache;
对于inode,xv6也提供了一个缓存区icache,用于对inode的分配,操作系统对于icache的使用时间是很短的,类似bcache(缓存区)和kmem(内存分配),使用一把自旋锁保护即可,而对于inode的使用可能会很久,因此使用睡眠锁进行保护。
后面有时间再讲讲addrs如何索引数据块的
分配inode
inode分为在磁盘上的和内存上的,分配一个inode分为两步,先分配dinode,再分配内存inode。
// Allocate an inode on device dev.
// Mark it as allocated by giving it type type.
// Returns an unlocked but allocated and referenced inode.
struct inode*
ialloc(uint dev, short type)
{
int inum;
struct buf *bp;
struct dinode *dip;
for(inum = 1; inum < sb.ninodes; inum++){
bp = bread(dev, IBLOCK(inum, sb)); // 计算inum在哪个块中
dip = (struct dinode*)bp->data + inum%IPB; // 该inum在这个块中的位置
if(dip->type == 0){ // a free inode 这个节点为空闲
memset(dip, 0, sizeof(*dip)); // 清空dinode
dip->type = type;
log_write(bp); // mark it allocated on the disk 写入log,为了同步到磁盘当中
brelse(bp);
return iget(dev, inum); // 分配内存上的inode, 以内存中的inode进行返回(文件系统实际工作时使用的是inode)
}
brelse(bp);
}
panic("ialloc: no inodes");
}
从头到尾枚举磁盘上的inode块的使用情况,如果type为0,说明该inode为空闲,就分配这个dinode,将分配操作同步到磁盘当中,然后调用iget函数返回文件系统实际使用的inode结构体。
// Find the inode with number inum on device dev
// and return the in-memory copy. Does not lock
// the inode and does not read it from disk.
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++){ // 遍历icache判断是否缓存
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. 记录空闲inode
empty = ip;
}
// Recycle an inode cache entry.
if(empty == 0) // 没有空闲inode,panic
panic("iget: no inodes");
ip = empty;
ip->dev = dev;
ip->inum = inum;
ip->ref = 1;
ip->valid = 0; // 有效位置0,表明还没更新数据
release(&icache.lock);
return ip;
}
iget函数:首先判断在icache中有没有缓存这个inum,如果没有则用空闲的inode进行分配,然后初始化对应参数。如果命中了就增加引用计数然后直接返回。
使用修改inode
由于系统使用inode的时间可能很长,同时inode中的字段为临界资源,因此需要对inode使用睡眠锁进行保护。
由于在分配inode的时候,还未从磁盘中读取数据,因此valid为0,在加锁的时候,判断valid,如果为0,那么此时读取磁盘上的dinode数据到inode中
// Lock the given inode.
// Reads the inode from disk if necessary.
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后,需要把修改操作写入磁盘当中进行同步(iupdate函数)。
// Copy a modified in-memory inode to disk.
// Must be called after every change to an ip->xxx field
// that lives on disk, since i-node cache is write-through.
// Caller must hold ip->lock.
void
iupdate(struct inode *ip)
{
struct buf *bp;
struct dinode *dip;
bp = bread(ip->dev, IBLOCK(ip->inum, sb)); // 在磁盘中读取相应的inode块
dip = (struct dinode*)bp->data + ip->inum%IPB; // 找到对应的inode
dip->type = ip->type; //更新dinode
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); // 写log,同步到磁盘操作
brelse(bp);
}
释放inode
用完inode后需要进行释放(调用iput),类似bread之后需要调用brelse(缓冲区buf的释放)
// Drop a reference to an in-memory inode.
// If that was the last reference, the inode cache entry can
// be recycled.
// If that was the last reference and the inode has no links
// to it, free the inode (and its content) on disk.
// All calls to iput() must be inside a transaction in
// case it has to free the inode.
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); // 将该inode块指向的数据块全部释放,其实就是在位图上清空
ip->type = 0;
iupdate(ip); // 当ip->type为0,并且更新到磁盘后,相当于就能够在ialloc中找到这个dip->type为0
ip->valid = 0;
releasesleep(&ip->lock);
acquire(&icache.lock);
}
ip->ref--; // 引用计数减一,若引用计数为0,那么在iget函数中会被视为empty inode
release(&icache.lock);
}
获取inode和数据块的连接(索引bmap)
// Return the disk block address of the nth block in inode ip.
// If there is no such block, bmap allocates one.
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){ // 二级索引对应的Bn块未分配,则分配
a[bn] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
panic("bmap: out of range");
}
释放inode指向的数据块(itrunc)
为bmap的逆操作,就是将inode所指向的数据块(包括直接和间接索引块)全部释放,等于删除文件。
// Truncate inode (discard contents).
// Caller must hold ip->lock.
void
itrunc(struct inode *ip)
{
int i, j;
struct buf *bp;
uint *a;
for(i = 0; i < NDIRECT; i++){ // 释放所有直接索引块
if(ip->addrs[i]){
bfree(ip->dev, ip->addrs[i]);
ip->addrs[i] = 0;
}
}
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);
}
读写inode
对于读写文件:自底向上是:读写磁盘 -》 读写缓冲区 -》写日志区 -》读写inode
注意:对于日志区,没有读操作
读inode:
// Read data from inode.
// Caller must hold ip->lock.
// If user_dst==1, then dst is a user virtual address; 如果user_dst为1,说明dst为进程虚拟地址空间
// otherwise, dst is a kernel address.
int
readi(struct inode *ip, int user_dst, uint64 dst, uint off, uint n) // 从inode指向的文件,从off开始读,读取n个字节到dst中
{
uint tot, m;
struct buf *bp;
if(off > ip->size || off + n < off) // 开始读取的数据超过文件末尾,或n为负数,非法
return 0;
if(off + n > ip->size) // 从偏移量读取n字节超过的文件末尾,则只读剩下的那部分
n = ip->size - off;
for(tot=0; tot<n; tot+=m, off+=m, dst+=m){ // tot:目前已读字节数,n为需要读的字节数,off为开始读取的位置(字节),dst为读取的目的地
bp = bread(ip->dev, bmap(ip, off/BSIZE)); // 读取off所在数据块到缓存块
m = min(n - tot, BSIZE - off%BSIZE); // 一次性最多读取m字节
if(either_copyout(user_dst, dst, bp->data + (off % BSIZE), m) == -1) { // 复制数据到dst或user_dst
brelse(bp);
break;
}
brelse(bp);
}
return tot;
}
写inode:
// Write data to inode.
// Caller must hold ip->lock.
// If user_src==1, then src is a user virtual address; 如果user_dst为1,说明dst为进程虚拟地址空间
// otherwise, src is a kernel address.
int
writei(struct inode *ip, int user_src, uint64 src, uint off, uint n) // 从src中写数据到inode所指向的文件,从off开始,长度为n
{
uint tot, m;
struct buf *bp;
if(off > ip->size || off + n < off) // 开始写的数据超过文件末尾,或n为负数,非法
return -1;
if(off + n > MAXFILE*BSIZE) // 写的最后一个数据超过文件的最大字节数
return -1;
for(tot=0; tot<n; tot+=m, off+=m, src+=m){ // tot表示已写的字节数,m为每次写的数量,off表示开始写的位置,src为起始位置
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) { // 从src写入缓存块
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); // 更新inode
}
return n;
}
基本是readi的反操作,只是需要将写操作写到日志区,如果文件信息变了(ip->size变化),需要调用iupdate将inode信息同步到磁盘上的dinode。
目录
目录也是文件,其数据是一个个目录项,包含字段有inode编号和文件名,inode和文件一一对应,目录项结构体将inode和文件名联合起来。
//kernel/fs.h
// Directory is a file containing a sequence of dirent structures.
#define DIRSIZ 14
struct dirent {
ushort inum; // inode编号
char name[DIRSIZ]; // 文件名
};
查找目录项
// Look for a directory entry in a directory.
// If found, set *poff to byte offset of entry.
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");
if(de.inum == 0) //inode为空
continue;
if(namecmp(name, de.name) == 0){ // 比对名字
// entry matches path element
if(poff)
*poff = off;
inum = de.inum;
return iget(dp->dev, inum); // 返回文件对应的inode
}
}
return 0;
}
添加目录项
// Write a new directory entry (name, inum) into the directory dp.
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)) //写回到inode中
panic("dirlink");
return 0;
}
路径
绝对路径:/a/b/c,相对路径:a/b/c
路径其实是由一个个文件名组成的,一般来说,中间项都是目录文件,最后一项是普通文件类型。
根据路径提取文件名
无论是绝对路径(以/开头)还是相对路径(不以/开头),都需要一个工具函数,提取文件名
// Copy the next path element from path into name.
// Return a pointer to the element following the copied one.
// The returned path has no leading slashes,
// so the caller can check *path=='\0' to see if the name is the last one.
// If no name to remove, return 0.
//
// Examples:
// skipelem("a/bb/c", name) = "bb/c", setting name = "a"
// skipelem("///a//bb", name) = "bb", setting name = "a"
// skipelem("a", name) = "", setting name = "a"
// skipelem("", name) = skipelem("", name) = 0
//
static char*
skipelem(char *path, char *name)
{
char *s;
int len;
while(*path == '/')
path++;
if(*path == 0)
return 0;
s = path;
while(*path != '/' && *path != 0)
path++;
len = path - s;
if(len >= DIRSIZ)
memmove(name, s, DIRSIZ);
else {
memmove(name, s, len);
name[len] = 0;
}
while(*path == '/')
path++;
return path;
}
根据路径返回inode
根据路径返回当前文件的inode或者当前文件的父目录的inode:
// Look up and return the inode for a path name.
// If parent != 0, return the inode for the parent and copy the final
// path element into name, which must have room for DIRSIZ bytes.
// Must be called inside a transaction since it calls iput().
// 获取当前目录的Inode,根据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);
while((path = skipelem(path, name)) != 0){
ilock(ip);
if(ip->type != T_DIR){
iunlockput(ip);
return 0;
}
if(nameiparent && *path == '\0'){
// Stop one level early.
iunlock(ip);
return ip;
}
if((next = dirlookup(ip, name, 0)) == 0){
iunlockput(ip);
return 0;
}
iunlockput(ip);
ip = next;
}
if(nameiparent){
iput(ip);
return 0;
}
return ip;
}
struct inode*
namei(char *path)
{
char name[DIRSIZ];
return namex(path, 0, name);
}
struct inode*
nameiparent(char *path, char *name)
{
return namex(path, 1, name);
}
inode -> 目录 -> 路径。给定一个路径,确定对应文件的inode:先根据路径,找到当前目录,然后根据文件名查找文件/目录,不断循环直至找到对应的文件,文件与inode是一一对应的,这就实现了给定文件路径找到inode。
文件描述符
xv6为每一个打开的文件维护两张表,分别是全局的打开文件表ftable和每个进程独有的打开文件表ofile(文件描述符表)。
每一个打开的文件都使用文件结构体file表示。文件描述符表是一个struct file*类型的指针数组,文件描述符fd就是文件描述符表的索引。
相关代码在kernel/file.c
中
文件结构体struct file
struct file {
#ifdef LAB_NET
enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE, FD_SOCK } type; // 文件类型:无类型、管道、inode、设备、套接字
#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; // file数组为临界资源,使用锁保护
struct file file[NFILE]; // #define NFILE 100 // open files per system
} ftable;
分配文件结构体
// Allocate a file structure.
struct file*
filealloc(void)
{
struct file *f;
acquire(&ftable.lock);
for(f = ftable.file; f < ftable.file + NFILE; f++){ // 从小到大遍历全局文件表,找到ref为0的作为新分配的文件结构体
if(f->ref == 0){
f->ref = 1;
release(&ftable.lock);
return f;
}
}
release(&ftable.lock);
return 0; // 没找到就返回0
}
文件描述符表(进程级)
// Per-process state
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
// lab3 add
pagetable_t ptb_k; // kernel page table
};
struct file *ofile[NOFILE]; // Open files
为进程级的文件描述符表
分配文件描述符(进程级)
// kernel/sysfile.c
// Allocate a file descriptor for the given file.
// Takes over file reference from caller on success.
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;
}