一、关键数据结构
1、TCB(线程控制块)
二、内核线程的管理
内核线程是一种特殊的进程,内核线程与用户进程的区别有:
内核线程只运行在内核态
用户进程会在在用户态和内核态交替运行
所有内核线程共用ucore内核内存空间,不需为每个内核线程维护单独的内存空间
而用户进程需要维护各自的用户内存空间
如果要让内核线程运行,我们首先要创建内核线程对应的进程控制块,还需把这些进程控制块通过链表连在一起,便于随时进行插入,删除和查找操作等进程管理事务。这个链表就是进程控制块链表。然后在通过调度器(scheduler)来让不同的内核线程在不同的时间段占用CPU执行,实现对CPU的分时共享。
1、实验执行流程
在kern_init函数中,当完成虚拟内存的初始化工作后,就调用了proc_init函数,这个函数完成了idleproc内核线程和initproc内核线程的创建或复制工作
idleproc内核线程的工作就是不停地查询,看是否有其他内核线程可以执行了,如果有,马上让调度器选择那个内核线程执行。所以idleproc内核线程是在ucore操作系统没有其他内核线程可执行的情况下才会被调用。接着就是调用kernel_thread函数来创建initproc内核线程。initproc内核线程的工作就是显示“Hello World”,表明自己存在且能正常工作了。
内核线程被uCore OS内核这个大“内核进程”所管理。内核线程都应该是从属于同一个唯一的“大内核进程”—uCore内核。
2、创建第 0 个内核线程 idleproc
在kern_init函数中,当完成虚拟内存的初始化工作后,就调用了proc_init函数
当前的执行上下文(从kern_init 启动至今)就可以看成是uCore内核进程中的一个上下文。为此,uCore通过给当前执行的上下文分配一个进程控制块以及对它进行相应初始化,将其打造成第0个内核线程 – idleproc。idleproc内核线程的工作就是不停地查询,看是否有其他内核线程可以执行了,如果有,马上让调度器选择那个内核线程执行
首先调用alloc_proc函数来通过kmalloc函数获得proc_struct结构的一块内存块-,作为第0个进程控制块。并把proc进行初步初始化:
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;//未初始化的的进程id为-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;//页目录设为内核页目录表的基址。由于该内核线程在内核中运行,故采用为uCore内核已经建立的页表,即设置为在uCore内核页表的起始地址boot_cr3。
proc->flags = 0;//标志位
memset(proc->name, 0, PROC_NAME_LEN);//进程名
}
return proc;
}
接下来,proc_init函数对idleproc内核线程进行进一步初始化:
idleproc->pid = 0;//给了idleprocID号--0,这表明了idleproc是第0个内核线程。
idleproc->state = PROC_RUNNABLE;//改变了idleproc的状态,使得它从“刚创建”转到了“准备工作”,就差uCore调度它执行了。
idleproc->kstack = (uintptr_t)bootstack;//设置了idleproc所使用的内核栈的起始地址。
idleproc->need_resched = 1;//是否需要调度设置为需要,如果当前idleproc在执行,则只要此标志为1,马上就调用schedule函数要求调度器切换其他进程执行。
set_proc_name(idleproc, "idle");
idleproc->need_resched = 1;//是否需要调度设置为需要,如果当前idleproc在执行,则只要此标志为1,马上就调用schedule函数要求调度器切换其他进程执行。
3、创建第 1 个内核线程 initproc
调用kernel_thread函数创建了第一个内核线程init_main。
int kernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags) {
struct trapframe tf;
memset(&tf, 0, sizeof(struct trapframe));//给tf进行清零初始化
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;//initproc内核线程从kernel_thread_entry处开始执行
return do_fork(clone_flags | CLONE_VM, 0, &tf);//do_fork是创建线程的主要函数。
}
**do_fork是创建线程的主要函数。**kernel_thread函数通过调用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) {
goto fork_out;
}
ret = -E_NO_MEM;
if ((proc = alloc_proc()) == NULL) {//分配并初始化进程控制块(alloc_proc函数)
goto fork_out;
}
proc->parent = current;
if (setup_kstack(proc) != 0) {//分配并初始化内核栈(setup_stack函数);
goto bad_fork_cleanup_proc;
}
if (copy_mm(clone_flags, proc) != 0) {//根据clone_flags标志复制或共享进程内存管理结构(copy_mm函数);
goto bad_fork_cleanup_kstack;
}
copy_thread(proc, stack, tf);//设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文(copy_thread函数);
bool intr_flag;
local_intr_save(intr_flag);
{
proc->pid = get_pid();
hash_proc(proc);
list_add(&proc_list, &(proc->list_link));//把设置好的进程控制块放入hash_list和proc_list两个全局进程链表中;
nr_process ++;
}
local_intr_restore(intr_flag);
wakeup_proc(proc);//唤醒进程,把进程状态设置为“就绪”态;
ret = proc->pid;//置返回码为子进程的id号
fork_out:
return ret;
bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
copy_thread函数:设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文
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);
}
此函数首先在内核堆栈的顶部设置中断帧大小的一块栈空间,并在此空间中拷贝在kernel_thread函数建立的临时中断帧的初始值,设置好中断帧后,最后就是设置initproc的进程上下文,(process context,也称执行现场)了。只有设置好执行现场后,一旦uCore调度器选择了initproc执行,就需要根据initproc->context中保存的执行现场来恢复initproc的执行。initproc的执行现场就是设置initproc实际执行的第一条指令地址和堆栈指针(eip和esp)。
至此,第一个线程创建好,并已经做好准备执行了。
4、调度并执行内核线程 initproc
在uCore执行完proc_init函数后,就创建好了两个内核线程:idleproc和initproc,这时uCore当前的执行现场就是idleproc
然后执行cpu_idle函数
void
cpu_idle(void) {
while (1) {
if (current->need_resched) {
schedule();
}
}
}
current 即 idleproc,idleproc->need_resched为1,所以会马上调用schedule函数(进程调度核心函数)找其他处于“就绪”态的进程执行。
简单的FIFO调度器
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;//设置当前内核线程idleproc->need_resched为0;
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;
}
}
} while (le != last);
if (next == NULL || next->state != PROC_RUNNABLE) {
next = idleproc;
}
next->runs ++;
if (next != current) {
proc_run(next);
}
}
local_intr_restore(intr_flag);
}
函数执行流程:1.设置当前内核线程current->need_resched为0; 2.在proc_list队列(实现简单的先进先出FIFO调度)中查找下一个处于“就绪”态的线程或进程next; 3.找到这样的进程后,就调用proc_run函数,保存当前进程current的执行现场(进程上下文),恢复新进程的执行现场,完成进程切换。
通过proc_run和进一步的switch_to函数完成两个执行现场的切换
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);
}
}
proc_run执行流程:
让current指向next内核线程initproc;
**内核栈切换:**设置任务状态段ts中特权态0下的栈顶指针esp0为next内核线程initproc的内核栈的栈顶,即next->kstack + KSTACKSIZE ;
(目的是建立好内核线程或将来用户线程在执行特权态切换(从特权态0<–>特权态3,或从特权态3<–>特权态3)时能够正确定位处于特权态0时进程的内核栈的栈顶)
**进程间页表切换:**设置CR3寄存器的值为next内核线程initproc的页目录表起始地址next->cr3,这实际上是完成进程间的页表切换;
(idleproc和initproc都是共用一个内核页表boot_cr3)
**各个寄存器的切换:**由switch_to函数完成具体的两个线程的执行现场切换,即切换各个寄存器,当switch_to函数执行完“ret”指令后,就切换到initproc执行了。
switch_to函数:参数是前一个进程和后一个进程的执行现场
switch_to汇编代码:
.globl switch_to
switch_to: # switch_to(from, to)
# save from's registers
movl 4(%esp), %eax # eax points to from
popl 0(%eax) # save eip !popl
movl %esp, 4(%eax)
movl %ebx, 8(%eax)
movl %ecx, 12(%eax)
movl %edx, 16(%eax)
movl %esi, 20(%eax)
movl %edi, 24(%eax)
movl %ebp, 28(%eax)
# restore to's registers
movl 4(%esp), %eax # not 8(%esp): popped return address already
# eax now points to to
movl 28(%eax), %ebp
movl 24(%eax), %edi
movl 20(%eax), %esi
movl 16(%eax), %edx
movl 12(%eax), %ecx
movl 8(%eax), %ebx
movl 4(%eax), %esp
pushl 0(%eax) # push eip
ret
保存前一个进程的执行现场,恢复下一个进程的执行现场
流程:
保存前一个进程的执行现场:
保存前一个进程的寄存器到context中相应的成员变量中
恢复下一个进程的执行现场:
将下一个进程要执行的地址eip放到内核栈顶,这样接下来执行时,会把栈顶的内容赋值给EIP寄存器,就切换到下一个进程执行了