banner
Hi my new friend!

OS-Lab3

Scroll down

Lab3实验报告

一、思考题

Thinking 3.1

答:

UVPT位于kuseg中,是在0x7fc0 0000 - 0x8000 0000的大小为4MB的区域,其作用是保存用户进程页表信息。PADDR(e->env_pgdir)是进程页目录所在物理地址,e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_V代表页目录中第PDX(UVPT)项映射到了页目录本身的地址,因此代码实现了自映射。

Thinking 3.2

答:

data 在函数load_icode_mapperelf_load_seg中用到。

作用:通过该指针可以得到页目录的的虚拟地址,从而将ELF加载到合适位置。

没有这个参数不可以。

这个参数联系了load_icode_mapperelf_load_segload_icode三个函数,这个参数可以使elf_load_seg更加灵活,可以调用不同的map_page函数从而实现不同操作。而这些不同的map_page函数可能调用不同的数据辅助映射,而由于datavoid *类型,可以强制转换同时没有影响。

Thinking 3.3

vava+bin_size相对位置如下

va+bin_sizeva+sg_size相对位置如下

Thinking 3.4

答:

虚拟地址。

Thinking 3.5

  • handle_int位于kern/genex.S

    通过阅读代码可知,BUILD_HANDLER是一个可以用于定义异常处理函数的宏。

    1
    2
    3
    4
    5
    6
    7
    .macro BUILD_HANDLER exception handler
    NESTED(handle_\exception, TF_SIZE, zero)
    move a0, sp
    jal \handler
    j ret_from_exception
    END(handle_\exception)
    .endm

    其中exceptionhandle_()括号中内容,handler是引用的处理函数。

1
2
3
4
5
6
BUILD_HANDLER tlb do_tlb_refill

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

根据以上代码可以知道其余异常处理函数的实现位置

  • handle_mod引用的函数 do_tlb_mod定义在 kern/tlbex.c
  • handle_tlb引用的函数do_tlb_refill定义在 kern/tlb_asm.S
  • handle_sys引用的函数do_syscall是系统调用函数。

Thinking 3.6

enable_irq代码和分析如下

1
2
3
4
5
6
7
8
9
10
11
LEAF(enable_irq)
li t0, (STATUS_CU0 | STATUS_IM4 | STATUS_IEc)
# 将SR寄存器的CU0, IM4, 和IEc位置1
# CU0:“协同处理器0可用”:能够在用户模式下使用一些名义上的特权指令
# IM4:4号中断可以被响应
# IEc:开启中断
mtc0 t0, CP0_STATUS
# 将以上内容写到CP0的Status Register中
jr ra
# 返回
END(enable_irq)

timer_irq代码和分析如下

1
2
3
4
5
6
7
timer_irq:
sw zero, (KSEG1 | DEV_RTC_ADDRESS | DEV_RTC_INTERRUPT_ACK)
# 在地址0xb5000110写0以响应时钟中断
li a0, 0
# 将第一个参数设置为0
j schedule
# 跳转至schedule函数

Thinking 3.7

实验设置静态变量count记录当前进程curenv剩余的时间片数量。每过一个时钟中断count--代表其消耗了一个时间片,当count等于0时代表这一进程的时间片已经耗尽,触发进程切换。系统保存上下文,跳转至schedule函数。

schedule函数进行判断:

若参数yield为0且进程时间片没有耗尽且进程是RUNNABLE状态,则继续执行该进程。

若参数yield不为0或进程时间片耗尽或进程不是RUNNABLE状态,此时如果当前有进程执行,则将其插入队尾,等待之后执行。之后选择队列中第一个进程,将count设置为其优先级(即env_pri),调用env_run使其运行。

二、实验难点

1.将进程加载到内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
int elf_load_seg(Elf32_Phdr *ph, const void *bin, elf_mapper_t map_page, void *data) {
u_long va = ph->p_vaddr;
size_t bin_size = ph->p_filesz;
size_t sgsize = ph->p_memsz;
u_int perm = PTE_V;
if (ph->p_flags & PF_W) {
perm |= PTE_D;
}

int r;
size_t i;
u_long offset = va - ROUNDDOWN(va, BY2PG);
if (offset != 0) {
if ((r = map_page(data, va, offset, perm, bin, MIN(bin_size, BY2PG - offset))) !=
0) {
return r;
}
}

/* Step 1: load all content of bin into memory. */
for (i = offset ? MIN(bin_size, BY2PG - offset) : 0; i < bin_size; i += BY2PG) {
if ((r = map_page(data, va + i, 0, perm, bin + i, MIN(bin_size - i, BY2PG))) != 0) {
return r;
}
}

/* Step 2: alloc pages to reach `sgsize` when `bin_size` < `sgsize`. */
while (i < sgsize) {
if ((r = map_page(data, va + i, 0, perm, NULL, MIN(bin_size - i, BY2PG))) != 0) {
return r;
}
i += BY2PG;
}
return 0;
}

理解elf_load_seg函数即可了解操作系统如何将进程写入内存。值得注意的是该函数是单页加载的。该函数将此过程分为三个部分执行:

  • 如果va并不是页对齐的,则加载到至多下一页的起始地址。如果是页对齐则不需要第一步过程。
  • 以页为单位将进程加载入内存。
  • 如果该段在文件中的内容的大小达不到为填入这段内容新分配的页面大小(即bin_size < sg_size),那么余下的部分用 0 来填充。

在实现回调函数load_icode_mapper时,需要注意在加载时地址以页操作,将页使用page2kva函数转化成内核虚拟地址再进行操作。

2.时钟中断的过程

  • 发生异常
  • 处理器进入异常分发程序(本试验中是exc_gen_entry函数,位于kern/entry.S),从异常向量组exception_handlers,定位对应异常处理函数。
  • 中断处理程序handle_int判断 Cause 寄存器是不是对应的 4 号中断位引发的中断,如果是,则执行
    中断服务函数 timer_irq
  • 中断服务函数 timer_irq响应时钟中断,并进入调度程序schedule调度新进程。
  • 调度程序schedule将时间片用尽的进程换为新进程,调用进程切换函数env_run使其运行。
  • 进程切换函数env_run将原进程上下文保存,将新进程寄存器状态写入cpu中,实现进程切换。

三、心得体会

本次实验主要学习了进程创建、加载、调度,理解CP0寄存器构成以及中断异常的处理过程,学习内容丰富,收获很多。

其他文章