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"

// state 枚举
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; // 进程页表
};

// 进程控制块 PCB
struct task_struct
{
volatile task_state state; // 进程当前状态
pid_t pid; // 进程标识符
void *stack; // 进程的内核栈地址
struct mm_struct *mm; // 当前进程的内存地址映像
struct context context; // 进程切换需要的上下文信息
struct task_struct *next; // 链表指针volatile task_state state;
};

// 全局pid 值
extern pid_t now_pid;

// 内核线程创建
int32_t kernel_thread(int (*fn)(void *), void *arg);

// 线程退出函数
void kthread_exit();

#endif

include/types.h 补充

1
typedef int32_t pid_t;

调度机制

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 值
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");

// 将栈低端结构信息初始化为 0
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");
//enable_intr();

while(1) {
if (flag == 0) {
printk_color(rc_black, rc_red, "A");
flag = 1;
}
}

while (1) {
asm volatile ("hlt");
}

通过中断实现线程切换的流程逻辑: 通过中断和简单的线程调度机制实现交替打印

  1. 首先创建了一个内存线程thread函数,并且,初始化线程
  2. init_timer(200) 每200ms出发中断,,中断中进行系统调用
  3. init_timer(200) —- timecallback()schedule()change_task_to()更换线程swicth_to()进行上下文切换

image-20240521164433586