官方提醒

  • 在之后的 Lab 中,运行 MOS 时容易出现的 too low 等错误信息就是访问了一个过低的地址导致的。此时应该检查代码中是否存在访问非法内存(如空指针、野指针)的操作,或者忘记将物理地址转化为 kseg0 内核虚拟地址的问题

实验代码阅读与把握

  • 整体结构把握
    • 完整的函数调用关系可以查看指导书每个Lab后面的调用顺序图(利用好指导书
    • 把握好顶层架构
      为什么这样划分文件结构?每部分文件的作用是什么?它们怎么协同?弄明白了这些问题,才能更顺利地编码
    • 用好外部变量
      很多时候要用的量和想实现的功能在相应的文件中都有宏定义或者函数定义。所以先阅读文件,找不到再自己写
  • 具体参数意义理解
    • 通过检索的方式在指导书、讲解 PPT 等多处资源中广泛找思路
      • 比如vprintfmt参数列表中的data是什么?
        指导书中没有说明,但讲解 PPT 中讲的很清楚。不要纠结与单一资料,学会广泛检索。
    • 通过阅读并理解更大范围(比如完整函数、上层调用函数)代码段,明白本质,进而把握单个参数意义
      • 比如vprintfmt参数列表中ap这一可变参数指的是哪些参数?
        vprintfmt函数实现的是格式化输出的主体逻辑,被printk调用,参数列表中的 ap 参数也是从上层函数中得来。结合printk的用法,比如printk("%d%c%ld", a, b, c),很容易明白可变参数就是需要输出的一系列变量,而fmt就是引号中的字符串。

编写

  • “由俭入奢易,由奢入俭难“,某种程度上也适用于代码编写。用最简单直接精炼的代码实现,不要为求安心无脑加一些似是似非的代码,容易导致一些意想不到的错误而且难找原因。一定要百分百确定有漏洞恰当地打补丁
  • 不是只填空就可以了,注意初始代码的改动。已经定义好的参数的初始化)代码本身的行为一定要明确! 比如处理好各变量初值(0、NULL),空指针使用。不要过渡依赖编译器
  • 解引用指针之前必须判断是否为空/试错(见后”常用函数“)!!!野指针会导致死循环或超时无法结束
    • 注意不同函数的错误值不一样不要混用!!
  • “仿照xxx部分进行实现”,有一些部分不确定要不要保留先保留!!(很有可能涉及到一些隐性机制的实现)
  • “当出现错误时返回相应错误值”常用操作是r记录返回值再判断**if(r>0)**
    • 是判断是否>0!!错误值一般都是负值,正常值不一定是0也可能是正数!!
  • 用 gitlab IDE 编辑提交之后,切回跳板机运行前记得git pull!!
  • 循环时一定注意
    • i++
    • 数据更新(比如每次开始前读入下一部分数据
  • 注意运算符优先级
    • 移位运算符的优先级低于加号,需要加括号
    • * 优先级低于 . ,故不需要加括号

调试/debug

  • debug 时优先检查条件判断是否出错!
    • less than,应该是-lt 不是 -le 🥲
  • 先本地开优化编译运行!根据报错或者运行异常去找原因,实在不行加printf

make 的使用

编译

  • make 编译完整内核
    • 需要在 init/init.c 的 mips_init 里添加自己的测试代码
  • make test lab=<x>_<y>编译指定测试点, lab 的第 y 个测试用例
    eg make test lab=1_2

运行与调试

  • make run 运行
  • make dbg 使用 QEMU 模拟器以调试模式运行内核,并进入 GDB 调试界面。
  • make objdump 将项目中的目标文件反汇编
  • Ctrl+A+X 退出 QEMU

实验代码结构

  • kern里的.c文件和include里面的.h文件很多是一一对应的,分别为某部分功能的实现以及相关函数、宏定义

  • include目录——存放系统头文件和一些常用函数定义

    • mmu.h——存储内存布局、与地址转换、虚拟地址管理相关的宏

      • 有一张内存布局图,在填写 linker script 的时候需要根据这个图设置相应节的加载地址
      • PDX(va) :页目录偏移量(查找遍历页表时常用)
      • PTX(va) :页表偏移量(查找遍历页表时常用)
      • PTE_ADDR(pte) :获取页表项中的物理地址(读取 pte 时常用)
      • PADDR(kva) :kseg0 处虚地址 物理地址
      • KADDR(pa) :物理地址 kseg0 处虚地址(读取 pte 后可进行转换)
    • pmap.h ——存储与地址相关的宏

      • va2pa(Pde *pgdir, u_long va) :查页表,虚地址 物理地址(测试时常用
      • pa2page(u_long pa) :物理地址 页控制块(读取 pte 后可进行转换)
      • page2pa(struct Page *pp) :页控制块 物理地址(填充 pte 时常用)
    • print.h

      • 声明了vprintfmt函数,解释了相关参数
    • string.h——定义了一些常用的字符串函数,忘记了具体参数含义可以来这里找

    • printk.h——定义了内核崩溃宏

    • elf.h

      • ELF_FOREACH_PHDR_OFF——for循环迭代遍历所有段头
    • type.h——定义和align有关的宏

      • ROUNDDOWN(a, n) 返回 ⌊$\frac{a}{n}$⌋n(将 a 按 n 向下对齐),要求 n 必须是 2 的非负整数次幂
      • ROUND(a, n) 返回 ⌈$\frac{a}{n}$⌉n(将 a 按 n 向上对齐),要求 n 必须是 2 的非负整数次幂
    • queue.h——链表、双向队列宏的定义

      太多了…具体功能见文件注释

    • trap.h——定义Trapframe结构体

    • env.h——和进程调度有关的结构体定义

    • stackframe.h——定义宏 SAVE_ALL

      • SAVE_ALL——将当前的 CPU 现场(上下文)保存到内核的异常栈中
    • kclock.h——定义RESET_KCLOCK 宏,完成了对 CP0 中 Timer 的配置

    • syscall.h——定义系统调用号

  • include.mk

  • init目录——初始化内核相关代码

    • start.S

      • _start 函数

        是 CPU 控制权被转交给内核后执行的第一个函数,主要工作是初始化 CPU 和栈指针,然后跳转至 MOS 的初始化函数( mips_init )

        内核栈空间的地址可以在 include/mmu.h 中看到,注意栈的增长方向

    • init.c

      • mips_init 函数

        内核中各模块的初始化函数都会在这里被调用

  • kern目录——存放内核的主体代码

    • machine.c——往 QEMU 的控制台输出、读入字符,重置系统

      原理为读写某一个特殊的内存地址

    • printk.c——实现了 printk

      实际上是把输出字符的函数,接受的输出参数给传递给了 vprintfmt 这个函数

    • console.c——实现printcharc(向某内存地址写入)

    • pmap.c——实现内存管理

      • page_init() 初始化 pages 数组中的 Page 结构体以及空闲链表
      • mips_detect_memory() 探测硬件可用内存,并对一些和内存管理相关的变量进行初始化
      • mips_vm_init 建立内存管理机制
      • alloc() 注意!只在建立页式内存管理机制之前使用
    • env.c——实现进程调度

    • traps.c——定义异常向量组,用于定位中断处理程序

    • genex.S —— 异常处理函数的声明,异常处理流程实现

    • env_asm.S

      • env_pop_tf ——用于设置 ASID 和重置时钟,以及最后从异常处理中返回
    • sched.c——实现时间片调度

    • syscall_all.c——系统调用相关函数实现

      • do_syscall——内核部分的系统调用机制实现
    • tlbex.c——写时复制相关函数的实现

  • kernel.lds

  • lib目录——存放一些常用库函数的实现

    • print.c——实现了 vprintfmt 函数(实现了格式化输出的主体逻辑)、打印字符、字符串和数字的函数
    • string——实现了一些常用的字符串函数
    • elfloader.c——解析 ELF 文件的相关函数和宏
  • Makefile

  • mk目录

  • tests目录——存放公开的测试用例

  • tools目录——构建时辅助工具的代码

    • readelf目录
      • elf.h ——存放解析ELF文件要用的三个关键数据结构

        • 包括三个结构体,第一个对应ELF 的文件头,第二个对应节(section)头表,第三个对应段(segment)头表
      • readelf.c ——用于解析ELF文件

        • is_elf_format函数——判断输入是否为ELF文件
        • readelf 函数——输出 ELF 文件中所有节头中的地址信息
    • fsformat.c——创建符合我们定义的文件系统镜像的工具
  • user目录

    • lib目录
      • debug.c——实现debugf函数(处理8号异常)
      • syscall_lib.c ——实现syscall_* 函数
      • fork.c
        • fork 函数——fork机制实现的核心
        • cow_entry 函数——写时复制处理的函数
        • duppage 函数——父进程对子进程页面空间进行映射以及相关标志的函数
      • entry.S——用户进程的入口
      • libos.c——用户进程入口的 C 语言部分
      • file.c、fd.c、fsipc.c —— 存放文件系统用户库
        • fsipc.c ——实现与文件系统服务进程的交互
        • file.c ——实现文件系统的用户接口
        • fd.c ——实现文件描述符,允许用户程序使用统一接口操作文件
    • include目录
      • lib.h——系统调用相关宏定义
      • syscall.h——定义系统调用号
  • fs目录——文件系统服务程序

    • fs.c ——实现文件系统的基本功能函数
    • ide.c ——通过系统调用与磁盘镜像进行交互
    • serv.c 中——文件系统服务进程主干函数,通过 IPC 通信与用户进程 user/lib/fsipc.c 内的通信函数进行交互

常用函数

  • 错误处理相关

    用try最稳妥

    • panic_on 直接崩溃
    • try / if(某操作返回值==错误值) 抛出去给上层函数处理
    • user_panic 用户态进程崩溃
      • user_panic(“syscall_set_trapframe returned %d”, r);
  • 清空内存 memset 实现于lib/string.c

  • ROUND(a, n) 返回 ⌈$\frac{a}{n}$⌉n(将 a 按 n 向上对齐),要求 n 必须是 2 的非负整数次幂

一些 Q & A

在编写操作系统的c代码时,变量分别存放在哪部分物理内存?page_alloc() 申请的物理页面用来存什么?

代码段、全局变量和静态变量在编译阶段被确定,链接时被链入0x8002 0000向上这部分物理内存

临时变量在内核栈区,被存放在0x8040 0000向下KSTKSIZE部分内存。

申请物理页面的用途:lab3用于给新进程申请页目录、给内核进程加载外部二进制elf文件并执行;lab4用于为用户进程申请用户栈、duppage()时为COW复制的页表申请物理空间;lab5在进程内存放文件内容、lab6用于给用户进程加载外部二进制elf文件来执行命令。申请的页面存放在0x8040 0000向上这部分物理内存