10.Lab Nine —— file system-上

首先切换分支到fs

git checkout fs
make clean

预备知识

mkfs程序创建xv6文件系统磁盘映像,并确定文件系统的总块数,这个大小在kernel/param.h中的FSSIZE写明

 // kernel/params.h
#define FSSIZE       200000  // size of file system in blocks

MakeFile文件系统和内核文件构建流程注释

# To compile and run with a lab solution, set the lab name in lab.mk
# (e.g., LB=util).  Run make grade to test solution with the lab's
# grade script (e.g., grade-lab-util).
​
-include conf/lab.mk
​
K=kernel
U=user
​
# 构建内核代码的文件组成列表
​
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 2>&1 | grep 'elf64-big' >/dev/null 2>&1; then echo 'riscv64-unknown-elf-'; elif riscv64-linux-gnu-objdump -i 2>&1 | grep 'elf64-big' >/dev/null 2>&1; then echo 'riscv64-linux-gnu-'; elif riscv64-unknown-linux-gnu-objdump -i 2>&1 | grep 'elf64-big' >/dev/null 2>&1; then echo 'riscv64-unknown-linux-gnu-'; else echo "***" 1>&2; echo "*** Error: Couldn't find a riscv64 version of GCC/binutils." 1>&2; echo "*** To turn off this error, run 'gmake TOOLPREFIX= ...'." 1>&2; echo "***" 1>&2; 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 += -mcmodel=medany
# -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 2>&1 && echo -fno-stack-protector)
​
# -D选项用于在预处理阶段定义宏并设置其值
ifeq ($(LAB),net)
CFLAGS += -DNET_TESTS_PORT=$(SERVERPORT)
endif
​
# 检查编译器是否支持PIE(Position 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-size=4096
​
# 指定了生成 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) 是编译器选项,-march=rv64g 指定编译为 RV64G 架构(RISC-V 64-bit,带乘法/除法指令集)# -nostdinc 禁止包含标准库头文件,-I. -Ikernel 指定头文件搜索路径# -c $U/initcode.S -o $U/initcode.o 指定编译 U/initcode.S 并输出为 U/initcode.o 目标文件$(CC) $(CFLAGS) -march=rv64g -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) _initetags *.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.plperl $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.hgcc $(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.cgcc -o ph -g -O2 notxv6/ph.c -pthread
​
barrier: notxv6/barrier.cgcc -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)
# QEMU's 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 file=fs.img,if=none,format=raw,id=x0: 这个选项指定虚拟机使用一个磁盘镜像文件作为虚拟硬盘。
# file=fs.img表示虚拟硬盘的文件路径和名称为fs.img
# if=none表示磁盘接口类型为none(即不使用默认接口)
# format=raw表示磁盘镜像文件的格式为raw(原始格式)
# id=x0表示为这个虚拟硬盘指定了一个唯一的标识符为x0。
QEMUOPTS += -drive file=fs.img,if=none,format=raw,id=x0
# -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0: 这个选项指定将一个virtio块设备连接到虚拟机上。
# virtio-blk-device表示设备类型为virtio块设备
# drive=x0表示将之前定义的虚拟硬盘x0连接到该设备上
# bus=virtio-mmio-bus.0表示设备连接到virtio总线的第一个MMIO总线上。
QEMUOPTS += -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0
​
ifeq ($(LAB),net)
QEMUOPTS += -netdev user,id=net0,hostfwd=udp::$(FWDPORT)-:2000 -object filter-dump,id=net0,netdev=net0,file=packets.pcap
QEMUOPTS += -device e1000,netdev=net0,bus=pcie.0
endif
​
# qemu目标依赖于内核文件和文件系统镜像构建完成
qemu: $K/kernel fs.img# 启动qemu,参数就是QEMUOPTS定义的$(QEMU) $(QEMUOPTS)
​
.gdbinit: .gdbinit.tmpl-riscvsed "s/:1234/:$(GDBPORT)/" < $^ > $@
​
qemu-gdb: $K/kernel .gdbinit fs.img@echo "*** Now run 'gdb' in another window." 1>&2$(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 numberbn = 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中对应为设置为1for(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 numberif(xint(din.addrs[fbn]) == 0){din.addrs[fbn] = xint(freeblock++);}// 获取这个直接块条目记录的block numberx = xint(din.addrs[fbn]);} else {// 如果属于间接块,并且该间接块条目的块号没有确定,那么赋值为当前空闲可用块的block numberif(xint(din.addrs[NDIRECT]) == 0){din.addrs[NDIRECT] = xint(freeblock++);}// 从磁盘读取出这个间接块 --- 间接块中记录的都是block numberrsect(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 numberx = 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 blocksnmeta = 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(2+nlog);// 第一个inode块块号sb.bmapstart = xint(2+nlog+ninodeblocks); // 第一个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指的是根目录对应的inoderootino = 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;elseshortname = 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;// 分配一个空闲inodeinum = 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 blockballoc(freeblock);
​exit(0);
}

最后是kernel/fs.h中的struct dinode定义

// 磁盘上存储的inode结构
// On-disk inode structure
struct dinode {short type;           // File typeshort major;          // Major device number (T_DEVICE only)short minor;          // Minor device number (T_DEVICE only)short nlink;          // Number of links to inode in file systemuint size;            // Size of file (bytes)uint addrs[NDIRECT+1];   // 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分配一个空闲的blockif((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*256+256+11 = 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字段,原本是NDIRECT+1,需要改为NDIRECT+2,因为inode的块号总数并没有变,但是NDIRECT减少了1

2.修改kernel/fs.c中的bmap()函数

该函数用于返回inode的相对块号对应的磁盘中的块号

添加对第NDIRECT+2即第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[NDIRECT+1] = 0;}// 将inode的大小设置为0,并更新inode信息ip->size = 0;iupdate(ip);
}

