pollux's Dairy

mineucore-内核线程管理学习笔记

字数统计: 2.1k阅读时长: 12 min
2020/02/23 Share

操作系统是以进程为中心设计的,所以其首要任务是为进程建立档案,进程档案用于表示、标识或描述进程,即进程控制块。下面通过实现init内核线程,学习进程从创建到运行的过程

内核线程是一种特殊的进程,内核线程和进程的区别主要有两个:

  1. 内核线程只运行在内核态,用户进程则会在用户态和内核态切换
  2. 所有内核线程共用操作系统内核内存空间,不需要为内核线程维护单独的内存空间;而每个用户进程都需要维护各自的内存空间

如何生成一个内核线程并让其运行呢?分三步

  1. 创建PCB块,设置相应属性
  2. 分配相应的资源(栈空间,虚拟内存)
  3. 被调度器执行

下面详细说如何实现这三步:

1、为内核线程创建PCB块并初始化

首先确定PCB的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct proc_struct {
enum proc_state state; // Process state
int pid; // Process ID
int runs; // the running times of Proces
uintptr_t kstack; // Process kernel stack
volatile bool need_resched; // bool value: need to be rescheduled to release CPU?
struct proc_struct *parent; // the parent process
struct mm_struct *mm; // Process's memory management field 描述用户态进程内存空间的情况
struct context context; // Switch here to run process
struct trapframe *tf; // Trap frame for current interrupt
uintptr_t cr3; // CR3 register: the base addr of Page Directroy Table(PDT)
uint32_t flags; // Process flag
char name[PROC_NAME_LEN + 1]; // Process name
list_entry_t list_link; // Process link list
list_entry_t hash_link; // Process hash list
};

调用alloc_proc完成PCB的初始化

1
2
3
4
5
6
7
8
9
10
11
12
proc->state = PROC_UNINIT;  //设置进程为uninitialized状态,已创建 未初始化
proc->pid = -1; //设置进程pid的未初始化值
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; //使用内核页目录表的基址,即内核线程共用一个映射内核空间的页表,这表示内核空间对所有内核线程都是“可见”的,所以更精确地说,这些内核线程都应该是从属于同一个唯一的“大内核进程”—uCore内核
proc->flags = 0;
memset(proc->name, 0, PROC_NAME_LEN);

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
2
proc->context.eip = (uintptr_t)forkret; //设置initproc的进程上下文,上次停止执行时的下一条指令地址context.eip和上次停止执行时的堆栈地址context.esp
proc->context.esp = (uintptr_t)(proc->tf);//因为initproc还没有执行过,所以这其实就是initproc实际执行的第一条指令的堆栈指针

forkret函数是一段汇编指令,其实现在trapentry.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.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
jmp __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
2
3
4
5
tf.tf_cs = KERNEL_CS;
tf.tf_ds = tf.tf_es = tf.tf_ss = KERNEL_DS;
tf.tf_regs.reg_ebx = (uint32_t)fn; //fn为要启动的线程的函数指针
tf.tf_regs.reg_edx = (uint32_t)arg; //arg为参数
tf.tf_eip = (uint32_t)kernel_thread_entry; //位于kern/process/entry.S

然后在copy_thread函数中,分配空间建立中断帧

1
2
3
4
5
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; // Interrupt Flag 表示此内核线程在执行过程中,能响应中断,打断当前的执行

当执行iret后,cpu跳转到kernel_thread_entry执行

1
2
3
4
5
6
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

当执行call *%ebx时,就开始执行initproc的主体了。Initprocde的主体函数很简单就是输出一段字符串,然后就返回到kernel_tread_entry函数,并进一步调用do_exit执行退出操作了,至此一个内核线程init的执行结束了。

通过ucore的实现,可以知道内核线程的启动,伴随着上下文切换,这里包括通用寄存器、段寄存器、标志寄存器等,ucore通过中断返回指令iret实现了段寄存器和标志寄存器的切换

CATALOG
  1. 1. 1、为内核线程创建PCB块并初始化
  2. 2. 2、为PCB分配资源
  3. 3. 3、线程切换(运行)