Thinking 3.1

请结合 MOS 中的页目录自映射应用解释代码中 e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_V 的含义。

UVPT是用户页表的起始地址,PDX(UVPT)取了虚拟地址UVPT的高十位,作为页目录偏移量;e->env_pgdir是该进程的页目录基地址(位于 kseg0 的虚拟地址),因此e->env_pgdir[PDX(UVPT)] 代表页目录的第PDX(UVPT)个页目录项

PADDR(e->env_pgdir)是页目录的物理地址,PTE_V是页表项的有效位

因此这句代码的含义是:把进程页目录的物理地址存在页目录的第PDX(UVPT)个页目录项中,并把有效位置1

进一步,根据自映射的知识我们知道,自映射页目录项$PDE_{self-mapping}$相对于页目录基地址的偏移是$PT_{base}>>22$,也就是这里的PDX(UVPT)

因此,这里实现了页目录的自映射,即让自映射页目录项存储了页目录的物理地址。

Thinking 3.2

elf_load_seg 以函数指针的形式,接受外部自定义的回调函数 map_page。 请你找到与之相关的 data 这一参数在此处的来源,并思考它的作用。没有这个参数可不可 以?为什么?

通过搜索我们可以找到env.c的load_icode函数中调用了elf_load_seg函数,调用语句为:

1
panic_on(elf_load_seg(ph, binary + ph->p_offset, load_icode_mapper, e));

这里传入的e类型为struct Env *e,继续往上找到load_icode的调用,发现在env.c的env_create函数中调用了load_icode,调用语句为:

1
load_icode(e, binary, size);

env_create函数中的e是调用env_alloc(&e, 0)新建的env。

由此我们找到了data这一参数的来源:在env_create中新建的Env结构体指针,指向新创建的pcb块,作用是使得load_icode_mapper得到进程的页目录env_pgdir和env_asid。

不可以没有这个参数,因为函数的作用就是向进程的内存中加载一个segment,所以必须能获取该进程的信息

Thinking 3.3

结合 elf_load_seg 的参数和实现,考虑该函数需要处理哪些页面加载的情况。

首先检查虚拟地址va是否页对齐,如果不是则需要单独处理offset部分

然后循环处理从i到bin_size的部分,按照整页进行复制

最后检查i,如果i<sgsize说明需求的段长大于实际的段文件长度bin_size,需要在余下部分补0

Thinking 3.4

​ “这里的 env_tf.cp0_epc 字段指示了进程恢复运行时 PC 应恢复到的位置。我们要运行的进 程的代码段预先被载入到了内存中,且程序入口为 e_entry,当我们运行进程时,CPU 将自动 从 PC 所指的位置开始执行二进制码。”

​ 思考上面这一段话,并根据自己在 Lab2 中的理解,回答: • 你认为这里的 env_tf.cp0_epc 存储的是物理地址还是虚拟地址?

虚拟地址。

env_tf.cp0_epc里面存储的是程序入口地址e_entry,通过查看elf.h我们可以知道e_entry是程序入口的虚拟地址。

Thinking 3.5

试找出 0、1、2、3 号异常处理函数的具体实现位置。8 号异常(系统调用) 涉及的 do_syscall() 函数将在 Lab4 中实现。

kern/genex.S

handle_int的实现:

1
2
3
4
5
6
7
8
9
10
20 NESTED(handle_int, TF_SIZE, zero)
21 mfc0 t0, CP0_CAUSE
22 mfc0 t2, CP0_STATUS
23 and t0, t2
24 andi t1, t0, STATUS_IM7
25 bnez t1, timer_irq
26 timer_irq:
27 li a0, 0
28 j schedule
29 END(handle_int)

handle_tlb mod sys reserved的实现:定义BUILD_HANDLER然后调用

1
2
3
4
5
6
7
8
9
 4 .macro BUILD_HANDLER exception handler
