操作系统能力大赛日志

这是2024系统能力大赛操作系统内核赛道作品Acore开发日志,本项目基于XV6-RISVCC开发。

2024.11.14

结束前置学习,克隆项。

2024.11.20

        我们完成前置学习之后,想尝试在Windows中运行Linux系统,根据网络上章资料显示,我们需要先安装wsl(Install WSL | Microsoft Learn),因为WSL 提供了一种在 Windows 上本地运行 Linux 命令行工具和应用程序的方法,是开发者和技术用户使用 Linux 工具链而不离开 Windows 系统的一种便捷方式。

        若想运行操作系统代码,则需要另下载交叉编译工具链,这是一种工具集,允许开发人员在一种平台(宿主平台)上编译代码,使其能够在另一种平台(目标平台)上运行。比如,在 Windows 上编译为 Linux 或 ARM 架构的代码。交叉编译工具链通常包括:

  • 交叉编译器(如 GCC)
  • 链接器
  • 目标平台的库
  • 调试工具

交叉编译工具链通常用于嵌入式系统开发、不同架构之间的开发,或在一个平台上为另一平台构建应用程序。

        途中,下载这两个东西真的是太麻烦了,版本不兼容,下载不上,安装失败,连不上网,找不到软件包等等……

        举步维艰的第一步!!!

2024.11.23 

        在完成前置的配置之后,我们学习了往期优秀作品中的GalaxyOS操作系统,因为,作品是没有编写shell,我们在学习完代码后,无法调试,故放弃。

        在和学校指导老师的交流后,加上galaxy也是基于RISVC架构,我们也选择在该架构上进行优化创新。

        经过老师的建议,我们决定在虚拟机中进行开发,因为这样会更稳定方便;此外,我们学习下载了RISVC架构下的组件:qemu,OpenSBI…….

2024.11.24(3:13am)

        在完成XV6的前置学习之后,我们了解到:XV6的内存分配方式是一次性全部加载。在原来的walk函数中,XV6在初始化的时候遍历了多级页表,并为空的页表项分配页框,但是在初始化的时候一次性全部分配会有内存浪费的问题,例如有的页面只在运行结束前很短的时间运行,那它在运行之前存储在内存中就是不合适的。因此这样是一种很浪费内存空间的分配方式,我们打算实现懒惰加载以节省内存空间。

        我们的构想是:只允许1号和2号进程在初始化的时候一次性分配内存,其它进程执行walk(能查到到,即查找,否则加载)。因为1号进程是init,2号进程是shell进程,这两个进程常驻内存。

2024.11.28

       遇到问题的第一天?

        我们编写了 walk 函数的副本 walk1,因为 uvmalloc 函数负责初始化,并且在初始化过程中需要判断页表项是否无效,而页表项无效的情况仅会在初始化时出现。后续在其他情况下,我们也可能需要处理页表项无效的情况,因此我们创建了 walk 函数的副本来满足这一需求,同时确保 uvmalloc 调用的是 walk1,以便保留其原有功能。

        如果我们要加载页面,首先需要确定加载哪一页,并且还需要知道该页在 ELF 文件中的范围。我们可以通过查询 ELF 文件的地址并将其保存在用户栈中,在加载完成后再进行回收。为此,我们修改了进程结构体,新增了一个变量用于记录 ELF 文件的地址。这样,在缺页处理中,我们可以根据缺页的虚拟地址计算出该虚拟地址所在虚拟页面的起始地址,并进一步计算该地址在 ELF 文件中的偏移量。然后,从该偏移量处加载一页大小的内容

2024.12.1

         在完全完成上一次的构想后,我们发现实际运行时并不可行。发生缺页异常时,系统会直接执行缺页异常处理,而不会调用 walk 函数。因此,我们推测:在判断页表内容时,并未使用 walk 函数,可能是 RISC-V 架构自带了用于解析页表的机制,实际运行时,执行的是这个机制。walk 函数的应用仅仅是与该机制的规则相似,因此这种方式是不可行的。

        我们进一步猜测,缺页异常的触发进入了 usertrap,并根据异常号进行了单独处理。打印一条信息后,程序进入了死循环,表明处理流程进入了 usertrap。我们通过调试确定,缺页异常触发后,流程确实来到了 usertrap。

        为了解决这个问题,我们修改了 trap.c 文件:在文件中对异常号进行了判断,确保在异常号为 12 或 15 时,触发缺页异常的处理。在 page_fault_tackle1 函数中,我们根据进程访问的虚拟地址查找 ELF 文件的偏移量,并计算该地址对应的页。然后,我们将该页加载到内存中,从而完成了内存的分配。

