清华大学操作系统课程 ucore Lab8 文件系统 试验报告
操作系统 Lab8 文件系统 试验报告
课程信息所在网址: chyyuu/os_course_info
- 试验目的
- 试验内容
- 基本练习
- 练习0:填写已有试验
- 练习1:完成读文件操作的实现(需要编码)
- 设计实现
- 问题答复
- 练习2: 完成基于文件系统的执行程序机制的实现(需要编码)
- 设计实现
- 问题答复
- 试验结果
- 参考答案分析
- 练习1
- 练习2
- 试验中涉及的知识点列举
- 试验中未涉及的知识点列举
- 参考文献
试验目的
- 理解基本的文件系统系统调用的实现方法;
- 理解一个基于索引节点组织方式的Simple FS文件系统的设计与实现;
- 理解文件系统笼统层-VFS的设计与实现;
试验内容
- 通过分析理解ucore文件系统的总体架构设计,完善读写文件操作;
- 实现基于文件系统的执行程序机制(即改写do_execve),从而可以完成执行存储在磁盘上的文件和实现文件读写等功能;
基本练习
练习0:填写已有试验
在本练习中将LAB1/2/3/4/5/6/7的试验内容移植到了LAB8的试验框架内,因为手动进行内容移植比较烦杂,因而考虑使用diff和patch工具进行自动化的移植,具体使用的命令如下所示:(对于patch工具进行合并的时候产生冲突的少部分内容,则使用*.rej, *.orig文件来手动处理冲突问题)
diff -r -u -P lab7_origin lab7 > lab7.patchcd lab8patch -p1 -u < ../lab7.patch
练习1:完成读文件操作的实现(需要编码)
首先理解打开文件的解决流程,而后参考本试验后续的文件读写操作的过程分析,编写在sfs_inode.c中sfs_io_nolock读文件中数据的实现代码。
设计实现
- 在完成练习1之前,首先需要对先前LAB中填写的代码进行升级,包括对进程控制块中新添加变量的初始化、以及在do_fork等函数中对这些变量进行相应的设置等,因为这些内容均比较琐碎,因而在本报告中将不对其进行赘述;
- 在本练习中需要进行具体编码实现的函数是sfs_node.c文件中的sfs_io_nolock函数,因而不妨对该函数进行分析:
- 根据对该函数的观察可以得知,该函数的功能为针对指定的文件(文件对应的内存中的inode信息已经给出),从指定偏移量进行指定长度的读或者者写操作,因而不妨分析系统调用的读操作到底是符合调用到这个函数的来理解这个函数在整个系统中的功能:
- 发起read系统调用后,通过正常的系统调用解决流程,进入sys_read函数,该函数进一步调用了sysfile_read函数,在这个函数中,创立了大小有限的缓冲区,用于从文件读取数据之后,进一步复制到客户空间的指定位置去;具体用于从文件读取数据的函数是file_read,在file_read函数中,通过文件形容符查找到了相应文件对应的内存中的inode信息,而后转交给vop_read进行读取解决,事实上就是转交到了sfs_read函数进行解决(通过函数指针),而后调用了sfs_io函数,再进一步调用了sfs_io_nolock函数,这就是我们在本次练习中需要完善的函数;
- 在sfs_io_nolock函数中,首先会进行一系列边界检查,检查能否访问合法,而后将具体的读/写操作使用函数指针统一起来,统一成针对整块的操作,以及不需要针对整块的操作两个解决函数,接下来的部分就是在本次试验中需要完成的部分了,这部分的主要功能为完成不落在整块数据块上的读/写操作,以及落在整块数据块上的读写,接下来将结合具体的代码来说明实际的实现过程:
if (offset % SFS_BLKSIZE != 0 || endpos / SFS_BLKSIZE == offset / SFS_BLKSIZE) { // 判断被需要读/写的区域所覆盖的数据块中的第一块能否是完全被覆盖的,假如不是,则需要调用非整块数据块进行读或者写的函数来完成相应操作 blkoff = offset % SFS_BLKSIZE; // 计算出在第一块数据块中进行读或者写操作的偏移量 size = (nblks != 0) ? (SFS_BLKSIZE - blkoff) : (endpos - offset); // 计算出在第一块数据块中进行读或者写操作需要的数据长度 if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) goto out; // 获取当前这个数据块对应到的磁盘上的数据块的编号 if ((ret = sfs_buf_op(sfs, buf, size, ino, blkoff)) != 0) goto out; // 将数据写入到磁盘中 alen += size; // 维护已经读写成功的数据长度信息 buf += size;}uint32_t my_nblks = nblks;if (offset % SFS_BLKSIZE != 0 && my_nblks > 0) my_nblks --;if (my_nblks > 0) { // 判断能否存在被需要读写的区域完全覆盖的数据块 if ((ret = sfs_bmap_load_nolock(sfs, sin, (offset % SFS_BLKSIZE == 0) ? blkno: blkno + 1, &ino)) != 0) goto out; // 假如存在,首先获取这些数据块对应到磁盘上的数据块的编号 if ((ret = sfs_block_op(sfs, buf, ino, my_nblks)) != 0) goto out; // 将这些磁盘上的这些数据块进行读或者写操作 size = SFS_BLKSIZE * my_nblks; alen += size; // 维护已经成功读写的数据长度 buf += size; // 维护缓冲区的偏移量}if (endpos % SFS_BLKSIZE != 0 && endpos / SFS_BLKSIZE != offset / SFS_BLKSIZE) { // 判断需要读写的最后一个数据块能否被完全覆盖(这里还需要确保这个数据块不是第一块数据块,由于第一块数据块已经操作过了) size = endpos % SFS_BLKSIZE; // 确定在这数据块中需要读写的长度 if ((ret = sfs_bmap_load_nolock(sfs, sin, endpos / SFS_BLKSIZE, &ino) == 0) != 0) goto out; // 获取该数据块对应到磁盘上的数据块的编号 if ((ret = sfs_buf_op(sfs, buf, size, ino, 0)) != 0) goto out; // 进行非整块的读或者者写操作 alen += size; buf += size;}
- 至此,练习1中的所有编码工作完成,实现了读文件操作;
- 根据对该函数的观察可以得知,该函数的功能为针对指定的文件(文件对应的内存中的inode信息已经给出),从指定偏移量进行指定长度的读或者者写操作,因而不妨分析系统调用的读操作到底是符合调用到这个函数的来理解这个函数在整个系统中的功能:
问题答复
- 请在试验报告中给出设计实现”UNIX的PIPE机制“的概要设方案,鼓励给出详细设计方案。
- 为了实现UNIX的PIPE机制,可以考虑在磁盘上保留一部分空间或者者是一个特定的文件来作为pipe机制的缓冲区,接下来将说明如何完成对pipe机制的支持:
- 当某两个进程之间要求建立管道,假定将进程A的标准输出作为进程B的标准输入,那么可以在这两个进程的进程控制块上新添加变量来记录进程的这种属性;并且同时生成一个临时的文件,并将其在进程A, B中打开;
- 当进程A使用标准输出进行write系统调用的时候,通过PCB中的变量可以知道,需要将这些标准输出的数据输出到先前提高的临时文件中去;
- 当进程B使用标准输入的时候进行read系统调用的时候,根据其PCB中的信息可以知道,需要从上述的临时文件中读取数据;
- 至此完成了对pipe机制的设计;
- 事实上,因为在真实的文件系统和客户之间还由一层虚拟文件系统,因而我们也可以不把数据缓冲在磁盘上,而是直接保存在内存中,而后完成一个根据虚拟文件系统的规范完成一个虚拟的pipe文件,而后进行输入输出的时候只需对这个文件进行操作就可;
- 为了实现UNIX的PIPE机制,可以考虑在磁盘上保留一部分空间或者者是一个特定的文件来作为pipe机制的缓冲区,接下来将说明如何完成对pipe机制的支持:
练习2: 完成基于文件系统的执行程序机制的实现(需要编码)
改写proc.c中的load_icode函数和其余相关函数,实现基于文件系统的执行程序机制。执行: make qemu。假如能看看到sh客户程序的执行界面,则基本成功了。假如在sh客户界面上可 以执行”ls”,”hello”等其余放置在sfs文件系统中的其余执行程序,则可以认为本试验基本成功。
设计实现
- 通过对试验代码的分析可以得知最终用于从磁盘上读取可执行文件,并且加载到内存中,完成内存空间的初始化的函数是load_icode函数,该函数在本LAB中的具体实现与先前的LAB区别在于,先前的LAB仅仅将原价就加载到了内核内存空间中的ELF可执行文件加载到客户内存空间中,而没有涉及从磁盘读取数据的操作,而且先前的时候也没有考虑到给需要执行的应用程度传递操作的可能性;
- 仿照先前lab中的load_icode实现,可以大致将该函数的实现流程分为以下几个步骤:
- 给要执行的客户进程创立一个新的内存管理结构mm,原价该进程的mm已经在do_execve中被释放掉了;
- 创立客户内存空间的新的页目录表;
- 将磁盘上的ELF文件的TEXT/DATA/BSS段正确地加载到客户空间中;
- 从磁盘中读取elf文件的header;
- 根据elfheader中的信息,获取到磁盘上的program header;
- 对于每一个program header:
- 为TEXT/DATA段在客户内存空间上的保存分配物理内存页,同时建立物理页和虚拟页的映射关系;
- 从磁盘上读取TEXT/DATA段,并且复制到客户内存空间上去;
- 根据program header得知能否需要创立BBS段,假如是,则分配相应的内存空间,并且一律初始化成0,并且建立物理页和虚拟页的映射关系;
- 将客户栈的虚拟空间设置为合法,并且为栈顶部分先分配4个物理页,建立好映射关系;
- 切换到客户地址空间;
- 设置好客户栈上的信息,即需要传递给执行程序的参数;
- 设置好中断帧;
- 接下来结合具体的代码实现来说明本试验中的实现:
static intload_icode(int fd, int argc, char **kargv) { if (current->mm != NULL) { // 判断当前进程的mm能否已经被释放掉了 panic("load_icode: current->mm must be empty.\n"); } int ret = -E_NO_MEM; struct mm_struct *mm; // (1) create a new mm for current process if ((mm = mm_create()) == NULL) { // 为进程创立一个新的mm goto bad_mm; } // (2) create a new PDT if ((ret = setup_pgdir(mm)) != 0) { // 进行页表项的设置 goto bad_pgdir_cleanup_mm; } // (3) copy TEXT/DATA/BSS section // (3.1) resolve elf header struct elfhdr elf, *elfp = &elf; off_t offset = 0; load_icode_read(fd, (void *) elfp, sizeof(struct elfhdr), offset); // 从磁盘上读取出ELF可执行文件的elf-header offset += sizeof(struct elfhdr); if (elfp->e_magic != ELF_MAGIC) { // 判断该ELF文件能否合法 ret = -E_INVAL_ELF; goto bad_elf_cleanup_pgdir; } struct proghdr ph, *php = &ph; uint32_t vm_flags, perm; struct Page *page; for (int i = 0; i < elfp->e_phnum; ++ i) { // 根据elf-header中的信息,找到每一个program header // (3.2) resolve prog header load_icode_read(fd, (void *) php, sizeof(struct proghdr), elfp->e_phoff + i * sizeof(struct proghdr)); // 读取program header if (php->p_type != ELF_PT_LOAD) { continue; } if (php->p_filesz > php->p_memsz) { ret = -E_INVAL_ELF; goto bad_cleanup_mmap; } if (php->p_filesz == 0) { continue; } // (3.3) build vma vm_flags = 0, perm = PTE_U; if (php->p_flags & ELF_PF_X) vm_flags |= VM_EXEC; // 根据ELF文件中的信息,对各个段的权限进行设置 if (php->p_flags & ELF_PF_W) vm_flags |= VM_WRITE; if (php->p_flags & ELF_PF_R) vm_flags |= VM_READ; if (vm_flags & VM_WRITE) perm |= PTE_W; if ((ret = mm_map(mm, php->p_va, php->p_memsz, vm_flags, NULL)) != 0) { // 将这些段的虚拟内存地址设置为合法的 goto bad_cleanup_mmap; } // (3.4) allocate pages for TEXT/DATA sections offset = php->p_offset; size_t off, size; uintptr_t start = php->p_va, end = php->p_va + php->p_filesz, la = ROUNDDOWN(start, PGSIZE); ret = -E_NO_MEM; while (start < end) { if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) { // 为TEXT/DATA段逐页分配物理内存空间 goto bad_cleanup_mmap; } off = start - la, size = PGSIZE - off, la += PGSIZE; if (end < la) { size -= la - end; } load_icode_read(fd, page2kva(page) + off, size, offset); // 将磁盘上的TEXT/DATA段读入到分配好的内存空间中去 //memcpy(page2kva(page) + off, page2kva(buff_page), size); start += size, offset += size; } // (3.5) allocate pages for BSS end = php->p_va + php->p_memsz; if (start < la) { // 假如存在BSS段,并且先前的TEXT/DATA段分配的最后一页没有被完全占用,则剩余的部分被BSS段占用,因而进行清零初始化 if (start == end) { continue; } off = start + PGSIZE - la, size = PGSIZE - off; if (end < la) { size -= la - end; } memset(page2kva(page) + off, 0, size); // init all BSS data with 0 start += size; assert((end < la && start == end) || (end >= la && start == la)); } while (start < end) { // 假如BSS段还需要更多的内存空间的话,进一步进行分配 if ((page = pgdir_alloc_page(mm->pgdir, la, perm)) == NULL) { // 为BSS段分配新的物理内存页 goto bad_cleanup_mmap; } off = start - la, size = PGSIZE - off, la += PGSIZE; if (end < la) { size -= la - end; } memset(page2kva(page), 0, size); // 将分配到的空间清零初始化 start += size; } } sysfile_close(fd); // 关闭传入的文件,由于在之后的操作中已经不需要读文件了 // (4) setup user stack vm_flags = VM_READ | VM_WRITE | VM_STACK; // 设置客户栈的权限 if ((ret = mm_map(mm, USTACKTOP - USTACKSIZE, USTACKSIZE, vm_flags, NULL)) != 0) { // 将客户栈所在的虚拟内存区域设置为合法的 goto bad_cleanup_mmap; } // setup args uint32_t stacktop = USTACKTOP; uint32_t argsize = 0; for (int j = 0; j < argc; ++ j) { // 确定传入给应用程序的参数具体应当占用多少空间 argsize += (1 + strlen(kargv[j])); // includinng the ending '\0' } argsize = (argsize / sizeof(long) + 1) * sizeof(long); //alignment argsize += (2 + argc) * sizeof(long); stacktop = USTACKTOP - argsize; // 根据参数需要在栈上占用的空间来推算出,传递了参数之后栈顶的位置 uint32_t pagen = argsize / PGSIZE + 4; for (int j = 1; j <= 4; ++ j) { // 首先给栈顶分配四个物理页 assert(pgdir_alloc_page(mm->pgdir, USTACKTOP - PGSIZE * j, PTE_USER) != NULL); } // for convinience, setup mm (5) mm_count_inc(mm); // 切换到客户的内存空间,这样的话后文中在栈上设置参数部分的操作将大大简化,由于具体由于空间不足而导致的分配物理页的操作已经交由page fault解决了,是完全透明的 current->mm = mm; current->cr3 = PADDR(mm->pgdir); lcr3(PADDR(mm->pgdir)); // (6) setup args in user stack uint32_t now_pos = stacktop, argvp; *((uint32_t *) now_pos) = argc; // 设置好argc参数(压入栈) now_pos += 4; *((uint32_t *) now_pos) = argvp = now_pos + 4; // 设置argv数组的位置 now_pos += 4; now_pos += argc * 4; for (int j = 0; j < argc; ++ j) { argsize = strlen(kargv[j]) + 1; // 将argv[j]指向的数据拷贝到客户栈中 memcpy((void *) now_pos, kargv[j], argsize); *((uint32_t *) (argvp + j * 4)) = now_pos; // 设置好客户栈中argv[j]的数值 now_pos += argsize; } // (7) setup tf struct trapframe *tf = current->tf; // 设置中断帧 memset(tf, 0, sizeof(struct trapframe)); tf->tf_cs = USER_CS; // 需要返回到客户态,因而使用客户态的数据段和代码段的选择子 tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS; tf->tf_esp = stacktop; // 栈顶位置为先前计算过的栈顶位置,注意在C语言的函数调用规范中,栈顶指针指向的位置应该是返回地址而不是第一个参数,这里让栈顶指针指向了第一个参数的起因在于,在中断返回之后,会跳转到ELF可执行程序的入口处,在该入口处会进一步使用call命令调用主函数,这时候也就完成了将Return address入栈的功能,因而这里无需画蛇添足压入返回地址 tf->tf_eip = elfp->e_entry; // 将返回地址设置为客户程序的入口 tf->tf_eflags = 0x2 | FL_IF; // 允许中断,根据IA32的规范,eflags的第1位需要恒为1 ret = 0; out: return ret;bad_cleanup_mmap: // 进行加载失败的一系列清除操作 exit_mmap(mm);bad_elf_cleanup_pgdir: put_pgdir(mm);bad_pgdir_cleanup_mm: mm_destroy(mm);bad_mm: goto out;}
- 至此,完成了本练习中的所有编码任务;
问题答复
- 请在试验报告中给出设计实现基于”UNIX的硬链接和软链接机制“的概要设方案,鼓励给出详细设计方案;
- 观察到保存在磁盘上的inode信息均存在一个nlinks变量用于表示当前文件的被链接的计数,因此支持实现硬链接和软链接机制;
- 假如在磁盘上创立一个文件A的软链接B,那么将B当成正常的文件创立inode,而后将TYPE域设置为链接,而后使用剩余的域中的一个,指向A的inode位置,而后再额外使用一个位来标记当前的链接是软链接还是硬链接;
- 当访问到文件B(read,write等系统调用),判断假如B是一个链接,则实际是将对B指向的文件A(已经知道了A的inode位置)进行操作;
- 当删除一个软链接B的时候,直接将其在磁盘上的inode删掉就可;
- 假如在磁盘上的文件A创立一个硬链接B,那么在按照软链接的方法创立完B之后,还需要将A中的被链接的计数加1;
- 访问硬链接的方式与访问软链接是一致的;
- 当删除一个硬链接B的时候,除了需要删除掉B的inode之外,还需要将B指向的文件A的被链接计数减1,假如减到了0,则需要将A删除掉;
- 观察到保存在磁盘上的inode信息均存在一个nlinks变量用于表示当前文件的被链接的计数,因此支持实现硬链接和软链接机制;
试验结果
最终的试验结果符合预期,并且能够通过make grade脚本的检查,如下图所示:
result1.png
result2.png
参考答案分析
练习1
比较练习1的实现与参考答案的实现的区别在于少量细节方面的实现,主要表现在对完全被读写区域覆盖的数据块进行读写的时候,提供的函数事实上是可以完成连续若干块数据块的读写的,但是参考答案没有利用这个特点,而是额外增加了一个循环,而后在循环中对每一个数据块逐次进行读取操作,这有可能会造成时间效率的降低;
练习2
本试验在练习2中的实现与参考答案的实现大致一致,但是经过仔细的比较,观察到一个细节,参考答案在确定参数的长度的时候使用的函数时strnlen,而本试验中的实现使用了strlen,然后者是不安全的,有可能遭到栈溢出攻击的,因而在这个区别上,参考答案的实现显著优于本试验的试验,这也其实我在完成实际的编程任务的时候需要充分考虑到鲁棒性、安全性等细节,这也是我自己觉得在本次操作系统试验中做得有所欠缺的地方;
试验中涉及的知识点列举
在本次试验中涉及到的知识点如下:
- 虚拟文件系统;
- SFS文件系统;
- 将设施笼统为文件的管理方式;
- 系统调用;
- 进程间的调度、管理;
- ELF文件格式;
- ucore中客户进程虚拟空间的划分;
对应的OS中的知识点如下:
- 在ucore中文件系统、虚拟文件系统、以及SFS文件系统的具体实现;
- 在ucore中将stdin,stdout笼统成文件的机制;
- 在ucore中系统调用的机制;
- 在ucore中完成ELF文件从磁盘到内存的加载的具体机制;
它们之间的关系为:
- 前者为后者提供了底层的支持,比方对SFS文件系统的理解才能够使得可以在OS中正确地实现对使用该文件系统的磁盘的访问;
- 前者给后者提供了必要的基础知识,比方只有理解了ELF文件的格式,以及理解了客户进程空间的划分之后,才能够正确地在OS中实现将指定ELF文件加载到内存中运行的操作(exec系统调用);
试验中未涉及的知识点列举
在本次试验中未涉及到的知识点列举如下:
- 进程之间的同步互斥机制;
- 操作系统的启动机制;
- 操作系统对网络协议栈的支持;
试验代码
AmadeusChan/ucore_os_lab/tree/master/lab8
参考文献
- INTEL 80386 PROGRAMMER’S REFERENCE MANUAL 1986
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 清华大学操作系统课程 ucore Lab8 文件系统 试验报告