Lab3 Report

Lab3 Report

思考题

Thinking 3.1

  • 请结合 MOS 中的页目录自映射应用解释代码中 e->env_pgdir[PDX(UVPT)]= PADDR(e->env_pgdir) | PTE_V 的含义
    • e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_V
      • Map its own page table at 'UVPT' with readonly permission.
      • 在 env_pgdir 基地址之上,加上 PDX(UVPT) 的 UVPT 的页目录项偏移
      • 找到指向自己页表的页目录项后,赋上用 PADDR(e->env_pgdir) 取得页目录的物理地址,和 PTE_V 标志位表示只读

Thinking 3.2

  • 请结合 MOS 中的页目录自映射应用解释代码中 e->env_pgdir[PDX(UVPT)]= PADDR(e->env_pgdir) | PTE_V 的含义
    • static void load_icode(struct Env e, const voidbinary, size_t size) 中,调用了 elf_load_seg(ph, binary + ph->p_offset, load_icode_mapper, e)
    • 其中 'e' 为待 program segments 从 'binary' 加载到的用户空间进程的环境,如果没有这个参数,则无法加载到用户空间进程的环境中

Thinking 3.3 (?)

  • 结合 elf_load_seg 的参数和实现,考虑该函数需要处理哪些页面加载的情况。
    • elf_load_seg 函数会从 ph 中获取 va(该段需要被加载到的虚地址)、sgsize(该段在内 存中的大小)、bin_size(该段在文件中的大小)和 perm(该段被加载时的页面权限)
      • 内存中的大小和文件中的大小不一定相同,因为文件中的大小可能不足以填满整个页面,余下的部分用 0 来填充。
      • 剩余空闲页面不够分配
      • 内存不够加载该段的所有数据
    • 图 1

Thinking 3.4

  • 你认为这里的 env_tf.cp0_epc 存储的是物理地址还是虚拟地址?
    • 是虚拟地址,因为程序入口为 e_entry (Entry point virtual address),当我们运行进程时 CPU 将自动从 PC 所指的位置开始执行二进制码,所以 PC 这个位置为虚拟地址

Thinking 3.5

  • 试找出上述 5 个异常处理函数的具体实现位置。
    • handle_int 在 genex.S 文件中

    • handle_mod、handle_tlb 和 handle_sys 都是通过 genex.S 文件中的宏函数 BUILD_HANDLER 实现的

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      .macro BUILD_HANDLER exception handler
      NESTED(handle_\exception, TF_SIZE, zero)
      move a0, sp
      jal \handler
      j ret_from_exception
      END(handle_\exception)
      .endm

      # ...

      BUILD_HANDLER tlb do_tlb_refill

      #if !defined(LAB) || LAB >= 4
      BUILD_HANDLER mod do_tlb_mod
      BUILD_HANDLER sys do_syscall

Thinking 3.6

  • 阅读 init.c、kclock.S、env_asm.S 和 genex.S 这几个文件,并尝试说出 enable_irq 和 timer_irq 中每行汇编代码的作用。
    • enable_irq

      1
      2
      3
      4
      5
      LEAF(enable_irq)
      li t0, (STATUS_CU0 | STATUS_IM4 | STATUS_IEc) # Timer interrupt (STATUS_IM4) will be enabled. 给 t0 为可以打开时钟中断的值
      mtc0 t0, CP0_STATUS #设置给 CP0 中断状态
      jr ra
      END(enable_irq)
    • timer_irq

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      NESTED(handle_int, TF_SIZE, zero)
      mfc0 t0, CP0_CAUSE # 中断原因
      mfc0 t2, CP0_STATUS # 中断开关状态
      and t0, t2 # 中断置 1 的位上且有中断 enable
      andi t1, t0, STATUS_IM4 # 再要求时钟中断 enable
      bnez t1, timer_irq # 如果 t1 为 1,表示确实有中断,则跳到 timer_irq
      // TODO: handle other irqs
      timer_irq:
      sw zero, (KSEG1 | DEV_RTC_ADDRESS | DEV_RTC_INTERRUPT_ACK) #关闭时钟中断,进行中断处理
      li a0, 0 # 传入参数 0?
      j schedule # 走到调度函数,进行进程调度
      END(handle_int)