2024.12.7

       在测试上次的结果后,我们发现 ls 能成功运行,但 mkdir 无法运行,问题出在 uvmunmap 中。原先结构体中的 sz 记录了已使用的内存大小,释放内存时是从 0 到 sz 一页一页地进行释放。但调查发现,问题的根源在于页表项的内容是 0,说明 ELF 文件的所有内容并不一定都被执行。因此,某些页面尚未加载。

        针对这个问题,我们进行了修改:增加了一条判断逻辑,若页表项的内容为 0,则跳过后续的物理页面释放。再次测试 mkdir,问题得到解决,操作成功。

        此时,我们认为懒惰加载已经实现,准备向老师展示。然而,老师指出我们的测试用例过于简单,建议使用更复杂的测试用例来验证系统。于是,我们编写了一个新的测试用例:首先打印一句话,然后创建一个子进程,在子进程中打印另一句话。但子进程没有成功打印该信息。

        分析原因后,我们发现问题出在 uvmcopy 中,具体是对页表项无效进行判断时,触发了 panic。这是因为父进程中的某些页面尚未加载,导致在判断页表项无效时出现了错误。因此,我们需要在判断页表项无效时,额外检查其内容是否为 0。

        为了解决这个问题,我们对缺页处理做了修改:当进程进入缺页处理时,如果它的父进程是 init 进程,我们按照原来的方式加载页面。如果父进程不是 init 进程,则从父进程的 ELF 文件中加载对应的内容。同时,我们修改了页面释放的逻辑:在判断页表项无效时,原本的条件触发了 panic。现在,我们不仅要判断页表项是否无效,还要确保其内容不为 0,避免不必要的 panic。

2024.12.10

        我们计划实现 COW(Copy-on-Write),即读后写。经过思考,首先需要修改的是 uvmmap 这个函数。原本,这个函数是用来让子进程复制父进程的所有内容,并申请一块内存空间。现在,我们希望它不再申请新的内存,而是让父子进程共享同一个物理页面,从而实现共享内存。因此,uvmmap 的逻辑需要做相应的调整,特别是在 uvmcopy 中共享的部分。我们需要修改共享逻辑:只有在页表项的内容非零时,才会进行共享;如果页表项为零,说明该页面还没有被加载。

        接下来,我们面临一个新问题:共享后的页面如何释放。我的想法是,所有通过 mappage 完成映射的页面,都将页表项的权限设置为可写。这样,当发生共享时,我们就可以修改权限,去掉可写权限,释放时通过判断页面是否可写来决定是否可以释放。此外,还需要检查父进程是否为 init 进程。因为 init 进程的页面永远不会被释放,我们假设每个进程的子进程都会在父进程释放之前完成资源释放。对于共享页面,在父进程为 init 进程的情况下,我们将释放所有页面,无论它们是否为只读。

        此外,读后写的核心问题是:只有在进程修改页面时,才会真正申请一块新的内存。我们通过权限来触发异常处理,以实现这一点。因为共享时已经去除了可写权限,所以进程修改页面时会触发异常(异常号为 5)。在这个异常处理中,我们会为该虚拟地址重新申请一块空间,并通过 mappage 完成映射,这样就能恢复可写权限,退出异常处理后问题就解决了。

