675 lines
28 KiB
ReStructuredText
675 lines
28 KiB
ReStructuredText
nfs文件系统
|
||
=======================================
|
||
|
||
本节导读
|
||
---------------------------------------
|
||
|
||
本节我们简单介绍一下我们实现的nfs操作系统。本章中新增加的代码很多,但是大家如果想研读的话理解起来不太困难。很多函数只看名字也可以知道其功效,不需要再探究其实现的方式。
|
||
|
||
文件系统布局
|
||
---------------------------------------
|
||
|
||
导言中提到,我们的nfs文件系统十分类似ext4文件系统,下面我们可以看一下nfs文件系统的布局::
|
||
// 基本信息:块大小 BSIZE = 1024B,总容量 FSSIZE = 1000 个 block = 1000 * 1024 B。
|
||
// Layout:
|
||
// 0号块留待后续拓展,可以忽略。superblock 固定为 1 号块,size 固定为一个块。
|
||
// 其后是储存 inode 的若干个块,占用块数 = inode 上限 / 每个块上可以容纳的 inode 数量,
|
||
// 其中 inode 上限固定为 200,每个块的容量 = BSIZE / sizeof(struct disk_inode)
|
||
// 再之后是数据块相关内容,包含一个 储存空闲块位置的 bitmap 和 实际的数据块,bitmap 块
|
||
// 数量固定为 NBITMAP = FSSIZE / (BSIZE * 8) + 1 = 1000 / 8 + 1 = 126 块。
|
||
// [ boot block | sb block | inode blocks | free bit map | data blocks ]
|
||
|
||
注意:不推荐同学们修改该布局,除非你完全看懂了 fs 的逻辑,所以最好不要改变 disk_inode 这个结构的大小,如果想要增删字段,一定使用 pad。这个布局具体定义的位置在nfs/fs.c之中。
|
||
|
||
我们定义的inode和data blocks实际上和ext4中同名的结构功能几乎是一样的。**索引节点** (Inode, Index Node) 是文件系统中的一种重要数据结构。逻辑目录树结构中的每个文件和目录都对应一个 inode ,我们前面提到的在文件系统实现中文件/目录的底层编号实际上就是指 inode 编号。在 inode 中不仅包含了我们通过 ``stat`` 工具能够看到的文件/目录的元数据(大小/访问权限/类型等信息),还包含实际保存对应文件/目录数据的数据块(位于最后的数据块区域中)的索引信息,从而能够找到文件/目录的数据被保存在磁盘的哪些块中。从索引方式上看,同时支持直接索引和间接索引。
|
||
|
||
下面我们看一下它们在我们C中对应的具体结构体:
|
||
|
||
.. code-block:: c
|
||
|
||
// 超级块位置固定,用来指示文件系统的一些元数据,这里最重要的是 inodestart 和 bmapstart
|
||
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 inodestart;// Block number of first inode block
|
||
uint bmapstart; // Block number of first free map block
|
||
};
|
||
|
||
// On-disk inode structure
|
||
// 储存磁盘 inode 信息,主要是文件类型和数据块的索引,其大小影响磁盘布局,不要乱改,可以用 pad
|
||
struct dinode {
|
||
short type; // File type
|
||
short pad[3];
|
||
uint size; // Size of file (bytes)
|
||
uint addrs[NDIRECT + 1];// Data block addresses
|
||
};
|
||
|
||
// in-memory copy of an inode
|
||
// dinode 的内存缓存,为了方便,增加了 dev, inum, ref, valid 四项管理信息,大小无所谓,可以随便改。
|
||
struct inode {
|
||
uint dev; // Device number
|
||
uint inum; // Inode number
|
||
int ref; // Reference count
|
||
int valid; // inode has been read from disk?
|
||
short type; // copy of disk inode
|
||
uint size;
|
||
uint addrs[NDIRECT+1]; // data block num
|
||
};
|
||
|
||
// 目录对应的数据块的内容本质是 filename 到 file inode_num 的一个 map,这里为了简单,就存为一个 `dirent` 数组,查找的时候遍历对比
|
||
struct dirent {
|
||
ushort inum;
|
||
char name[DIRSIZ];
|
||
};
|
||
|
||
// 数据块缓存结构体。
|
||
struct buf {
|
||
int valid; // has data been read from disk?
|
||
int disk; // does disk "own" buf?
|
||
uint dev;
|
||
uint blockno;
|
||
uint refcnt;
|
||
struct buf *prev; // LRU cache list
|
||
struct buf *next;
|
||
uchar data[BSIZE];
|
||
};
|
||
|
||
注意几个量的概念:
|
||
- block num: 表示某一个磁盘块的编号。我们操作数据块会把它读入内存的数据块缓存之中,其结构体见上。
|
||
- inode num: 表示某一个 inode 在所有 inode 项里的编号。注意 inode blocks 其实就是一个 inode 的大数组。
|
||
|
||
同时,目录本身是一个 filename 到 file对应的inode_num的map,可以完成 filename 到 inode_num 的转化。
|
||
|
||
OS启动后是没有inode的内存缓存的。下面我们自底向上走一遍OS打开已存在在磁盘上文件的过程,让大家熟悉一下nfs的具体实现方式。
|
||
|
||
virtio 磁盘驱动
|
||
---------------------------------------
|
||
|
||
注意:这一部分代码不需要同学们详细了解细节,但需要知道大概的过程。
|
||
|
||
在 uCore-Tutorial 中磁盘块的读写是通过中断处理的。在 virtio.h 和 virtio-disk.c 中我们按照 qemu 对 virtio 的定义,实现了 virtio_disk_init 和 virtio_disk_rw 两个函数,前者完成磁盘设备的初始化和对其管理的初始化。virtio_disk_rw 实际完成磁盘IO,当设定好读写信息后会通过 MMIO 的方式通知磁盘开始写。然后,os 会开启中断并开始死等磁盘读写完成。当磁盘完成 IO 后,磁盘会触发一个外部中断,在中断处理中会把死循环条件解除。内核态只会在处理磁盘读写的时候短暂开启中断,之后会马上关闭。
|
||
|
||
.. code-block:: c
|
||
|
||
virtio_disk_rw(struct buf *b, int write) {
|
||
/// ... set IO config
|
||
*R(VIRTIO_MMIO_QUEUE_NOTIFY) = 0; // notify the disk to carry out IO
|
||
struct buf volatile * _b = b; // Make sure complier will load 'b' form memory
|
||
intr_on();
|
||
while(_b->disk == 1); // _b->disk == 0 means that this IO is done
|
||
intr_off();
|
||
}
|
||
|
||
// 开启和关闭中断的函数。
|
||
static inline void intr_on() { w_sstatus(r_sstatus() | SSTATUS_SIE); }
|
||
|
||
// disable device interrupts
|
||
static inline void intr_off() { w_sstatus(r_sstatus() & ~SSTATUS_SIE); }
|
||
|
||
对于内核中断处理的修改在trap.c之中。之前我们的trap from kernel会直接panic,现在我们需要添加对外部中断的处理。kerneltrap也需要类似usertrap的保存上下文以及回到原处的kernelvec以及kernelret函数。进入内核之后要单独设置stvec指向kernelvec处。
|
||
|
||
.. code-block:: riscv64
|
||
|
||
# kernelvec.S
|
||
|
||
kernelvec:
|
||
// make room to save registers.
|
||
addi sp, sp, -256
|
||
// save the registers expect x0
|
||
sd ra, 0(sp)
|
||
sd sp, 8(sp)
|
||
sd gp, 16(sp)
|
||
// ...
|
||
sd t4, 224(sp)
|
||
sd t5, 232(sp)
|
||
sd t6, 240(sp)
|
||
|
||
call kerneltrap
|
||
|
||
kernelret:
|
||
// restore registers.
|
||
// 思考:为什么直接就使用了sp?
|
||
ld ra, 0(sp)
|
||
ld sp, 8(sp)
|
||
ld gp, 16(sp)
|
||
// restore all registers expect x0
|
||
ld t4, 224(sp)
|
||
ld t5, 232(sp)
|
||
ld t6, 240(sp)
|
||
addi sp, sp, 256
|
||
sret
|
||
|
||
kerneltrap具体的修改如下:
|
||
|
||
.. code-block:: c
|
||
|
||
void kerneltrap() {
|
||
// 老三样,不过在这里把处理放到了 C 代码中
|
||
uint64 sepc = r_sepc();
|
||
uint64 sstatus = r_sstatus();
|
||
uint64 scause = r_scause();
|
||
|
||
if ((sstatus & SSTATUS_SPP) == 0)
|
||
panic("kerneltrap: not from supervisor mode");
|
||
|
||
if (scause & (1ULL << 63)) {
|
||
// 可能发生时钟中断和外部中断,我们的主要目标是处理外部中断
|
||
devintr(scause & 0xff);
|
||
} else {
|
||
// kernel 发生异常就挣扎了,肯定出问题了,杀掉用户线程跑路
|
||
error("invalid trap from kernel: %p, stval = %p sepc = %p\n", scause, r_stval(), sepc);
|
||
exit(-1);
|
||
}
|
||
}
|
||
|
||
// 外部中断处理函数
|
||
void devintr(uint64 cause) {
|
||
int irq;
|
||
switch (cause) {
|
||
case SupervisorTimer:
|
||
set_next_timer();
|
||
// 时钟中断如果发生在内核态,不切换进程,原因分析在下面
|
||
// 如果发生在用户态,照常处理
|
||
if((r_sstatus() & SSTATUS_SPP) == 0) {
|
||
yield();
|
||
}
|
||
break;
|
||
case SupervisorExternal:
|
||
irq = plic_claim();
|
||
if (irq == UART0_IRQ) { // UART 串口的终端不需要处理,这个 rustsbi 替我们处理好了
|
||
// do nothing
|
||
} else if (irq == VIRTIO0_IRQ) { // 我们等的就是这个中断
|
||
virtio_disk_intr();
|
||
}
|
||
if (irq)
|
||
plic_complete(irq); // 表明中断已经处理完毕
|
||
break;
|
||
}
|
||
}
|
||
|
||
virtio_disk_intr() 会把 buf->disk 置零,这样中断返回后死循环条件解除,程序可以继续运行。具体代码在 virtio-disk.c 中。
|
||
|
||
这里还需要注意的一点是,为什么始终不允许内核发生进程切换呢?只是由于我们的内核并没有并发的支持,相关的数据结构没有锁或者其他机制保护。考虑这样一种情况,一个进程读写一个文件,内核处理等待磁盘相应时,发生时钟中断切换到了其他进程,然而另一个进程也要读写同一个文件,这就可能发生数据访问上的冲突,甚至导致磁盘出现错误的行为。这也是为什么内核态一直不处理时钟中断,我们必须保证每一次内核的操作都是原子的,不能被打断。大家可以想一想,如果内核可以随时切换,当前有那些数据结构可能被破坏。提示:想想 kalloc 分配到一半,进程 switch 切换到一半之类的。
|
||
|
||
磁盘块缓存
|
||
---------------------------------------
|
||
|
||
为了加快磁盘访问的速度,在内核中设置了磁盘缓存 struct buf,一个 buf 对应一个磁盘 block,这一部分代码也不要求同学们深入掌握。大致的作用机制是,对磁盘的读写都会被转化为对 buf 的读写,当 buf 有效时,读写 buf,buf 无效时(类似页表缺页和 TLB 缺失),就实际读写磁盘,将 buf 变得有效,然后继续读写 buf。详细的内容在 buf.h 和 bio.c 中。buf 写回的时机是 buf 池满需要替换的时候(类似内存的 swap 策略) 手动写回。如果 buf 没有写回,一但掉电就 GG 了,所以手动写回还是挺重要的。
|
||
|
||
.. code-block:: c
|
||
|
||
// os/bio.c
|
||
struct buf *
|
||
bread(uint dev, uint blockno) {
|
||
struct buf *b;
|
||
b = bget(dev, blockno);
|
||
if (!b->valid) {
|
||
virtio_disk_rw(b, R);
|
||
b->valid = 1;
|
||
}
|
||
return b;
|
||
}
|
||
|
||
// Write b's contents to disk.
|
||
void bwrite(struct buf *b) {
|
||
virtio_disk_rw(b, W);
|
||
}
|
||
|
||
读取文件数据实际就是读取文件inode指向数据块的数据。读数据块到缓存的数据需要使用bread,而写回缓存需要用到bwrite函数。文件系统首先使用bget去查缓存中是否已有对应的block,如果没有会分配内存来缓存对应的块。之后会调用bread/bwrite进行从磁盘读数据块、写回数据块。要注意释放块缓存的brelse函数。
|
||
|
||
.. code-block:: c
|
||
|
||
// os/bio.c
|
||
void brelse(struct buf *b) {
|
||
b->refcnt--;
|
||
if (b->refcnt == 0) {
|
||
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;
|
||
}
|
||
}
|
||
|
||
需要特别注意的是 brelse 不会真的如字面意思释放一个 buf。它的准确含义是暂时不操作该 buf 了并把它放置在bcache链表的首部,buf 的真正释放会被推迟到 buf 池满,无法分配的时候,就会把最近最久未使用的 buf 释放掉(释放 = 写回 + 清空)。这是为了尽可能保留内存缓存,因为读写磁盘真的太太太太慢了。
|
||
|
||
此外,brelse 的数量必须和 bget 相同,因为 bget 会是的引用计数加一。如果没有相匹配的 brelse,就好比 new 了之后没有 delete。千万注意。
|
||
|
||
|
||
|
||
inode的操作
|
||
---------------------------------------
|
||
|
||
现在我们来看看nfs如何读取磁盘上的dinode到内存之中。我们通过file name对应的inode num去从磁盘读取对应的inode。为了解决共享问题(不同进程可以打开同一个磁盘文件),也有一个全局的 inode table,每当新打开一个文件的时候,会把一个空闲的 inode 绑定为对应 dinode 的缓存,这一步通过 iget 完成。
|
||
|
||
.. code-block:: c
|
||
|
||
// 找到 inum 号 dinode 绑定的 inode,如果不存在新绑定一个
|
||
static struct inode *
|
||
iget(uint dev, uint inum) {
|
||
struct inode *ip, *empty;
|
||
// 遍历查找 inode table
|
||
for (ip = &itable.inode[0]; ip < &itable.inode[NINODE]; ip++) {
|
||
// 如果有对应的,引用计数 +1并返回
|
||
if (ip->ref > 0 && ip->dev == dev && ip->inum == inum) {
|
||
ip->ref++;
|
||
return ip;
|
||
}
|
||
}
|
||
// 如果没有对于的,找一个空闲 inode 完成绑定
|
||
empty = find_empty()
|
||
// GG,inode 表满了,果断自杀.lab7正常不会出现这个情况。
|
||
if (empty == 0)
|
||
panic("iget: no inodes");
|
||
// 注意这里仅仅是写了元数据,没有实际读取,实际读取推迟到后面
|
||
ip = empty;
|
||
ip->dev = dev;
|
||
ip->inum = inum;
|
||
ip->ref = 1;
|
||
ip->valid = 0; // 没有实际读取,valid = 0
|
||
return ip;
|
||
}
|
||
|
||
当已经得到一个文件对应的 inode 后,可以通过 ivalid 函数确保其是有效的。
|
||
|
||
.. code-block:: c
|
||
|
||
// Reads the inode from disk if necessary.
|
||
void ivalid(struct inode *ip) {
|
||
struct buf *bp;
|
||
struct dinode *dip;
|
||
if (ip->valid == 0) {
|
||
// bread 可以完成一个块的读取,这个在将 buf 的时候说过了
|
||
// IBLOCK 可以计算 inum 在几个 block
|
||
bp = bread(ip->dev, IBLOCK(ip->inum, sb));
|
||
// 得到 dinode 内容
|
||
dip = (struct dinode *) bp->data + ip->inum % IPB;
|
||
// 完成实际读取
|
||
ip->type = dip->type;
|
||
ip->size = dip->size;
|
||
memmove(ip->addrs, dip->addrs, sizeof(ip->addrs));
|
||
// buf 暂时没用了
|
||
brelse(bp);
|
||
// 现在有效了
|
||
ip->valid = 1;
|
||
}
|
||
}
|
||
|
||
在 inode 有效之后,可以通过 writei, readi 完成读写。这又是bwrite和bread的上级接口了。和其他OS支持的文件系统一样,我们首先计算出文件的偏移量,并通过bmap得到对应的block num。之后调用bwrite/bread来进行文件的读写操作。
|
||
|
||
.. code-block:: c
|
||
|
||
// 从 ip 对应文件读取 [off, off+n) 这一段数据到 dst
|
||
int readi(struct inode *ip, char* dst, uint off, uint n) {
|
||
uint tot, m;
|
||
// 还记得 buf 吗?
|
||
struct buf *bp;
|
||
for (tot = 0; tot < n; tot += m, off += m, dst += m) {
|
||
// bmap 完成 off 到 block num 的对应,见下
|
||
bp = bread(ip->dev, bmap(ip, off / BSIZE));
|
||
// 一次最多读一个块,实际读取长度为 m
|
||
m = MIN(n - tot, BSIZE - off % BSIZE);
|
||
memmove(dst, (char*)bp->data + (off % BSIZE), m);
|
||
brelse(bp);
|
||
}
|
||
return tot;
|
||
}
|
||
|
||
// 同 readi
|
||
int writei(struct inode *ip, char* src, uint off, uint n) {
|
||
uint tot, m;
|
||
struct buf *bp;
|
||
|
||
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);
|
||
memmove(src, (char*)bp->data + (off % BSIZE), m);
|
||
bwrite(bp);
|
||
brelse(bp);
|
||
}
|
||
|
||
// 文件长度变长,需要更新 inode 里的 size 字段
|
||
if (off > ip->size)
|
||
ip->size = off;
|
||
|
||
// 有可能 inode 信息被更新了,写回
|
||
iupdate(ip);
|
||
|
||
return tot;
|
||
}
|
||
|
||
其中bmap函数是连接inode和block的重要函数。但由于我们支持了间接索引,同时还设计到文件大小的改变,所以也拉出来看看:
|
||
|
||
.. code-block:: c
|
||
|
||
// bn = off / BSIZE
|
||
uint bmap(struct inode *ip, uint bn) {
|
||
uint addr, *a;
|
||
struct buf *bp;
|
||
// 如果 bn < 12,属于直接索引, block num = ip->addr[bn]
|
||
if (bn < NDIRECT) {
|
||
// 如果对应的 addr, 也就是 block num = 0,表明文件大小增加,需要给文件分配新的 data block
|
||
// 这是通过 balloc 实现的,具体做法是在 bitmap 中找一个空闲 block,置位后返回其编号
|
||
if ((addr = ip->addrs[bn]) == 0)
|
||
ip->addrs[bn] = addr = balloc(ip->dev);
|
||
return addr;
|
||
}
|
||
bn -= NDIRECT;
|
||
// 间接索引块,那么对应的数据块就是一个大 addr 数组。
|
||
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);
|
||
bwrite(bp);
|
||
}
|
||
brelse(bp);
|
||
return addr;
|
||
}
|
||
|
||
panic("bmap: out of range");
|
||
return 0;
|
||
}
|
||
|
||
balloc(位于nfs/fs.c)会分配一个新的buf缓存。而iupdate函数则是把修改之后的inode重新写回到磁盘上。不然掉电了就凉了。
|
||
|
||
.. code-block:: c
|
||
|
||
// Copy a modified in-memory inode to disk.
|
||
// Must be called after every change to an ip->xxx field
|
||
// that lives on disk.
|
||
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->size = ip->size;
|
||
memmove(dip->addrs, ip->addrs, sizeof(ip->addrs));
|
||
bwrite(bp);
|
||
brelse(bp);
|
||
}
|
||
|
||
文件在进程中的结构
|
||
---------------------------------------
|
||
inode是由操作系统统一控制的dinode在内存中的映射,但是每个进程在具体使用文件的时候,除了需要考虑使用的是哪个inode对应的文件外,还需要根据对文件的使用情况来记录其它特性,因此,在进程中我们使用file结构体来标识一个被进程使用的文件:
|
||
|
||
.. code-block:: c
|
||
|
||
|
||
// Defines a file in memory that provides information about the current use of the file and the corresponding inode location
|
||
struct file {
|
||
enum { FD_NONE = 0,FD_INODE, FD_STDIO } type;
|
||
int ref; // reference count
|
||
char readable;
|
||
char writable;
|
||
struct inode *ip; // FD_INODE
|
||
uint off;
|
||
};
|
||
|
||
struct file filepool[FILEPOOLSIZE];
|
||
我们采用预分配的方式来对file进行分配,每一个需要使用的file都要与filepool中的某一个file完成绑定。file结构中,ref记录了其引用次数,type表示了文件的类型,在本章中我们主要使用FD_NONE和FD_INODE属性,其中FD_INODE表示file已经绑定了一个文件(可能是目录或普通文件),FD_NONE表示该file还没完成绑定,FD_STDIO用来做标准输入输出,这里不做讨论;readbale和writeble规定了进程对文件的读写权限;ip标识了file所对应的磁盘中的inode编号,off即文件指针,用作记录文件读写时的偏移量。
|
||
|
||
分配文件时,我们从filepool中寻找还没有被分配的file进行分配:
|
||
|
||
.. code-block:: c
|
||
|
||
// os/file.c
|
||
struct file* filealloc() {
|
||
for(int i = 0; i < FILE_MAX; ++i) {
|
||
if(filepool[i].ref == 0) {
|
||
filepool[i].ref = 1;
|
||
return &filepool[i];
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
进程关闭文件时,也要去filepool中放回:(注意需要根据ref来判断是否需要回收该file)
|
||
|
||
.. code-block:: c
|
||
|
||
void fileclose(struct file *f)
|
||
{
|
||
if (f->ref < 1)
|
||
panic("fileclose");
|
||
if (--f->ref > 0) {
|
||
return;
|
||
}
|
||
switch (f->type) {
|
||
case FD_STDIO:
|
||
// Do nothing
|
||
break;
|
||
case FD_INODE:
|
||
iput(f->ip);
|
||
break;
|
||
default:
|
||
panic("unknown file type %d\n", f->type);
|
||
}
|
||
|
||
f->off = 0;
|
||
f->readable = 0;
|
||
f->writable = 0;
|
||
f->ref = 0;
|
||
f->type = FD_NONE;
|
||
}
|
||
|
||
注意文件对于进程而言也是其需要记录的一种资源,因此我们在进程对应的PCB结构体之中也需要记录进程打开的文件信息。我们给PCB增加文件指针数组。
|
||
|
||
.. code-block:: c
|
||
|
||
// proc.h
|
||
// Per-process state
|
||
struct proc {
|
||
// ...
|
||
|
||
+ struct file* files[16];
|
||
};
|
||
|
||
// os/proc.c
|
||
int fdalloc(struct file* f) {
|
||
struct proc* p = curr_proc();
|
||
// fd = 0,1,2 is reserved for stdio/stdout/stderr
|
||
for(int i = 3; i < FD_MAX; ++i) {
|
||
if(p->files[i] == 0) {
|
||
p->files[i] = f;
|
||
return i;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
一个进程能打开的文件是有限的(我们设置为16)。一个进程如果要打开某一个文件,其文件指针数组必须有空位。如果有,就把下标做为文件的fd,并把指定文件指针存入数组之中。
|
||
|
||
|
||
获取文件对应的inode
|
||
---------------------------------------
|
||
现在我们回到文件-inode的关系上。我们怎么获取文件对应的inode呢?上文中提到了我们是去查file name对应inode的表来实现这个过程的。这个功能由目录来提供。我们看一下代码是如何实现这个过程的。
|
||
|
||
首先用户程序要打开指定文件名文件,发起系统调用sys_openat:
|
||
|
||
.. code-block:: c
|
||
#define O_RDONLY 0x000 // 只读
|
||
#define O_WRONLY 0x001 // 只写
|
||
#define O_RDWR 0x002 // 可读可写
|
||
#define O_CREATE 0x200 // 如果不存在,创建
|
||
#define O_TRUNC 0x400 // 舍弃原有内容,从头开始写
|
||
|
||
uint64 sys_openat(uint64 va, uint64 omode, uint64 _flags) {
|
||
// 还记得flags的定义吗?看看上面。
|
||
struct proc *p = curr_proc();
|
||
char path[200];
|
||
copyinstr(p->pagetable, path, va, 200);
|
||
return fileopen(path, omode);
|
||
}
|
||
|
||
int fileopen(char *path, uint64 omode) {
|
||
int fd;
|
||
struct file *f;
|
||
struct inode *ip;
|
||
if (omode & O_CREATE) {
|
||
// 新常见一个路径为 path 的文件
|
||
ip = create(path, T_FILE);
|
||
} else {
|
||
// 尝试寻找一个路径为 path 的文件
|
||
ip = namei(path);
|
||
ivalid(ip);
|
||
}
|
||
// 还记得吗?从全局文件池和进程 fd 池中找一个空闲的出来,参考 lab6
|
||
f = filealloc();
|
||
fd = fdalloc(f);
|
||
// 初始化
|
||
f->type = FD_INODE;
|
||
f->off = 0;
|
||
f->ip = ip;
|
||
f->readable = !(omode & O_WRONLY);
|
||
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
|
||
if ((omode & O_TRUNC) && ip->type == T_FILE) {
|
||
itrunc(ip);
|
||
}
|
||
return fd;
|
||
}
|
||
|
||
打开文件的方式根据flags有很多种。我们先来看最简单的,就是打开已经存在的文件的方法。fileopen在处理这类打开时调用了namei这个函数。
|
||
|
||
.. code-block:: c
|
||
|
||
// namei = 获得根目录,然后在其中遍历查找 path
|
||
struct inode *namei(char *path) {
|
||
struct inode *dp = root_dir();
|
||
return dirlookup(dp, path, 0);
|
||
}
|
||
|
||
// root_dir 位置固定
|
||
struct inode *root_dir() {
|
||
struct inode* r = iget(ROOTDEV, ROOTINO);
|
||
ivalid(r);
|
||
return r;
|
||
}
|
||
|
||
// 便利根目录所有的 dirent,找到 name 一样的 inode。
|
||
struct inode *
|
||
dirlookup(struct inode *dp, char *name, uint *poff) {
|
||
uint off, inum;
|
||
struct dirent de;
|
||
// 每次迭代处理一个 block,注意根目录可能有多个 data block
|
||
for (off = 0; off < dp->size; off += sizeof(de)) {
|
||
readi(dp, 0, (uint64) &de, off, sizeof(de));
|
||
if (strncmp(name, de.name, DIRSIZ) == 0) {
|
||
if (poff)
|
||
*poff = off;
|
||
inum = de.inum;
|
||
// 找到之后,绑定一个内存 inode 然后返回
|
||
return iget(dp->dev, inum);
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
由于我们是单目录结构。因此首先我们调用root_dir获取根目录对应的inode。之后就遍历这个inode索引的数据块中存储的文件信息到dirent结构体之中,比较名称和给定的文件名是否一致。dirlookup的逻辑对于我们本章的练习十分重要。
|
||
|
||
fileopen 还可能会导致文件 truncate,也就是截断,具体做法是舍弃全部现有内容,释放inode所有 data block 并添加到 free bitmap 里。这也是目前 nfs 中唯一的文件变短方式。
|
||
|
||
比较复杂的就是使用fileopen以创建的方式打开一个文件。fileopen函数调用了create这个函数。
|
||
|
||
.. code-block:: c
|
||
|
||
static struct inode *
|
||
create(char *path, short type) {
|
||
struct inode *ip, *dp;
|
||
if(ip = namei(path) != 0) {
|
||
// 已经存在,直接返回
|
||
return ip;
|
||
}
|
||
// 创建一个文件,首先分配一个空闲的 disk inode, 绑定内存 inode 之后返回
|
||
ip = ialloc(dp->dev, type);
|
||
// 注意 ialloc 不会执行实际读取,必须有 ivalid
|
||
ivalid(ip);
|
||
// 在根目录创建一个 dirent 指向刚才创建的 inode
|
||
dirlink(dp, path, ip->inum);
|
||
// dp 不用了,iput 就是释放内存 inode,和 iget 正好相反。
|
||
iput(dp);
|
||
return ip;
|
||
}
|
||
|
||
// nfs/fs.c
|
||
uint ialloc(ushort type) {
|
||
uint inum = freeinode++;
|
||
struct dinode din;
|
||
|
||
bzero(&din, sizeof(din));
|
||
din.type = xshort(type);
|
||
din.size = xint(0);
|
||
winode(inum, &din);
|
||
return inum;
|
||
}
|
||
|
||
// os/fs.c
|
||
// 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))
|
||
panic("dirlink");
|
||
return 0;
|
||
}
|
||
|
||
ialloc 干的事情:遍历 inode blocks 找到一个空闲的inode,初始化并返回。dirlink对于本章的练习也十分重要。和dirlookup不同,我们没有现成的dirent存储在磁盘上,而是要在磁盘上创建一个新的dirent。他遍历根目录数据块,找到一个空的 dirent,设置 dirent = {inum, filename} 然后返回,注意这一步可能找不到空位,这时需要找一个新的数据块,并扩大 root_dir size,这是由 bmap 自动完成的。需要注意本章创建硬链接时对应inode num的处理。
|
||
|
||
文件关闭
|
||
---------------------------------------
|
||
|
||
文件读写结束后需要fclose释放掉其inode,同时释放OS中对应的file结构体和fd。其实 inode 文件的关闭只需要调用 iput 就好了,iput 的实现简单到让人感觉迷惑,就是 inode 引用计数减一。诶?为什么没有计数为 0 就写回然后释放 inode 的操作?和 buf 的释放同理,这里会等 inode 池满了之后自行被替换出去,重新读磁盘实在太太太太慢了。对了,千万记得 iput 和 iget 数量相同,一定要一一对应,否则你懂的。
|
||
|
||
.. code-block:: c
|
||
|
||
void
|
||
fileclose(struct file *f)
|
||
{
|
||
if(--f->ref > 0) {
|
||
return;
|
||
}
|
||
// 暂时不支持标准输入输出文件的关闭
|
||
|
||
if(f->type == FD_INODE) {
|
||
iput(f->ip);
|
||
}
|
||
|
||
f->off = 0;
|
||
f->readable = 0;
|
||
f->writable = 0;
|
||
f->ref = 0;
|
||
f->type = FD_NONE;
|
||
}
|
||
|
||
void iput(struct inode *ip) {
|
||
ip->ref--;
|
||
}
|
||
|