lab3 实验报告 [BUAA-OS]
Lab3实验报告
Part1. 思考题
Thinking 3.1 对物理地址和虚拟地址的理解
请结合MOS中的页目录自映射应用解释代码中
e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_V
的含义。
UVPT
是一个虚拟地址,表示用户进程页表的起始地址。PDX(UVPT)
是UVPT
地址的页目录项的索引(页目录中第PDX(UVPT)
个页表项)。e->env_pgdir[PDX(UVPT)]
是用户进程UVPT
的页目录项。PADDR(e->env_pgdir)
是将页目录的虚拟地址转换为物理地址。PTE_V
表示该页表项有效。e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_V
的含义是将页目录中第PDX(UVPT)
个页表项设置为指向物理地址PADDR(e->env_pgdir)
,并标记为有效。这样做的目的是实现自映射,使得用户进程可以通过虚拟地址UVPT
访问自己的页目录,从而实现对页表的访问和操作。
Thinking 3.2 data 的作用
elf_load_seg
以函数指针的形式,接受外部自定义的回调函数map_page
。请你找到与之相关的data
这一参数在此处的来源,并思考它的作用。没有这个参数可不可以?为什么?
1 | // in lib/elfloader.c |
- 查阅代码可以发现,
data
参数的来源是load_icode
函数中传入的参数e
,它是一个指向struct Env
类型的指针,表示当前进程的控制块指针,包含了进程的相关信息,如env_pgdir
、env_asid
等。 - 如果没有
data
参数,那么load_icode_mapper
函数将无法获取到当前进程的页目录和ASID,也就无法正确地调用page_insert
函数来将物理页映射到虚拟地址空间中。所以必须要有data
参数提供上下文信息,使得回调函数能够正确地执行映射操作。
Thinking 3.3 elf_load_esg 的不同情况
结合
elf_load_seg
的参数和实现,考虑该函数需要处理哪些页面加载的情况。
elf_load_seg
函数的实现主要负责将 ELF 文件的一个段加载到内存中。有以下几种情况需要处理:
- 段的起始地址未对齐到页面边界
ELF 段的虚拟地址ph->p_vaddr
可能不是页面大小的整数倍。如果地址未对齐(offset != 0
),需要特殊处理段的第一个页面:只加载从offset
开始的部分内容,并使用MIN(bin_size, PAGE_SIZE - offset)
确保不会越界。
1 | u_long offset = va - ROUNDDOWN(va, PAGE_SIZE); |
- 加载段的文件内容到内存
按页面大小(PAGE_SIZE
)循环加载文件内容,直到文件内容全部加载完毕。每次调用map_page
将一页数据映射到虚拟地址空间。
1 | /* Step 1: load all content of bin into memory. */ |
- 段的文件大小小于内存大小,分配额外的内存页面
当文件大小(bin_size
)小于段的内存大小(sgsize
)时,分配额外的页面直到达到sgsize
,传递 NULL 表示这些页面不需要初始化数据。
1 | /* Step 2: alloc pages to reach `sgsize` when `bin_size` < `sgsize`. */ |
Thinking 3.4 EPC 的含义
你认为这里的
env_tf.cp0_epc
存储的是物理地址还是虚拟地址?
- 存储的是虚拟地址。
env_tf.cp0_epc
保存的是发生异常时的指令地址,load_icode
函数中e->env_tf.cp0_epc
设置为ehdr->e_entry
,即 ELF 文件中的程序入口地址。这个地址是虚拟地址。
Thinking 3.5 异常处理函数的实现位置
试找出0、1、2、3号异常处理函数的具体实现位置。8号异常(系统调用)涉及的
do_syscall()
函数将在Lab4中实现。
首先查看kern/trap.c
文件
1 | // kern/trap.c |
发现在kern/genex.S
中有异常函数的定义
1 | # kern/genex.S |
- 0号异常处理函数
handle_int
在kern/genex.S
中实现 - 其他异常处理函数则通过宏
BUILD_HANDLER
实现。BUILD_HANDLER
是一个汇编宏,用于生成异常处理函数的框架代码,其中exception
是异常名称,用于生成我们在kern/trap.c
看到的异常处理函数的名称(如handle_tlb
),handler
是实际的异常处理函数(如do_tlb_refill
),也就是我们要找的函数具体实现,do_reserved
在kern/trap.c
中,其余在kern/tlbex.c
中。
1 | // in kern/tlbex.c |
Thinking 3.6 时钟的设置
阅读
entry.S
、genex.S
和env_asm.S
这几个文件,并尝试说出时钟中断在哪些时候开启,在哪些时候关闭。
- 关闭时钟中断
在kern/entry.S
中,.text.exc_gen_entry
段通过and t0,t0, ~(STATUS_UM | STATUS_EXL| STATUS_IE)
关闭时钟中断。STATUS_UM
表示用户模式,STATUS_EXL
表示异常模式,STATUS_IE
表示中断使能位,随后取出对应的异常码,跳转到exception_handlers
数组中的中断处理函数中。 - 开启时钟中断
时钟中断由kern/genex.S
中的handle_int
函数处理,handle_int
函数中通过bnez t1, timer_irq
判断是否是时钟中断,如果是,则调用schedule
函数。查看kern/sched.c
中的schedule
函数,发现它会调用kern/env.c
中的env_run
函数,env_run
函数中又不返回地调用了kern/env_asm.S
中的env_pop_tf
函数,通过它的RESET_KCLOCK
重设时钟中断,随后j ret_from_exception
指令跳转到kern/genex.S
中的ret_from_exception
函数,在RESTORE_ALL
宏中恢复了Status寄存器,开启了中断,最终调用eret
指令,会将EXL
设置为0,返回用户态。此时Status中的UM
、IE
均已被设置为1,表示在用户模式下且开启中断。之后第一个进程成功以用户模式运行,这时操作系统也可以正常响应中断了。
Thinking 3.7 进程的调度
阅读相关代码,思考操作系统是怎么根据时钟中断切换进程的。
RESET_KCLOCK
宏设定了时钟中断的周期。指导书中提到,一旦时钟中断产生,就会触发4KC硬件的异常中断处理流程。系统将PC指向0x80000180,跳转到.text.exc_gen_entry
代码段执行。对于时钟引起的中断,通过.text.exc_gen_entry
代码段的分发,最终会调用handle_int
函数进行处理。
handle_int
函数会在时钟中断时进入schedule
函数进行调度。进程切换会在四种情况下发生:
- 进程主动放弃CPU(调用
sys_yield
); - 进程时间片耗尽(
count <= 0
); - 进程被阻塞或退出(
e->env_status == ENV_NOT_RUNNABLE
); - 当前无进程运行(
e == NULL
)。
接着检查如果当前有进程运行(e != NULL
),则从env_sched_list
队列中移除当前进程,检查该进程是否仍然处于可运行状态(e->env_status == ENV_RUNNABLE
),如果是,则将其插入到队列的末尾,从队首取一个新的进程(e = TAILQ_FIRST(&env_sched_list)
),并根据优先级设置时间片长度(count = e->env_pri
)。最后,时钟片数量减一,调用env_run
函数。
Part2. 难点分析
进程与内存空间
每个进程都认为自己是独占内存空间的,因此,在实现进程时,需要为每个进程分配独立的内存空间。在env.c
中,env_setup_vm
函数为每个进程分配了独立的页表,并初始化了页表。env_create
函数则根据envs
数组中的进程信息,为每个进程分配了独立的内存空间。
值得一提的是,“MOS 操作系统特意将一些内核的数据暴露到用户空间”,因此,在env_setup_vm
函数中,UTOP
以上到 UVPT
之间的只读空间被拷贝到了用户进程的页表中,这样,用户进程就可以访问内核的数据了。
关于异常分发
中断处理和分发的过程用到了很多函数和宏,需要对指导书和相关代码进行仔细阅读和理解。Thinking 3.5 和 3.6 中的分析可以帮助我们更好理解异常分发的过程。
异常重入
在异常分发中,我们使用了 SAVE_ALL
宏来保存上下文到内核的异常栈中,查看include/stackframe.h
中 SAVE_ALL
宏的实现:
1 | .macro SAVE_ALL |
SAVE_ALL
宏首先判断了当前是否已经陷入内核态(异常重入),否则需要将sp寄存器指向内核异常栈(KSTACKTOP),从而实现在异常中处理异常,避免破坏已有的异常栈。
Part3. 实验体会
通过本次实验,我深入理解进程的概念并进行实践,掌握了进程的创建、调度、异常分发等过程。本次实验需要完成的代码并不算困难,但涉及的知识点较多,尤其是进程调度的实现,需要仔细阅读相关代码,理解每个函数的作用和实现原理。