5 NESTED(handle_\exception, TF_SIZE + 8, zero)
6 move a0, sp
7 addiu sp, sp, -8
8 jal \handler
9 addiu sp, sp, 8
10 j ret_from_exception
11 END(handle_\exception)
12 .endm
1
2
3
4
5
6
7
8
31 BUILD_HANDLER tlb do_tlb_refill
32
33 #if !defined(LAB) || LAB >= 4
34 BUILD_HANDLER mod do_tlb_mod
35 BUILD_HANDLER sys do_syscall
36 #endif
37
38 BUILD_HANDLER reserved do_reserved

Thinking 3.6

阅读 entry.S、genex.S 和 env_asm.S 这几个文件,并尝试说出时钟中断在哪些时候开启,在哪些时候关闭。

在enable_irq开启,timer_irq处理,处理后关闭

entry.S中实现了exc_gen_entry,这是一个异常分发函数,根据异常的不同类型选择不同的异常处理函数。

env_asm.S文件中实现了函数env_pop_tf

1
2
3
4
5
6
7
8
LEAF(env_pop_tf)
.set reorder
.set at
sll a1, a1, 6
mtc0 a1, CP0_ENTRYHI
move sp, a0
j ret_from_exception
END(env_pop_tf)

该函数将传入的 asid 值设置到 EntryHi 寄存器中,表示之后的虚拟内存访问都来自于 asid 所对应的进程。另外该函数将sp 寄存器地址设置为当前进程的 trap frame 地址,这样在最后调用 ret_from_exception 从异常处理中返回时,将使用当前进程的 trap frame 恢复上下文。程序也将从当前进程的 epc 中执行(epc 的值在 load_icode 中根据 elf 头设置为程序入口地址)。

Thinking 3.7

阅读相关代码,思考操作系统是怎么根据时钟中断切换进程的。

首先,模拟器通过kclock_init函数完成时钟中断的初始化,设置了时钟中断发生的频率;然后,调用enable_irq函数开启中断;

在进程运行过程中,若时钟中断产生,则会触发MIPS中断,系统将PC指向 0x800000080,即跳转到 .text.exc_gen_entry代码段,进行异常分发;

由于是中断,判断为0号异常,则跳转到中断处理函数handle_init;

进而判断属于中断中的 IM4(时钟中断),进而跳转到 timer_irq函数处理;

timer_irq函数调用schedule函数开始进行进程调度;

在schedule函数中,将当前运行的进程控制块curenv取出来;对当前进程的可用时间片数量减一(即 count–;);当满足以下四种条件之一:

  • 尚未调度过任何进程(curenv == NULL)

  • 当前进程已经用完了时间片(count == 0)

  • 当前进程不再就绪,如:被阻塞或退出(e->env_status != ENV_RUNNABLE)

  • yield 参数指定必须发生切换(yield != 0)

    则进行进程切换:

判断若curven的状态为ENV_RUNNABLE,则把 curven再次插入调度队列的尾部;从调度队列头取出一个进程;将剩余时间片数量设置为新进程的优先级;env_run运行当前选中进程

难点分析

cur_pgdir 是一个在 kern/pmap.c 定义的全局变量,其中存储了当前进程一级页表基地址 位于 kseg0 的虚拟地址。

函数整理

![image-20240424164739200](C:\Users\86199\Documents\A blog\myblog\source_posts\image-20240424164739200.png)

map_segment

  • 声明:void map_segment(Pde *pgdir, u_int asid, u_long pa, u_long va, u_long size, u_int perm)

  • 作用:在一级页表基地址为 pgdir 的页表中做段地址映射,将虚拟地址段 [va,va+size) 映射到物理地址段 [pa,pa+size),因为是按页映射,要求 size 必须是页面大小的整数倍,同时为相关页表项的权限为设置为 perm。

  • 调用场景:kern/env.c:173: map_segment(base_pgdir, 0, PADDR(pages), UPAGES, ROUND(npage * sizeof(struct Page), PAGE_SIZE), PTE_G);
    kern/env.c:175: map_segment(base_pgdir, 0, PADDR(envs), UENVS, ROUND(NENV * sizeof(struct Env), PAGE_SIZE), PTE_G);

    将内核中的 Page 和 Env 数据结构映射到用户地址,以供用户程序读取。

