练习0:填写已有实验
本实验依赖实验1/2/3。请把你做的实验1/2/3的代码填入本实验中代码中有“LAB1”,“LAB2”,“LAB3”的注释相应部分。
练习1:分配并初始化一个进程控制块(需要编码)
说明proc_struct中 struct context context 和 struct trapframe *tf 成员变量含义和在本实验中的作用是啥?
结构体定义:
enum proc_state { //进程状态
PROC_UNINIT = 0, //未初始状态
PROC_SLEEPING, //睡眠(阻塞)状态
PROC_RUNNABLE, //运行与就绪态
PROC_ZOMBIE, //僵死状态
};
struct context { //进程上下文
uint32_t eip;
uint32_t esp;
uint32_t ebx;
uint32_t ecx;
uint32_t edx;
uint32_t esi;
uint32_t edi;
uint32_t ebp;
};
struct proc_struct { //进程控制块
enum proc_state state; //进程状态
int pid; //进程ID
int runs; //运行时间
uintptr_t kstack; //内核栈位置
volatile bool need_resched; //是否需要调度
struct proc_struct *parent; //父进程
struct mm_struct *mm; //进程的虚拟内存
struct context context; //进程上下文
struct trapframe *tf; //当前中断帧的指针
uintptr_t cr3; //当前页表地址
uint32_t flags; //进程
char name[PROC_NAME_LEN + 1];//进程名字
list_entry_t list_link; //进程链表
list_entry_t hash_link;
};
宏定义:
#define PROC_NAME_LEN 15
alloc()函数的实现:
static struct proc_struct *
alloc_proc(void) {
struct proc_struct *proc = kmalloc(sizeof(struct
proc_struct));
if (proc != NULL) {
proc->state = PROC_UNINIT; //进程为初始化状态
proc->pid = -1; //进程PID为-1
proc->runs = 0; //初始化时间片
proc->kstack = 0; //内核栈地址
proc->need_resched = 0; //不需要调度
proc->parent = NULL; //父进程为空
proc->mm = NULL; //虚拟内存为空
memset(&(proc->context), 0, sizeof(struct
context)); //初始化上下文
proc->tf = NULL; //中断帧指针为空
proc->cr3 = boot_cr3; //页目录为内核页目录表的基址
proc->flags = 0; //标志位为0
memset(proc->name, 0, PROC_NAME_LEN);//进程名为0
}
return proc;
}
总结一下过程:
1 在堆上分配一块内存空间用来存放进程控制块
2 初始化进程控制块内的各个参数
3 返回分配的进程控制块
问题一:struct context context和struct trapframe *tf 成员 变量的含义和作用
context作用:
进程的上下文,用于进程切换。主要保存了前一个进程的现场(各个寄存器的状态)。在uCore中,所有的进程在内核中也是相对独立的。使用context 保存寄存器的目的就在于在内核态中能够进行上下文之间的切换。实际利用context进行上下文切换的函数是在kern/process/switch.S中定义switch_to。
tf:中断帧的指针,总是指向内核栈的某个位置:当进程从用户空间跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器值。除此之外,uCore内核允许嵌套中断。因此为了保证嵌套中断发生时tf 总是能够指向当前的trapframe,uCore 在内核栈上维护了 tf 的链。
练习2:为新创建的内核线程分配资源(需要编码)
请说明ucore是否做到给每个新fork的线程一个唯一的id?请说明你的分析和理由。
函数定义:
static int //内核栈复制函数
setup_kstack(struct proc_struct *proc) {
struct Page *page = alloc_pages(KSTACKPAGE);
if (page != NULL) {
proc->kstack = (uintptr_t)page2kva(page);
return 0;
}
return -E_NO_MEM;
}
static int //该函数在本次实验并没有实现
copy_mm(uint32_t clone_flags, struct proc_struct *proc) {
assert(current->mm == NULL); //判断当前函数的虚拟内存非空
return 0;
}
static void
copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) {
proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1;
*(proc->tf) = *tf;
proc->tf->tf_regs.reg_eax = 0;
proc->tf->tf_esp = esp;
proc->tf->tf_eflags |= FL_IF;
proc->context.eip = (uintptr_t)forkret;
proc->context.esp = (uintptr_t)(proc->tf);
}
void
intr_disable(void) { //禁止中断函数
cli(); //禁止中断
}
static inline bool
__intr_save(void) {
if (read_eflags() & FL_IF){ //如果允许屏蔽中断,即IF=1.则中断
intr_disable(); //禁止中断
return 1;
}
return 0;
}
static inline void
__intr_restore(bool flag) { //如果中断被屏蔽,则恢复中断
if (flag) {
intr_enable(); //恢复中断
}
}
eflags寄存器
宏定义:
#define local_intr_save(x) \
do { x = __intr_save(); } while (0)
#define FL_IF 0x00000200
#define local_intr_restore(x) __intr_restore(x);
do_fork()函数的实现:
int
do_fork(uint32_t clone_flags, uintptr_t stack, struct trapframe *tf) {
int ret = -E_NO_FREE_PROC; //尝试为进程分配内存
struct proc_struct *proc; //定义新进程
if (nr_process >= MAX_PROCESS) { //分配进程数大于4096,返回
goto fork_out; //返回
}
ret = -E_NO_MEM; //因内存不足而分配失败
if ((proc = alloc_proc()) == NULL) { //分配内存失败
goto fork_out; //返回
}
proc->parent = current; //设置父进程名字
if (setup_kstack(proc) != 0) {//分配内核栈
goto bad_fork_cleanup_proc; //返回
}
if (copy_mm(clone_flags, proc) != 0) { //复制父进程内存信息
goto bad_fork_cleanup_kstack; //返回
}
copy_thread(proc, stack, tf); //复制中断帧和上下文信息
bool intr_flag;
local_intr_save(intr_flag); //屏蔽中断,intr_flag置为1
{
proc->pid = get_pid(); //获取当前进程PID
hash_proc(proc); //建立hash映射
list_add(&proc_list,&(proc->list_link));//加入进程链表
nr_process ++; //进程数加一
}
local_intr_restore(intr_flag); //恢复中断
wakeup_proc(proc); //唤醒新进程
ret = proc->pid; //返回当前进程的PID
fork_out: //已分配进程数大于4096
return ret;
bad_fork_cleanup_kstack: //分配内核栈失败
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
问题:ucore是否做到给每个新fork的线程一个唯一的id?
查看get_pid函数
static int
get_pid(void) {
static_assert(MAX_PID > MAX_PROCESS);
struct proc_struct *proc;
list_entry_t *list = &proc_list, *le;
static int next_safe = MAX_PID, last_pid = MAX_PID;
if (++ last_pid >= MAX_PID) {
last_pid = 1;
goto inside;
}
if (last_pid >= next_safe) {
inside:
next_safe = MAX_PID;
repeat:
le = list;
while ((le = list_next(le)) != list) {
proc = le2proc(le, list_link);
if (proc->pid == last_pid) {
if (++ last_pid >= next_safe) {
if (last_pid >= MAX_PID) {
last_pid = 1;
}
next_safe = MAX_PID;
goto repeat;
}
}
else if (proc->pid > last_pid && next_safe > proc->pid) {
next_safe = proc->pid;
}
}
}
return last_pid;
}
从上面的代码可以看出。在使用 fork 或 clone 系统调用时产生的进程均会由内核分配一个新的唯一的PID值。具体来说,就是在分配PID时,设置一个保护锁,暂时不允许中断,这样在就唯一地分配了一个PID。
练习3:阅读代码,理解 proc_run 函数和它调用的函数如何完成进程切换的。(无编码工作)
在本实验的执行过程中,创建且运行了几个内核线程?
语句 local_intr_save(intr_flag);….local_intr_restore(intr_flag); 在这里有何作用?请说明理由。
首先分析schedule函数的源码:
/* 宏定义:
#define le2proc(le, member) \
to_struct((le), struct proc_struct, member)*/
void
schedule(void) {
bool intr_flag; //定义中断变量
list_entry_t *le, *last; //当前list,下一list
struct proc_struct *next = NULL; //下一进程
local_intr_save(intr_flag); //中断禁止函数
{
current->need_resched = 0; //设置当前进程不需要调度
//last是否是idle进程(第一个创建的进程),如果是,则从表头开始搜索
//否则获取下一链表
last = (current == idleproc) ? &proc_list : &(current->list_link);
le = last;
do { //一直循环,直到找到可以调度的进程
if ((le = list_next(le)) != &proc_list) {
next = le2proc(le, list_link);//获取下一进程
if (next->state == PROC_RUNNABLE) {
break; //找到一个可以调度的进程,break
}
}
} while (le != last); //循环查找整个链表
if (next == NULL || next->state != PROC_RUNNABLE) {
next = idleproc; //未找到可以调度的进程
}
next->runs ++; //运行次数加一
if (next != current) {
proc_run(next); //运行新进程,调用proc_run函数
}
}
local_intr_restore(intr_flag); //允许中断
}
可以看到ucore实现的是FIFO调度算法:
1 调度开始时,先屏蔽中断。
2 在进程链表中,查找第一个可以被调度的程序
3 运行新进程,允许中断
再分析switch_to函数
switch_to: # switch_to(from, to)
# save from's registers
movl 4(%esp), %eax #保存from的首地址
popl 0(%eax) #将返回值保存到context的eip
movl %esp, 4(%eax) #保存esp的值到context的esp
movl %ebx, 8(%eax) #保存ebx的值到context的ebx
movl %ecx, 12(%eax) #保存ecx的值到context的ecx
movl %edx, 16(%eax) #保存edx的值到context的edx
movl %esi, 20(%eax) #保存esi的值到context的esi
movl %edi, 24(%eax) #保存edi的值到context的edi
movl %ebp, 28(%eax) #保存ebp的值到context的ebp
# restore to's registers
movl 4(%esp), %eax #保存to的首地址到eax
movl 28(%eax), %ebp #保存context的ebp到ebp寄存器
movl 24(%eax), %edi #保存context的ebp到ebp寄存器
movl 20(%eax), %esi #保存context的esi到esi寄存器
movl 16(%eax), %edx #保存context的edx到edx寄存器
movl 12(%eax), %ecx #保存context的ecx到ecx寄存器
movl 8(%eax), %ebx #保存context的ebx到ebx寄存器
movl 4(%eax), %esp #保存context的esp到esp寄存器
pushl 0(%eax) #将context的eip压入栈中
ret
所以switch_to函数主要完成的是进程的上下文切换,先保存当前寄存器的值,然后再将下一进程的上下文信息保存到对于寄存器中。
proc_run函数的源码:
void
proc_run(struct proc_struct *proc) {
if (proc != current) {
bool intr_flag; //定义中断变量
struct proc_struct *prev = current, *next = proc;
local_intr_save(intr_flag); //屏蔽中断
{
current = proc; //修改当前进程为新进程
load_esp0(next->kstack + KSTACKSIZE); //修改esp
lcr3(next->cr3); //修改页表项
//上下文切换
switch_to(&(prev->context), &(next->context));
}
local_intr_restore(intr_flag); //允许中断
}
}
所以整个函数的执行过程为:
1 屏蔽中断
2 修改esp0,页表项和进行上下文切换
3 允许中断
问题一:在本实验的执行过程中,创建且运行了几个内核线程?
idleproc: ucore: 第一个内核进程,完成内核中各个子系统的初始化,之后立即调度,执行其他进程。
initproc:用于完成实验的功能而调度的内核进程。
问题二:语句A在这里有何作用?请说明理由。
屏蔽中断和打开中断,以免进程切换时其他进程再进行调度。