发现自己之前写教程时忘记放自己代码仓库地址了若是有需要的友友可以直接看我仓库代码1664178416/xv6-lab-2020: MIT 6.S081实验代码若有帮助也请不要吝啬你手里的star这对我非常重要感谢感谢首先切换分支到fsgit checkout fs make clean预备知识mkfs程序创建xv6文件系统磁盘映像并确定文件系统的总块数这个大小在kernel/param.h中的FSSIZE写明// kernel/params.h #define FSSIZE 200000 // size of file system in blocksMakeFile文件系统和内核文件构建流程注释# To compile and run with a lab solution, set the lab name in lab.mk # (e.g., LButil). Run make grade to test solution with the labs # grade script (e.g., grade-lab-util). ​ -include conf/lab.mk ​ Kkernel Uuser ​ # 构建内核代码的文件组成列表 ​ OBJS $K/entry.o $K/start.o $K/console.o $K/printf.o $K/uart.o $K/kalloc.o $K/spinlock.o $K/string.o $K/main.o $K/vm.o $K/proc.o $K/swtch.o $K/trampoline.o $K/trap.o $K/syscall.o $K/sysproc.o $K/bio.o $K/fs.o $K/log.o $K/sleeplock.o $K/file.o $K/pipe.o $K/exec.o $K/sysfile.o $K/kernelvec.o $K/plic.o $K/virtio_disk.o ​ ifeq ($(LAB),pgtbl) OBJS $K/vmcopyin.o endif ​ ifeq ($(LAB),$(filter $(LAB), pgtbl lock)) OBJS $K/stats.o $K/sprintf.o endif ​ ​ ifeq ($(LAB),net) OBJS $K/e1000.o $K/net.o $K/sysnet.o $K/pci.o endif ​ ​ # riscv64-unknown-elf- or riscv64-linux-gnu- # perhaps in /opt/riscv/bin #TOOLPREFIX ​ # Try to infer the correct TOOLPREFIX if not set # 确定适用于 RISC-V 架构的交叉编译器的前缀 ifndef TOOLPREFIX TOOLPREFIX : $(shell if riscv64-unknown-elf-objdump -i 21 | grep elf64-big /dev/null 21; then echo riscv64-unknown-elf-; elif riscv64-linux-gnu-objdump -i 21 | grep elf64-big /dev/null 21; then echo riscv64-linux-gnu-; elif riscv64-unknown-linux-gnu-objdump -i 21 | grep elf64-big /dev/null 21; then echo riscv64-unknown-linux-gnu-; else echo *** 12; echo *** Error: Couldnt find a riscv64 version of GCC/binutils. 12; echo *** To turn off this error, run gmake TOOLPREFIX .... 12; echo *** 12; exit 1; fi) endif ​ # qemu模拟器可执行文件名称 QEMU qemu-system-riscv64 ​ # c编译器命令 CC $(TOOLPREFIX)gcc # 汇编器命令 AS $(TOOLPREFIX)gas # 链接器命令 LD $(TOOLPREFIX)ld # 目标文件复制工具命令 OBJCOPY $(TOOLPREFIX)objcopy # 目标文件反汇编工具命令 OBJDUMP $(TOOLPREFIX)objdump ​ # 定义c编译器选项: -Wall - 开启所有警告信息 , -Werror - 将所有警告视为错误 # -O - 启用基本的优化选项 , -fno-omit-frame-pointer - 禁用省略帧指针优化 # --ggdb - 生成适用于GDB调试器的调试信息 CFLAGS -Wall -Werror -O -fno-omit-frame-pointer -ggdb ​ # -D选项用于在预处理阶段定义宏并设置其值 ifdef LAB LABUPPER $(shell echo $(LAB) | tr a-z A-Z) XCFLAGS -DSOL_$(LABUPPER) -DLAB_$(LABUPPER) endif ​ # 继续追加定义C编译器相关选项 # 包含了在特定实验或解决方案中定义的宏 -- 如上面的LAB实验中定义的宏 CFLAGS $(XCFLAGS) # 在编译源文件时自动生成依赖关系文件。 # 这些依赖关系文件记录了每个源文件所依赖的头文件以便在后续编译中自动处理文件间的依赖关系。 CFLAGS -MD # 设置代码模型code model为 medany其中 medany 表示 medium code model, any address -- 地址无关的代码 # 这意味着编译器可以生成代码适用于位于任何地址空间中的程序但是有一些限制。这通常用于 64 位 RISC-V 架构 CFLAGS -mcmodelmedany # -ffreestanding: 生成独立运行的代码即代码不依赖于标准库或操作系统提供的额外支持。通常用于裸机嵌入式系统或操作系统内核的开发 # -fno-common: 禁止编译器将未初始化的全局变量和函数定义放置在公共common段中。这是为了避免因为全局变量在多个源文件中重复定义而导致链接错误。 # -nostdlib: 不链接标准 C 库因此代码必须自己实现所有必需的运行时函数 # -mno-relax: 不要使用指令重定位优化。在链接阶段可能会进行指令重定位但该选项可以避免这种情况确保代码的准确性 CFLAGS -ffreestanding -fno-common -nostdlib -mno-relax # 在当前目录中查找头文件以便能够正确包含本地头文件 CFLAGS -I. # 检查编译器是否支持 -fno-stack-protector 选项。如果支持则将其加入 CFLAGS 中。 # 这个选项用于禁用栈保护即禁用编译器对栈溢出的保护措施。这在一些特定的裸机或操作系统内核开发场景中可能是必需的 CFLAGS $(shell $(CC) -fno-stack-protector -E -x c /dev/null /dev/null 21 echo -fno-stack-protector) ​ # -D选项用于在预处理阶段定义宏并设置其值 ifeq ($(LAB),net) CFLAGS -DNET_TESTS_PORT$(SERVERPORT) endif ​ # 检查编译器是否支持PIEPosition Independent Executable选项并根据检查结果添加对应的编译选项 # Disable PIE when possible (for Ubuntu 16.10 toolchain) # PIE是一种在内存中加载程序时地址空间随机化的安全特性它可以增加程序的安全性防止某些类型的攻击。 # 如果编译器支持PIE选项那么程序在编译和链接时会启用PIE特性从而生成一个位置无关的可执行文件。 ifneq ($(shell $(CC) -dumpspecs 2/dev/null | grep -e [^f]no-pie),) # 如果编译器支持-fno-pie选项就将-fno-pie和-no-pie添加到CFLAGS中。 # -fno-pie选项告诉编译器不要生成位置无关的可执行文件而-no-pie选项告诉链接器不要生成位置无关的可执行文件 CFLAGS -fno-pie -no-pie endif ifneq ($(shell $(CC) -dumpspecs 2/dev/null | grep -e [^f]nopie),) # 如果编译器支持-fno-pie选项就将-fno-pie和-nopie添加到CFLAGS中。-nopie选项告诉链接器不要生成位置无关的可执行文件 CFLAGS -fno-pie -nopie endif ​ # 在链接时它告诉链接器将生成的程序的最大页大小设置为4096字节4KB LDFLAGS -z max-page-size4096 ​ # 指定了生成 kernel 可执行文件的依赖关系。 # 它依赖于 OBJS 中列出的一系列目标文件如entry.o, start.o, console.o, 等等、kernel.ld 文件和 U/initcode 文件 $K/kernel: $(OBJS) $K/kernel.ld $U/initcode # 这是生成 kernel 可执行文件的命令。$(LD) 是链接器的路径和名称$(LDFLAGS) 是链接器选项 # -T $K/kernel.ld 指定链接器使用 kernel.ld 文件作为链接脚本,-o $K/kernel 指定输出文件名为 kernel$(OBJS) 是链接的目标文件列表。 $(LD) $(LDFLAGS) -T $K/kernel.ld -o $K/kernel $(OBJS) # 将 kernel 可执行文件的反汇编内容输出到 kernel.asm 文件中 $(OBJDUMP) -S $K/kernel $K/kernel.asm # 它从 kernel 可执行文件中提取符号表信息并将其输出到 kernel.sym 文件中 $(OBJDUMP) -t $K/kernel | sed 1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d $K/kernel.sym ​ # 指定了生成 initcode 目标文件的依赖关系。它依赖于 U/initcode.S 汇编代码文件 $U/initcode: $U/initcode.S # 这是生成 initcode.o 目标文件的命令。 # $(CC) 是 C 编译器的路径和名称$(CFLAGS) 是编译器选项-marchrv64g 指定编译为 RV64G 架构RISC-V 64-bit带乘法/除法指令集 # -nostdinc 禁止包含标准库头文件-I. -Ikernel 指定头文件搜索路径 # -c $U/initcode.S -o $U/initcode.o 指定编译 U/initcode.S 并输出为 U/initcode.o 目标文件 $(CC) $(CFLAGS) -marchrv64g -nostdinc -I. -Ikernel -c $U/initcode.S -o $U/initcode.o # 这是生成 initcode.out 文件的命令-N 表示生成无符号的可执行文件-e start 指定程序的入口地址为 start-Ttext 0 指定代码段的起始地址为0 # -o $U/initcode.out 指定输出文件名为 initcode.out。 $(LD) $(LDFLAGS) -N -e start -Ttext 0 -o $U/initcode.out $U/initcode.o # 将 initcode.out 文件中的内容复制到 initcode 文件中并且去除了目标文件中的一些附加信息 # 使得 initcode 文件只包含可执行的初始化代码而不包含目标文件的元数据和调试信息 $(OBJCOPY) -S -O binary $U/initcode.out $U/initcode # 将 initcode.o 目标文件的反汇编内容输出到 initcode.asm 文件中 $(OBJDUMP) -S $U/initcode.o $U/initcode.asm ​ ​ # tags 目标是用来生成一个文本文件其中包含了代码中定义的所有函数、变量、结构体等的标签信息。这个文件通常被用于代码编辑器进行代码导航和跳转。 tags: $(OBJS) _init etags *.S *.c ​ # ULIB变量用来存储用户程序的依赖目标文件 ULIB $U/ulib.o $U/usys.o $U/printf.o $U/umalloc.o ​ ifeq ($(LAB),$(filter $(LAB), pgtbl lock)) ULIB $U/statistics.o endif ​ # 匹配: 目标文件是以 _ 开头的并且依赖于同名的 .o 文件和 ULIB 变量中的目标文件 _%: %.o $(ULIB) # 将目标文件链接成一个没有可执行代码的目标文件并指定程序入口地址为 main并将输出文件的名称设置为当前规则的目标文件 $(LD) $(LDFLAGS) -N -e main -Ttext 0 -o $ $^ # 将生成的可执行文件进行反汇编并将反汇编的结果保存到同名的 .asm 文件中 $(OBJDUMP) -S $ $*.asm # 将生成的可执行文件进行符号表提取并将符号表保存到同名的 .sym 文件中。sed 命令用于过滤掉符号表中的不需要的信息 $(OBJDUMP) -t $ | sed 1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d $*.sym ​ # 根据 usys.pl 脚本生成 usys.S 汇编文件 $U/usys.S : $U/usys.pl perl $U/usys.pl $U/usys.S ​ # 将 usys.S 汇编文件编译成目标文件 usys.o $U/usys.o : $U/usys.S $(CC) $(CFLAGS) -c -o $U/usys.o $U/usys.S ​ # 将 forktest.o,ulib.o,usys.o 目标文件链接成一个可执行文件 _forktest # 生成 _forktest 可执行文件的反汇编代码并将结果输出到 forktest.asm 文件 $U/_forktest: $U/forktest.o $(ULIB) # forktest has less library code linked in - needs to be small # in order to be able to max out the proc table. $(LD) $(LDFLAGS) -N -e main -Ttext 0 -o $U/_forktest $U/forktest.o $U/ulib.o $U/usys.o $(OBJDUMP) -S $U/_forktest $U/forktest.asm ​ # 将mkfs/mkfs.c文件编译成mkfs可执行文件 mkfs/mkfs: mkfs/mkfs.c $K/fs.h $K/param.h gcc $(XCFLAGS) -Werror -Wall -I. -o mkfs/mkfs mkfs/mkfs.c ​ # Prevent deletion of intermediate files, e.g. cat.o, after first build, so # that disk image changes after first build are persistent until clean. More # details: # http://www.gnu.org/software/make/manual/html_node/Chained-Rules.html .PRECIOUS: %.o ​ # 构建用户程序lib库的文件组成列表 UPROGS $U/_cat $U/_echo $U/_forktest $U/_grep $U/_init $U/_kill $U/_ln $U/_ls $U/_mkdir $U/_rm $U/_sh $U/_stressfs $U/_usertests $U/_grind $U/_wc $U/_zombie ​ ​ ifeq ($(LAB),$(filter $(LAB), pgtbl lock)) UPROGS $U/_stats endif ​ ifeq ($(LAB),traps) UPROGS $U/_call $U/_bttest endif ​ ifeq ($(LAB),lazy) UPROGS $U/_lazytests endif ​ ifeq ($(LAB),cow) UPROGS $U/_cowtest endif ​ ifeq ($(LAB),thread) UPROGS $U/_uthread ​ ​ $U/uthread_switch.o : $U/uthread_switch.S $(CC) $(CFLAGS) -c -o $U/uthread_switch.o $U/uthread_switch.S ​ $U/_uthread: $U/uthread.o $U/uthread_switch.o $(ULIB) $(LD) $(LDFLAGS) -N -e main -Ttext 0 -o $U/_uthread $U/uthread.o $U/uthread_switch.o $(ULIB) $(OBJDUMP) -S $U/_uthread $U/uthread.asm ​ ph: notxv6/ph.c gcc -o ph -g -O2 notxv6/ph.c -pthread ​ barrier: notxv6/barrier.c gcc -o barrier -g -O2 notxv6/barrier.c -pthread endif ​ ifeq ($(LAB),lock) UPROGS $U/_kalloctest $U/_bcachetest endif ​ ifeq ($(LAB),fs) UPROGS $U/_bigfile endif ​ ​ ifeq ($(LAB),net) UPROGS $U/_nettests endif ​ UEXTRA ifeq ($(LAB),util) UEXTRA user/xargstest.sh endif ​ # 构建fs.img文件系统镜像文件 fs.img: mkfs/mkfs README $(UEXTRA) $(UPROGS) # 传递个给mkfs可执行程序的参数,由mkfs可执行程序完成文件系统镜像构建功能 mkfs/mkfs fs.img README $(UEXTRA) $(UPROGS) ​ -include kernel/*.d user/*.d ​ # 清理掉所有编译生成的文件 clean: rm -f *.tex *.dvi *.idx *.aux *.log *.ind *.ilg */*.o */*.d */*.asm */*.sym $U/initcode $U/initcode.out $K/kernel fs.img mkfs/mkfs .gdbinit $U/usys.S $(UPROGS) ​ # try to generate a unique GDB port GDBPORT $(shell expr id -u % 5000 25000) # QEMUs gdb stub command line changed in 0.11 QEMUGDB $(shell if $(QEMU) -help | grep -q ^-gdb; then echo -gdb tcp::$(GDBPORT); else echo -s -p $(GDBPORT); fi) ​ # 设置QEMU模拟器启动时CPU数量 ifndef CPUS CPUS : 3 endif ifeq ($(LAB),fs) CPUS : 1 endif ​ FWDPORT $(shell expr id -u % 5000 25999) ​ # -machine virt: 这个选项指定虚拟机使用virtio模式运行用于支持virtio设备的虚拟化。 # -bios none: 这个选项指定不使用BIOS固件即不加载任何BIOS。 # -kernel $K/kernel: 这个选项指定虚拟机启动时使用的内核文件的路径和名称。其中$K是一个变量代表内核文件所在的目录kernel是内核文件的名称。 # -m 128M: 这个选项指定虚拟机的内存大小为128MB。 # -smp $(CPUS): 这个选项指定虚拟机的CPU核心数$(CPUS)是一个变量表示指定的CPU核心数。 # -nographic: 这个选项指定虚拟机以非图形化模式运行即在命令行终端中显示输出而不是使用图形界面。 QEMUOPTS -machine virt -bios none -kernel $K/kernel -m 128M -smp $(CPUS) -nographic # -drive filefs.img,ifnone,formatraw,idx0: 这个选项指定虚拟机使用一个磁盘镜像文件作为虚拟硬盘。 # filefs.img表示虚拟硬盘的文件路径和名称为fs.img # ifnone表示磁盘接口类型为none即不使用默认接口 # formatraw表示磁盘镜像文件的格式为raw原始格式 # idx0表示为这个虚拟硬盘指定了一个唯一的标识符为x0。 QEMUOPTS -drive filefs.img,ifnone,formatraw,idx0 # -device virtio-blk-device,drivex0,busvirtio-mmio-bus.0: 这个选项指定将一个virtio块设备连接到虚拟机上。 # virtio-blk-device表示设备类型为virtio块设备 # drivex0表示将之前定义的虚拟硬盘x0连接到该设备上 # busvirtio-mmio-bus.0表示设备连接到virtio总线的第一个MMIO总线上。 QEMUOPTS -device virtio-blk-device,drivex0,busvirtio-mmio-bus.0 ​ ifeq ($(LAB),net) QEMUOPTS -netdev user,idnet0,hostfwdudp::$(FWDPORT)-:2000 -object filter-dump,idnet0,netdevnet0,filepackets.pcap QEMUOPTS -device e1000,netdevnet0,buspcie.0 endif ​ # qemu目标依赖于内核文件和文件系统镜像构建完成 qemu: $K/kernel fs.img # 启动qemu,参数就是QEMUOPTS定义的 $(QEMU) $(QEMUOPTS) ​ .gdbinit: .gdbinit.tmpl-riscv sed s/:1234/:$(GDBPORT)/ $^ $ ​ qemu-gdb: $K/kernel .gdbinit fs.img echo *** Now run gdb in another window. 12 $(QEMU) $(QEMUOPTS) -S $(QEMUGDB) ...mkfs/mkfs.c中程序源码注释工具类方法// fsfd是fs.img文件系统镜像文件的文件描述符 // 将buf内容写入文件系统第sec个block中 void wsect(uint sec, void *buf) { if(lseek(fsfd, sec * BSIZE, 0) ! sec * BSIZE){ perror(lseek); exit(1); } if(write(fsfd, buf, BSIZE) ! BSIZE){ perror(write); exit(1); } } ​ // 读取文件系统第sec个块到buf中 void rsect(uint sec, void *buf) { if(lseek(fsfd, sec * BSIZE, 0) ! sec * BSIZE){ perror(lseek); exit(1); } if(read(fsfd, buf, BSIZE) ! BSIZE){ perror(read); exit(1); } } ​ // 更新磁盘上对应inode的信息 void winode(uint inum, struct dinode *ip) { char buf[BSIZE]; uint bn; struct dinode *dip; // 获取当前inode所在的inode block number bn IBLOCK(inum, sb); // 将该inode block读取到内存中来 rsect(bn, buf); // 通过偏移得到buf中inode的地址 dip ((struct dinode*)buf) (inum % IPB); // 将内存中Inode的值赋值为传入的ip *dip *ip; // 重新将这块Block写入磁盘 wsect(bn, buf); } ​ // 从磁盘上读取出对应inode的信息,然后赋值给ip void rinode(uint inum, struct dinode *ip) { char buf[BSIZE]; uint bn; struct dinode *dip; ​ bn IBLOCK(inum, sb); rsect(bn, buf); dip ((struct dinode*)buf) (inum % IPB); *ip *dip; } ​ // 分配下一个空闲的inode uint ialloc(ushort type) { uint inum freeinode; struct dinode din; // 将dinode清零 bzero(din, sizeof(din)); // 清零后,重新赋值 // inode类型,链接数和大小 din.type xshort(type); din.nlink xshort(1); din.size xint(0); // 将该inode写入磁盘 winode(inum, din); return inum; } ​ // 分配bitmap block,used表示已经使用的block数量 void balloc(int used) { uchar buf[BSIZE]; int i; printf(balloc: first %d blocks have been allocatedn, used); assert(used BSIZE*8); bzero(buf, BSIZE); // 将已经使用的block,在bitmap block中对应为设置为1 for(i 0; i used; i){ buf[i/8] buf[i/8] | (0x1 (i%8)); } printf(balloc: write bitmap block at sector %dn, sb.bmapstart); // 更新bitmap block到磁盘 wsect(sb.bmapstart, buf); }iappend方法向某个inode代表的文件中追加数据// xp是数据缓冲区,n是追加数据大小 void iappend(uint inum, void *xp, int n) { char *p (char*)xp; uint fbn, off, n1; struct dinode din; char buf[BSIZE]; uint indirect[NINDIRECT]; uint x; // 从磁盘读取出inum对应的inode信息到din中 rinode(inum, din); // 每个inode代表一个文件,size表示文件的已有的数据量大小 off xint(din.size); // printf(append inum %d at off %d sz %dn, inum, off, n); // 如果写入数据量比较大那么会分批次多次写入 while(n 0){ // 计算写入地址在当前inode的blocks数组中对应的索引 fbn off / BSIZE; assert(fbn MAXFILE); // 1.定位数据需要写入到哪个block中 // 默认情况下,每个Inode都有12个直接块和1个间接块 // 如果处于直接块范畴 if(fbn NDIRECT){ // 如果当前直接块条目的块号还没确定,那么赋值为当前空闲可用块的block number if(xint(din.addrs[fbn]) 0){ din.addrs[fbn] xint(freeblock); } // 获取这个直接块条目记录的block number x xint(din.addrs[fbn]); } else { // 如果属于间接块,并且该间接块条目的块号没有确定,那么赋值为当前空闲可用块的block number if(xint(din.addrs[NDIRECT]) 0){ din.addrs[NDIRECT] xint(freeblock); } // 从磁盘读取出这个间接块 --- 间接块中记录的都是block number rsect(xint(din.addrs[NDIRECT]), (char*)indirect); // 判断对应间接块中的条目是否为0,如果为0赋值为新的空闲块号 if(indirect[fbn - NDIRECT] 0){ indirect[fbn - NDIRECT] xint(freeblock); // 该间接块内容被修改了,需要重新写入磁盘 wsect(xint(din.addrs[NDIRECT]), (char*)indirect); } // 获得对应的间接块条目号记录的block number x xint(indirect[fbn-NDIRECT]); } // 2. 现在x记录着数据需要写入的block number,下一步将数据写入对应的Block中 // 在目标写入块剩余大小和当前要写入数据大小之间取较小者 n1 min(n, (fbn 1) * BSIZE - off); // 读取对应目标块,然后在指定位置写入对应的数据,最后将目标块重新写入磁盘 rsect(x, buf); bcopy(p, buf off - (fbn * BSIZE), n1); wsect(x, buf); // 剩余写入数据量减少 -- 一次写不完,会分多次写入 n - n1; // 当前inode写入数据偏移量增加 off n1; // 源数据缓冲区写入指针前推 p n1; } // 更新当前Inode的写入偏移量,然后写入磁盘 din.size xint(off); winode(inum, din); }核心main方法int main(int argc, char *argv[]) { int i, cc, fd; uint rootino, inum, off; struct dirent de; char buf[BSIZE]; struct dinode din; ​ ​ static_assert(sizeof(int) 4, Integers must be 4 bytes!); ​ if(argc 2){ fprintf(stderr, Usage: mkfs fs.img files...n); exit(1); } ​ assert((BSIZE % sizeof(struct dinode)) 0); assert((BSIZE % sizeof(struct dirent)) 0); // argv[1] 是 fs.img 文件系统镜像文件 fsfd open(argv[1], O_RDWR|O_CREAT|O_TRUNC, 0666); if(fsfd 0){ perror(argv[1]); exit(1); } ​ // 1 fs block 1 disk sector // 元数据块数量 boot block sb block log blocks inode blocks bit map blocks data blocks nmeta 2 nlog ninodeblocks nbitmap; // 计算数据块数量 nblocks FSSIZE - nmeta; // 填充super block块内容 sb.magic FSMAGIC; // 魔数 sb.size xint(FSSIZE); // 总block数量 sb.nblocks xint(nblocks); // 数据块数量 sb.ninodes xint(NINODES); // inode块数量 sb.nlog xint(nlog); // 日志块数量 sb.logstart xint(2); // 第一个日志块块号 sb.inodestart xint(2nlog);// 第一个inode块块号 sb.bmapstart xint(2nlogninodeblocks); // 第一个bit map块块号 ​ printf(nmeta %d (boot, super, log blocks %u inode blocks %u, bitmap blocks %u) blocks %d total %dn, nmeta, nlog, ninodeblocks, nbitmap, nblocks, FSSIZE); // 文件系统初始化情况下可用数据块起始块号 freeblock nmeta; // the first free block that we can allocate // 将文件系统所有block都清零 for(i 0; i FSSIZE; i) wsect(i, zeroes); // 将buf清零 memset(buf, 0, sizeof(buf)); // 向buf写入super block内容 memmove(buf, sb, sizeof(sb)); // 向文件系统编号为1的block写入buf的内容,也就是将super block内容写入到磁盘上 wsect(1, buf); ​ // 分配一个空闲的inode -- 该inode作为root inode的inode number // 这里root ionde指的是根目录对应的inode rootino ialloc(T_DIR); assert(rootino ROOTINO); // 每个目录下都默认存在两个目录条目项: . 和 .. // 清空内存中直接块结构体 bzero(de, sizeof(de)); // 清空后重新赋值 -- inode number 和 文件名 de.inum xshort(rootino); strcpy(de.name, .); // 把这个目录项追加到root目录对应Inode block的直接块中 iappend(rootino, de, sizeof(de)); // 清空de inode,重用该结构体,向磁盘追加一个名为 .. 的目录文件 bzero(de, sizeof(de)); de.inum xshort(rootino); strcpy(de.name, ..); // 同样把这个目录项追加到对应的直接块中 iappend(rootino, de, sizeof(de)); // 处理需要打包进文件系统镜像的剩余文件 // 主要是user目录下的文件 for(i 2; i argc; i){ // get rid of user/ // 移除user目录下路径名的目录前缀 -- 如果有的话就移除 char *shortname; if(strncmp(argv[i], user/, 5) 0) shortname argv[i] 5; else shortname argv[i]; assert(index(shortname, /) 0); ​ // 获取第i个用户lib库程序的文件描述符 if((fd open(argv[i], 0)) 0){ perror(argv[i]); exit(1); } ​ // Skip leading _ in name when writing to file system. // The binaries are named _rm, _cat, etc. to keep the // build operating system from trying to execute them // in place of system binaries like rm and cat. // 传递给mkfs程序的用户程序文件名都是_开头的二进制文件,这里将_移除 if(shortname[0] _) shortname 1; // 分配一个空闲inode inum ialloc(T_FILE); // 清空inode,然后填入本次获取的用户程序文件相关信息 bzero(de, sizeof(de)); de.inum xshort(inum); strncpy(de.name, shortname, DIRSIZ); // 所有用户程序文件都放置在root目录下,所以这里将当前文件对应目录项追加到root目录对应的直接块中 // 如果文件很多,可能会追加到间接块中 iappend(rootino, de, sizeof(de)); // 将当前文件内容读取出来,追加到当前文件inode对应的block中 while((cc read(fd, buf, sizeof(buf))) 0) iappend(inum, buf, cc); ​ close(fd); } ​ // fix size of root inode dir // 更新root inode的size大小 rinode(rootino, din); off xint(din.size); off ((off/BSIZE) 1) * BSIZE; din.size xint(off); winode(rootino, din); // 更新bitmap block balloc(freeblock); ​ exit(0); }最后是kernel/fs.h中的struct dinode定义// 磁盘上存储的inode结构 // 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[NDIRECT1]; // Data block addresses }; ​ // 目录block中存储的目录项结构 struct dirent { ushort inum; char name[DIRSIZ]; };其中NDIRECT是直接块的数目NINDIRECT是间接块的数目MAXFILE是最大文件数在磁盘上查找文件数据的代码位于kernel/fs.c中的bmap()当中在读取和写入文件时都会调用bmap()写入时bmap()会根据需要分配块以保存文件内容如果不够还会分配间接块以保存块地址bmap()处理两种类型的块编号。bn参数是一个“逻辑块号”——文件中相对于文件开头的块号。ip-addrs[]中的块号和bread()的参数都是磁盘块号。您可以将bmap()视为将文件的逻辑块号映射到磁盘块号。// Return the disk block address of the nth block in inode ip. // If there is no such block, bmap allocates one. // 传入inode和希望读取的逻辑块号,返回该逻辑块号对应的磁盘块号 static uint bmap(struct inode *ip, uint bn) { uint addr, *a; struct buf *bp; // 如果逻辑块号指向的是直接块,然后直接块条目中存储的就是对应的磁盘块号 if(bn NDIRECT){ // 如果直接块还没有分配,那么先调用balloc分配一个空闲的block 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; // 那么在间接块block中定位到一级间接块条目存储的磁盘块号,并返回 if((addr a[bn]) 0){ // 没分配先分配,由于这里更改了间接块中某个一级间接条目的内容,所以需要记录log日志,等待后续刷脏 a[bn] addr balloc(ip-dev); log_write(bp); } brelse(bp); return addr; } ​ panic(bmap: out of range); }Large files在本实验中增加xv6文件的最大大小目前xv6限制为268个块也就是268*BSIZE 个字节其中BSIZE 为 1024原因是xv6 inode包含12 个 直接block和一个间接block一个间接块包含256个块号的块为了增大文件容量我们需要更改系统代码以支持每个inode可包含256个一级间接块地址的二级间接块每个一级间接块最多可以包含256个块地址即有256*25625611 65803个块修改bmap()以便除了直接快和一级间接块之外还实现了二级间接块不允许更改磁盘inode的大小。ip-addrs[]的前11个元素应该是直接块第12个应该是一个一级间接块与当前的一样13号应该是你的新二级间接块。当bigfile写入65803个块并成功运行usertests时此练习完成1.在kernel/fs.h中更改宏定义//直接块 #define NDIRECT 11 //二级块数量 #define NDOUBLYINDIRECT (NINDIRECT * NINDIRECT) //文件最大块数 #define MAXFILE (NDIRECT NINDIRECT NDOUBLYINDIRECT)同时修改包括kernle/fs.h中磁盘inode结构体dinode的addr字段和kernel/file.h中的内存inode结构体的addrs字段原本是NDIRECT1需要改为NDIRECT2因为inode的块号总数并没有变但是NDIRECT减少了12.修改kernel/fs.c中的bmap()函数该函数用于返回inode的相对块号对应的磁盘中的块号添加对第NDIRECT2即第13个块的二级间接索引的处理代码处理方法与前两个类似只是需要索引两次// bmap函数根据给定的inode和块号bn返回对应的数据块地址。 // 如果该数据块不存在则分配一个新的数据块并更新inode。 // 参数: // ip: 指向inode的指针。 // bn: 数据块号。 // 返回值: // 数据块的物理地址。 static uint bmap(struct inode *ip, uint bn) { uint addr, *a; struct buf *bp; // 如果块号bn小于直接块的数量处理直接块。 if(bn NDIRECT){ // 如果该直接块尚未分配则分配一个新的块并更新inode。 if((addr ip-addrs[bn]) 0) ip-addrs[bn] addr balloc(ip-dev); return addr; } // 减去直接块的数量准备处理间接块。 bn - NDIRECT; // 如果bn小于间接块的数量处理单级间接块。 if(bn NINDIRECT){ // 如果单级间接块尚未分配则分配一个新的块并更新inode。 // 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); log_write(bp); } // 释放单级间接块的缓冲区。 brelse(bp); return addr; } // 减去间接块的数量准备处理双级间接块。 bn - NINDIRECT; // 128, 129, 130, ... // 如果bn小于双级间接块的数量处理双级间接块。 if(bn NDOUBLYINDIRECT) { // 如果双级间接块尚未分配则分配一个新的块并更新inode。 if((addr ip-addrs[NDIRECT 1]) 0){ ip-addrs[NDIRECT 1] addr balloc(ip-dev); } // 读取双级间接块的第一级。 bp bread(ip-dev,addr); a (uint*)bp-data; // 如果对应的一级间接块尚未分配则分配一个新的块并更新双级间接块的一级。 if((addr a[bn / NINDIRECT]) 0){ a[bn / NINDIRECT] addr balloc(ip-dev); log_write(bp); } // 释放双级间接块的第一级缓冲区。 brelse(bp); // 读取双级间接块的二级。 bp bread(ip-dev,addr); a (uint*)bp-data; // 取余以得到正确的二级间接块索引。 bn % NINDIRECT; // 如果对应的二级间接块尚未分配则分配一个新的块并更新双级间接块的二级。 // 得到直接的块 if((addr a[bn]) 0){ a[bn] addr balloc(ip-dev); log_write(bp); } // 释放双级间接块的二级缓冲区。 brelse(bp); return addr; } // 如果bn超出所有间接块的数量范围则表示块号无效触发panic。 panic(bmap: out of range); }3.修改kernel/fs.c中的itrunc()函数该函数用于释放inode的数据块由于添加了二级间接块的结构, 因此也需要添加对该部分的块的释放的代码释放的方式同一级间接块号的结构, 只需要两重循环去分别遍历二级间接块以及其中的一级间接块.// itrunc函数用于清空一个inode指向的数据块即从文件系统中删除该文件的所有数据。 // 它遍历并释放inode的所有直接块、间接块和双间接块。 // 参数: // ip: 指向要操作的inode的指针。 void itrunc(struct inode *ip) { int i, j, k; struct buf *bp,*bp2; uint *a, *a2; // 遍历并释放所有直接块 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[NDIRECT 1]){ bp bread(ip-dev,ip-addrs[NDIRECT 1]); a (uint*)bp-data; for(j 0; j NINDIRECT; j){ if(a[j]){ bp2 bread(ip-dev,a[j]); a2 (uint*)bp2-data; for(k 0; k NINDIRECT; k){ if(a2[k]){ bfree(ip-dev,a2[k]); } } brelse(bp2); bfree(ip-dev,a[j]); a[j] 0; } } brelse(bp); bfree(ip-dev,ip-addrs[NDIRECT 1]); ip-addrs[NDIRECT1] 0; } // 将inode的大小设置为0并更新inode信息 ip-size 0; iupdate(ip); }测试在xv6中执行bigfile