这个项目系列到这里差很少到一半了,前半部分的 segment,虚拟内存,中断等,其实能够看做只是准备工做。是的,咱们花了大量时间去准备这些基石工做,以致于到如今,整个所谓的 kernel 好像仍然处于一种“静止“状态,可能已经让你以为困倦了。从本篇开始,这个 kernel 将会真正地”动“起来,开始搭建一个操做系统应有的核心能力,那就是任务管理。java
从用户的角度来说,操做系统的本质功能就是为他们运行任务,不然就难以称之为操做系统了。这是一个复杂的工程,万事开头难,因此做为开始阶段的本篇,会尽量地简单,从运行一个单线程开始。在后面的篇章中,会逐渐进入多线程切换与管理,同步与竞争等问题,以及最终来到更上层的进程管理。git
thread
和 process
的相关概念应该不须要多解释了,都是老生常谈。在接下来的行文和代码中,我会将 task
等同于 thread
,二者混用,都表示线程;而 process
则是进程。shell
操做系统调度的对象是 thread
,也是接下来须要讨论和实现的核心概念。 也许 thread
听上去很抽象,从本质上来讲,它能够归结为如下两个核心要素:segmentfault
代码 + stack
代码控制它时间维度上的流转,stack
则是它空间维度的依托,这二者构成了 thread
运行的核心。多线程
因此每一个 thread 都有它本身的 stack,例如运行在 kernel 态的一堆 threads,大概是这样的格局:app
每一个 thread 运行在它本身的 stack 上,而操做系统则负责调度这些 threads 的启停。从本质上说,自从咱们进入 kernel 的 main 函数运行到如今,也能够归为一个 thread,它是一个引导。再日后,操做系统将建立更多的 threads,而且 CPU 将会在操做系统的控制下,在这些 threads 之间来回跳转切换,其实质就是在这些 threads 各自所属代码(指令)和 stack 上进行跳转切换。函数
本篇代码主要在 src/task/thread.c,仅供参考。gitlab
首先创建 thread 结构 task_struct
,或者叫 tcb_t
,即 task control block
:布局
struct task_struct { uint32 kernel_esp; uint32 kernel_stack; uint32 id; char name[32]; // ... }; typedef struct task_struct tcb_t;
这里有两个关键字段,是关于这个 thread 的 stack 信息:测试
每一个 thread 都以 page 为单位分配 kernel stack 空间,Linux 好像是 2 pages,因此咱们也分配 2 pages,用 kernel_stack
字段指向它,这个字段后面再也不变化:
#define KERNEL_STACK_SIZE 8192 tcb_t* init_thread(char* name, thread_func function, uint32 priority, uint8 is_user_thread) { // ... thread = (tcb_t*)kmalloc(sizeof(struct task_struct)); // ... uint32 kernel_stack = (uint32)kmalloc_aligned(KERNEL_STACK_SIZE); for (int32 i = 0; i < KERNEL_STACK_SIZE / PAGE_SIZE; i++) { map_page(kernel_stack + i * PAGE_SIZE); } memset((void*)kernel_stack, 0, KERNEL_STACK_SIZE); thread->kernel_stack = kernel_stack; // ... }
注意这里分配了 2 pages 给 kernel_stack 后,马上为它创建了 physical 内存的映射。这是由于 page fault
做为一个中断,它的处理是要在 kernel stack 上完成的,所以对 kernel stack 自己的访问不能够再触发 page fault
,因此这里提早解决了这个问题。
另外一个字段 kernel_esp
,标识的是当前这个 thread 在 kernel stack 上运行的 esp 指针。目前是 thread 建立阶段,咱们须要初始化这个 esp,因此首先须要对整个 stack 的版图作一个初始化。咱们为 stack 定义以下结构:
struct switch_stack { // Switch context. uint32 edi; uint32 esi; uint32 ebp; uint32 ebx; uint32 edx; uint32 ecx; uint32 eax; // For thread first run. uint32 start_eip; void (*unused_retaddr); thread_func* function; };
这个 stack 结构第一眼看上去可能比较奇怪,后面会慢慢展开解释。它既是 thread 第一次启动运行时的初始 stack,也是后面 multi-threads 上下文切换时的 stack,因此也能够叫 context switch stack
,或者 switch stack
。咱们将它铺设到刚才分配的 2 pages 的 stack 空间上去:
switch stack
上方的虚线空间是预留的,这是之后做为返回用户空间用的 interrupt stack
,暂时能够无视。目前你只须要知道它的结构定义为 interrupt_stack_t
,也就是以前的 src/interrupt/interrupt.h 里定义的 isr_params
这个结构,你能够回顾一下中断处理这一篇,它是中断发生时的 CPU 和操做系统压栈,用于保存中断 context 的。
因此整个 stack 的初始化,就是在最上方分配了一个 interrupt stack
+ switch stack
结构:
thread->kernel_esp = kernel_stack + KERNEL_STACK_SIZE - (sizeof(interrupt_stack_t) + sizeof(switch_stack_t));
因而 kernel_esp
就被初始化为上图标出的位置,实际上指向了 switch stack
这个结构。
接下来就是初始化这个 switch stack
:
switch_stack_t* switch_stack = (switch_stack_t*)thread->kernel_esp; switch_stack->edi = 0; switch_stack->esi = 0; switch_stack->ebp = 0; switch_stack->ebx = 0; switch_stack->edx = 0; switch_stack->ecx = 0; switch_stack->eax = 0; switch_stack->start_eip = (uint32)kernel_thread; switch_stack->function = function;
start_eip
是 thread 入口地址,设置为 kernel_thread
这个函数;function
是 thread 真正要运行的工做函数,它由 kernel_thread
函数来启动运行;static void kernel_thread(thread_func* function) { function(); schedule_thread_exit(0); }
这里若是不明白的话能够先接着往下看 thread 的运行,而后再来回顾。
建立 thread,而且运行 thread:
void test_thread() { monitor_printf("first thread running ...\n"); while (1) {} } int main() { tcb_t* thread = init_thread( "test", test_thread, THREAD_DEFAULT_PRIORITY, false); asm volatile ( "movl %0, %%esp; \ jmp resume_thread": : "g" (thread->kernel_esp) : "memory"); }
测试代码很简单,建立了一个 thread,它要运行的函数是 test_thread
,仅仅是打印一下。
这里在 C 语言里用内联 asm 代码,触发了 thread 开始运行,来看一下它的原理。首先将 esp 寄存器赋值为该 thread 的 kernel_esp
,而后跳转到 resume_thread 这个函数:
resume_thread: pop edi pop esi pop ebp pop ebx pop edx pop ecx pop eax sti ret
它实际上是 context_switch 函数的下半部分,这个是用于 multi-threads 切换的,这个下一篇再讲。
来看 resume_thread
作的事情,对照图中的 kernel_esp
位置开始,运行代码:
pop
了全部通用寄存器,在 multi-threads 切换里它是用来恢复 thread 的 context 数据的,可是如今 thread 是第一次运行,因此它们这里全被 pop 成了 0;ret
指令,使程序跳转到了 start_eip
处,它被初始化为为函数 kernel_thread
,从这里 thread 正式开始运行,它的运行 stack 为右图中浅蓝色部分;注意到 kernel_thread
函数,传入并运行了参数 function
,这是 thread
真正的工做函数:
static void kernel_thread(thread_func* function) { function(); schedule_thread_exit(0); }
这里可能有几个问题须要解释一下:
[问题 1] 为何不直接运行 function
,而要在外面嵌套一层 kernel_thread
函数做为 wrapper?
由于 thread 运行结束后须要一个退出机制,函数 schedule_thread_exit
会完成 thread 的收尾和清理工做;schedule_thread_exit
函数不会返回,而是直接引导该 thread 消亡,而后切换到下一个待运行的 thread。关于 schedule_thread_exit
也会在下一篇中再细讲。
[问题 2] 图中灰色的 unused
部分是什么?
它是 kernel_thread
函数的返回值,实际上 kernel_thread
不会返回,由于函数 schedule_thread_exit
不会返回。这里 unused
仅仅是一个占位。
OK,至此咱们的第一个 thread 就运行起来了,能够看到它的打印:
本篇运行了第一个 thread,它作的事情其实比较简单,就是找了一块内存作 stack
,而后在上面创造出了一个函数运行的环境,而后跳转指令到 thread 的入口处开始运行。可能有不少细节处仍是雾里看花,不知其因此然,本篇都没有详细展开,例如:
stack
布局?这些都留待下一篇详解,待到下一篇完成后,结合这两篇的内容,应该会对 threaad 的运行机制有一个全面的认识。