共计 3551 个字符,预计需要花费 9 分钟才能阅读完成。
系列目录
- 序篇
- 筹备工作
- BIOS 启动到实模式
- GDT 与保护模式
- 虚拟内存初探
- 加载并进入 kernel
- 显示与打印
- 全局描述符表 GDT
- 中断解决
- 虚拟内存欠缺
- 实现堆和 malloc
- 第一个内核线程
- 多线程切换
- 锁与多线程同步
- 过程的实现
- 进入用户态
- 一个简略的文件系统
- 加载可执行程序
- 零碎调用的实现
- 键盘驱动
- 运行 shell
筹备
接上一篇,咱们启动了第一个 thread
,如同只是让一个一般的函数在一个全新的 stack 上运行起来而已。作为一个真正的操作系统,须要能运行调度多个 threads。创立多个 threads 的过程很简略,问题是如何让它们交替切换运行起来。回到上一篇提到的对于 thread 的两个外围形成因素:
- 代码
- stack
本篇将进入多线程的世界。在这里会展现,两个 thread 如何实现 stack
的转换,并且在 stack 转换的同时,代码的执行流也主动产生了切换。并且基于 multi-threads 的切换能力,咱们将初步构建出一个调度器 scheduler
。
线程切换
首先回顾上一篇,thread 启动后的 stack 情况,程序运行在图中浅蓝色局部的 stack 区域内:
如果没有外界触发,程序将始终在这里运行上来,不可能产生 thread 切换,即便在别的中央另外有一个 thread,领有它本人独立的 stack;因而这里须要一个触发者,这就是时钟 interrupt,在中断解决一篇的最初,咱们尝试关上了时钟 interrupt,那里的 handler 只是简略的打印函数。当初咱们须要在外面实现 threads 切换。
构想程序正在上图的 stack 运行,这时产生了时钟 interrupt,CPU 将主动进入中断解决,回顾一下中断解决一篇的内容,此时 stack 会变成这样:
通过 CPU 和 kernel 的一系列压栈操作,程序执行流程大略是这样:
isr32 (32 是时钟 interrupt 中断号)
--> isr_common_stub
--> isr_handler
--> timer_callback
最终来到 timer_callback
函数:
struct task_struct* crt_thread;
struct task_struct* next_thread;
static void timer_callback(isr_params_t regs) {
// For only 2 threads switch.
struct task_struct* old_thread = crt_thread;
struct task_struct* new_thread = next_thread;
// swap, for next time switch.
crt_thread = new_thread;
next_thread = old_thread;
context_switch(old_thread, new_thread);
}
如果你不记得 task_struct
定义了,它就是 thread 的形容构造,这里再贴一下:
struct task_struct {
uint32 kernel_esp;
uint32 kernel_stack;
uint32 id;
// ...
};
下面的示例代码应该不难理解,假设咱们只有 2 个 threads,那么在 timer_callback
里就是实现它们两者的相互切换。
要害局部是最初一行 context_switch 函数,它实现了两个 thread 的切换,它其实分了高低两半局部:
上半局部实现对以后 thread 的 context 保留:
context_switch:
push eax
push ecx
push edx
push ebx
push ebp
push esi
push edi
mov eax, [esp + 32]
mov [eax], esp
上班局部,实现对下一个待运行 thread 的复原:
; jump to next thread's kernel stack
; and recover its execution
mov eax, [esp + 36]
mov esp, [eax]
resume_thread:
pop edi
pop esi
pop ebp
pop ebx
pop edx
pop ecx
pop eax
sti
ret
留神 esp + 32
和 esp + 36
是拿到 context_switch
函数的两个参数,即待切换的新老两个 thread:
void context_switch(old_thread, new_thread);
参数是指向 task_struct
的指针,而 task_struct
的第一个字段就是 kernel_esp
,也就是说这里将 threads 产生切换时的 esp,保留在了 kernel_esp
这个字段里;待下一次 thread 复原运行时,再读出 kernel_esp
找到它被切走前的 最初一瞬间的 esp,持续运行。
而且通过 stack 转换后,代码的执行流也主动复原到了下一个 thread 原来的执行流上,事实上它们原本就是同一个流。因为待切换的 thread,之前被切走时,也是通过 context_switch
函数,push 了所有通用寄存器,保留 esp,而后被切走,当初则是在雷同的指令地位原地复原,继续执行。
所以这里须要了解的最要害的一点是,context_switch
函数的高低两半局部的执行,是分属于新老两个 thread 的。老 thread 执行完前半段,就被切走挂起;它要等到下次再被调度时,原地复原,持续运行后半段,拼成一个残缺的 context_switch
执行流;而后向上层层 return,最终回到时钟 interrupt 之前打断它的那个中央持续运行。
切换到新创建的 thread 上
下面探讨的状况中,待运行的 next thread 是一个之前运行过,被切走挂起的 thread,所以它的 stack 布局是和以后运行的 thread 是一样的,都是来到 context_switch
函数的运行 stack。但如果待运行的 thread 是一个新创建的呢?如果回顾上一章的内容,就会发现这里在设计上是无缝连接的。
上图是新创建的 thread 的 stack,它恰好初始化了一个 switch stack,并且将 kernel_esp
字段指向了该 stack,因而从 context_switch
函数的下半局部开始运行,它就能立即正确初始化 esp,并且 pop 所有通用寄存器,这和第一个 thread 启动的形式是统一的。
也就是说,第一个 thread,是从 resume_thread
开始启动;而前面的所有 thread,都是通过 context_swith
以雷同的形式完满启动。
线程调度器
有了 2 个 threads 的切换能力,咱们实际上能够创立更多的 threads,并且开始构建一个调度器 scheduler
。最简略调度模式就是所有 threads 顺次运行,周而复始,所以这首先须要一个链表来保留所有的 threads。
我在 src/utils/linked_list.c 实现了一个链表构造,它是一个通用链表,每个节点保留的是指向理论对象的指针,在咱们这里就是 task_struct*
,具体的实现能够参考代码,比较简单。
struct linked_list_node {
type_t ptr;
struct linked_list_node* prev;
struct linked_list_node* next;
};
typedef struct linked_list_node linked_list_node_t;
struct linked_list {
linked_list_node_t* head;
linked_list_node_t* tail;
uint32 size;
};
typedef struct linked_list linked_list_t;
因而咱们能够定义所有待运行 threads 的链表,即 ready_tasks,这外面所有的 thread 状态都是 TASK_READY
,也就是就绪态,它们是能够被下一次调度运行的 task:
static linked_list_t ready_tasks;
因而 scheduler
的逻辑很简略,这里以伪代码展现:
next_thread = Fetch head from ready_tasks queue;
set next_thread.status = TASK_RUNNING;
set crt_thread.status = TASK_READY;
Enqueue crt_thread to tail of ready_tasks queue;
context_switch(crt_thread, next_thread);
总结
本篇实现了多线程运行和 scheduler 的根本框架,实际上到此为止,这个 kernel 曾经能够称得上是一个真正的 OS 了,它曾经具备了一个最简略的操作系统应有的根本骨架:
- 内存治理
- 中断解决
- 工作治理
前面的工作,咱们将持续丰盛这个 kernel,使之更多的性能和更好的用户交互能力。