系列目录
- 序篇
- 筹备工作
- BIOS 启动到实模式
- GDT 与保护模式
- 虚拟内存初探
- 加载并进入 kernel
- 显示与打印
- 全局描述符表 GDT
- 中断解决
- 虚拟内存欠缺
- 实现堆和 malloc
- 第一个 kernel 线程
- 多线程运行与切换
- 锁与多线程同步
- 过程的实现
- 进入用户态
- 一个简略的文件系统
- 加载可执行程序
- 零碎调用的实现
- 键盘驱动
- 运行 shell
筹备
这个我的项目系列到这里差不多到一半了,前半部分的 segment,虚拟内存,中断等,其实能够看作只是筹备工作。是的,咱们花了大量工夫去筹备这些基石工作,以至于到当初,整个所谓的 kernel 如同依然处于一种“静止“状态,可能曾经让你感觉困倦了。从本篇开始,这个 kernel 将会真正地”动“起来,开始搭建一个操作系统应有的外围能力,那就是工作治理。
从用户的角度来讲,操作系统的实质性能就是为他们运行工作,否则就难以称之为操作系统了。这是一个简单的工程,万事开头难,所以作为开始阶段的本篇,会尽可能地简略,从运行一个单线程开始。在前面的篇章中,会逐步进入多线程切换与治理,同步与竞争等问题,以及最终来到更下层的过程治理。
线程
thread
和 process
的相干概念应该不须要多解释了,都是陈词滥调。在接下来的行文和代码中,我会将 task
等同于 thread
,两者混用,都示意线程;而 process
则是过程。
操作系统调度的对象是 thread
,也是接下来须要探讨和实现的外围概念。兴许 thread
听下来很形象,从实质上来说,它能够归结为以下两个外围因素:
代码 + stack
代码管制它工夫维度上的流转,stack
则是它空间维度的依靠,这两者形成了 thread
运行的外围。
所以每个 thread 都有它本人的 stack,例如运行在 kernel 态的一堆 threads,大略是这样的格局:
每个 thread 运行在它本人的 stack 上,而操作系统则负责调度这些 threads 的启停。从实质上说,自从咱们进入 kernel 的 main 函数运行到当初,也能够归为一个 thread,它是一个疏导。再往后,操作系统将创立更多的 threads,并且 CPU 将会在操作系统的管制下,在这些 threads 之间来回跳转切换,其实质就是在这些 threads 各自所属代码(指令)和 stack 上进行跳转切换。
创立 thread
本篇代码次要在 src/task/thread.c,仅供参考。
首先建设 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 信息:
- kernel_esp
- kernel_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;
- 所有的通用寄存器初始化为 0,因为这是 thread 第一次运行;
start_eip
是 thread 入口地址,设置为kernel_thread
这个函数;function
是 thread 真正要运行的工作函数,它由kernel_thread
函数来启动运行;
static void kernel_thread(thread_func* function) {function();
schedule_thread_exit(0);
}
这里如果不明确的话能够先接着往下看 thread 的运行,而后再来回顾。
运行 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
布局? - thread 运行完结后的退出机制是什么样的?
- 以及最重要的,thread 之间怎么切换运行?
这些都留待下一篇详解,待到下一篇实现后,联合这两篇的内容,应该会对 threaad 的运行机制有一个全面的意识。