主要姓名:冯成 , 学号:19021221183 , 学院:电子工程学院
在lab4中流程算是线性的,主要增加了内核线程进程的相关实现。
首先从整体上把握一下这个lab的思路,在lab4中,切换线程的主要思路是固定一个函数入口switch_to用来切换pre进程和next进程的上下文context,并且调整好返回地址,执行ret指令的时候会跳转到forkrets入口,这个函数将栈顶指向了该进程的trapframe,然后跳转到异常返回函数__trapret。换句话说进程的切换和lab1的challenge特权级切换是一样的,主要依赖iret指令以及相应的trapframe设置。在初始化initproc线程时,kernel_thread将tf.tf_eip设置为了kernel_thread_entry入口函数,所以iret之后会跳转到kernel_thread_entry。initproc线程所需要的参数通过tf.tf_regs.reg_ebx和tf.tf_regs.reg_edx传递,而后就开始执行initproc线程。在函数执行完毕之后通过eax返回其返回值,然后跳转到do_exit函数,以上就是lab4流程。
首先进入proc_init函数,该函数主要初始化了一个进程一个线程,同属于内核,视作同一个东西。idle的主要功能是空闲进程,cpu闲置时不断查询调度,init线程则是打印hello world的线程。
void proc_init(void)
{
int i;
list_init(&proc_list);//初始化进程块链表
for (i = 0; i < HASH_LIST_SIZE; i ++)
{//初始化进程块哈希链表
list_init(hash_list + i);
}
if ((idleproc = alloc_proc()) == NULL) {//创建第一个内核进程idleproc
panic("cannot alloc idleproc.\n");
}
idleproc->pid = 0;//最初始的内核进程pid为0
idleproc->state = PROC_RUNNABLE;//初始化为就绪态
idleproc->kstack = (uintptr_t)bootstack;//该进程与内核共享堆栈
idleproc->need_resched = 1;//作为空闲进程使用,设置需要被调度
set_proc_name(idleproc, "idle");//设置进程名字
nr_process ++;//进程数+1
current = idleproc;//将当前运行进程指针指向idleproc
int pid = kernel_thread(init_main, "Hello world!!", 0);//为init_main创建内核线程
if (pid <= 0) {
panic("create init_main failed.\n");
}
initproc = find_proc(pid);//根据pid找到进程控制块
set_proc_name(initproc, "init");//设置其进程名字
assert(idleproc != NULL && idleproc->pid == 0);
assert(initproc != NULL && initproc->pid == 1);
}
接着分析alloc_proc函数的功能,也就是练习1的内容,这个函数基本上没有做太多的事情,就是初始化了几个特殊的参数,在注释信息里没有,参数手册上有详细的说明。
// alloc_proc - alloc a proc_struct and init all fields of proc_struct
static struct proc_struct *alloc_proc(void)
{
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
if (proc != NULL)
{
memset(proc, 0, sizeof(struct proc_struct));//直接清空结构体,再对特殊的参数赋值
proc->state = PROC_UNINIT;//程序还正在初始化,所以状态应该为PROC_UNINIT
proc->pid = -1;
proc->cr3 = boot_cr3;//内核线程与内核共用同一张页表
}
return proc;
}
接着进入kernel_thread函数,先建立了一个暂时的trapframe,用于该进程的中断异常处理
// kernel_thread - create a kernel thread using "fn" function
// NOTE: the contents of temp trapframe tf will be copied to
// proc->tf in do_fork-->copy_thread function
int kernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags)
{
struct trapframe tf;
memset(&tf, 0, sizeof(struct trapframe));
tf.tf_cs = KERNEL_CS;//内核代码段
tf.tf_ds = tf.tf_es = tf.tf_ss = KERNEL_DS;//内核数据段
tf.tf_regs.reg_ebx = (uint32_t)fn;//需要线程执行的函数指针
tf.tf_regs.reg_edx = (uint32_t)arg;//传入参数
tf.tf_eip = (uint32_t)kernel_thread_entry;//设置统一的跳转入口点
return do_fork(clone_flags | CLONE_VM, 0, &tf);//调用do_fork复制需要的资源
}
作为中转站,在entry.S文件中的加载代码是这样写的,这是从iret跳转过来的
.text
.globl kernel_thread_entry
kernel_thread_entry: # void kernel_thread(void)
pushl %edx # push arg
call *%ebx # call fn
pushl %eax # save the return value of fn(arg)
call do_exit # call do_exit to terminate current thread
进入do_fork函数,也就是lab4的练习2,这里大部分需要填写的代码都在注释中,不过还有一点坑需要注意
/* do_fork - parent process for a new child process
* @clone_flags: used to guide how to clone the child process
* @stack: the parent's user stack pointer. if stack==0, It means to fork a kernel thread.
* @tf: the trapframe info, which will be copied to child process's proc->tf
*/
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) {
goto fork_out;
}
ret = -E_NO_MEM;
proc = alloc_proc();//创建进程块
proc->parent = current;//其父进程为当前进程
setup_kstack(proc);//建立其堆栈
copy_mm(clone_flags, proc);//复制内存信息,这个lab用不到
copy_thread(proc, stack, tf);//初始化trapframe和context
proc->pid = get_pid();//获得pid,注意要在hash之前获取pid,因为哈希需要pid作为参数,否则报页错误
hash_proc(proc);//添加至hash链表
list_add(&proc_list, &(proc->list_link));//添加到链表中
nr_process++;//进程数增加1
wakeup_proc(proc);//唤醒该进程
ret = proc->pid;//返回pid值
fork_out:
return ret;
bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
接着我们从调度器的角度看看线程是如何切换的,切换所需要的资源都是什么样的。一路快进到cup_idle中,这个函数是闲置线程,不断查询有没有线程需要使用cpu。
void cpu_idle(void)
{
while (1) {
if (current->need_resched) {
schedule();
}
}
}
进入schedule函数,能够发现它首先会把标志寄存器的值保存起来,因为要关中断,接着将当前执行的线程调度关闭,也就是切换当前线程到就绪态,
void schedule(void)
{
bool intr_flag;//用于暂存标志寄存器
list_entry_t *le, *last;
struct proc_struct *next = NULL;
local_intr_save(intr_flag);
{
current->need_resched = 0;
last = (current == idleproc) ? &proc_list : &(current->list_link);
le = last;
do {//找到第一个PROC_RUNNABLE的线程,或者找一圈都没有就退出循环
if ((le = list_next(le)) != &proc_list) {
next = le2proc(le, list_link);
if (next->state == PROC_RUNNABLE) {
break;
}
}
} while (le != last);
if (next == NULL || next->state != PROC_RUNNABLE) {//没找到可以运行的线程就运行idle闲置线程
next = idleproc;
}
next->runs ++;//运行时长增加
if (next != current) {//找到的线程不是当前线程就开始切换
proc_run(next);
}
}
local_intr_restore(intr_flag);
}
接下来看看proc_run是如何完成切换线程工作的
// proc_run - make process "proc" running on cpu
// NOTE: before call switch_to, should load base addr of "proc"'s new PDT
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);//加载其堆栈
lcr3(next->cr3);
switch_to(&(prev->context), &(next->context));//切换进程
}
local_intr_restore(intr_flag);
}
}
调用switch函数
.text
.globl switch_to
switch_to: # switch_to(from, to)
# save from's registers
movl 4(%esp), %eax # esp指向上一层返回地址,esp+4则存放参数1即from的context指针
popl 0(%eax) # 把返回地址放入context中保存
movl %esp, 4(%eax) # save esp::context of from
movl %ebx, 8(%eax) # save ebx::context of from
movl %ecx, 12(%eax) # save ecx::context of from
movl %edx, 16(%eax) # save edx::context of from
movl %esi, 20(%eax) # save esi::context of from
movl %edi, 24(%eax) # save edi::context of from
movl %ebp, 28(%eax) # save ebp::context of from
# restore to's registers
movl 4(%esp), %eax # 进行过一次弹栈,所以esp+4存放to的context指针
movl 28(%eax), %ebp # restore ebp::context of to
movl 24(%eax), %edi # restore edi::context of to
movl 20(%eax), %esi # restore esi::context of to
movl 16(%eax), %edx # restore edx::context of to
movl 12(%eax), %ecx # restore ecx::context of to
movl 8(%eax), %ebx # restore ebx::context of to
movl 4(%eax), %esp # restore esp::context of to
pushl 0(%eax) # 压入返回地址,ret指令即跳到该处执行
# 对应的就是之前copy_thread设置好的forkret入口
ret
之后跳到forkret
......
# return falls through to trapret...
.globl __trapret
__trapret:
# restore registers from stack
popal
# restore %ds, %es, %fs and %gs
popl %gs
popl %fs
popl %es
popl %ds
# get rid of the trap number and error code
addl $0x8, %esp
iret
.globl forkrets
forkrets:
# set stack to this new process's trapframe
movl 4(%esp), %esp #将esp指向trapframe,之后跳转到异常返回函数
jmp __trapret
到这里lab4思路分析完毕