操作系统是以进程为中心设计的,所以其首要任务是为进程建立档案,进程档案用于表示、标识或描述进程,即进程控制块。下面通过实现init内核线程,学习进程从创建到运行的过程
内核线程是一种特殊的进程,内核线程和进程的区别主要有两个:
- 内核线程只运行在内核态,用户进程则会在用户态和内核态切换
- 所有内核线程共用操作系统内核内存空间,不需要为内核线程维护单独的内存空间;而每个用户进程都需要维护各自的内存空间
如何生成一个内核线程并让其运行呢?分三步
- 创建PCB块,设置相应属性
- 分配相应的资源(栈空间,虚拟内存)
- 被调度器执行
下面详细说如何实现这三步:
1、为内核线程创建PCB块并初始化
首先确定PCB的数据结构
1 | struct proc_struct { |
调用alloc_proc完成PCB的初始化
1 | proc->state = PROC_UNINIT; //设置进程为uninitialized状态,已创建 未初始化 |
2、为PCB分配资源
主要函数如下:(这里不贴代码了,太多了,陈述下思路)
1、setup_kstack
通过物理内存管理器申请8K内存作为内核栈
2、copy_mm(clone_flags, proc)
根据clone_flag标志复制或共享进程内存管理结构,内核线程设为NULL,由于在操作系统启动后,已经对整个核心内存空间进行了管理,通过设置页表建立了核心虚拟空间(即 boot_cr3 指向的二级页表描述的空间)。所以内核中的所有线程都不需要再建立各自的页表,只需共享这个核心虚拟空间就可以访问整个物理内存。
3、copy_thread
重点函数,设置了proc->context,context存储着线程运行时的通用寄存器的值,这里设置了init内核线程的eip和esp,即init运行时指令指针和栈指针位置。
这里的eip并不是直接指向init线程的代码,而是指向了forkret这个汇编函数,而esp则指向了中断帧。
这里中断帧完成了真正的上下文切换,包括各种段寄存器、标志寄存器等。
forkret函数做的功能就是将eip指向中断帧trapfram.eip,而trapfram.eip指向kernel_thread_entry这个汇编函数,其中trapfram.ebp为线程要执行的函数,edx为参数,在kernel_thread_entry中call ebp
总的来说,copy_thread设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文,context完成了通用寄存器上下文的切换,trapfram完成了处理机上下文的切换
3、线程切换(运行)
通过proc_run
启动线程,proc_run
的执行过程为:
- 保存 IF 位并且禁止中断;
- 将 current 指针指向将要执行的进程;
- 更新 TSS 中的栈顶指针;
- 页表切换;
- 调用 switch_to 进行上下文切换;
- 当执行 proc_run 的进程恢复执行之后,需要恢复 IF 位。
主要函数时switch_to函数,当 switch_to 函数执行完“ret”指令后,就跳转到init线程执行了
1 | switch_to(&(prev->context), &(next->context)); |
通过switch_to函数实现两个内核线程上下文的切换,这个函数是一段汇编代码,实现了通用寄存器和eip的替换,最后push eip; ret,就跳转到了next->context的eip执行,这个context被赋值成什么了呢?
在copy_thread函数中对这个context.eip进行了初始化,同时栈寄存器esp也被更新了,我们先不看这个esp有什么用,先看forkret这个函数
1 | proc->context.eip = (uintptr_t)forkret; //设置initproc的进程上下文,上次停止执行时的下一条指令地址context.eip和上次停止执行时的堆栈地址context.esp |
forkret函数是一段汇编指令,其实现在trapentry.S
1 | .globl __trapret |
这个函数更新了栈指针寄存器,然后将栈上的数据弹回寄存器,调用中断返回指令iret。这里的内核线程上下文切换参考了内核栈到用户栈的中断实现。
当发生中断时,硬件会依次压栈ss、esp、eflags、cs、eip、errno,这是硬件实现的部分,最后通过iret中断返回指令将栈上的ss、esp、eflags、cs、eip、errno弹回对应的寄存器。在内核栈到用户栈的中断实现中,当上述寄存器在栈上时,修改上述寄存器的值,当iret时,就实现了切栈的效果。
在这里我们不需要中断,但是通过iret中断返回指令实现线程切换时的上下文切换,为此我们需要建立中断帧trapframe,proc->context.esp = (uintptr_t)(proc->tf)
这句代码就起作用了,
首先在 kernel_thread函数中建立临时中断帧
1 | tf.tf_cs = KERNEL_CS; |
然后在copy_thread函数中,分配空间建立中断帧
1 | proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1;//在内核堆栈的顶部设置中断帧大小的一块栈空间 |
当执行iret后,cpu跳转到kernel_thread_entry执行
1 | kernel_thread_entry: |
当执行call *%ebx时,就开始执行initproc的主体了。Initprocde的主体函数很简单就是输出一段字符串,然后就返回到kernel_tread_entry函数,并进一步调用do_exit执行退出操作了,至此一个内核线程init的执行结束了。
通过ucore的实现,可以知道内核线程的启动,伴随着上下文切换,这里包括通用寄存器、段寄存器、标志寄存器等,ucore通过中断返回指令iret实现了段寄存器和标志寄存器的切换