2024.12.15

        在新的调试过程中,我遇到了一个难以理解的问题。当子进程共享了父进程的页面时,按理来说不会触发缺页异常。经过检查,进入缺页异常的虚拟地址的页表项与父进程的页表项一致。接着,我逐步排查了问题:首先查看了 uvmcopy 中共享了哪些页面,接着检查了父进程触发缺页异常后分配的地址(总共四页,大小为 16384 字节)。经过检查,我发现父子进程共享的页面是相同的(这里浪费了不少时间,因为没有更好的调试方法,只能通过 printf 一一找出来),只是权限位发生了变化。

        后来,我请教了路浩东学长,他告诉我,可能是 A 位和 D 位的权限发生了变化。我尝试在缺页异常处理中直接设置这两个权限(因为这是一个有效的映射页表项进入了缺页异常)。我以为这样就能解决问题,但结果依然没有成功。后来我试着赋值所有权限,问题依旧没有解决,这说明问题并不在于权限设置。

        进一步调查后,我发现父进程仅处理了一次缺页异常,但共有三页需要处理。我继续调查另外两页的来源,并在 exec 中发现另外两页是用户栈。比较父子进程的页时,发现进入缺页异常的有效页是用户栈的页面。查看内存分配方式后,我发现所有页的处理方式相同,没有特殊处理。推测可能是某些未知的页面机制影响了内存分配。因此,我决定不对用户栈的两页进行 COW,其他页面继续使用 COW。修改后,父子进程都能够正常运行,但依然存在一些问题。

        如果完全禁用 COW,进程只能访问第一页和第三、四页,第二页根本没有加载;而启用 COW 后,第二页开始被访问。这个第二页的问题比较复杂,尽管它进入了缺页异常并完成了加载,但一旦退出异常处理,它马上又进入缺页异常处理。这意味着尽管页面加载完成,但没有实际生效,且由于加载了两次,导致出现了 panic: remap 错误。

        至此,我们所有人都感到困惑,不理解为什么共享页面会改变进程的页面访问,为什么加载了页面后依然会进入缺页异常处理,并且已经加载的页面为何无法生效。经过多次尝试和调试,依然没有找到明确的解决方案。路浩东学长也没有提出新的解决办法。

2024.12.28

        在开始实现 rename 系统调用时,首先需要在内核中完成系统调用的注册与声明。这是确保系统调用能够被内核正确识别并执行的基础步骤。具体来说,我在 syscall.h 文件中为 rename 系统调用分配了一个唯一的系统调用号(SYS_rename),并在 syscall.c 文件中声明了 sys_rename 函数。接下来,我通过将 sys_rename 函数添加到系统调用表(syscalls[])中,使其可以根据系统调用号进行调用。

        在实现系统调用的核心逻辑时,我在 sysfile.c 文件中编写了 sys_rename 函数。该函数通过调用 argstr 获取原文件路径和新文件路径,并通过 rename1 函数实现文件的重命名操作。在文件系统的操作中,我使用了 begin_op 和 end_op 来确保事务的原子性,防止文件系统操作在未完成时被中断。rename1 函数则负责删除原文件路径中的目录项,并在新文件路径下创建新的目录项,从而完成重命名和移动文件的操作。至此,rename 系统调用的注册与实现部分顺利完成。

2024.1.3

        在完成 rename 系统调用的实现后,我想要在用户程序中测试它。然而,当我尝试直接在用户程序中调用 sys_rename 时,程序却报错,提示系统调用没有被正确执行。这个问题让我感到非常困惑,因为我明明已经在内核中完成了 rename 系统调用的实现。于是,我开始仔细检查并思考可能的问题所在。

        经过一番排查,我发现问题的关键在于用户程序并不能直接通过 sys_* 这样的函数调用系统调用。在内核中,所有的系统调用都封装在 sys_* 函数中,但在用户程序中,我们并不会直接调用这些内核函数。为什么呢?这时我发现了一个重要的细节:mkdir 等命令并不是直接调用 sys_* 函数,而是通过汇编代码文件 usys.S 来封装系统调用。原来,所有的系统调用都是通过汇编语言中的 ecall 指令来触发的,而 usys.S 文件正是实现这一封装的地方。

        usys.S 文件负责将系统调用号通过 ecall 指令传递给内核,并且正确处理系统调用的参数。这使得用户程序能够通过简单的函数调用来触发复杂的系统调用,而不需要直接与内核代码交互。看到这一点,我突然明白了自己的问题所在:我需要在 usys.S 中为 rename 系统调用添加相应的汇编代码,才能让用户程序正确调用这个系统调用。

        我参考了其他系统调用的实现方式,在 usys.S 文件中为 rename 系统调用添加了对应的汇编代码。通过这种方式,用户程序能够通过调用封装后的 rename 函数,正确地传递系统调用号和参数到内核。与此同时,我还修改了 user.h 中的 rename 函数声明,确保它能够接受两个参数——即旧文件路径和新文件路径。这样,用户程序就能够像调用普通函数一样,轻松地使用 rename 系统调用。