测试,在xv6中执行bigfile

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/54715.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

牛客小白月赛101

考点为&#xff1a;A题 滑动窗口、B题 栈、C题 找规律、D 差分、E 筛约数。C题可能会卡住&#xff0c;不过手搓几组&#xff0c;或者模拟几组规律就显而易见了 A&#xff1a; 思路&#xff1a; 无论去头还是去尾&#xff0c;最后所留下的数据长度一定为&#xff1a;n - k &am…

Dbt自动化测试实战教程

数据团队关键核心资产是给消费者提供可信赖的数据。如果提供了不被信任的数据&#xff0c;那么支持决策智能依赖于猜测和直觉。原始数据从不同来源被摄取智数据仓库&#xff0c;数据产品团队有责任定义转换逻辑&#xff0c;将源数据整合到有意义的数据产品中&#xff0c;用于报…

Redisson分布式锁的概念和使用

Redisson分布式锁的概念和使用 一 简介1.1 什么是分布式锁&#xff1f;1.2 Redisson分布式锁的原理1.3 Redisson分布式锁的优势1.4 Redisson分布式锁的应用场景 二 案例2.1 锁竞争案例2.2 看门狗案例2.3 参考文章 前言 这是我在这个网站整理的笔记,有错误的地方请指出&#xff…

生活英语口语柯桥学英语“再确认一下“ 说成 “double confirm“?这是错误的!

在追求英语表达的过程中&#xff0c;我们常常会遇到一些看似合理实则错误的表达习惯。今天&#xff0c;我们就来聊聊一个常见的误区——“再确认一下”被误译为“double confirm”。 “再次确认”不是double confirm 首先&#xff0c;我们需要明确&#xff0c;“double confi…

Springboot2笔记核心技术——1.基础入门

目录 1.spring和springboot的区别 1. 框架 vs. 工具 2. 配置方式 3. 启动方式 4. 项目结构 5. 生态系统 2.SpringBoot2入门 1. 创建项目 2. 项目结构 3. 编写主类 4. 添加Controller 5. 配置应用 6. 启动应用 7. 测试应用 8. 进一步学习 3.了解自动配置原理 1…

R 语言 | 取数据框一列子集时,如何保持数据框结构?drop=F

数据框取多列时&#xff0c;返回的还是数据框。 取一列时&#xff0c;默认退化为一个向量&#xff1a; > class(iris) [1] "data.frame" > t1iris[, 1:2] > class(t1) [1] "data.frame"> t2iris[,1] > class(t2) [1] "numeric" …

2.1 HuggingFists系统架构(二)

部署架构 上图为HuggingFists的部署架构。从架构图可知&#xff0c;HuggingFists主要分为服务器(Server)、计算节点(Node)以及数据库(Storage)三部分。这三部分可以分别部署在不同的机器上&#xff0c;以满足系统的性能需求。为部署方便&#xff0c;HuggingFists社区版将这三部…

YOLOv9改进,YOLOv9主干网络替换为GhostNetV2(华为提出的轻量化架构)

