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