实验目的
实验内容
练习0:填写已有实验
本实验依赖实验1/2/3。请把你做的实验1/2/3的代码填入本实验中代码中有“LAB1”,“LAB2”,“LAB3”的注释相应部分。
1 | 相对与实验三,实验四中主要改动如下: |
将实验3的kdebug.c、trap.c、default_pmm.c、pmm.c、vmm.c、swap_fifo.c
进行相应补充即可,并注意并非所有的不同都要替换掉,要根据上述改动考虑是否改动
练习1:分配并初始化一个进程控制块
alloc_proc函数(位于kern/process/proc.c中)负责分配并返回一个新的struct proc_struct结构,用于存储新建立的内核线程的管理信息。ucore需要对这个结构进行最基本的初始化,你需要完成这个初始化过程。
【提示】在alloc_proc函数的实现中,需要初始化的proc_struct结构中的成员变量至少包括: state/pid/runs/kstack/need_resched/parent/mm/context/tf/cr3/flags/name。
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
- 请说明proc_struct中
struct context context
和struct trapframe *tf
成员变量含义和在本实验中的作用是啥?(提示通过看代码和编程调试可以判断出来)
实验过程
所需要填写的代码
1 | // alloc_proc - alloc a proc_struct and init all fields of proc_struct |
查看proc.h各元素的定义说明
1 |
|
根据注释初始化进程结构体proc各元素即可
1 | // alloc_proc - alloc a proc_struct and init all fields of proc_struct |
回答问题
请说明proc_struct中
struct context context
和struct trapframe *tf
成员变量含义和在本实验中的作用是啥?(提示通过看代码和编程调试可以判断出来)
context
对x86系统而言,进程/线程上下文就是CPU内部的一堆寄存器的信息。
其定义在kern/process/proc.h中:
1 | struct context { |
trapframe *tf
用于保存前一个被(中断或异常)打断的进程的状态信息。
其定义在kern/trap/trap.h中:
1 | struct trapframe { |
练习2:为新创建的内核线程分配资源
创建一个内核线程需要分配和设置好很多资源。kernel_thread函数通过调用do_fork函数完成具体内核线程的创建工作。do_kernel函数会调用alloc_proc函数来分配并初始化一个进程控制块,但alloc_proc只是找到了一小块内存用以记录进程的必要信息,并没有实际分配这些资源。ucore一般通过do_fork实际创建新的内核线程。do_fork的作用是,创建当前内核线程的一个副本,它们的执行上下文、代码、数据都一样,但是存储位置不同。在这个过程中,需要给新内核线程分配资源,并且复制原进程的状态。你需要完成在kern/process/proc.c中的do_fork函数中的处理过程。它的大致执行步骤包括:
- 调用alloc_proc,首先获得一块用户信息块。
- 为进程分配一个内核栈。
- 复制原进程的内存管理信息到新进程(但内核线程不必做此事)
- 复制原进程上下文到新进程
- 将新进程添加到进程列表
- 唤醒新进程
- 返回新进程号
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
- 请说明ucore是否做到给每个新fork的线程一个唯一的id?请说明你的分析和理由。
实验过程
查看proc.c
1 | /* do_fork - parent process for a new child process |
根据提示写出代码有
1 | /* do_fork - parent process for a new child process |
回答问题
请说明ucore是否做到给每个新fork的线程一个唯一的id?请说明你的分析和理由。
查看get_pid函数,了解PID的生成方法
1 | // get_pid - alloc a unique pid for process |
从上可知
-
在该函数中使用了两个变量next_dafe和last_pid,在初次进入函数时,若next_safe > last_pid + 1,(last_pid,next_safe)的区间取值均是合法pid,如果满足条件,可直接返回last_pid + 1作为新的pid
-
否则,进入循环,在循环之中首先通过
if (proc->pid == last_pid)
这一分支确保了不存在任何进程的pid与last_pid重合,然后再通过if (proc->pid > last_pid && next_safe > proc->pid)
这一判断语句保证了不存在任何已经存在的pid满足:last_pid<pid<next_safe,这样就确保了最后能够找到这么一个满足条件的区间,获得合法的pid; -
之所以在该函数中使用了如此曲折的方法,维护一个合法的pid的区间,是为了优化时间效率,如果简单的暴力的话,每次需要枚举所有的pid,并且遍历所有的线程,这就使得时间代价过大,并且不同的调用get_pid函数的时候不能利用到先前调用这个函数的中间结果
因此可以给每个新fork的线程一个唯一的id
练习3:阅读代码,理解 proc_run 函数和它调用的函数如何完成进程切换的。
请在实验报告中简要说明你对proc_run函数的分析。并回答如下问题:
- 在本实验的执行过程中,创建且运行了几个内核线程?
- 语句
local_intr_save(intr_flag);....local_intr_restore(intr_flag);
在这里有何作用?请说明理由
实验过程
查看proc_run函数
1 | // proc_run - make process "proc" running on cpu |
1、让 current指向 next内核线程initproc;
2、设置任务状态ts中特权态0下的栈顶指针esp0 为 next 内核线程 initproc 的内核栈的栈顶,即 next->kstack + KSTACKSIZE ;
3、设置 CR3 寄存器的值为 next 内核线程 initproc 的页目录表起始地址 next->cr3,这实际上是完成进程间的页表切换;
4、由 switch_to函数完成具体的两个线程的执行现场切换,即切换各个寄存器,当 switch_to 函数执行完“ret”指令后,就切换到initproc执行了。
运行代码,输入make qemu
回答问题
在本实验的执行过程中,创建且运行了几个内核线程?
查看proc_init函数
1 | // proc_init - set up the first kernel thread idleproc "idle" by itself and |
从代码可知,有
- idle_proc,为第 0 个内核线程,在完成新的内核线程的创建以及各种初始化工作之后,进入死循环,用于调度其他进程或线程;
- init_proc,被创建用于打印 “Hello World” 的线程。本次实验的内核线程,只用来打印字符串。
语句
local_intr_save(intr_flag);....local_intr_restore(intr_flag);
在这里有何作用?请说明理由
在进行进程切换的时候,需要避免出现中断干扰这个过程,所以需要在上下文切换期间清除 IF 位屏蔽中断,并且在进程恢复执行后恢复 IF 位。
- 该语句是屏蔽中断操作,使得在这个语句块内的内容不会被其他中断打断,是一个原子操作
- 这就使得某些关键的代码不会被打断,从而不会一起不必要的错误
- 例如在 proc_run 函数中,将 current 指向了要切换到的线程,但是此时还未真正将控制权转移过去,若此时出现其他中断打断这些操作,便会出现 current 中保存的并不是正在运行的线程的中断控制块,从而出现错误
实验总结
本次实验是关于内核线程的相关实验,实验涉及进程控制块的设计、内核线程分配问题,任务量较小,但是分析代码仍然是一件令人头疼的事情,get_pid函数中用到的多个跳转和循环使得代码难以理解。对于实验中出现的各种错误,还是得耐心debug…