摘要 摘要:轻量级卷积神经网络(CNN)专为移动设备上的应用而设计,具有更快的推理速度。卷积操作只能在窗口区域内捕捉局部信息,这限制了性能的进一步提升。将自注意力引入卷积可以很好地捕捉全局信息,但会极大地拖累实际速度。本文提出了一种硬件友好的注意力机制(称为 D…

CSP-S 2024 提高级 第一轮(初赛) 完善程序(2)

【题目】 CSP-S 2024 提高级 第一轮&#xff08;初赛&#xff09; 完善程序&#xff08;2&#xff09; (2)&#xff08;次短路&#xff09;已知一个n个点m条边的有向图G&#xff0c;并且给定图中的两个点s和t&#xff0c;求次短路&#xff08;长度严格大于最短路的最短路径&am…

TFTP协议

目录 一、TFTP协议概述 1.1 TFTP协议简介 1.2 TFTP协议特点 二、TFTP协议原理 2.1 TFTP协议工作流程 2.2 TFTP协议数据包格式 三、TFTP协议应用场景 3.1 网络设备配置文件传输 3.2 虚拟机镜像文件传输 3.3 IoT设备固件升级 四、TFTP协议优化方法 4.1 增加超时重传机…

深入理解Python中的数据结构:OrderedDict

目录 1. 前言 2. OrderedDict的基本概念 2.1 OrderedDict的创建 2.2 排序特性 2.3 比较OrderedDict和标准字典 3. OrderedDict的高级功能 3.1 元素的移动 3.2 重新排序 3.3 反转顺序 4. OrderedDict的性能表现 4.1 插入性能测试 4.2 读取性能测试 5. OrderedDict的…

数据结构之——队列

一、队列概述 队列是一种操作受限的线性表&#xff0c;其限制条件为允许在表的一端进行插入&#xff0c;而在表的另一端进行删除。插入的一端叫做队尾&#xff0c;删除的一端叫做队头。向队列中插入新元素的行为称为进队&#xff0c;从队列中删除元素的行为称为出队。例如军训的…

Java服务端开发中的API版本管理:从URI到Header的不同策略

Java服务端开发中的API版本管理&#xff1a;从URI到Header的不同策略 大家好&#xff0c;我是微赚淘客返利系统3.0的小编&#xff0c;是个冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;在Java服务端开发中&#xff0c;API版本管理是一个重要而复杂的问题。随着业…

前端文件上传全过程

特别说明&#xff1a;ui框架使用的是蚂蚁的antd 这里主要是学习前端上传接口的传递参数包括前端上传之前对于代码的整理 一、第一步将前端页面画出来 源代码&#xff1a; /** 费用管理 - IT费用管理 - 费用数据上传 */ import { useState } from "react"; import {…

Leetcode 56.合并区间-Python

链接&#xff1a;56. 合并区间 - 力扣&#xff08;LeetCode&#xff09; 题目描述&#xff1a; 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&#xff0c;并返回 一个不重叠的区间数组&#xff…

【基础知识】网络套接字编程

套接字 IP地址 port&#xff08;端口号&#xff09; socket&#xff08;套接字&#xff09; socket常见API //创建套接字 int socket(int domain, int type, int protocol); //绑定端口 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); //监听套接字…

Prometheus篇之利用promtool工具校验配置正确性

promtool工具 promtool是Prometheus的一个命令行工具&#xff0c;它提供了一些功能来帮助用户进行Prometheus配置文件&#xff08;如prometheus.yml&#xff09;的检查、规则检查和调试。 解释 promtool check config: 验证Prometheus配置文件的语法和设置。 promtool命令&…

【最基础最直观的排序 —— 选择排序算法】

最基础最直观的排序 —— 选择排序算法 选择排序算法是一种简单直观的排序算法。其基本思想是每一次从待排序的数据元素中选出最小&#xff08;或最大&#xff09;的一个元素&#xff0c;存放在序列的起始位置&#xff0c;然后&#xff0c;再从剩余未排序元素中继续寻找最小&a…

vue3常用的组件间通信

一 props props 可以实现父子组件通信&#xff0c;props数据是只读。 1. 基本用法 在父组件中&#xff0c;你可以这样传递 props&#xff1a; <template><ChildComponent message"Hello, Vue 3!" /> </template><script setup> import C…

WebPage-Bootstrap框架(container类,container-fluid类,栅格系统)

1.Bootstrap Bootstrap为页面内容和栅格系统包裹了一个.container容器&#xff0c;框架预先定义类 1.1container类 响应式布局容器的宽度 手机-小于768px 宽度设置100%&#xff1b; 平板-大于等于768px 设置宽度为750px 桌面显示器-大于等于992px 设置宽度 970px 大屏幕显…