xv6文件系统Lab9深度解析:大文件与软链接实现原理
发布时间:2026/9/30 6:39:38来源:尧图网络
这周终于把 6.S081 的 Lab 9file system交了说句实话前面几个 lab 玩的是页表、系统调用、trap虽然也有绕人的地方但基本还能靠 printk 和 gdb 硬怼。到了文件系统这里你不得不先停下来把磁盘上那一坨字节怎么变成 inode、目录项和数据块这件事彻底想明白然后才敢动手改代码。Lab 9 一共两块内容给 xv6 支持“大文件”large files把单文件上限从 268KB 撑到 64MB 左右以及实现 symbolic link也就是我们常说的软链接。这两件事在真实操作系统里都是基本功但放到一个教学内核里实现牵涉到的细节比想象中多得多尤其是 crash safety 和路径解析这两个地方踩一次坑就能记一辈子。这篇文章我尽量按自己做 lab 时的实际流程来讲包括一开始的思路梳理、关键代码怎么改、常见 bug 怎么排查以及一些网上资料里不会写但非常实用的小技巧。如果你正在做这个 lab或者单纯想搞懂文件系统内核机制应该能省下不少排查时间。1. Lab 9 在干什么先把整体设计思路理顺1.1 两个任务拆开看大文件与软链接先说 large files。xv6 原始版本里一个 inode 的磁盘结构里保存 12 个直接块地址和一个一级间接块地址。直接块就是直接指向数据块的地址间接块则是一个单独的块里面存了最多 256 个数据块地址因为块大小是 1024 字节每个地址 4 字节。所以在改之前一个文件最多能装下 12 256 268 个块也就是 268KB。这个数字在 2020 年看起来确实小得可怜但它很清楚地向你展示了“直接寻址 一级间接寻址”的模型。Lab 要求你在这个基础上再增加一个二级间接块doubly-indirect block。二级间接块是这样一个块它里面存的不是数据块地址而是“一级间接块的地址”一个一级间接块又能指向 256 个数据块。于是新上限变成 11 个直接块 1 个一级间接块 1 个二级间接块总共 11 256 256×256 65803 个块按每块 1KB 算就是大概 64.3MB。你可能注意到为什么直接块从 12 变成了 11这是为了给二级间接块腾出 inode 里的一个槽位同时又能保持 on-disk inode 结构体大小不变mkfs 就不需要做额外调整。再说 symbolic link。硬链接我们在 Lab 里已经见过了它的本质是让多个目录项指向同一个 inode大家共享同一个 nlink 计数和同一份数据。软链接不一样它本身也是一个独立的文件但这个文件里面存的是一个字符串路径指向另一个文件或目录。当你 open 一个软链接时内核需要沿着这个字符串一层一层找下去直到找到真正的文件或者报错。这也意味着软链接可以指向一个不存在的路径悬空链接也可以指向另一个软链接这就带来了循环引用的可能所以内核必须设置一个最大解析层数来防止无限循环。1.2 先搞清楚 xv6 的磁盘布局和 inode 结构动手改代码之前我强烈建议先把 xv6 文件系统的磁盘布局画一遍。整个磁盘从块 0 开始依次是boot block、superblock、log blocks、inode blocks、bitmap blocks、data blocks。这个布局在 kernel/fs.h 里写得很清楚但真正要记住的是 superblock 里每个字段的含义魔数、总块数、数据块数、inode 数、log 块数、各个区域的起始块号。后面跑大文件测试时如果磁盘满了或者 inode 不够用多半就是 mkfs 的 FSSIZE 或者块数量没给够。inode 是文件系统的核心它分为磁盘版和内存版。磁盘版叫struct dinode保存在固定区域的 inode blocks 里内存版叫struct inode是在内核启动后从磁盘加载到内存中并且带了一把 sleep lock 来保护关键字段。两者都有一串addrs[]数组来存放数据块地址。这里要注意修改文件系统代码时两个结构体都得同步改因为struct inode里会拷贝一份磁盘 inode 的字段用于操作最终通过iupdate()写回磁盘。#define NDIRECT 12 #define NINDIRECT (BSIZE / sizeof(uint)) #define MAXFILE (NDIRECT NINDIRECT) struct dinode { short type; short major; short minor; short nlink; uint size; uint addrs[NDIRECT1]; };上面这段是原始的 fs.h 定义。注意NDIRECT1表示数组里有 12 个直接块槽位和 1 个间接块槽位所以数组大小是 13。做 large files 时我的做法是把NDIRECT改成 11然后数组大小改成NDIRECT2也就是 11 个直接、1 个间接、1 个二级间接总共还是 13 个槽位。这样struct dinode的字节数不变磁盘上 inode 区域的布局就不用动mkfs 那边也省心。1.3 为什么这些改动会影响整个文件系统这个 lab 表面上只是改一个 bmap 函数但它的影响范围是整个文件读写链路。bmap 负责把一个文件的逻辑块号block number映射成物理磁盘地址所有对文件数据的访问包括 read、write、甚至 exec 加载可执行文件都会经过它。一旦你在 bmap 里支持了二级间接那么 itrunc删除文件时释放所有块也必须同步支持二级块释放否则删除大文件时就会泄漏一堆磁盘块跑几次大文件读写之后磁盘就满了。另外日志系统log的存在也要求你在修改间接块内容时调用log_write()否则写了一半崩溃数据块引用可能丢失或不一致。2. 动手之前这些核心细节必须吃透2.1 三种块地址直接、一级间接、二级间接理解 bmap 的关键是先分清楚文件逻辑块号bn的区间划分。原始版本里bn从 0 到 11 走直接块bn从 12 开始走一级间接。改成大文件之后bn的划分变成三档bn 11直接块直接查ip-addrs[bn]11 bn 11256一级间接块11256 bn 11256256×256二级间接块不过代码里最好用宏来表达而不是写死数字。改完后 fs.h 里应该长这样#define NDIRECT 11 #define NINDIRECT (BSIZE / sizeof(uint)) #define NDINDIRECT (NINDIRECT * NINDIRECT) #define MAXFILE (NDIRECT NINDIRECT NDINDIRECT)这样代码的可读性会好很多。MAXFILE在别的地方也会被引用比如一些校验逻辑所以一定要跟着改。很多人在 bmap 里写二级间接时容易搞混索引计算。二级间接块的思路是把第二层的bn减去直接块和间接块的偏移之后拆成bn / NINDIRECT和bn % NINDIRECT前者作为一级间接块数组里的下标后者作为数据块数组里的下标。这个除法取余的逻辑和二维数组的展平/反展一样先定位到第几个一级间接块再定位到该一级间接块里的第几个数据块。2.2 bmap 和 itrunc 各自要负什么责任bmap 的核心逻辑是“没有就分配分配了就写回”。读文件时如果地址不存在应该报错写文件时如果某个地址还是 0就说明这个逻辑块还没有分配需要调用balloc()从 bitmap 里分配一个空闲块然后把地址填到正确的位置。重点是如果你修改的是间接块里的内容也就是把块地址写入一个 buffer block那么修改之后必须调用log_write(bp)把这个 buffer 标记为“已修改”并且之后要brelse(bp)释放 buffer cache 引用。itrunc 则完全相反它负责把 inode 占用的所有块全部释放。修改完成后需要把ip-addrs[]清零ip-size归零然后调用iupdate()把干净的 dinode 写回磁盘。漏掉任何一层间接块释放都会造成磁盘空间永久泄漏。后面我会在调试实录里详细说一个我踩过的泄漏 bug。2.3 symlink 的语义要提前理清软链接的实现难点不在于“创建一个特殊文件”而在于 open 时的路径解析。xv6 的namei()只负责按照路径组件一层层查目录找 inode它本身不理解符号链接所以我们需要在sys_open()里手动检测如果打开得到的 inode 类型是T_SYMLINK而且调用者没有设置O_NOFOLLOW那就读出链接内容把目标路径替换掉重新namei()一次。这个逻辑有几个关键边界点。第一链接可以指向不存在的目标symlink()系统调用本身应该成功只有 open 这个链接时才可能失败。第二必须设置最大解析层数官方一般建议用 10 层左右防止a - b - a这样的循环把内核卡死。第三链接内容是一个路径字符串如果目标路径是相对路径它是相对于当前进程当前工作目录cwd解析的因为namei()内部就是拿进程的 cwd 作为起点。第四O_NOFOLLOW是一个 flag如果设置了它open 应该直接返回软链接对应的 fd而不是继续解析到底层文件。3. 实操一步步实现 Large Files3.1 修改 fs.h 和 file.h第一步先把常量定义改掉。在kernel/fs.h里把NDIRECT改成 11并新增NDINDIRECT同时修改MAXFILE。在kernel/file.h里把struct inode的addrs数组改成NDIRECT2。注意这里不是改成NDIRECT1因为我们需要 13 个槽位而NDIRECT1只能装 12 个。很多第一次做的人容易在这里翻车改完编译后访问addrs[NDIRECT1]时会踩到越界内存出现各种诡异问题。改完之后记得检查一下struct dinode和struct inode的字节数前后是否一致。xv6 中一个磁盘 inode 块能放多少个 inode是由IPB BSIZE / sizeof(struct dinode)计算出来的如果 dinode 尺寸变了mkfs 和内核的 inode 布局会不匹配甚至可能出现 inode 读取错误。我们用NDIRECT2保持 13 个槽位就是为了避免这个问题。3.2 重写 bmap 函数bmap 的原始版本只处理直接块和一级间接块我们把它扩展成三个区间。核心代码框架如下static uint bmap(struct inode *ip, uint bn) { uint addr, *a; struct buf *bp; if(bn NDIRECT){ if((addr ip-addrs[bn]) 0){ addr balloc(ip-dev); if(addr 0) return 0; ip-addrs[bn] addr; } return addr; } bn - NDIRECT; // 一级间接 if(bn NINDIRECT){ if((addr ip-addrs[NDIRECT]) 0){ addr balloc(ip-dev); if(addr 0) return 0; ip-addrs[NDIRECT] addr; } bp bread(ip-dev, addr); a (uint*)bp-data; if((addr a[bn]) 0){ addr balloc(ip-dev); if(addr){ a[bn] addr; log_write(bp); } } brelse(bp); return addr; } bn - NINDIRECT; // 二级间接 if(bn NDINDIRECT){ uint idx1 bn / NINDIRECT; uint idx2 bn % NINDIRECT; if((addr ip-addrs[NDIRECT1]) 0){ addr balloc(ip-dev); if(addr 0) return 0; ip-addrs[NDIRECT1] addr; } bp bread(ip-dev, addr); a (uint*)bp-data; if((addr a[idx1]) 0){ addr balloc(ip-dev); if(addr 0){ brelse(bp); return 0; } a[idx1] addr; log_write(bp); } brelse(bp); bp bread(ip-dev, addr); a (uint*)bp-data; if((addr a[idx2]) 0){ addr balloc(ip-dev); if(addr){ a[idx2] addr; log_write(bp); } } brelse(bp); return addr; } panic(bmap: out of range); }这里有一个非常容易漏的细节每进入下一层判断之前要对bn做一次减法。比如从直接块区间进入一级间接区间时先减去NDIRECT从一级间接进入二级间接时再减去NINDIRECT。很多人只记得算二级间接触发条件时用bn NDIRECT NINDIRECT但忘了在函数体内把bn降到相对二级间接块的偏移量结果下标算错直接写坏块。其次在balloc()返回 0 时说明磁盘满了要返回 0 让上层报错。但注意如果balloc已经成功分配了一个一级间接表块而随后在分配二级间接表里的条目时失败不要忘记brelse(bp)否则 buffer cache 会被占满。代码里我在那个分支前加了brelse(bp)再返回 0就是这个目的。3.3 修改 itrunc 释放所有块删除文件时所有被 inode 引用的块都必须释放。一级间接需要先遍历 256 个地址释放其中的数据块再释放一级间接表块本身二级间接则需要套两层遍历先遍历二级表里的 256 个一级表地址对每个一级表再遍历 256 个数据地址最后释放一级表和二级表。完整实现大概是这样void itrunc(struct inode *ip) { int i, j; struct buf *bp, *bp2; uint *a, *b; 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; } if(ip-addrs[NDIRECT1]){ bp bread(ip-dev, ip-addrs[NDIRECT1]); a (uint*)bp-data; for(i 0; i NINDIRECT; i){ if(a[i]){ bp2 bread(ip-dev, a[i]); b (uint*)bp2-data; for(j 0; j NINDIRECT; j){ if(b[j]) bfree(ip-dev, b[j]); } brelse(bp2); bfree(ip-dev, a[i]); } } brelse(bp); bfree(ip-dev, ip-addrs[NDIRECT1]); ip-addrs[NDIRECT1] 0; } ip-size 0; iupdate(ip); }注意释放顺序不能乱先释放内层数据块再释放一层表块最后释放外层表块。如果把外层表块先释放了内层表块的物理地址就彻底丢了磁盘块就永久泄漏了。还有一点容易忘记bfree之后要把对应的数组槽位置 0否则 inode 写回磁盘时会带上一个失效的块号下次访问时可能读到错误数据。3.4 让 mkfs 留出足够的空间xv6 的 mkfs 工具在创建文件系统镜像时会遍历 Makefile 里指定的 UPROGS 和普通文件把它们写入镜像。默认的 FSSIZE 在 mkfs/mkfs.c 里定义我记得是 200000 块也就是接近 200MB。bigfile 测试最多写 65803 个块所以空间本身够用。但有个细节如果你的 xv6 是从旧版本仓库拉下来的或者修改了某些宏导致 inode 尺寸变大导致 inode block 数量不够mkfs 可能会报错或者生成的镜像里 inode 数量太少。所以如果你改完代码后 mkfs 报错先检查struct dinode的 size 是不是变了尤其是addrs数组是不是弄成NDIRECT1导致少了一个槽位。3.5 跑通 bigfile 测试xv6 仓库里通常会给你一个user/bigfile.c它会创建一个文件写入超过旧上限的数据然后检查读取是否正确。Makefile 里已经包含了 bigfile user 程序。直接make qemu后运行 bigfile如果得到类似wrote 65803 blocks或者读取校验通过就说明 large files 实现成功。如果没有 bigfile也可以自己写核心逻辑就是循环写入块直到接近 MAXFILE。我实测时遇到的一个坑是跑 bigfile 时文件系统空间不够write 返回 -1。后来发现是 itrunc 泄漏导致磁盘被一堆孤儿块占满。解决 itrunc 的泄漏后bigfile 就能一次性写完。这其实是个不错的自测方法bigfile 不仅是验收工具还能当作块泄漏检测器。4. 实操实现 Symbolic Links4.1 添加新的文件类型和系统调用首先在kernel/stat.h里定义T_SYMLINK。xv6 现有的文件类型是T_DIR、T_FILE、T_DEVICE我们再加一个比如#define T_SYMLINK 4。接着在kernel/syscall.h、kernel/syscall.c、user/user.h里把symlink系统调用加上。如果做 readlink也要一并注册。系统调用的编号不要搞错syscall.h里的编号要和syscall.c里的syscalls数组顺序一致否则调用会直接跳到别的 handler跑起来像神经病一样。4.2 实现 sys_symlink在kernel/sysfile.c里添加sys_symlink。语义是创建一个 type 为T_SYMLINK的文件路径是第二个参数 path文件内容数据块是第一个参数 target 字符串。创建方式可以直接复用现有的create()函数因为它本身就能创建指定类型的 inode。创建之后用writei()把 target 字符串写进 inode 数据区。uint64 sys_symlink(void) { char target[MAXPATH], path[MAXPATH]; struct inode *ip; if(argstr(0, target, MAXPATH) 0) return -1; if(argstr(1, path, MAXPATH) 0) return -1; begin_op(); if((ip create(path, T_SYMLINK, 0, 0)) 0){ end_op(); return -1; } int n strlen(target); if(writei(ip, 0, (uint64)target, 0, n) ! n){ iunlockput(ip); end_op(); return -1; } iunlockput(ip); end_op(); return 0; }这里有个细节create()返回的 inode 是带锁的而writei()也要求 inode 是锁定的所以顺序上没问题。写完 target 之后不要忘记iunlockput()。另外writei()写入的是 target 字符串的字节不包含结尾的\0。也就是说链接内容的长度就是strlen(target)后面读取链接内容时我们需要额外补一个\0才能当字符串用。4.3 修改 sys_open核心解析逻辑sys_open()原本的逻辑是普通打开文件。我们需要在拿到 inode、锁定 inode 之后插入一段“解析符号链接”的代码。判断条件是ip-type T_SYMLINK且omode O_NOFOLLOW为假。然后进入一个循环最多解析 10 层。if(ip-type T_SYMLINK !(omode O_NOFOLLOW)){ int depth 0; char target[MAXPATH]; while(ip-type T_SYMLINK){ if(depth MAXSYMLINKS){ iunlockput(ip); end_op(); return -1; } if(ip-size MAXPATH) panic(symlink: target too long); if(readi(ip, 0, (uint64)target, 0, ip-size) ! ip-size){ iunlockput(ip); end_op(); return -1; } target[ip-size] 0; iunlockput(ip); if((ip namei(target)) 0){ end_op(); return -1; } ilock(ip); } }这段循环有几个需要注意的地方。第一每次解析完一个链接要用iunlockput()释放当前 inode 的锁和引用然后再namei()下一个路径否则会带着锁去查路径可能死锁。第二namei()返回的 inode 是未加锁的所以拿到之后必须ilock(ip)再继续判断类型。第三如果目标路径不存在namei()返回 0直接返回 -1。第四target数组需要足够大MAXPATH是 512一般路径够用如果链接目标长度接近 MAXPATH记得做保护。这段代码插入的位置很有讲究必须在 open 后续的T_DEVICE或T_DIR检查之前因为如果不先解析软链接后面的类型判断会拿到一个T_SYMLINK类型导致行为错误。我踩过的坑就是一开始把解析逻辑放得太靠后结果对软链接调用 open 时老是进到T_DEVICE分支直接报 major 越界查了半天才发现是顺序问题。4.4 处理 O_NOFOLLOW 与 readlinkO_NOFOLLOW的实现其实很简单就是在判断条件里加一个否定!(omode O_NOFOLLOW)。设置了它就跳过解析循环直接把软链接 inode 返回给用户。这样用户就能通过 read 系统调用读到软链接文件本身的内容也就是目标路径字符串。readlink 系统调用更直接它接收路径和用户缓冲区先namei()找到 inode如果是T_SYMLINK就调用readi()读取链接内容然后通过copyout()写给用户。注意拷贝长度不能超过用户缓冲区长度同时要返回实际拷贝的字节数。xv6 的 user 库函数默认会以-1表示失败所以 readlink 返回真实字节数是合规的。有些实现还会在末尾补\0但 readlink 的标准语义是不含\0所以这里以字节数为准。最后别忘了在 Makefile 的 UPROGS 里把 symlinktest 加进去否则你写好了内核代码shell 里却找不到symlinktest命令那就有得你折腾了。4.5 跑 symlinktest 测试xv6 官方仓库一般自带user/symlinktest.c它会测这些场景创建软链接并读取链接到不存在的文件时open 失败但 symlink 本身成功链接到软链接的软链接循环引用readlink 读取内容。测试代码如果通过了会打印类似OK的输出。我在跑测试时卡得最久的是循环引用那一项。当时我没设置最大层数用的是while循环直接解析结果在a - b、b - a这种场景下死循环系统直接卡死。加了MAXSYMLINKS 10的计数器之后测试很快就过了。这个限制不是 lab 特有的Linux 内核也有类似的MAXSYMLINKS约定比如 40 层算是硬上限超过就报ELOOP。xv6 教学环境里设 10 就够了。5. 调试实录与避坑指南5.1 常见错误一bmap 越界与死循环如果你的 bigfile 测试在写入某个临界块号时挂掉大概率是 bmap 里的区间判断和bn偏移没处理好。我自己的第一次实现里二级间接分支的条件写成了if(bn NINDIRECT)忘了先减掉一级间接区间的偏移导致本来属于一级间接的块号被送进二级间接逻辑创建了一堆多余的一级间接表而且索引完全错位最后想读回数据时对不上直接 panic。排查方法很简单在 bmap 入口加一两条打印输出bn和ip-addrs[NDIRECT]等关键值再对比你手工估算的期望值。比如写入第 300 个逻辑块时它应该走二级间接idx1应该是(300-11-256)/256也就是约 0idx2则是 33。对照打印结果一眼就能看出问题。5.2 常见错误二itrunc 泄漏磁盘块这个错误最隐蔽因为它不一定当场报错。你写一个文件删除它再写再删反复几次之后文件系统空间越来越少最终 write 返回 -1。这就是典型的 itrunc 漏了某层块释放。我第一次实现时只写了直接块和一级间接的释放忘了处理二级间接结果删除一个超过 267 块的大文件后二级间接指向的几千个数据块全丢了。后来用printk在 itrunc 里打印释放块数和写入时的块数对比才发现少了。另一个常见错误是在bfree之后没有把ip-addrs对应槽位置 0导致iupdate()写回磁盘的 inode 仍然带着旧地址文件删除后过一段时间重新访问这个 inode 时系统又去读那些已经被标记为空闲的块数据混乱不堪。避免泄漏的终极办法是写完 itrunc 后跑一遍 bigfile然后 reboot再跑一遍 dfxv6 没有 df但是可以看 mkfs 打印的空闲块数如果空闲块数明显少了说明还有泄漏。或者更粗暴一点反复创建和删除大文件直到空间耗尽然后用tune2fs那种方式去数 bitmap 里的空闲位太麻烦不值得打印块数是性价比最高的方法。5.3 常见错误三symlink 反复解析导致死锁在实现 symlink 时我遇到过两个死锁。第一次是因为在解析循环里没有对当前 inode 做iunlockput就直接namei()于是带着一把锁去查路径而namei()内部可能又要读同一个目录 inode锁就冲突了。第二次是解析到目标后没有重新ilock(ip)就判断类型结果读到一个未锁定的 inode 的 type 字段有时读到 0有时读到垃圾值然后代码走错了分支完全没办法定位。gdb 单步跟进去才发现是锁的问题。所以记住这条铁律在路径解析过程中每解析完一层当前的 inode 锁必须释放下一个 inode 的锁必须重新获取。xv6 里锁是 sleep lock不释放就等待最容易造成“都在等对方”的场面。5.4 用 gdb 和打印调试 xv6 的小技巧xv6 的调试环境其实很舒服make qemu-gdb会启动带 gdb stub 的 QEMU然后用 riscv64-linux-gnu-gdb 连接。如果你和我一样第一次搞 riscv 架构的 gdb记得先layout src打开源码窗口然后在bmap、sys_open、sys_symlink这些函数上打断点。函数名是符号可以直接b bmap不需要地址。不过 gdb 在调试 panic 的时候容易让人懵因为 xv6 的 panic 是会直接停机并打印栈的。我更常用的调试手段是printk。xv6 的printk支持%d、%x、%s在关键函数里加打印然后用make qemu跑测试看输出定位。打印最好不要太多否则会被大量 I/O 刷屏我一般只在条件分支里打一次比如if (bn NINDIRECT) cprintf(bmap: bn%d idx1%d idx2%d\n, ...)。测试跑完把打印删掉或注释掉不然最后提交代码时会被线上 grader 判成多余输出可能导致意外失败。5.5 关于性能和扩展性的感悟这个 lab 做完后再看真实文件系统会豁然开朗。ext4 的多级间接块、NTFS 的 MFT、ZFS 的间接块树本质上都是同一套“把逻辑块号逐级映射到物理块号”的模型只不过换成了 B 树之类的结构来降低查找复杂度并且加上了更大的块号位数。xv6 的 64MB 上限在 2020 年看着很小但你要知道它用一块 1024 字节的表就完成了 4 万倍的文件大小扩展这个收益全靠“表项里放地址地址再指向下一张表”的递归思想。理解了这一层后面学任何文件系统都轻松得多。6. 最后的一些个人体会这个 lab 应该是 6.S081 里“牵一发动全身”感觉最明显的一个。改一个常量、加一个分支影响的却是整个磁盘布局、日志系统、buffer cache 和系统调用。我做完之后最深的感受是文件系统代码不像前面的 lab 那样可以靠“碰运气”调试它必须建立在精确的静态结构认知上脑子里得时刻有一张“哪块是什么、谁指向谁”的图。建议你在动手前先花半小时把fs.h、file.h、bio.c、log.c这几个文件通读一遍重点搞懂 buffer 层的bread/brelse、日志层的log_write以及 inode 的ilock/iunlockput这一整套锁和缓存规则。这些机制理解到位了后面写代码其实是水到渠成的事。
网站建设高端定制企业官网