2024.1.4

        在实现 rename1 系统调用的过程中,我首先需要编写两个函数来提取文件路径中的目录部分和文件部分。第一个函数用于提取路径中的文件名部分,另一个则是提取路径中的目录部分。这些功能对于 rename 操作至关重要,因为我需要先获取目标文件的完整路径和目录,以便正确处理文件的重命名。

        在编写 basename 函数时,我遇到了一个非常棘手的问题。函数设计的初衷是提取路径中的文件部分,去掉目录信息,最终返回文件名。但是在测试过程中,我发现了一个异常现象:每当我在同一个函数中连续处理两个路径时,第二次的路径竟然覆盖了第一次的结果。更具体地说,路径字符串的内容被意外修改了,导致路径的结果变得不正确。

        这个问题一开始让我感到非常困惑,因为我从未遇到过一个函数调用两次后,前一次的变量会被覆盖。经过一番深入分析,我发现问题的根源在于函数内部对指针的操作。在 C 语言中,指针的传递是按地址传递的,而不是按值传递。这意味着当我在两个不同路径上操作时,指针指向的内存位置被改变,造成了前一次操作的数据被覆盖。

        为了应对这个问题,我想出了一个“取巧”的解决办法:我将函数 basename 拆分成了两个完全相同的副本,每次分别处理新旧路径时,分别调用不同的副本。通过这种方式,我避免了指针重用的冲突,从而解决了路径覆盖的问题。虽然这种做法相对“浪费”了一些内存(因为我们重复了相同的代码),但它在短期内解决了问题,让我能够继续推进 rename 功能的实现。

        经过这次小小的折腾,我的 basename 函数终于能够正确地处理路径,准确提取出文件名部分,为 rename1 函数的顺利实现打下了基础。这一过程不仅让我理解了 C 语言中指针操作的细节,也让我更加小心地对待内存管理问题,尤其是在多次调用函数时,如何避免数据被意外覆盖。

2024.1.7

        在进行文件重命名操作时,我遇到了一个奇怪的问题——被重命名或移动的文件竟然“变形”了!具体地说,文件的内容发生了奇怪的变化,磁盘块编号和文件大小改变了,导致文件的内容不再正确。

        于是,我开始仔细排查,逐行调试我的代码,查找所有可能的问题。经过一番深思熟虑,我终于意识到,问题的根源竟然藏在 rename1 函数中的一个细节里——文件引用计数没有管理好!看起来,文件的目录项被修改了,路径也更新了,但文件的引用计数依然是“原地踏步”。如果引用计数没有及时更新,文件的 inode 就可能出现不一致的情况,进而导致文件内容被篡改。这就像是一本书的目录页被修改了,但内容却没更新,读者读到的内容就会乱套。

        但问题并不仅仅停留在这里。我最开始在调试的时候并没有在用户程序中打开被操作的文件,这导致在执行 rename 系统调用时,被操作文件的名称发生了变化,甚至整个文件的内容也被改变了。这时,文件看似被重命名,但实际文件内容变成了重命名后的文件内容,这让我非常困惑。经过进一步调试,我使用 GDB 调查了 iget 函数中的 itable 操作,发现它是遍历 itable 中的 inode。开始时,我以为 inode 是目录项指向的数量,也就是文件的引用计数。于是,我打印并查看了 itable 中的内容,结合调试,发现总有 3 个 inode 是常驻的,其中一个 inode 是在发生变化的。

        这让我非常疑惑,因为如果 itable 中的 inode 是目录项指向的个数,那么所有文件的 inode 都应该在其中。然而,我发现实际情况并非如此。我开始怀疑,itable 应该是用来记录引用计数的,也就是引用某个文件的进程数,而不仅仅是目录项的指向数。这时,我想到 ls 这个命令,它本身就是一个类似的文件操作程序。我用 ls 进行了调试,发现它在操作文件时有显式的 open 和 close 行为,这才恍然大悟:itable 中记录的是文件被多少个进程打开了。原来,open 会显式增加文件的引用计数,而 close 则减少引用计数。

        在调试过程中,我也逐步明白了错误的根源。因为我在用户程序中没有显式打开文件,itable 中自动获取的 inode 总是最后一个 inode,即程序本身的 inode。而由于没有打开被操作的文件,itable 中并没有对应的 inode,结果导致被重命名的文件与当前程序 i 的内容混合了。

        意识到这一点后,我决定在用户程序中增加文件打开的操作。每次执行系统调用前,我会通过 open 系统调用显式打开被操作的文件,这样文件的引用计数就会正确增加,并且 itable 中也能够正确地找到对应的 inode。修改完后,我重新运行程序,发现这次问题迎刃而解:文件内容恢复了正常,inode 也不再混乱。

        这个修改让文件的引用计数得到了合理的管理,inode 被正确加载和释放,文件的内容也恢复了正常。通过这个过程,我不仅解决了文件重命名的问题,也更加深刻地理解了操作系统中文件引用计数与 inode 管理的机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值