Thinking 3.7

  • 阅读相关代码,思考操作系统是怎么根据时钟中断切换进程的。
    • 首先设置实时中断的频率,每次发生实时中断时在 handle_int 中的 timer_irq 里,调用 schedule 函数,进行进程调度
    • 调度中每次首先判断当前要运行的进程地址是否需要让步,是否还有剩余时间片,进程地址是否存在,以及运行状态是否是可运行,即:(yield || !count || !e || e->env_status != ENV_RUNNABLE)
    • 如果都满足,则将其的时间片数量 count-- ,再运行这一个时间片。

难点分析

Exercise 3.1

  • 要保证 LIST_INSERT_HEAD 之后的顺序和在数组中顺序相同,需要反向遍历插入

Exercise 3.3

  • 每一个进程刚被分配的时候最开始要分配一个页面,作为页目录,并且被分配表示页面的引用次数要 +1
  • env_pgdir 是 Kernel virtual address of page dir,要将 page 转换为虚拟地址

Exercise 3.4

  • 注意 env_setup_vm 成功的返回值为 0,失败为 -4
  • 要分配 env_user_tlb_mod_entry'(lab4),'env_runs'(lab6),'env_id'(lab3),'env_asid'(lab3),'env_parent_id' (lab3)
    • asid_alloc(&(e->env_asid)) 也可能因为最多 32 个 ASID 满了而分配失败

Exercise 3.6

  • e->env_tf.cp0_epc = ehdr->e_entry; 给出出错时返回 EPC 的地方,即程序的第一条指令的虚拟地址

Exercise 3.7

  • 创建进程的时候需要赋予其初始优先级,设置为 RUNNABLE 状态,加载某一个需要运行的 ELF 文件到进程地址中,即 load_icode(e,binary,size),最后需要将进程插入到进程调度队列中

Exercise 3.8

  • 进程运行时如果当前有进程在运行,则需要先保存存在于 KSTACKTOP 以下的一个 sizeof(TrapFrame) 区域中的寄存器栈帧。即 curenv->env_tf = ((struct Trapframe)KSTACKTOP - 1);
  • 运行完毕之后需要回到用户态,要把寄存器栈帧里的东西还原,即 env_pop_tf(&(curenv->env_tf), curenv->env_asid);

Exercise 3.9 && 3.10

  • 异常处理函数入口分配
1
2
3
4
5
6
7
8
9
10
11
12
13
.section .text.tlb_miss_entry # tlb_miss_entry 异常 地址在. = 0x80000000;
tlb_miss_entry:
j exc_gen_entry

.section .text.exc_gen_entry # exc_gen_entry 异常 地址在. = 0x80000080;
exc_gen_entry:
SAVE_ALL # 使用 SAVE_ALL 宏将当前上下文保存到内核的异常栈中。
/* Exercise 3.9: Your code here. */

mfc0 t0, CP0_CAUSE # 保存 CP0_CAUSE 到 t0
andi t0, 0x7c # 0111,1100: ExcCode
lw t0, exception_handlers(t0) # 以 ExcCode 为索引,在 exception_handlers 地址区域的对应的中断处理函数,加载函数跳转地址到 t0
jr t0 # 跳转到对应的中断处理函数中
  • 入口分配之后在 kernal.lds 中定义异常处理程序的入口地址

实验体会

  • 用 DEBUGK 或者 printk 可以看整个程序走到了哪一步卡死,方便调试
  • 进程调度的方式,以及背后涉及到的中断控制需要理解
  • 注意控制进程的属性,对应到的可执行文件关联,以及每个进程建立一个页表