首先切换分支到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) _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)
# 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-riscv
sed "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 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(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指的是根目录对应的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[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分配一个空闲的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*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
标签:10,文件,kernel,ip,Nine,file,间接,inode,block From: https://blog.csdn.net/wang1664178416/article/details/142535496