此次课设是基于之前学习的bochs的,这里需要配置好bochs环境和相关命令操作。
参考我之前写的博客
- bochs安装+安装hd60M盘:https://blog.csdn.net/luohaojia123/article/details/130854714
- 降低版本gcc为4.4:https://blog.csdn.net/luohaojia123/article/details/131700018
- 安装hd80M盘: 参考一位博主:https://blog.csdn.net/kanshanxd/article/details/132026480?spm=1001.2014.3001.5502
先明确几个概念:
- inode:记录文件与磁盘位置的映射。磁盘分区内会有一个inode数组,根据文件记录的inode数组下标,即可查找对应inode信息。
- 目录:特殊的文件,由目录项构成,目录项记录了文件名与inode的映射。
- 超级块:在磁盘分区的第二个位置,包含了关于inode、目录、位图等元数据信息。
- 文件结构:构成全局文件结构数组——文件表,描述文件与进程的操作状态。
- 文件描述符:在进程的PCB中,指向文件表中的某个特定文件。
主要思路:(例如访问/home/test.c)
(超级块------>根目录------->具体文件)
- 由于超级块位置固定,我们可以去磁盘中直接访问。
- 从超级块中,我们能知道根目录的inode标号和inode数组的位置。
- 使用根目录的inode标号与inode数组位置,我们可以找到根目录文件对应的inode,然后在磁盘中找到根目录文件。
- 在根目录文件中,我们查找一个名叫home的目录项,从中取出home的inode数组标号。
- 使用home的inode标号,我们再次访问inode数组,找到home目录文件在磁盘上的实际位置。
- 最后,在home目录文件中,我们查找一个名叫test.c的目录项,从中取出test.c的inode数组标号,进而找到test.c在磁盘上的位置。
书上内容实现功能是逐步完善的,也即n文件夹是最终实现的文件系统,具体每一步是如何实现的这里直接参考(很详细):https://blog.csdn.net/kanshanxd/article/details/132521696
关于makefile文件,这里在博主基础上:
- 首先记得把hd60M盘位置改为自己的路径:
- 将bochs的启动命令也封装在mak all命令中,记得依据自己盘的路径进行配置,提高便利性(命令行中每次输入make all命令,即可将文件系统功能烧录到hd60M盘,同时直接启动bochs)
完整makefile文件配置
#定义一大堆变量,实质就是将需要多次重复用到的语句定义一个变量方便使用与替换
BUILD_DIR=./build
ENTRY_POINT=0xc0001500
HD60M_PATH=/home/lhj/Public/bochs/hd60M.img
#只需要把hd60m.img路径改成自己环境的路径,整个代码直接make all就完全写入了,能够运行成功
AS=nasm
CC=gcc-4.4
LD=ld
LIB= -I lib/ -I lib/kernel/ -I lib/user/ -I kernel/ -I device/ -I thread/ -I userprog/ -I fs/
ASFLAGS= -f elf -g
CFLAGS= -Wall $(LIB) -c -fno-builtin -W -Wstrict-prototypes -Wmissing-prototypes -m32 -fno-stack-protector -g
#-Wall warning all的意思,产生尽可能多警告信息,-fno-builtin不要采用内部函数,
#-W 会显示警告,但是只显示编译器认为会出现错误的警告
#-Wstrict-prototypes 要求函数声明必须有参数类型,否则发出警告。-Wmissing-prototypes 必须要有函数声明,否则发出警告
LDFLAGS= -Ttext $(ENTRY_POINT) -e main -Map $(BUILD_DIR)/kernel.map -m elf_i386
#-Map,生成map文件,就是通过编译器编译之后,生成的程序、数据及IO空间信息的一种映射文件
#里面包含函数大小,入口地址等一些重要信息
OBJS=$(BUILD_DIR)/main.o $(BUILD_DIR)/init.o \
$(BUILD_DIR)/interrupt.o $(BUILD_DIR)/timer.o $(BUILD_DIR)/kernel.o \
$(BUILD_DIR)/print.o $(BUILD_DIR)/debug.o $(BUILD_DIR)/string.o $(BUILD_DIR)/bitmap.o \
$(BUILD_DIR)/memory.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o $(BUILD_DIR)/switch.o \
$(BUILD_DIR)/sync.o $(BUILD_DIR)/console.o $(BUILD_DIR)/keyboard.o $(BUILD_DIR)/ioqueue.o \
$(BUILD_DIR)/tss.o $(BUILD_DIR)/process.o $(BUILD_DIR)/syscall.o $(BUILD_DIR)/syscall-init.o \
$(BUILD_DIR)/stdio.o $(BUILD_DIR)/stdio-kernel.o $(BUILD_DIR)/ide.o $(BUILD_DIR)/fs.o $(BUILD_DIR)/inode.o \
$(BUILD_DIR)/file.o $(BUILD_DIR)/dir.o
#顺序最好是调用在前,实现在后
######################编译两个启动文件的代码#####################################
boot:$(BUILD_DIR)/mbr.o $(BUILD_DIR)/loader.o
$(BUILD_DIR)/mbr.o:boot/mbr.S
$(AS) -I boot/include/ -o build/mbr.o boot/mbr.S
$(BUILD_DIR)/loader.o:boot/loader.S
$(AS) -I boot/include/ -o build/loader.o boot/loader.S
######################编译C内核代码###################################################
$(BUILD_DIR)/main.o:kernel/main.c
$(CC) $(CFLAGS) -o $@ $<
# $@表示规则中目标文件名的集合这里就是$(BUILD_DIR)/main.o $<表示规则中依赖文件的第一个,这里就是kernle/main.c
$(BUILD_DIR)/init.o:kernel/init.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/interrupt.o:kernel/interrupt.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/timer.o:device/timer.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/debug.o:kernel/debug.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/string.o:lib/string.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/bitmap.o:lib/kernel/bitmap.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/memory.o:kernel/memory.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/thread.o:thread/thread.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/list.o:lib/kernel/list.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/sync.o:thread/sync.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/console.o:device/console.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/keyboard.o:device/keyboard.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/ioqueue.o:device/ioqueue.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/tss.o:userprog/tss.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/process.o:userprog/process.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/syscall.o:lib/user/syscall.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/syscall-init.o:userprog/syscall-init.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/stdio.o:lib/stdio.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/stdio-kernel.o:lib/kernel/stdio-kernel.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/ide.o:device/ide.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/fs.o:fs/fs.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/inode.o:fs/inode.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/file.o:fs/file.c
$(CC) $(CFLAGS) -o $@ $<
$(BUILD_DIR)/dir.o:fs/dir.c
$(CC) $(CFLAGS) -o $@ $<
###################编译汇编内核代码#####################################################
$(BUILD_DIR)/kernel.o:kernel/kernel.S
$(AS) $(ASFLAGS) -o $@ $<
$(BUILD_DIR)/print.o:lib/kernel/print.S
$(AS) $(ASFLAGS) -o $@ $<
$(BUILD_DIR)/switch.o:thread/switch.S
$(AS) $(ASFLAGS) -o $@ $<
##################链接所有内核目标文件##################################################
$(BUILD_DIR)/kernel.bin:$(OBJS)
$(LD) $(LDFLAGS) -o $@ $^
# $^表示规则中所有依赖文件的集合,如果有重复,会自动去重
.PHONY:mk_dir hd clean build all boot gdb_symbol #定义了7个伪目标
mk_dir:
if [ ! -d $(BUILD_DIR) ];then mkdir $(BUILD_DIR);fi
#判断build文件夹是否存在,如果不存在,则创建
hd:
dd if=build/mbr.o of=$(HD60M_PATH) count=1 bs=512 conv=notrunc && \
dd if=build/loader.o of=$(HD60M_PATH) count=4 bs=512 seek=2 conv=notrunc && \
dd if=$(BUILD_DIR)/kernel.bin of=$(HD60M_PATH) bs=512 count=200 seek=9 conv=notrunc
clean:
@cd $(BUILD_DIR) && rm -f ./* && echo "remove ./build all done"
#-f, --force忽略不存在的文件,从不给出提示,执行make clean就会删除build下所有文件
build:$(BUILD_DIR)/kernel.bin
#执行build需要依赖kernel.bin,但是一开始没有,就会递归执行之前写好的语句编译kernel.bin
run_bochs:
cd /home/lhj/Public/bochs/bin && ./bochs -f lhjbochsrc.disk
#生成可以被GDB理解的符号表,用于GDB调试
gdb_symbol:
objcopy --only-keep-debug $(BUILD_DIR)/kernel.bin $(BUILD_DIR)/kernel.sym
all:mk_dir boot build hd run_bochs
#make all 就是依次执行mk_dir build hd run_bochs
在完成书本上基本实现后,为了更好地演示文件系统所有功能,我重新编写/kernel/main函数,以在bochs窗口中能清晰看到文件系统的各个功能实现。
#include "print.h"
#include "init.h"
#include "fs.h"
#include "stdio.h"
#include "string.h"
#include "dir.h"
void director_operator(const char *op, const char *pathname);
void file_operator(const char *op, const char *pathname, const char *w_con);
int main(void)
{
put_str("I am kernel\n");
init_all();
// show root director
director_operator("show","/");
// create /dir1
director_operator("create","/dir1");
// create /dir1/file2
char file2[]="/dir1/file2";
file_operator("create",file2,"");
// show /dir1 content
director_operator("show","/dir1");
// write file2
file_operator("write",file2,"123");
// read file
file_operator("read",file2,"");
// delete file
file_operator("delete",file2,"");
// get file attr
director_operator("get_attr","/");
// show /dir1 content
director_operator("show","/dir1");
// delete director
// director_operator("delete","/dir1");
// show root content
sys_rewinddir(sys_opendir("/"));
director_operator("show","/");
while (1);
return 0;
}
void director_operator(const char *op, const char *pathname){
if( strcmp(op,"create")==0 ){
printf("==========create dir: %s==========\n",pathname);
printf(" %s create %s!\n", pathname, sys_mkdir(pathname) == 0 ? "done" : "fail");
}else if( strcmp(op,"show")==0 ){
printf("==========show director: %s ===========\n", pathname);
struct dir *dir = sys_opendir(pathname);
char *type = NULL;
struct dir_entry *dir_e = NULL;
while ((dir_e = sys_readdir(dir)))
{
if (dir_e->f_type == FT_REGULAR){
type = "regular";
}
else{
type = "directory";
}
printf(" %s: %s", type, dir_e->filename);
}
printf("\n");
sys_close(dir);
}else if( strcmp(op,"delete")==0 ){
printf("==========delete director: %s===========\n",pathname);
if (sys_rmdir(pathname) == 0)
{
printf("%s delete done!\n",pathname);
}
}else if( strcmp(op,"get_attr")==0 ){
printf("==========get %s attr===========\n",pathname);
struct stat obj_stat;
sys_stat(pathname, &obj_stat);
printf(" inode: %d\n size: %d\n filetype: %s\n",
obj_stat.st_ino,
obj_stat.st_size,
obj_stat.st_filetype == 2 ? "directory" : "regular");
}
}
void file_operator(const char *op, const char *pathname, const char *w_con){
if( strcmp(op,"create")==0 ){
printf("==========create file: %s===========\n", pathname);
uint32_t fd = sys_open(pathname, O_CREAT);
if(fd!=-1){
printf(" %s create done!\n",pathname);
}
sys_close(fd);
}else if( strcmp(op,"write")==0 ){
printf("==========write %s===========\n", pathname);
int con_len = strlen(w_con);
uint32_t fd = sys_open(pathname, O_RDWR);
sys_write(fd, w_con, con_len);
sys_close(fd);
}else if( strcmp(op,"read")==0 ){
printf("==========read %s===========\n",pathname);
uint32_t fd = sys_open(pathname, O_RDWR);
char buf[64]={0};
int read_bytes = sys_read(fd, buf, 20);
printf(" file content: %s\n", buf);
sys_close(fd);
}else if( strcmp(op,"delete")==0 ){
printf("==========delete %s===========\n",pathname);
printf(" %s delete %s!\n", pathname, sys_unlink(pathname) == 0 ? "done" : "fail");
}
}
解释这两个函数:
- director_operator 函数
此函数用于处理目录(文件夹)相关的操作。它根据提供的操作类型 (op) 和路径名 (pathname) 执行相应的动作。
- 输入参数:
- op: 字符串,表示要执行的操作类型,比如 “create”, “show”, “delete”, “get_attr”。
- pathname: 字符串,表示目录的路径。
- 操作类型:
- “create”: 创建一个新的目录。使用 sys_mkdir 系统调用来创建目录,并打印操作结果。
- “show”: 显示目录内容。使用 sys_opendir 和 sys_readdir 系统调用来遍历目录中的文件和子目录,并打印它们的类型(普通文件或目录)和名称。
- “delete”: 删除目录。使用 sys_rmdir 系统调用来删除目录,并打印操作结果。
- “get_attr”: 获取目录的属性。使用 sys_stat 系统调用来获取目录的属性,如 inode 编号、大小和文件类型,并打印这些信息。- file_operator 函数
此函数用于处理文件相关的操作。根据提供的操作类型 (op)、文件路径 (pathname) 和在写操作时需要的内容 (w_con) 执行相应的动作。
- 输入参数:
- op: 字符串,表示要执行的操作类型,比如 “create”, “write”, “read”, “delete”。
- pathname: 字符串,表示文件的路径。
- w_con: 字符串,表示写入文件的内容(只在 op 为 “write” 时使用)。
- 操作类型:
- “create”: 创建一个新文件。使用 sys_open 系统调用以创建模式打开文件,并打印操作结果。然后关闭文件描述符
- “write”: 向文件写入内容。使用 sys_open 系统调用以读写模式打开文件,然后使用 sys_write 写入内容,并关闭文件描述符
- “read”: 读取文件内容。使用 sys_open 系统调用以读写模式打开文件,然后使用 sys_read 读取内容,并关闭文件描述符
- “delete”: 删除文件。使用 sys_unlink 系统调用来删除文件,并打印操作结果
实现效果:
根据图中运行结果的数字:
1.首先展示根目录/:
结果显示:
分析:原先我已经在磁盘中根目录建立了一个目录dir1和一个文件file1,所以这里会有初始文件和目录。
对应代码:上述main代码中的
,调用自己额外封装的目录操作director_operator函数,参数”show”表示展示目录内容,”/”表示要展示的目录路径名称。在其内部,它使用 sys_opendir 打开指定的目录,通过 sys_readdir 循环读取目录中的每个条目,并打印出每个文件或子目录的类型和名称。 这两个函数在路径为:综合所有功能/fs/fs.c里实现,上述已经阐述了它的实现流程,这里不再赘述,下同。
2.尝试在根目录下创建一个新目录dir1:
结果显示:
分析: 因为根目录下已经存在目录dir1了,所以提示创建失败。
对应代码:对应上述main代码中的
,参数”create”表示创建目录内容,”/dir1”表示要创建的目录路径名称。在其内部,它使用 sys_mkdir 函数来创建指定路径的目录,并打印操作的结果。
3.尝试在/dir1目录下新建文件file2:
结果显示:
分析:先扫描了/dir1下目录是否存在file2文件,然后再进行创建。
对应代码:对应上述main代码中的
,先创建一个file2变量,方便后续使用,然后调用文件操作函数file_operator,”create”表示创建文件,”/dir1/file2”表示要创建的文件路径。在其内部,它使用 sys_open 函数以创建模式打开文件,然后关闭文件描述符。
4.展示/目录dir1内容:
结果显示:
分析: file2是上述步骤创建的, . 和 … 表示当面目录项和根目录项。
对应代码:对应上述main代码中的
,参数 “show” 表示显示目录内容的操作,“/dir1” 表示要显示内容的目录路径。在其内部,它使用 sys_opendir 和 sys_readdir 来遍历目录并打印出目录中的文件和子目录。
5.对/dir1/file2写入内容:
结果显示:
分析:表示写入的块地址,方便我们可以使用xxd命令去验证它是否真的有写入。
对应代码:对应上述main代码中的
,参数 “write” 表示写入文件内容的操作,file2(路径 “/dir1/file2”)表示目标文件的路径,而 “123” 是要写入的内容。在其内部,它使用 sys_open 打开文件,然后使用 sys_write 将内容写入文件。
6.读取/dir1/file2的内容:
结果显示:
分析:这是上一步骤写入的内容,因此表明写入和读取成功。
对应代码:
,参数 “read” 表示读取文件内容的操作,file2(路径 “/dir1/file2”)表示要读取内容的文件路径。在其内部,它使用 sys_open 打开文件,然后使用 sys_read 读取内容,并打印出来。
7.删除文件/dir1/file2:
结果显示:
分析:表明文件/dir1/file2删除成功,到目前,file2分析:文件在一次程序中,经历了创建、写入、读出、删除,因此下次再次运行该main程序时,依然会重新创建file2。
对应代码:
,参数 “delete” 表示删除文件的操作,file2(路径 “/dir1/file2”)表示要删除的文件路径。在其内部,它使用 sys_unlink 函数来删除指定的文件,并打印操作的结果。
8.获取根目录/的属性:
结果显示:
分析:作为根目录。其inode为执行inode数组的第一个,因此其inode=0;而size=96,是因为根目录下有四个目录项:. 、 … 、 dir1 、 file1 ,而每个目录项占24个字节,因此总共大小为96字节。
对应代码:
,参数 “get_attr” 表示获取目录属性的操作,“/” 表示要获取属性的目录路径。在其内部,它使用 sys_stat 函数来获取目录的属性,如 inode 编号、大小和文件类型,并打印这些信息。
9.再次展示目录/dir1的所有目录项:
结果显示:
分析:我们在第3步和第7步分别对/dir1/file2进行了创建和删除,因此最终/dir1目录下只有本目录项和根目录项。
对应代码:
,这是再次显示 “/dir1” 目录内容的操作。它使用 sys_opendir 和 sys_readdir 来遍历目录并打印出目录中的文件和子目录。
10.再次展示根目录/的所有目录项
结果显示:
分析:在此测试程序中,没有对file1进行有关操作,主要是对/dir1里的file2进行操作,因此根目录内容同1依旧不变。
对应代码:
,首先使用 sys_rewinddir 和 sys_opendir 重置并打开根目录的读取指针,然后使用 director_operator 显示根目录的内容,参数 “show” 和路径 “/” 作用如之前所述。
通过这次课程设计,从理论到实践的转变,使我得以将书本上抽象的概念具体实现。
从知识理解方面:对文件系统中相关定义的有了更加深刻的理解,如inode、超级块、文件结构、文件描述符、目录项等,另外,不仅要理解它们的定义,我觉得还很关键的是要熟知它们的所在位置(内存、磁盘、进程等等),实现文件系统本质就是在内存和磁盘上对这些概念进行来回交互。
从代码实践的角度:跟着原书参考和博客讲解,每个小节的内容是逐步完善的,稍微比较麻烦的便是c小节创建文件,因为要声明许多相关的辅助操作函数,而其他小节模块的实现本身其实并不难。源码虽多,其实每个模块可以主要分为:底层逻辑实现+sys_封装实现,从而在main.c中直接调用sys_系列的操作函数,这样提高了测试代码的可读性和编写性,从而实现各种文件操作。最后,在完成给的的实验任务上,我为了更加充分地理解各个模块的实现功能,自行将a-n小节的模块功能进行结合。在结合过程中,也遇到不少麻烦,最主要的bug是sys_open打开文件后,没有及时关闭文件就进行了下一步操作。同时,在调试过程中,鉴于bochs的特性,基于目前已学的知识,并不能实现像平常C++语言出现的黑框进行交互从而方便测试,只能一次一次地make all编译,因此增加了调试的难度。另外,由于某些模块的功能代码比较繁杂,如遍历目录的功能,它涉及sys_opendir、sys_readdir、sys_closedir,而且一次程序中肯定要前后展示两三次目录,这样下来代码的可读性比较差,且有冗余重复的代码段,因此为了再增强代码的可读性,我又将sys_系列的相关操作函数进行了一层封装,大大方便了代码调试和测试功能。