12.内核线程的创建与切换
此处的内核线程作为运行在内核态的一个逻辑执行流,用于私有的栈空间,但是除了私有的栈空间外,不拥有其他资源
所以的内核线程拥有相同的页表,共享所有的全局数据
一般OS 都不会完全采用硬件切换机制,但本简单的内核,只是涉及到内核态,不涉及特权级的转移过程,所以完全可以用硬件实现
任务的切换必然涉及到现场的保护与恢复,所以就必然需要一个数据结构来保存这 些现场信息。这个数据结构中一般也会放置任务相关的一些信息并且以链表之类的方式 组织起来,这个结构被称之为PCB(Process Control Block)
或者TCB(Task Control Block)
include/task.h 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #ifndef INCLUDE_TASK_H_ #define INCLUDE_TASK_H_
#include "types.h" #include "pmm.h" #include "vmm.h"
typedef enum task_state{ TASK_UNINIT = 0, TASK_SLEEPING = 1, TASK_RUNNABLE = 2, TASK_ZOMBIE = 3, }task_state;
struct context { uint32_t esp; uint32_t ebp; uint32_t ebx; uint32_t esi; uint32_t edi; uint32_t eflags; };
struct mm_struct { pgd_t *pgd_dir; };
struct task_struct { volatile task_state state; pid_t pid; void *stack; struct mm_struct *mm; struct context context; struct task_struct *next; };
extern pid_t now_pid;
int32_t kernel_thread(int (*fn)(void *), void *arg);
void kthread_exit();
#endif
|
include/types.h 补充
调度机制
include/sched.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #ifndef INCLUDE_SCHEDULER_H_ #define INCLUDE_SCHEDULER_H_
#include "types.h" #include "task.h"
extern struct task_struct *running_proc_head;
extern struct task_struct *wait_proc_head;
extern struct task_struct *current;
void init_sched();
void schedule();
void change_task_to (struct task_struct *next);
void switch_to (struct context *prev, struct context *next);
#endif
|
所以任务组织的方式就是一个单向循环链表,调 度程序每次选择当前任务的下一个任务运行
没有采用复杂的调度策略
在进行任务切换之前,内核原先的执行流还没有一个结构来保存其信息,所以需要在初 始化调度之前给原始的执行流创建PCB信息。这里模仿Linux内核早期的做法,将PCB放置 在线程栈的最低处。
kernel/sched/sched.c 任务调度初始化代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include "sched.h" #include "heap.h" #include "debug.h"
struct task_struct *running_proc_head = NULL;
struct task_struct *wait_proc_head = NULL;
struct task_struct *current = NULL;
void init_sched() { current = (struct task_struct *) (kern_stack_top - STACK_SIZE);
current->state = TASK_RUNNABLE; current->pid = now_pid++; current->stack = current; current->mm = NULL;
current->next = current; running_proc_head = current; }
|
init/entry.c 中加入 栈顶变量
1 2
| uint32_t kern_stack_top;
|
调度函数实现,每次都返回当前任务的下一个任务 , 可以实现更复杂的调度函数
kernel/sched/sched.c
1 2 3 4 5 6 7 8 9 10 11 12 13
| void schedule() { if (current) { change_task_to(current->next); } }
void change_task_to (struct task_struct *next){ if (current != next) { struct task_struct *prev = current; current = next; switch_to(&(prev->context), &(current->context)); } }
|
由汇编实现上下文切换
kernel/sched/switch_to.s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| [global switch_to] ; ;具体的线程切换操作 寄存器的保护和恢复 switch_to: mov eax, [esp+4]
mov [eax+0], esp mov [eax+4], ebp mov [eax+8], ebx mov [eax+12], esi mov [eax+16], edi pushf pop ecx mov [eax+20], ecx
mov eax, [esp+8]
mov esp, [eax+0] mov ebp, [eax+4] mov ebx, [eax+8] mov esi, [eax+12] mov edi, [eax+16] mov eax, [eax+20] push eax popf ret
|
在ret指令返回之前,由于之前的执行现场已经被切换,特别是esp指针指向 的栈被切换了,所以ret指令弹出的返回地址自然就变成了另一个执行流之前调用任务切换 函数之前保存的返回地址了。kernel_thread函数便是通过构造出这样一个切换后可以弹 出执行地址的初始栈来实现的。
内核线程的创建和 退出函数
kernel/task/task.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| #include "gdt.h" #include "pmm.h" #include "vmm.h" #include "heap.h" #include "task.h" #include "sched.h" #include "string.h" #include "debug.h"
pid_t now_pid = 0;
int32_t kernel_thread(int (*fn)(void *), void *arg) { struct task_struct *new_task = (struct task_struct *)kmalloc(STACK_SIZE); assert(new_task != NULL, "kern_thread: kmalloc error");
bzero(new_task, sizeof(struct task_struct));
new_task->state = TASK_RUNNABLE; new_task->stack = current; new_task->pid = now_pid++; new_task->mm = NULL;
uint32_t *stack_top = (uint32_t *)((uint32_t)new_task + STACK_SIZE);
*(--stack_top) = (uint32_t)arg; *(--stack_top) = (uint32_t)kthread_exit; *(--stack_top) = (uint32_t)fn;
new_task->context.esp = (uint32_t)new_task + STACK_SIZE - sizeof(uint32_t) * 3;
new_task->context.eflags = 0x200; new_task->next = running_proc_head; struct task_struct *tail = running_proc_head; assert(tail != NULL, "Must init sched!");
while (tail->next != running_proc_head) { tail = tail->next; } tail->next = new_task;
return new_task->pid; }
void kthread_exit() { register uint32_t val asm ("eax");
printk("Thread exited with value %d\n", val);
while (1); }
|
内核创建函数解析:
内核退出函数:
内核退出函数在这里只实现了简陋的一部分,标准做法是将退出线程的PCB结构转移到 不可调度链表去,等待其他线程join后再清理结构。
时间片,修改timer.c函数
drivers/timers.c
1 2 3 4 5 6
| #include "sched.h"
void timer_callback(pt_regs *regs) { schedule(); }
|
修改entry.c
init/entry.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include "sched.h" #include "task.h"
int flag = 0; int thread(void *arg) { while(1) { if (flag == 1) { printk_color(rc_black, rc_green, "B"); flag = 0; } } return 0; }
test_heap();
init_sched();
kernel_thread(thread, NULL);
asm volatile ("sti");
while(1) { if (flag == 0) { printk_color(rc_black, rc_red, "A"); flag = 1; } }
while (1) { asm volatile ("hlt"); }
|
通过中断实现线程切换的流程逻辑: 通过中断和简单的线程调度机制实现交替打印
- 首先创建了一个内存线程thread函数,并且,初始化线程
init_timer(200)
每200ms出发中断,,中断中进行系统调用
init_timer(200)
—- timecallback()
— schedule()
— change_task_to()更换线程
— swicth_to()
进行上下文切换