env_alloc

  • 声明:int env_alloc(struct Env **new, u_int parent_id)

  • 作用:申请并初始化一个进程控制块

  • 调用场景:kern/env.c:365: env_alloc(&e, 0);

    在env_create中用于在初始化过程中创建env

env_create

声明:struct Env *env_create(const void *binary, size_t size, int priority)

作用:创建进程

调用场景:init/init.c通过宏ENV_CREATE_PRIORITY使用

1
2
ENV_CREATE_PRIORITY(user_bare_loop, 1);
ENV_CREATE_PRIORITY(user_bare_loop, 2);

代码解读:

  1. 分配PCB

    env_alloc(&e, 0);

  2. 设置PCB (priority和status)

    e->env_pri = priority;

    e->env_status = ENV_RUNNABLE;

  3. 将程序载入到该进程地址空间

    load_icode(e, binary, size);

    binary是一个二进制的数据数组,数组大小为size,存储的是要创建进程的程序。关于程序的加载,这里应用wokron学长的总结来说明:

    “我们还没有实现文件系统,我们所 “加载” 的程序实际上是被一同编译到内核中的一段 ELF 格式的数据。这段数据中存在标签 binary_user_bare_loop_startbinary_user_bare_loop_size,所以我们才可以只通过引用外部变量的形式就 “加载” 了程序文件。”

    TAILQ_INSERT_HEAD(&env_sched_list, e, env_sched_link);

env_setup_vm

  • 声明:static int env_setup_vm(struct Env *e)

  • 作用:初始化新进程的地址空间

  • 调用场景:在env_alloc中初始化用户地址空间

    kern/env.c:242: if ((r = env_setup_vm(e)) != 0)

  • 代码解读:

    1. 申请物理页
    2. 复制模板页表
    3. 实现自映射

elf_load_seg

  • 声明:int elf_load_seg(Elf32_Phdr *ph, const void *bin, elf_mapper_t map_page, void *data);

  • 作用:加载ELF文件的一个 segment到内 存

  • 代码解读:

    1. 加载va最前端未对齐的部分
    2. i到bin_size对齐的部分按照整页进行加载
    3. 如果存在bin_size到sg_size之间为空但是需要分配的部分,置空

load_icode

  • 声明:static void load_icode(struct Env *e, const void *binary, size_t size);

  • 作用:加载可执行文件 binary 到进程 e 的内存中

  • 代码解读:

    1. 用elf_from得到可执行文件的elf_header
    2. 遍历elf文件的每个segment,并调用elf_load_seg加载到内存
    3. 将e->env_tf.cp0_epc 设置为程序入口虚拟地址 ehdr->e_entry

load_icode_mapper

  • 声明:static int load_icode_mapper(void *data, u_long va, size_t offset, u_int perm, const void *src, size_t len);
  • 作用:作为elf_load_seg的回调函数map_page, 完成单个页面的加载过程
  • 代码解读:
    1. 用page_alloc申请物理页p
    2. 从src复制 len bytes到页内偏移为offset处(如果src不为空)
    3. 建立物理页p到进程页表中虚拟地址va的映射

schedule

  • 声明:void schedule(int yield)
  • 作用:进程调度

时钟中断

时间片:进程被允许运行的时间,用时钟中断的个数来衡量

当时钟中断产生时,当前运行的进程被挂起,MOS 需要在调度队列中选取一个合适的进程 运行。

时钟中断的初始化发生在调度执行每一个进程之前。从代码角度,就是在 env_pop_tf 中调用了宏 RESET_KCLOCK,随后又在宏 RESTORE_ALL 中恢复了 Status 寄存器,开启了中断。