关键数据结构
进程控制块
在实验四中,进程管理信息用struct proc_struct表示,在kern/process/proc.h中定义如下:
42 struct proc_struct {
43 enum proc_state state; // Process state
44 int pid; // Process ID
45 int runs; // the running times of Proces
46 uintptr_t kstack; // Process kernel stack
47 volatile bool need_resched; // bool value: need to be rescheduled to release CPU?
48 struct proc_struct *parent; // the parent process
49 struct mm_struct *mm; // Process's memory management field
50 struct context context; // Switch here to run process
51 struct trapframe *tf; // Trap frame for current interrupt
52 uintptr_t cr3; // CR3 register: the base addr of Page Directroy Table(PDT)
53 uint32_t flags; // Process flag
54 char name[PROC_NAME_LEN + 1]; // Process name
55 list_entry_t list_link; // Process link list
56 list_entry_t hash_link; // Process hash list
57 };
下面重点解释一下几个比较重要的成员变量:
- mm:内存管理的信息,包括内存映射列表、页表指针等。mm成员变量在lab3中用于虚存管理,但在实际OS中,内核线程常驻内存,不需要考虑swap page问题,在lab5中涉及到了用户进程,才考虑进程用户内存空间的swap page问题,mm才会发挥作用。所以在lab4中mm对于内核线程就没有用了,这样内核线程的proc_struct的成员变量*mm=0是合理的。mm里有个很重要的项pgdir,记录的是该进程使用的一级页表的物理地址。由于*mm=NULL,所以在proc_struct数据结构中需要有一个代替pgdir项来记录页表起始地址,这就是proc_struct数据结构中的cr3成员变量。
- state:进程所处的状态。
- parent:用户进程的父进程(创建它的进程)。在所有进程中,只有一个进程没有父进程,就是内核创建的首个内核线程idleproc。内核根据这个父子关系建立一个树形结构,用于维护一些特殊的操作,例如确定某个进程是否可以对另外一个进程进行某种操作等等。
- context:进程的上下文,用于进程切换(参见switch.S)。在 uCore中,所有的进程在内核中也是相对独立的(例如独立的内核堆栈以及上下文等等)。使用 context 保存寄存器的目的就在于在内核态中能够进行上下文之间的切换。实际利用context进行上下文切换的函数是在kern/process/switch.S中定义switch_to。
- tf:中断帧的指针,总是指向内核栈的某个位置:当进程从用户空间跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器值。除此之外,uCore内核允许嵌套中断。因此为了保证嵌套中断发生时tf 总是能够指向当前的trapframe,uCore 在内核栈上维护了 tf 的链,可以参考trap.c::trap函数做进一步的了解。
- cr3: cr3 保存页表的物理地址,目的就是进程切换的时候方便直接使用 lcr3实现页表切换,避免每次都根据 mm 来计算 cr3。mm数据结构是用来实现用户空间的虚存管理的,但是内核线程没有用户空间,它执行的只是内核中的一小段代码(通常是一小段函数),所以它没有mm结构,也就是NULL。当某个进程是一个普通用户态进程的时候,PCB 中的 cr3 就是 mm 中页表(pgdir)的物理地址;而当它是内核线程的时候,cr3 等于boot_cr3,而boot_cr3指向了uCore启动时建立好的内核虚拟空间的页目录表首地址。
- kstack:每个线程都有一个内核栈,并且位于内核地址空间的不同位置。对于内核线程,该栈就是运行时的程序使用的栈;而对于普通进程,该栈是发生特权级改变的时候使保存被打断的硬件信息用的栈。uCore在创建进程时分配了 2 个连续的物理页(参见memlayout.h中KSTACKSIZE的定义)作为内核栈的空间。这个栈很小,所以内核中的代码应该尽可能的紧凑,并且避免在栈上分配大的数据结构,以免栈溢出,导致系统崩溃。kstack记录了分配给该进程/线程的内核栈的位置。主要作用有以下几点。首先,当内核准备从一个进程切换到另一个的时候,需要根据kstack 的值正确的设置好 tss (可以回顾一下在实验一中讲述的 tss 在中断处理过程中的作用),以便在进程切换以后再发生中断时能够使用正确的栈。其次,内核栈位于内核地址空间,并且是不共享的(每个线程都拥有自己的内核栈),因此不受到 mm 的管理,当进程退出的时候,内核能够根据 kstack 的值快速定位栈的位置并进行回收。uCore 的这种内核栈的设计借鉴的是 linux 的方法(但由于内存管理实现的差异,它实现的远不如 linux 的灵活),它使得每个线程的内核栈在不同的位置,这样从某种程度上方便调试,但同时也使得内核对栈溢出变得十分不敏感,因为一旦发生溢出,它极可能污染内核中其它的数据使得内核崩溃。如果能够通过页表,将所有进程的内核栈映射到固定的地址上去,能够避免这种问题,但又会使得进程切换过程中对栈的修改变得相当繁琐。感兴趣的同学可以参考 linux kernel 的代码对此进行尝试。
状态切换枚举
10 // process's state in his life cycle
11 enum proc_state {
12 PROC_UNINIT = 0, // uninitialized
13 PROC_SLEEPING, // sleeping 等待
14 PROC_RUNNABLE, // runnable(maybe running) 就绪
15 PROC_ZOMBIE, // almost dead, and wait parent proc to reclaim his resource
16 };
(k/p/proc.h)
上下文
18 // Saved registers for kernel context switches.
19 // Don't need to save all the %fs etc. segment registers,
20 // because they are constant across kernel contexts.
21 // Save all the regular registers so we don't need to care
22 // which are caller save, but not the return register %eax.
23 // (Not saving %eax just simplifies the switching code.)
24 // The layout of context must match code in switch.S.
25 struct context {
26 uint32_t eip;
27 uint32_t esp;
28 uint32_t ebx;
29 uint32_t ecx;
30 uint32_t edx;
31 uint32_t esi;
32 uint32_t edi;
33 uint32_t ebp;
34 };
(k/p/proc.h)
关键的全局变量
为了管理系统中所有的进程控制块,uCore维护了如下全局变量(位于kern/process/proc.c)
static struct proc *current
当前占用CPU且处于“运行”状态进程控制块指针。通常这个变量是只读的,只有在进程切换的时候才进行修改,并且整个切换和修改过程需要保证操作的原子性,目前至少需要屏蔽中断。可以参考 switch_to 的实现。
static struct proc *initproc
本实验中,指向一个内核线程。本实验以后,此指针将指向第一个用户态进程。
static list_entry_t hash_list[HASH_LIST_SIZE]
所有进程控制块的哈希表,proc_struct中的成员变量hash_link将基于pid链接入这个哈希表中。
list_entry_t proc_list
所有进程控制块的双向线性列表,proc_struct中的成员变量list_link将链接入这个链表中。
执行流程
lab2和lab3完成了对内存的虚拟化,但整个控制流还是一条线串行执行。lab4将在此基础上进行CPU的虚拟化,即让ucore实现分时共享CPU,实现多条控制流能够并发执行。从某种程度上,我们可以把控制流看作是一个内核线程。本次实验将首先接触的是内核线程的管理。
内核线程是一种特殊的进程,内核线程与用户进程的区别有两个,
- 内核线程只运行在内核态而用户进程会在在用户态和内核态交替运行;
- 所有内核线程直接使用共同的ucore内核内存空间,不需为每个内核线程维护单独的内存空间而用户进程需要维护各自的用户内存空间。从内存空间占用情况这个角度上看,我们可以把线程看作是一种共享内存空间的轻量级进程。
为了实现内核线程,需要设计管理线程的数据结构,即进程控制块(在这里也可叫做线程控制块)。如果要让内核线程运行,我们首先要创建内核线程对应的进程控制块,还需把这些进程控制块通过链表连在一起,便于随时进行插入,删除和查找操作等进程管理事务。这个链表就是进程控制块链表。然后在通过调度器(scheduler)来让不同的内核线程在不同的时间段占用CPU执行,实现对CPU的分时共享。那lab4中是如何一步一步实现这个过程的呢?
我们还是从lab4/kern/init/init.c中的kern_init函数入手分析。在kern_init函数中,当完成虚拟内存的初始化工作后,就调用了proc_init函数,这个函数完成了idleproc内核线程和initproc内核线程的创建或复制工作,这也是本次实验要完成的练习。
idleproc内核线程的工作就是不停地查询,看是否有其他内核线程可以执行了,如果有,马上让调度器选择那个内核线程执行(请参考cpu_idle函数的实现)。所以idleproc内核线程是在ucore操作系统没有其他内核线程可执行的情况下才会被调用。接着就是调用kernel_thread函数来创建initproc内核线程。initproc内核线程的工作就是显示“Hello World”,表明自己存在且能正常工作了。
调度器会在特定的调度点上执行调度,完成进程切换。在lab4中,这个调度点就一处,即在cpu_idle函数中,此函数如果发现当前进程(也就是idleproc)的need_resched置为1(在初始化idleproc的进程控制块时就置为1了),则调用schedule函数,完成进程调度和进程切换。进程调度的过程其实比较简单,就是在进程控制块链表中查找到一个“合适”的内核线程,所谓“合适”就是指内核线程处于“PROC_RUNNABLE”状态。在接下来的switch_to函数(在后续有详细分析,有一定难度,需深入了解一下)完成具体的进程切换过程。一旦切换成功,那么initproc内核线程就可以通过显示字符串来表明本次实验成功。
接下来将主要介绍了进程创建所需的重要数据结构--进程控制块 proc_struct,以及ucore创建并执行内核线程idleproc和initproc的两种不同方式,特别是创建initproc的方式将被延续到实验五中,扩展为创建用户进程的主要方式。另外,还初步涉及了进程调度(实验六涉及并会扩展)和进程切换内容。
创建并执行内核线程
建立进程控制块(proc.c中的alloc_proc函数)后,现在就可以通过进程控制块来创建具体的进程/线程了。首先,考虑最简单的内核线程,它通常只是内核中的一小段代码或者函数,没有自己的“专属”空间。这是由于在uCore OS启动后,已经对整个内核内存空间进行了管理,通过设置页表建立了内核虚拟空间(即boot_cr3指向的二级页表描述的空间),所以uCore OS内核中的所有线程都不需要再建立各自的页表,只需共享这个内核虚拟空间就可以访问整个物理内存了。从这个角度看,内核线程被uCore OS内核这个大“内核进程”所管理。
创建第0个内核线程
在init.c::kern_init函数调用了proc.c::proc_init函数。proc_init函数启动了创建内核线程的步骤。首先当前的执行上下文(从kern_init 启动至今)就可以看成是uCore内核(也可看做是内核进程)中的一个内核线程的上下文。为此,uCore通过给当前执行的上下文分配一个进程控制块以及对它进行相应初始化,将其打造成第0个内核线程 -- idleproc。具体步骤如下:
首先调用alloc_proc函数来通过kmalloc函数获得作为proc_struct结构的一块内存块并将其作为第0个进程控制块,并把proc进行初步初始化(即把proc_struct中的各个成员变量清零),但有些成员变量设置了特殊的值,比如:
proc->state = PROC_UNINIT; 设置进程为“初始”态
proc->pid = -1; 设置进程pid的未初始化值
proc->cr3 = boot_cr3; 使用内核页目录表的基址
...
上述三条语句中,
- 第一条设置了进程的状态为“初始”态,这表示进程已经 “出生”了,正在获取资源茁壮成长中;
- 第二条语句设置了进程的pid为-1,这表示进程的“身份证号”还没有办好;
- 第三条语句表明由于该内核线程在内核中运行,故采用为uCore内核已经建立的页表,即设置为在uCore内核页表的起始地址boot_cr3。
从后续实验可进一步看出所有内核线程的内核虚地址空间(也包括物理地址空间)是相同的,既然内核线程共用一个映射内核空间的页表,这表示内核空间对所有内核线程都是“可见”的,所以更精确地说,这些内核线程都应该是从属于同一个唯一的“大内核进程”—uCore内核。
首个内核线程是idle_proc,代表ucoreOS去完成后面的一系列工作,包括完成后面对init_proc()的创建和调度执行过程,所以他是代表ucoreOS去管理。他需要完成自身的初始化工作,比如要创建自己的TCB(使用alloc_proc()函数)。
alloc_proc()
84 // alloc_proc - alloc a proc_struct and init all fields of proc_struct
85 static struct proc_struct *
86 alloc_proc(void) {
87 struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
88 if (proc != NULL) {
89 //LAB4:EXERCISE1 YOUR CODE
90 /*
91 * below fields in proc_struct need to be initialized
92 * enum proc_state state; // Process state
93 * int pid; // Process ID
94 * int runs; // the running times of Proces
95 * uintptr_t kstack; // Process kernel stack
96 * volatile bool need_resched; // bool value: need to be rescheduled to release CPU?
97 * struct proc_struct *parent; // the parent process
98 * struct mm_struct *mm; // Process's memory management field
99 * struct context context; // Switch here to run process
100 * struct trapframe *tf; // Trap frame for current interrupt
101 * uintptr_t cr3; // CR3 register: the base addr of Page Directroy Table(PDT)
102 * uint32_t flags; // Process flag
103 * char name[PROC_NAME_LEN + 1]; // Process name
104 */
# 作为通用的创建PCB的函数,这里最重要的就是分配内存kmalloc(),其次才是对其
# 进行最初步的初始化工作,由于是通用的,所以这里的初始化工作也必须是通用的。
105 memset(proc, 0, sizeof(struct proc_struct));
106 proc->state = PROC_UNINIT;
107 proc->pid = -1;
108 proc->cr3 = boot_cr3;
109 }
110 return proc;
111 }
接下来,proc_init函数对idleproc内核线程进行进一步初始化:
idleproc->pid = 0;
idleproc->state = PROC_RUNNABLE;
idleproc->kstack = (uintptr_t)bootstack;
idleproc->need_resched = 1;
set_proc_name(idleproc, "idle");
需要注意前4条语句。
- 第一条语句给了idleproc合法的身份证号——0,这名正言顺地表明了idleproc是第0个内核线程。通常可以通过pid的赋值来表示线程的创建和身份确定。“0”是第一个的表示方法是计算机领域所特有的,比如C语言定义的第一个数组元素的小标也是“0”。
- 第二条语句改变了idleproc的状态,使得它从“出生”转到了“准备工作”,就差uCore调度它执行了。
- 第三条语句设置了idleproc所使用的内核栈的起始地址。需要注意以后的其他线程的内核栈都需要通过分配获得,因为uCore启动时设置的内核栈直接分配给idleproc使用了。
- 第四条很重要,因为uCore希望当前CPU应该做更有用的工作,而不是运行idleproc这个“无所事事”的内核线程,所以把idleproc->need_resched设置为“1”,结合idleproc的执行主体--cpu_idle函数的实现,可以清楚看出如果当前idleproc在执行,则只要此标志为1,马上就调用schedule函数要求调度器切换其他进程执行。
进程号为0;当前状态是“就绪态”;堆栈被设置为内核堆栈;设置为“需要被调度”,因为它被调度之后才能切换到其他内核线程去执行,这么设置是表示它是需要被重新调度的;
proc_init()
61 // the process set's list
62 list_entry_t proc_list;
68 // has list for process set based on pid
# 哈希尺寸是0~1023,以进程号pid经过哈希计算后作为索引,hash_list每个元素都是一个链表头
69 static list_entry_t hash_list[HASH_LIST_SIZE];
331 // proc_init - set up the first kernel thread idleproc "idle" by itself and
332 // - create the second kernel thread init_main
# 创建首个线程idleproc和第二个内核线程init_main
333 void
334 proc_init(void) {
335 int i;
336
# proc_list:所有进程控制块的双向线性列表,proc_struct中的成员变
# 量list_link将链接入这个链表中
337 list_init(&proc_list);
# 0~1023
338 for (i = 0; i < HASH_LIST_SIZE; i ++) {
# 所有进程控制块的哈希表,proc_struct中的成员变量hash_link将基于pid链接入这个哈希表中。
# 初始化好每个链表头,一共初始化了1024个链表头(未来有1024个链表)
339 list_init(hash_list + i);
340 }
341
# 创建内核首个进程idleproc,这是内核唯一一个没有父进程的进程
# 注意这种创建进程的方式,相当“赤裸”
342 if ((idleproc = alloc_proc()) == NULL) {
343 panic("cannot alloc idleproc.\n");
344 }
345
346 idleproc->pid = 0;
347 idleproc->state = PROC_RUNNABLE;
# 注意,给idleproc分配的栈是在entry.S中给内核设置的栈,8KB
348 idleproc->kstack = (uintptr_t)bootstack;
349 idleproc->need_resched = 1;
350 set_proc_name(idleproc, "idle");
# 应该用于记录当前的至少进入就绪态的进程个数(即当前存活的进程个数)
351 nr_process ++;
352
# 让current指向idleproc,使idleproc成为当前正在运行的进程!!!
# current指向谁谁就是处于“正在运行状态”的进程。
353 current = idleproc;
......
365 }
(k/p/proc.c)
创建第1个内核线程
第0个内核线程主要工作是完成内核中各个子系统的初始化,然后就通过执行cpu_idle函数开始过退休生活了。所以uCore接下来还需创建其他进程来完成各种工作,但idleproc内核子线程自己不想做,于是就通过调用kernel_thread函数创建了一个内核线程init_main。在实验四中,这个子内核线程的工作就是输出一些字符串,然后就返回了(参看init_main函数)。但在后续的实验中,init_main的工作就是创建特定的其他内核线程或用户进程(实验五涉及)。
proc_init()
331 // proc_init - set up the first kernel thread idleproc "idle" by itself and
332 // - create the second kernel thread init_main
333 void
334 proc_init(void) {
......
# 注意这种创建进程的方式,与创建idleproc的方式不同。
355 int pid = kernel_thread(init_main, "Hello world!!", 0);
356 if (pid <= 0) {
357 panic("create init_main failed.\n");
358 }
359
360 initproc = find_proc(pid);
361 set_proc_name(initproc, "init");
362
363 assert(idleproc != NULL && idleproc->pid == 0);
364 assert(initproc != NULL && initproc->pid == 1);
365 }
kernel_thread()
# “第一个内核线程”的执行体
322 // init_main - the second kernel thread used to create user_main kernel threads
323 static int
324 init_main(void *arg) {
325 cprintf("this initproc, pid = %d, name = \"%s\"\n", current->pid, get_proc_name(current));
326 cprintf("To U: \"%s\".\n", (const char *)arg);
327 cprintf("To U: \"en.., Bye, Bye. :)\"\n");
328 return 0;
329 }
333 void
334 proc_init(void) {
......
355 int pid = kernel_thread(init_main, "Hello world!!", 0);
356 if (pid <= 0) {
357 panic("create init_main failed.\n");
358 }
......
365 }
210 // kernel_thread - create a kernel thread using "fn" function
211 // NOTE: the contents of temp trapframe tf will be copied to
212 // proc->tf in do_fork-->copy_thread function
# 注意,这是个通用函数,每次创建新的内核线程,都要调用该函数,所以这里的操作必须也通用。
213 int
214 kernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags) {
#注意,kernel_thread函数采用了局部变量tf来放置保存内核线程的临时中断帧,
#并把中断帧的指针传递给do_fork函数,而do_fork函数会调用copy_thread函数
#来在新创建的进程内核栈上专门给进程的中断帧分配一块空间,并将这里初始化好的
#tf拷贝给未来将要使用的tf。
# 给中断帧分配空间(用局部变量的方式)
215 struct trapframe tf;
# 以下内容为构造新进程的中断帧
# 首先给tf进行清零初始化
216 memset(&tf, 0, sizeof(struct trapframe));
# 设置中断帧的代码段(tf.tf_cs)和数据段(tf.tf_ds/tf_es/tf_ss)为内核空
# 间的段(KERNEL_CS/KERNEL_DS),这实际上也说明了initproc内核线程在内核空间中执行
217 tf.tf_cs = KERNEL_CS;
218 tf.tf_ds = tf.tf_es = tf.tf_ss = KERNEL_DS;
# fn是该线程的执行体,arg是fn的参数,注意这里将fn的地址给了ebx,是为了将来的调用
219 tf.tf_regs.reg_ebx = (uint32_t)fn;
220 tf.tf_regs.reg_edx = (uint32_t)arg;
# tf.tf_eip指出initproc内核线程是从kernel_thread_entry
#(位于kern/process/entry.S中)开始执行的,kernel_thread_entry
# 是entry.S中实现的汇编函数。通过查看tf.tf_eip的含义可知,tf.tf_eip原本是用来
# 记录中断发生时指令指针的地址,也就是当中断例程结束后要返回继续执行的地址。这里其实
# 是在“构造”中断过程,原本产生中断的时候,CPU会填充一部分tf的成员,但是这里全部都是
# 人工填充的,所以这是个“构造中断”的过程。
221 tf.tf_eip = (uint32_t)kernel_thread_entry;
# 构造好新进程的中断帧,开始调用do_fork
222 return do_fork(clone_flags | CLONE_VM, 0, &tf);
223 }
(k/p/proc.c)
1 .text
2 .globl kernel_thread_entry
3 kernel_thread_entry: # void kernel_thread(void)
4
# tf.tf_regs.reg_edx = (uint32_t)arg
# 在kernel_thread()里已经将参数放到了edx寄存器
5 pushl %edx # push arg
6 call *%ebx # call fn
7
8 pushl %eax # save the return value of fn(arg)
9 call do_exit # call do_exit to terminate current thread
10
(k/p/entry.S)
从上可以看出,kernel_thread_entry函数主要为内核线程的主体fn函数做了一个准备开始和结束运行的“壳”,并把函数fn的参数arg(保存在edx寄存器中)压栈,然后调用fn函数,把函数返回值eax寄存器内容压栈,调用do_exit函数退出线程执行。
do_fork()
do_fork是创建线程的主要函数,kernel_thread函数(注意,idleproc并不是通过该函数创建的!!!)通过调用do_fork函数最终完成了内核线程的创建工作(其实只是创建并初始化进程控制块并将其挂链,该线程的实体并未被执行,也并未被加载)。do_fork函数主要做了以下7件事情:
- 分配并初始化进程控制块(alloc_proc函数);
- 分配并初始化内核栈(setup_stack函数);
- 根据clone_flag标志复制或共享进程内存管理结构(copy_mm函数);
- 设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文(copy_thread函数);
- 把设置好的进程控制块放入hash_list和proc_list两个全局进程链表中;
- 自此,进程已经准备好执行了,把进程状态设置为“就绪”态;
- 设置返回码为子进程的id号。
这里需要注意的是,如果上述前3步执行没有成功,则需要做对应的出错处理,把相关已经占有的内存释放掉。copy_mm函数目前只是把current->mm设置为NULL,这是由于目前在实验四中只能创建内核线程,proc->mm描述的是进程用户态空间的情况,所以目前mm还用不上。
copy_thread()
k/t/trapentry.S
46 forkrets:
47 # set stack to this new process's trapframe
48 movl 4(%esp), %esp
49 jmp __trapret
181 // forkret -- the first kernel entry point of a new thread/process
182 // NOTE: the addr of forkret is setted in copy_thread function
183 // after switch_to, the current proc will execute here.
184 static void
185 forkret(void) {
186 forkrets(current->tf);
187 }
251 // copy_thread - setup the trapframe on the process's kernel stack top and
252 // - setup the kernel entry point and stack of process
# 设置进程控制块的中断帧,设置进程控制块的上下文结构
253 static void
254 copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) {
# 把内核线程自己栈顶的区域设置为自己的tf,注意栈顶是高地址
255 proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1;
# 结构体拷贝,把tf的值全部拷贝给proc->tf,而tf的值在kernel_thread()里面已经过初步初始化
# 初步初始化的内容包括:tf.tf_cs/tf.tf_ds/tf.tf_es/tf.tf_ss/
# tf.tf_regs.reg_ebx/tf.tf_regs.reg_edx/tf.tf_eip
256 *(proc->tf) = *tf;
# 设置子进程/线程执行完do_fork后的返回值(???)
257 proc->tf->tf_regs.reg_eax = 0;
# 设置中断帧中的栈指针esp(???)
258 proc->tf->tf_esp = esp;
# 使能中断
259 proc->tf->tf_eflags |= FL_IF;
260
# 设置进程控制块中有关上下文的部分,注意这个proc->context.eip
# 设置好中断帧后,最后就是设置initproc的进程上下文,(process context,也称执行
# 现场)了。只有设置好执行现场后,一旦uCore调度器选择了initproc执行,就需要根据
# initproc->context中保存的执行现场来恢复initproc的执行。这里设置了initproc的执行
# 现场中主要的两个信息:
# 1.上次停止执行时的下一条指令地址context.eip
# 2.上次停止执行时的堆栈地址context.esp。
# 其实initproc还没有执行过,所以这其实就是initproc实际执行的第一条指令地址和堆栈指针。
# 可以看出,由于initproc的中断帧占用了实际给initproc分配的栈空间的顶部,
# 所以initproc就只能把栈顶指针context.esp设置在initproc的中断帧的起始位置。
# 根据context.eip的赋值,可以知道initproc实际开始执行的地方在forkret函数
#(主要完成do_fork函数返回的处理工作)处。至此,initproc内核线程已经做好准备执行了。
261 proc->context.eip = (uintptr_t)forkret;
262 proc->context.esp = (uintptr_t)(proc->tf);
263 }
此函数首先在内核堆栈的顶部设置中断帧大小的一块栈空间,并在此空间中拷贝在kernel_thread函数建立的临时中断帧的初始值,并进一步设置中断帧中的栈指针esp和标志寄存器eflags,特别是eflags设置了FL_IF标志,这表示此内核线程在执行过程中,能响应中断,打断当前的执行。执行到这步后,此进程的中断帧就建立好了,对于initproc而言,它的中断帧如下所示:
# 所在位置的起始地址
initproc->tf= (proc->kstack+KSTACKSIZE) – sizeof (struct trapframe);
# 具体内容
initproc->tf.tf_cs = KERNEL_CS;
initproc->tf.tf_ds = initproc->tf.tf_es = initproc->tf.tf_ss = KERNEL_DS;
initproc->tf.tf_regs.reg_ebx = (uint32_t)init_main;
initproc->tf.tf_regs.reg_edx = (uint32_t) ADDRESS of "Helloworld!!";
# 相当于记录中断结束后要返回的地址
initproc->tf.tf_eip = (uint32_t)kernel_thread_entry;
initproc->tf.tf_regs.reg_eax = 0;
initproc->tf.tf_esp = esp;
initproc->tf.tf_eflags |= FL_IF;
do_fork()
265 /* do_fork - parent process for a new child process
266 * @clone_flags: used to guide how to clone the child process
267 * @stack: the parent's user stack pointer. if stack==0, It means to fork a kernel thread.
268 * @tf: the trapframe info, which will be copied to child process's proc->tf
269 */
270 int
271 do_fork(uint32_t clone_flags, uintptr_t stack, struct trapframe *tf) {
272 int ret = -E_NO_FREE_PROC;
273 struct proc_struct *proc;
274 if (nr_process >= MAX_PROCESS) {
275 goto fork_out;
276 }
277 ret = -E_NO_MEM;
278 //LAB4:EXERCISE2 YOUR CODE
279 /*
280 * Some Useful MACROs, Functions and DEFINEs, you can use them in below implementation.
281 * MACROs or Functions:
282 * alloc_proc: create a proc struct and init fields (lab4:exercise1)
283 * setup_kstack: alloc pages with size KSTACKPAGE as process kernel stack
284 * copy_mm: process "proc" duplicate OR share process "current"'s mm according clone_flags
285 * if clone_flags & CLONE_VM, then "share" ; else "duplicate"
286 * copy_thread: setup the trapframe on the process's kernel stack top and
287 * setup the kernel entry point and stack of process
288 * hash_proc: add proc into proc hash_list
289 * get_pid: alloc a unique pid for process
290 * wakeup_proc: set proc->state = PROC_RUNNABLE
291 * VARIABLES:
292 * proc_list: the process set's list
293 * nr_process: the number of process set
294 */
295
296 bool intr_flag;
297
298 // 1. call alloc_proc to allocate a proc_struct
299 if ((proc = alloc_proc()) == NULL) {
300 cprintf(">>>>WQ DETECT: Fail to alloc_proc\n");
301 goto fork_out;
302 }
303
304 proc->parent = current;
305
306 // 2. call setup_kstack to allocate a kernel stack for child process
# 由此看出,每个内核线程都会专门申请一块内存区域作为自己的堆栈,而不会共用其他内核线程
# 的堆栈或者内核自己的堆栈,当然,idleproc除外,idleproc使用的是内核的堆栈。
307 ret = setup_kstack(proc);
308 if (0 != ret) {
309 cprintf(">>>>WQ DETECT: Fail to setup_kstack\n");
310 goto bad_fork_cleanup_proc;
311 }
312
313 // 3. call copy_mm to dup OR share mm according clone_flag
314 ret = copy_mm(clone_flags, proc);
315 if (0 != ret) {
316 cprintf(">>>>WQ DETECT: Fail to copy_mm\n");
317 goto bad_fork_cleanup_kstack;
318 }
319
320 // 4. call copy_thread to setup tf & context in proc_struct
321 copy_thread(proc, stack, tf);
322
# 注意这里的操作需要关闭中断
323 local_intr_save(intr_flag);
324 {
325 // 5. insert proc_struct into hash_list && proc_list
326 proc->pid = get_pid();
327 hash_proc(proc);
328 list_add(&proc_list, &proc->list_link);
329 nr_process++;
330 }
331 local_intr_restore(intr_flag);
332
333 // 6. call wakeup_proc to make the new child process RUNNABLE
334 wakeup_proc(proc);
335
336 // 7. set ret vaule using child proc's pid
337 ret = proc->pid;
338
339 fork_out:
340 return ret;
341
342 bad_fork_cleanup_kstack:
343 put_kstack(proc);
344 bad_fork_cleanup_proc:
345 kfree(proc);
346 goto fork_out;
347 }
该内核线程的初始化过程要比idle_proc()要复杂一些,由do_fork()完成后续一系列的初始化工作。上图所示是初始化一些地址空间,初始化成这两个值表明代码段和数据段都是在内核空间里面的。
设置线程的起始地址,因为当切换完成之后,线程要从他的起始地址开始执行。fn代表实际的入口地址,但是在进入实际的入口地址之前,要完成一些简单的初始化工作,这就是kernel_thread_entry(最开始初始化的地方),arg是设置与fn相关的一些参数。
tf_esp设置的是当前的esp值;context.esp的设置意味着内核的堆栈里面有一块区域保存了trapframe的全部内容,即trapframe和进程相关的内容保存在内核堆栈里面;context.eip的设置意味着当完成上下文切换之后,当前这个init_proc首先执行的是forkret,forkret主要完成对返回中断的一个处理过程,也意味着它会设置好相应的一些操作,最后执行iret,而iret会根据trapframe里面设置的一些信息跳到init_proc这个内核线程的入口地址去执行,即forkret是进行了一个中断的恢复执行的过程。
setup_stack()设置内核堆栈,之后相应的初始化工作就基本准备就绪,可以将init_proc放到就绪队列里面去管理起来,以及将他的状态设置为runnable,代表它可以被调度执行了。
设置完之后,接下来的工作就是去执行他,从idle_proc里面去执行,因为idle_proc就是当前的ucoreOS。首先要将其设置为unable reschedule状态,让他不能再被调度,因为现在他已经开始运行了,不能递归调度。接着从当前就绪队列里面查找哪些是处于就绪态的,此时至少存在init_proc,接着就做好切换之前的准备。开始执行切换,就要首先切换两个线程的kernel stack,接着切换地址空间(这个地址空间与LAB5的进程有关系,对内核来说是公用同一块地址空间),再接下来是切换context,最后是返回,进入到forkret里面去,来完成一个ret的返回,从而跳到kernel thread entry,由此再执行fn。
proc_init()
创建第0个和第1个线程,设置好进程控制块,但是并未运行相应的进程/线程,仅仅是完成初始化,而且要注意这两个线程的创建方式并不相同,第1个线程的创建方式才是通用的。
# 根据进程号查找对应的进程控制块的过程,是通过哈希表实现的。
195 // find_proc - find proc frome proc hash_list according to pid
196 struct proc_struct *
197 find_proc(int pid) {
198 if (0 < pid && pid < MAX_PID) {
199 list_entry_t *list = hash_list + pid_hashfn(pid), *le = list;
200 while ((le = list_next(le)) != list) {
201 struct proc_struct *proc = le2proc(le, hash_link);
202 if (proc->pid == pid) {
203 return proc;
204 }
205 }
206 }
207 return NULL;
208 }
367 // proc_init - set up the first kernel thread idleproc "idle" by itself and
368 // - create the second kernel thread init_main
369 void
370 proc_init(void) {
371 int i;
372
373 list_init(&proc_list);
374 for (i = 0; i < HASH_LIST_SIZE; i ++) {
375 list_init(hash_list + i);
376 }
377
# 用非常赤裸的方式创建第0个线程!!!
378 if ((idleproc = alloc_proc()) == NULL) {
379 panic("cannot alloc idleproc.\n");
380 }
381
382 idleproc->pid = 0;
383 idleproc->state = PROC_RUNNABLE;
384 idleproc->kstack = (uintptr_t)bootstack;
385 idleproc->need_resched = 1;
386 set_proc_name(idleproc, "idle");
387 nr_process ++;
388
# idleproc被设置为当前正在运行的线程
389 current = idleproc;
390
# 使用标准的kernel_thread方式创建第1个线程
391 int pid = kernel_thread(init_main, "Hello world!!", 0);
392 if (pid <= 0) {
393 panic("create init_main failed.\n");
394 }
395
# initproc在本实验中,指向一个内核线程。本实验以后,此指针将指向第一个用户态进程。
# 将新创建的线程作为initproc。
396 initproc = find_proc(pid);
397 set_proc_name(initproc, "init");
398
399 assert(idleproc != NULL && idleproc->pid == 0);
400 assert(initproc != NULL && initproc->pid == 1);
401 }
cpu_idle()
在uCore执行完proc_init函数后,就创建好了两个内核线程:idleproc和initproc,这时uCore当前的执行现场就是idleproc,等到执行到init函数的最后一个函数cpu_idle之前,uCore的所有初始化工作就结束了,idleproc将通过执行cpu_idle函数让出CPU,给其它内核线程执行,具体过程如下:
403 // cpu_idle - at the end of kern_init, the first kernel thread idleproc will do below works
404 void
405 cpu_idle(void) {
406 while (1) {
# 在proc_init()中,current被设置为idleproc,且其need_resched是1
407 if (current->need_resched) {
408 schedule();
409 }
410 }
411 }
首先,判断当前内核线程idleproc的need_resched是否不为0,回顾前面“创建第一个内核线程idleproc”中的描述,proc_init函数在初始化idleproc中,就把idleproc->need_resched置为1了,所以会马上调用schedule函数找其他处于“就绪”态的进程执行。
schedule()
uCore在实验四中只实现了一个最简单的FIFO调度器,其核心就是schedule函数。它的执行逻辑很简单:
- 设置当前内核线程current->need_resched为0;
- 在proc_list队列中查找下一个处于“就绪”态的线程或进程next;
- 找到这样的进程后,就调用proc_run函数,保存当前进程current的执行现场(进程上下文),恢复新进程的执行现场,完成进程切换。
至此,新的进程next就开始执行了。由于在本实验只有两个内核线程,且idleproc要让出CPU给initproc执行,我们可以看到schedule函数通过查找proc_list进程队列,只能找到一个处于“就绪”态的initproc内核线程。通过proc_run和进一步的switch_to函数完成两个执行现场的切换。
13 void
14 schedule(void) {
15 bool intr_flag;
16 list_entry_t *le, *last;
17 struct proc_struct *next = NULL;
# 开始调度的时候要关闭中断,说明该过程不能被打断。
18 local_intr_save(intr_flag);
19 {
20 current->need_resched = 0;
# 从这里可以看出idleproc的作用,尽管该进程没有执行实体,但它的作用就是当队列
# 里没有就绪态进程时,利用idleproc的进程控制块作为current,在cpu_idle()里面
# 不断的循环不断的进入schedule()函数,从而不断在队列中搜索已经进入就绪态的
# 进程,当找到该进程的时候便切换到该进程去执行。其实idleproc的感觉就像是哨兵。
21 last = (current == idleproc) ? &proc_list : &(current->list_link);
22 le = last;
23 do {
# 从这里可以看出,ucore并没有根据不同的状态去设置进程链表,进程链表就一个,
# 是从当前进程节点往后逐个根据状态查找进入就绪态的进程,这个过程当然就会路过
# 非就绪态的节点。注意,并不是从链表头开始往后查的,而是从当前进程节点往后查
# 的,这个细节很重要,因为从链表头到该进程节点之间的节点很可能是之前已经被调度
# 过了的节点,为保证每个进程都有被执行的机会,所以要从当前节点之后开始继续查找
24 if ((le = list_next(le)) != &proc_list) {
25 next = le2proc(le, list_link);
26 if (next->state == PROC_RUNNABLE) {
27 break;
28 }
29 }
30 } while (le != last);
# 没有合适的进程,就让调度给idleproc去继续跑,继续查合适的进程
31 if (next == NULL || next->state != PROC_RUNNABLE) {
32 next = idleproc;
33 }
# 进程被调度的次数要加一
34 next->runs ++;
# 有一种可能是,进schedule()的时候current是idleproc,到了这个位置next仍然是
# idleproc,所以要做一个判断,因为如果调度前后始终是idleproc的话就没必要调度。
# 总之,真正允许调度的条件就是调度前后的进程不能是同一个!!!
35 if (next != current) {
36 proc_run(next);
37 }
38 }
39 local_intr_restore(intr_flag);
40 }
proc_run()
通过proc_run和进一步的switch_to函数完成两个执行现场的切换,具体流程如下:
- 让current指向next内核线程initproc;
- 设置任务状态段ts中特权态0下的栈顶指针esp0为next内核线程initproc的内核栈的栈顶,即next->kstack + KSTACKSIZE ;
- 设置CR3寄存器的值为next内核线程initproc的页目录表起始地址next->cr3,这实际上是完成进程间的页表切换;
- 由switch_to函数完成具体的两个线程的执行现场切换,即切换各个寄存器,当switch_to函数执行完“ret”指令后,就切换到initproc执行了。
注意,在第二步设置任务状态段ts中特权态0下的栈顶指针esp0的目的是建立好内核线程或将来用户线程在执行特权态切换(从特权态0<-->特权态3,或从特权态3<-->特权态3)时能够正确定位处于特权态0时进程的内核栈的栈顶,而这个栈顶其实放了一个trapframe结构的内存空间。如果是在特权态3发生了中断/异常/系统调用,则CPU会从特权态3-->特权态0,且CPU从此栈顶(当前被打断进程的内核栈顶)开始压栈来保存被中断/异常/系统调用打断的用户态执行现场;如果是在特权态0发生了中断/异常/系统调用,则CPU会从当前内核栈指针esp所指的位置开始压栈保存被中断/异常/系统调用打断的内核态执行现场。反之,当执行完对中断/异常/系统调用打断的处理后,最后会执行一个“iret”指令。在执行此指令之前,CPU的当前栈指针esp一定指向上次产生中断/异常/系统调用时CPU保存的被打断的指令地址CS和EIP,“iret”指令会根据ESP所指的保存的址CS和EIP恢复到上次被打断的地方继续执行。
在页表设置方面,由于idleproc和initproc都是共用一个内核页表boot_cr3,所以此时第三步其实没用,但考虑到以后的进程有各自的页表,其起始地址各不相同,只有完成页表切换,才能确保新的进程能够正常执行。
第四步proc_run函数调用switch_to函数,参数是前一个进程和后一个进程的执行现场:process context。
163 // proc_run - make process "proc" running on cpu
164 // NOTE: before call switch_to, should load base addr of "proc"'s new PDT
# 参数就是将要被调度的进程的进程控制块,该函数的作用就是“将参数进程调度为当前正在运行的进程”
165 void
166 proc_run(struct proc_struct *proc) {
# 只有当前的和被调度的不是同一个,才允许调度
167 if (proc != current) {
168 bool intr_flag;
169 struct proc_struct *prev = current, *next = proc;
# 关闭中断(如果之前已经关闭中断,则不影响)
170 local_intr_save(intr_flag);
171 {
# 被调度的进程成为当前进程
172 current = proc;
# 将新线程的栈顶加载到TSS,这里会修改TSS的esp0的值
173 load_esp0(next->kstack + KSTACKSIZE);
# 将新线程的页目录表的起始地址加载到cr3寄存器,这也会修改cr3寄存器的值
# (其实并不会,因为页目录表都是boot_pgdir)
174 lcr3(next->cr3);
# 开始实际的调度动作,切换上下文等操作。
175 switch_to(&(prev->context), &(next->context));
176 }
177 local_intr_restore(intr_flag);
178 }
179 }
switch_to
+| 栈底方向 | 高位地址
| ... |
| ... |
| 参数3 |
| 参数2 |
| 参数1 |
| 返回地址 | <-------- [ebp/esp]
| 上一层[ebp] | (原本需要在进入函数后将ebp压栈,但是switch_to里没这个动作,所以进入switch_to后esp指向如本图所示)
| 局部变量 |
| | 低位地址
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;
};
1 .text
2 .globl switch_to
3 switch_to: # switch_to(from, to)
4
5 # save from's registers
# 如上图所示,此时的esp指向switch_to返回之后的地址
# 所以4(%esp)就表示从esp再往高地址走4B,正好就是switch_to第一个参数的地址
# 而第一个参数就是from的地址,所以eax指向了from
6 movl 4(%esp), %eax # eax points to from
# 此时esp指向switch_to返回之后的地址,弹栈就是将该地址放到from->eip,
# 按理说,保存的应该是被打断执行的进程再次被调度时该进程接着要执行的指令地址,
# 但是从这里的代码看,弹栈的结果明明是把switch_to返回后的指令执行地址保存在了
# from->eip里面,感觉不对呀!!!哪里的问题???
# 经过分析,发现没问题,因为这里是从idleproc调度过来的,idleproc是个空的线程,
# 并没有执行体,from->eip记录的正是“再次被调度时该进程接着要执行的指令地址”,也就是
# 最终会回到cpu_idle()里面循环调用schedule()那里,但其实并没有回到这里,因为调度
# 到initproc之后就exit了。就算到了实验五,所有调度都是手工调度,而不是通过定时器中断
# 去自动调度,都是通过手工调用schedule()实现的调度,进程并不能实现真正自动的切换。
7 popl 0(%eax) # save eip !popl
# 此时的esp应该指向switch_to第一个参数的地址,因为刚刚有一个弹栈的动作,esp被改动
8 movl %esp, 4(%eax) # save esp::context of from
9 movl %ebx, 8(%eax) # save ebx::context of from
10 movl %ecx, 12(%eax) # save ecx::context of from
11 movl %edx, 16(%eax) # save edx::context of from
12 movl %esi, 20(%eax) # save esi::context of from
13 movl %edi, 24(%eax) # save edi::context of from
14 movl %ebp, 28(%eax) # save ebp::context of from
15
# 对于initproc刚开始的状态,如下
# proc->context.eip = (uintptr_t)forkret;
# proc->context.esp = (uintptr_t)(proc->tf);
# proc->context的其他值都是0
16 # restore to's registers
# 刚刚有一个弹栈的动作,所以现在esp指向switch_to第一个参数,4(%esp)为第二个参数
17 movl 4(%esp), %eax # not 8(%esp): popped return address already
18 # eax now points to argument "to"
19 movl 28(%eax), %ebp # restore ebp::context of to
20 movl 24(%eax), %edi # restore edi::context of to
21 movl 20(%eax), %esi # restore esi::context of to
22 movl 16(%eax), %edx # restore edx::context of to
23 movl 12(%eax), %ecx # restore ecx::context of to
24 movl 8(%eax), %ebx # restore ebx::context of to
25 movl 4(%eax), %esp # restore esp::context of to
26
# 直接将eip压栈,不用给eip寄存器赋值,因为ret返回时硬件会自动将存到栈里的eip地址弹栈
# 并跳到该地址继续执行指令。其实“pushl 0(%eax)”把context中保存的下一个进程要执行的
# 指令地址context.eip放到了堆栈顶,这样接下来执行最后一条指令“ret”时,会把栈顶的内容
# 赋值给EIP寄存器,这样就直接切换到下一个进程执行了。
27 pushl 0(%eax) # push eip
28
# 如果是切换到initproc,那么ret一执行,就到了forkret了。个人感觉用jmp是不是也行。
29 ret
forkret
181 // forkret -- the first kernel entry point of a new thread/process
182 // NOTE: the addr of forkret is setted in copy_thread function
183 // after switch_to, the current proc will execute here.
184 static void
185 forkret(void) {
# 在proc_run()里已经将current改为initproc
186 forkrets(current->tf);
187 }
1 #include <memlayout.h>
2
3 # vectors.S sends all traps here.
4 .text
5 .globl __alltraps
6 __alltraps:
7 # push registers to build a trap frame
8 # therefore make the stack look like a struct trapframe
9 pushl %ds
10 pushl %es
11 pushl %fs
12 pushl %gs
13 pushal
14
15 # load GD_KDATA into %ds and %es to set up data segments for kernel
16 movl $GD_KDATA, %eax
17 movw %ax, %ds
18 movw %ax, %es
19
20 # push %esp to pass a pointer to the trapframe as an argument to trap()
21 pushl %esp
22
23 # call trap(tf), where tf=%esp
24 call trap
25
26 # pop the pushed stack pointer
27 popl %esp
28
29 # return falls through to trapret...
30 .globl __trapret
31 __trapret:
32 # restore registers from stack
initproc->tf.tf_cs = KERNEL_CS;
initproc->tf.tf_ds = initproc->tf.tf_es = initproc->tf.tf_ss = KERNEL_DS;
initproc->tf.tf_regs.reg_ebx = (uint32_t)init_main;
initproc->tf.tf_regs.reg_edx = (uint32_t) ADDRESS of "Helloworld!!";
initproc->tf.tf_eip = (uint32_t)kernel_thread_entry;
initproc->tf.tf_regs.reg_eax = 0;
initproc->tf.tf_esp = esp;
initproc->tf.tf_eflags |= FL_IF;
# 由于栈顶是initproc->tf.tf_regs,所以弹栈的时候,相当于
# 将initproc->tf.tf_regs里面的内容先全部弹出!!!
# popal指令弹出顺序:edi->esi->ebp->esp->ebx->edx->ecx->eax
33 popal
34
35 # restore %ds, %es, %fs and %gs
36 popl %gs
37 popl %fs
38 popl %es
39 popl %ds
40
41 # get rid of the trap number and error code
42 addl $0x8, %esp
struct trapframe {
struct pushregs tf_regs;# 注意这里!!!
uint16_t tf_gs;
uint16_t tf_padding0;
uint16_t tf_fs;
uint16_t tf_padding1;
uint16_t tf_es;
uint16_t tf_padding2;
uint16_t tf_ds;
uint16_t tf_padding3;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint16_t tf_padding4;
uint32_t tf_eflags;
/* below here only when crossing rings, such as from user to kernel */
uintptr_t tf_esp;
uint16_t tf_ss;
uint16_t tf_padding5;
} __attribute__((packed));
# 由此看出,执行iret指令的时候,硬件会自动跳到tf->tf_eip地址去执行指令,
# 从kernel_thread()中可以看出,initproc->tf.tf_eip = (uint32_t)kernel_thread_entry
# 所以iret执行完之后CPU就跳到了kernel_thread_entry!!!
43 iret
44
45 .globl forkrets
46 forkrets:
47 # set stack to this new process's trapframe
+| 栈底方向 | 高位地址
| ... |
| ... |
| 参数3 |
| 参数2 |
| 参数1 |
| 返回地址 | <-------- [ebp/esp]
| 上一层[ebp] | (原本需要在进入函数后将ebp压栈,但是forkrets里没这个动作,所以进入forkrets后esp指向如本图所示)
| 局部变量 |
| | 低位地址
# 4(%esp)就是forkrets的第一个参数,即current->tf,也就是initproc->tf,
# 而且在copy_thread里将initproc自己的栈的栈顶地址设置为initproc->tf,
# 所以这里“movl 4(%esp), %esp”才能直接将current->tf的地址直接赋值给esp作为栈顶
# 注意该指令结束后,现在的栈已经是创建initproc线程的时候新建立的栈了,即该线程自己的栈。
48 movl 4(%esp), %esp
49 jmp __trapret
kernel_thread_entry
1 .text
2 .globl kernel_thread_entry
3 kernel_thread_entry: # void kernel_thread(void)
4
# 见kernel_thread中,tf.tf_regs.reg_edx = (uint32_t)arg
# __trapret里面popal的过程将tf.tf_regs.reg_edx弹栈给edx
5 pushl %edx # push arg
# 见kernel_thread中,tf.tf_regs.reg_ebx = (uint32_t)fn
# __trapret里面popal的过程将tf.tf_regs.reg_ebx弹栈给ebx
# 到这里终于开始执行线程函数init_main了!!!
6 call *%ebx # call fn
7
8 pushl %eax # save the return value of fn(arg)
# 整个实验四结束
9 call do_exit # call do_exit to terminate current thread
本文围绕uCore实验四的内核线程管理展开。介绍了进程控制块的关键成员变量,如mm、state等,以及相关全局变量。阐述了内核线程与用户进程的区别,详细分析了创建并执行内核线程idleproc和initproc的流程,还涉及进程调度和切换的内容。

4892

被折叠的 条评论
为什么被折叠?



