第09章下 任务调度
Posted perfy576
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第09章下 任务调度相关的知识,希望对你有一定的参考价值。
1 任务表
1.1 双向链表
进程中的就绪队列,锁的等待队列都需要带链表这一数据结构。
然后加上,和链表相关的一些操作。代码直接在最后贴上
2 任务初始化
2.1 扩展TaskStruct
需要在TaskStruct
中加入:
- 该任务每次换上cpu后,能够执行的cpu嘀嗒数。可能是时钟中断间隔的倍数
- 该任务已经执行的嘀嗒数。不是每次发生中断都会进行任务调度的
- 链表节点,用于构建任务表
2.2 任务表
需要维护两个链表:
- 已就绪链表,将来的任务调度器从该链表中取出一个任务放上cpu执行
- 全部任务表,所有的任务都将放在这个表中
正在运行的任务,不在已就绪链表中,只在全部任务表中。(全部代码太长,直接在后面贴了)
// 就绪队列
struct List thread_ready_list;
// 所有任务队列
struct List thread_all_list;
// 当前,或是在中断中马上要切换到的任务
static struct ListElem *thread_tag;
2.2 任务创建
为了任务调度,需要给PCB结构加上和任务调度相关的数据:
- 一个任务每次换上cpu能够执行的嘀嗒数(是发生时钟中断的次数),这个值将等于任务的优先级
- 一个链表的节点,用于将该节点放入任务表中
因此扩充为:
// 进程的PCB控制块
struct TaskStruct
{
uint32_t *self_kstack;
enum TaskStatus status;
uint8_t priority;
char name[6];
// 该任务从本次换上cpu后,还能执行的嘀嗒数,每次换上cpu后,都设置为 priority
// 然后在时钟中断中,每次都--,当为0,的时候就进行调度
uint8_t ticks;
// 该任务从开始运行,一共执行的嘀嗒数
uint32_t elapsed_ticks;
// 链表的节点元素,用于添加在队列中
struct ListElem general_tag;
// 同上,但是这个元素只能添加在全部任务表中
struct ListElem all_list_tag;
// 这是个魔术,完全使我们自定义的
// 用于检查是否越界
uint32_t stack_magic;
// 还有一些字段,以后陆续添加
};
然后,thread_start()
和init_thread()
需要初始化这些数据thread_create()
不变。
// 全局变量用来记录第一个执行流的PCB结构
// 只是用于第一次初始化第一个执行流的PCB结构
struct TaskStruct *main_thread;
// 3. 初始化需要执行的任务
void thread_create(struct TaskStruct *pthread, ThreadFunc func, void *func_arg)
{
// 当中断发生的时候,在idm.asm代码中会压入一系列的数据,
// 这些数据在栈中,然后我们以压入数据结束时刻的栈的esp指针为首地址的,定义了一个
// 结构提,该结构体就是 IntrStack ,
// 也就是说在此时内核栈不为空,所以要跳过这些数据
pthread->self_kstack -= sizeof(struct IntrStack);
// 然后接下来偏移 ThreadStack 大小,
// 此时esp就指向了 ThreadStack 的首指针
// 然后填充该结构体
pthread->self_kstack -= sizeof(struct ThreadStack);
// 先将指针转换为 ThreadStack 结构体的指针,然后下面开始填充
struct ThreadStack *kthread_stack = (struct ThreadStack *)pthread->self_kstack;
// 本质上是调用 kernel_thread() 时候call之前栈中的数据,当然eip是call自己压入的
// 这主要是为了 ret 时候,弹栈,能够使用
// 填充的地址,pop到cs ip寄存器中,改变执行流
kthread_stack->eip = kernel_thread;
kthread_stack->func = func;
kthread_stack->func_arg = func_arg;
kthread_stack->ebp = 0;
kthread_stack->ebx = 0;
kthread_stack->esi = 0;
kthread_stack->edi = 0;
}
// 2. 初始化PCB结构 TaskStruct
void init_thread(struct TaskStruct *pthread, char *name, int prio)
{
// 清零,都是在获取页以后清零,在回收页的时候不会清零
memset(pthread, 0, sizeof(*pthread));
// 设置任务的名字
strcpy(pthread->name, name);
// 每一个新建的任务都是就绪态,然后放入就绪队列中
// 但是 第一个执行流特殊,第一个执行流为自己创建PCB结构的时候
// 他已经在运行了,所以是设置为TASK_RUNNING
if (pthread == main_thread)
{
pthread->status = TASK_RUNNING;
}
else
{
pthread->status = TASK_READY;
}
// 设置该任务的栈为栈顶,后面会在 thread_create 中跳过cpu压栈和
// 中断历程的压栈数据
pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE);
// 设置任务的优先级
pthread->priority = prio;
// 每个每次换上cpu能够执行的滴答数等于其优先级
pthread->ticks = prio;
// 该任务从开始运行,到某个时刻一共运行的嘀嗒数
pthread->elapsed_ticks = 0;
// 因为 stack_magic 字段后面到该页的结束都是该任务的内核栈
// 因此为了防止栈溢出以后破坏PCB结构,因此需要这魔数
pthread->stack_magic = 0x19870916;
}
// 任务初始化流程,先分配一页,然后初始化该页,再初始化要执行的函数
struct TaskStruct *thread_start(char *name, int prio, ThreadFunc func, void *func_arg)
{
// 1. 首先是去获取一页,作为存储PCB结构的页
struct TaskStruct *thread = get_kernel_pages(1);
// 2. 然后初始化该PCB结构
init_thread(thread, name, prio);
// 3. 初始化需要执行的任务
thread_create(thread, func, func_arg);
// 最后添加到全部任务队列和已就绪任务队列
ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
list_append(&thread_ready_list, &thread->general_tag);
ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
list_append(&thread_all_list, &thread->all_list_tag);
return thread;
}
2.3 初始化第一个执行流
从开机到内核加载,一直只有一个执行流,该执行流进行了各种初始化。该执行流没有PCB结构。而其他任务创建的时候会使用thread_start()
初始化后拥有PCB。为了能够让第一个执行流进行调度,需要为第一个执行流初始化,创建PCB结构。
第一个执行流的栈设置在0xc0009f00
。而栈是PCB所在页高位向下扩展,因此第一个执行流的PCB在0xc0009e00
,不需要去获取物理页了,同时也不需要去初始化执行流任务,所以它所需要的只是去创建PCB结构:
// 获取当前正在运行的pcb结构的首地址
// 其实就是将 esp 抹掉低4位,就可以了
// 因为都是在一页中,抹掉低4位,就能够获取该页的首地址,也就是PCB的首地址了
struct TaskStruct *running_thread()
{
uint32_t esp;
asm("mov %%esp, %0":"=g"(esp));
return (struct TaskStruct *)(esp & 0xfffff000);
}
// 为第一个执行流创建PCB结构
static void make_main_thread(void)
{
// 第一个执行流的栈是0xc0009f00,因此所在的页就是0xc0009e00
main_thread = running_thread();
init_thread(main_thread, "main", 31);
ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag));
list_append(&thread_all_list, &main_thread->all_list_tag);
}
2.4 任务初始化
任务初始化,除了为第一个执行流创建PCB还需要,初始化全局任务表,和已就绪任务表:
void thread_init()
{
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
make_main_thread();
put_str("thread_init done\n");
}
3 任务调度
有了上面的准备,那么就可以做任务调度了
3.1 任务调度原理
任务调度是基于时钟中断的,每次时钟中断的中断函数,都去切换要执行的任务。
而"切换"的含义:每次中断发生,cpu将ss
,esp
,eflags
,cs
,eip
,error_code
(可能有)压入栈中,然后去执行对应的中断处理函数,而我么你编写的中断处理函数又会将段寄存器,通用寄存器压入栈中,紧接着去执行真正的中断处理函数(真正的意思是为每种中断编写的中断处理函数)。当中真正的中断处理函数执行完毕以后,就需要弹栈了:从esp
开始线上,将之前压入的值弹出,直到cpu将它压入的eip
,cs
等弹出去,以及ss
,esp
弹出去,那么此次中断结束了。而如果我们在真正的中断处理函数中,将esp
的值替换掉,替换为其他任务的esp
值,换句话说,也就是替换其他任务的栈,(内核态的栈基址ss
没有更换,只要控制esp
,弹出的数据就可以了),也就是说:另一个任务的寄存器映像被加载到了寄存器中。
3.2 switch函数
根据上面的的,我们就可以编写一个替换esp
的汇编代码
[bits 32]
section .text
; 两个参数依次是 当前任务,和要执行任务的 TaskStruct 结构的首指针
global switch_to
switch_to:
; 根据ABI规定,压入四个主调用这使用的寄存器
push esi
push edi
push ebx
push ebp
; TaskStruct 首指针,其实是指向他的self_kstack字段
; 该字段,其实正好记录的是该任务的 esp
; 因此先将当前任务的esp保存
mov eax, [esp + 20]
mov [eax], esp
; 然后将要替换任务的esp加载上
mov eax, [esp + 24]
mov esp, [eax]
; 弹栈
pop ebp
pop ebx
pop edi
pop esi
ret
这段代码完成了新旧esp的替换。
然后这里分两种情况:
3.2.1 任务第一次执行
当任务第一次执行的时候:那么此时的栈是这样的(假设中断处理函数直接压入两个TaskStruct
结构的首指针,然后call switch_to
):
此时,新任务的栈是:
因此,弹栈后,直接开始执行kernel_thread()
,换句话说,也就是此时其实相当于还是在中断处理中,也就是eflags
的if
位是0,这是中断一开始的时候cpu主动关闭的。
因此我们需要在kernel_thread()
打开中断:
// 这是一个包裹函数
// ThreadStack 结构体本质上是 kernel_thread call 之前的压栈数据
static void kernel_thread(ThreadFunc *func, void *func_arg)
{
// 在中断处理函数中关中断,然后切换完成后开中断
intr_enable();
func(func_arg);
}
另外,我们之前在idt.asm
中代码是这样的:
; 省略...
; 暴露给外部的接口,没有参数
global idt_entry_table
idt_entry_table:
; 模板,一共两个参数,第一个参数是中断的编号,第2个参数,根据需要压入一个0
%macro VECTOR 2
section .text
; %1 表示第一个参数,直接替换
intr%1entry:
%2
push ds
push es
push fs
push gs
pushad ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
push %1 ;压栈中断号,作为 idt_handle 的参数
call [idt_table+%1*4]
add esp,4 ;跳过参数
; 手动模式下,需要主动的向主片和从片发送EOI信号
mov al,0x20
out 0xa0,al
out 0x20,al
popad
pop gs
pop fs
pop es
pop ds
add esp,4 ;跳过 error_code
iretd
section .data
dd intr%1entry
%endmacro
; 省略...
注意,主动的向主片和从片发送EOI信号是在调用玩真正的处理函数以后才发送,也就是说,在真正的中断处理函数未执行完以前,即使我们在kernel_thread()
中,打开中断intr_enable()
,8259A也不会给我们发送新的中断,因为之前设置的是手动模式,8259A认为之前的中断处理函数还未执行完。因此,需要将发送EOI信号,提call [idt_table+%1*4]
之前。也就是说在调用真正的信号处理函数之前就发送EOI信号,但是此时eflags
的if
位为0,不会响应中断。
因此新代码就是:
; 省略
mov al,0x20
out 0xa0,al
out 0x20,al
call [idt_table+%1*4]
; 省略
这样,即使我们在中断处理函数中改变了执行流,去执行kernel_thread()
,只要事先向8259A发送了EOI信号,那8259A也会认为中断处理函数已经执行完了。但其实,我们骗了它。
当然在调用真正的信号处理函数这段时间,因为eflags
的if
位为0,因此即使8259A发来了新的中断,cpu也不会相应的。
3.2.2 任务不是第一次执行
相比上一个,好理解的多。
因为,切换到不是第一次执行的任务,它的栈是这样的:
那么就会沿这原路返回,完成中断的后半部分,就是弹栈的那一部分。
3.3 任务调度函数
主要是处理当前任务:是否再次加入就绪队列。然后选取一个已就绪任务。
然后调用switch()
// 任务调度函数
void schedule()
{
ASSERT(intr_get_status() == INTR_OFF);
struct TaskStruct *cur = running_thread();
// 如果一个任务被阻塞,那么在进入 schedule 之前就会被设置为 TASK_BLOCKED
// 因此如果在 schedule 中是 TASK_RUNNING ,那么证明时间片用完了
if (cur->status == TASK_RUNNING)
{
// 时间片用完了,那么直接加入到就绪队列中,然后重新设置嘀嗒数(时间片)
ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
list_append(&thread_ready_list, &cur->general_tag);
cur->ticks = cur->priority;
// 并设置为 TASK_READY 表示随时可以执行
cur->status = TASK_READY;
}
else
{
// 如果需要等待资源,那么需要另外处理
}
ASSERT(!list_empty(&thread_ready_list));
// 取一个任务,thread_tag 是要换上cpu的那个任务的PCB的general_tag字段的指针
thread_tag = NULL;
thread_tag = list_pop(&thread_ready_list);
// 根据其 thread_tag 计算该任务的PCB
struct TaskStruct *next = elem2entry(struct TaskStruct, general_tag, thread_tag);
// 该任务设置为 TASK_RUNNING
next->status = TASK_RUNNING;
// 调用 switch_to ,去切换esp
switch_to(cur, next);
}
3.4 修改时钟中断的中断处理函数
这里初始化了8253,是一个专门的时钟中断的硬件,这部分太麻烦,所以跳过了8253的初始化,最后直接贴代码。知道就行。
和时钟中断相关的代码被放在./device/timer.c
中
// 时钟中断处理函数
static void intr_timer_handler(void)
{
struct TaskStruct *cur_thread = running_thread();
// 检查是 内核栈是否溢出
ASSERT(cur_thread->stack_magic == 0x19870916);
// 该任务从开始执行到现在所执行的嘀嗒数
cur_thread->elapsed_ticks++;
// 计算机从开机到现在所执行的滴答数
ticks++;
// 如果时间片(嘀嗒数)用完,那么需要切换。否则嘀嗒数--,
// 退出中断处理函数
if (cur_thread->ticks == 0)
{
schedule();
}
else
{
cur_thread->ticks--;
}
}
另外,需要在interrupt.c
中,加一个函数,用于为指定的中断,设置一个新的中断处理函数,同时在interput.h
中暴露接口。
// interrupt.c
// 为指定的中断,设置一个新的中断处理函数
void register_handler(uint8_t vector_no, void* function) {
idt_table[vector_no] = function;
}
最终的timer_init()
// 初始化 timer
void timer_init()
{
put_str("timer_init start\n");
// 初始化8253
frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
// 为 0x20 时钟中断,设置新的中断处理函数
register_handler(0x20, intr_timer_handler);
put_str("timer_init done\n");
}
4 代码
目录结构:
└── bochs
├── 02.tar.gz
├── 03.tar.gz
├── 04.tar.gz
├── 05a.tar.gz
├── 05b.tar.gz
├── 06a.tar.gz
├── 07a.tar.gz
├── 07b.tar.gz
├── 07c.tar.gz
├── 08.tar.gz
├── 09a.tar.gz
├── 09b
│?? ├── boot
│?? │?? ├── include
│?? │?? │?? └── boot.inc
│?? │?? ├── loader.asm
│?? │?? └── mbr.asm
│?? ├── build
│?? │?? ├── bitmap.o
│?? │?? ├── debug.o
│?? │?? ├── idt.o
│?? │?? ├── init.o
│?? │?? ├── interrupt.o
│?? │?? ├── kernel.bin
│?? │?? ├── kernel.o
│?? │?? ├── list.o
│?? │?? ├── loader.bin
│?? │?? ├── mbr.bin
│?? │?? ├── memory.o
│?? │?? ├── print.o
│?? │?? ├── string.o
│?? │?? ├── switch.o
│?? │?? ├── thread.o
│?? │?? └── timer.o
│?? ├── device
│?? │?? ├── timer.c
│?? │?? └── timer.h
│?? ├── kernel
│?? │?? ├── debug.c
│?? │?? ├── debug.h
│?? │?? ├── global.h
│?? │?? ├── idt.asm
│?? │?? ├── init.c
│?? │?? ├── init.h
│?? │?? ├── interrupt.c
│?? │?? ├── interrupt.h
│?? │?? ├── main.c
│?? │?? ├── memory.c
│?? │?? └── memory.h
│?? ├── lib
│?? │?? ├── kernel
│?? │?? │?? ├── bitmap.c
│?? │?? │?? ├── bitmap.h
│?? │?? │?? ├── io.h
│?? │?? │?? ├── list.c
│?? │?? │?? ├── list.h
│?? │?? │?? ├── print.asm
│?? │?? │?? ├── print.h
│?? │?? │?? ├── string.c
│?? │?? │?? └── string.h
│?? │?? └── libint.h
│?? ├── makefile
│?? ├── start.sh
│?? └── thread
│?? ├── switch.asm
│?? ├── thread.c
│?? └── thread.h
└── hd60m.img
4.0 bochs任意断点
bochs的断点只能在调试的时候打。但是如果开了?magic_break: enabled=1
选项,则可以在编写代码的时候打上断点。然后在调试程序的时候,就可以不用再打断点了。
因此配置文件改为:
# bochs 能够使用的内存,单位MB
megs:32
# 真是机器的Bios和VGA BIOS
romimage:file=/usr/share/bochs/BIOS-bochs-latest
vgaromimage:file=/usr/share/bochs/VGABIOS-lgpl-latest
# 启动盘
boot:disk
# 日志输出
log:/tmp/bochs.log
# 开关功能
mouse:enabled=0
keyboard:keymap=/usr/share/bochs/keymaps/x11-pc-us.map
# 硬盘设置
ata0-master: type=disk, path="hd60m.img", mode=flat, cylinders=121, heads=16, spt=63
magic_break: enabled=1
就新加了最后一行。
然后在代码中,需要打断点的地方,加上汇编代码
__asm__("xchg %%bx,%%bx"::);
就可以实现在任何地方实现断点。
我们将这条指令封装在debug.h
中
#ifndef __KERNEL_DEBUG_H
#define __KERNEL_DEBUG_H
void panic(char *filename, int line, const char *func, const char *cond);
#define PANIC(cond) panic(__FILE__, __LINE__, __func__, cond)
#ifdef NDEBUG
#define ASSERT(cond) ((void)0)
#define BREAK() ((void)0);
#else
#define ASSERT(cond) \
if (cond) \
{ \
} \
else \
{ \
PANIC(#cond); \
}
#define BREAK() __asm__("xchg %%bx,%%bx"::);
#endif /*__NDEBUG */
#endif /*__KERNEL_DEBUG_H*/
4.1 switch.asm(新)
[bits 32]
section .text
; 两个参数依次是 当前任务,和要执行任务的 TaskStruct 结构的首指针
global switch_to
switch_to:
; 根据ABI规定,压入四个主调用这使用的寄存器
push esi
push edi
push ebx
push ebp
; TaskStruct 首指针,其实是指向他的self_kstack字段
; 该字段,其实正好记录的是该任务的 esp
; 因此先将当前任务的esp保存
mov eax, [esp + 20]
mov [eax], esp
; 然后将要替换任务的esp加载上
mov eax, [esp + 24]
mov esp, [eax]
; 弹栈
pop ebp
pop ebx
pop edi
pop esi
ret
4.2 thread.h/thread.c
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "libint.h"
#include "list.h"
// 被线程执行的函数
typedef void ThreadFunc(void *);
// 几种任务的状态
enum TaskStatus
{
TASK_RUNNING,
TASK_READY,
TASK_BLOCKED,
TASK_WAITING,
TASK_HANGING,
TASK_DIED,
};
// 由时钟中断保存的任务上下文.
struct IntrStack
{
// 下面的代码是我们idm.asm代码压入的东西
// 这个是在idt.asm汇编的时候,push %1 压入的
// push %1 最后压入,所以在第一个
// 中断向量号,在这里面应该都是时钟中断
uint32_t vec;
uint32_t edi;
uint32_t esi;
uint32_t ebp;
// 这里esp没看懂
// 解释是:虽然pushad,会把esp也压入,但是esp因为压栈一直在变化
// 所以会被popad忽略
uint32_t esp_dummy;
uint32_t ebx;
uint32_t edx;
uint32_t ecx;
uint32_t eax;
uint32_t gs;
uint32_t fs;
uint32_t es;
uint32_t ds;
// 下面是cpu从地特权先搞特权是压入
// 是cpu自己压入的。
uint32_t err_code;
void (*eip)(void);
uint32_t cs;
uint32_t eflags;
void *esp;
uint32_t ss;
};
// 线程第一次执行时候需要的数据
// 例如函数地址,函数参数
struct ThreadStack
{
// 这四个寄存器是因为ABI的缘故,需要压入
// 由我们写的汇编代码 pop
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;
// 下面就是 ret 后 eip的值 pop到eip中,是近跳转,所以没cs
// 剩下的都是参数
void (*eip)(ThreadFunc *func, void *arg);
// 这些都是为 kernel_thread(ThreadFunc *func,void* func_arg)
// 准备的参数
void(*unused_retaddr);
ThreadFunc *func; // 这是个参数,以后讲。
void *func_arg;
};
// 进程的PCB控制块
struct TaskStruct
{
uint32_t *self_kstack;
enum TaskStatus status;
uint8_t priority;
char name[6];
// 该任务从本次换上cpu后,还能执行的嘀嗒数,每次换上cpu后,都设置为 priority
// 然后在时钟中断中,每次都--,当为0,的时候就进行调度
uint8_t ticks;
// 该任务从开始运行,一共执行的嘀嗒数
uint32_t elapsed_ticks;
// 链表的节点元素,用于添加在队列中
struct ListElem general_tag;
// 同上,但是这个元素只能添加在全部任务表中
struct ListElem all_list_tag;
// 这是个魔术,完全使我们自定义的
// 用于检查是否越界
uint32_t stack_magic;
// 还有一些字段,以后陆续添加
};
void thread_create(struct TaskStruct *pthread, ThreadFunc function, void *func_arg);
void init_thread(struct TaskStruct *pthread, char *name, int prio);
struct TaskStruct *thread_start(char *name, int prio, ThreadFunc function, void *func_arg);
void thread_init(void);
struct TaskStruct *running_thread(void);
void schedule(void);
void thread_init(void);
#endif
#include "thread.h"
#include "string.h"
#include "print.h"
#include "memory.h"
#include "interrupt.h"
#include "debug.h"
#define PG_SIZE 4096
extern void switch_to(struct TaskStruct *cur, struct TaskStruct *next);
// 全局变量用来记录第一个执行流的PCB结构
// 只是用于第一次初始化第一个执行流的PCB结构
struct TaskStruct *main_thread;
// 就绪队列
struct List thread_ready_list;
// 所有任务队列
struct List thread_all_list;
// 当前,或是在中断中马上要切换到的任务
static struct ListElem *thread_tag;
// 这是一个包裹函数
// ThreadStack 结构体本质上是 kernel_thread call 之前的压栈数据
static void kernel_thread(ThreadFunc *func, void *func_arg)
{
// 在中断处理函数中关中断,然后切换完成后开中断
intr_enable();
func(func_arg);
}
// 获取当前正在运行的pcb结构的首地址
// 其实就是将 esp 抹掉低4位,就可以了
// 因为都是在一页中,抹掉低4位,就能够获取该页的首地址,也就是PCB的首地址了
struct TaskStruct *running_thread()
{
uint32_t esp;
asm("mov %%esp, %0":"=g"(esp));
return (struct TaskStruct *)(esp & 0xfffff000);
}
// 3. 初始化需要执行的任务
void thread_create(struct TaskStruct *pthread, ThreadFunc func, void *func_arg)
{
// 当中断发生的时候,在idm.asm代码中会压入一系列的数据,
// 这些数据在栈中,然后我们以压入数据结束时刻的栈的esp指针为首地址的,定义了一个
// 结构提,该结构体就是 IntrStack ,
// 也就是说在此时内核栈不为空,所以要跳过这些数据
pthread->self_kstack -= sizeof(struct IntrStack);
// 然后接下来偏移 ThreadStack 大小,
// 此时esp就指向了 ThreadStack 的首指针
// 然后填充该结构体
pthread->self_kstack -= sizeof(struct ThreadStack);
// 先将指针转换为 ThreadStack 结构体的指针,然后下面开始填充
struct ThreadStack *kthread_stack = (struct ThreadStack *)pthread->self_kstack;
// 本质上是调用 kernel_thread() 时候call之前栈中的数据,当然eip是call自己压入的
// 这主要是为了 ret 时候,弹栈,能够使用
// 填充的地址,pop到cs ip寄存器中,改变执行流
kthread_stack->eip = kernel_thread;
kthread_stack->func = func;
kthread_stack->func_arg = func_arg;
kthread_stack->ebp = 0;
kthread_stack->ebx = 0;
kthread_stack->esi = 0;
kthread_stack->edi = 0;
}
// 2. 初始化PCB结构 TaskStruct
void init_thread(struct TaskStruct *pthread, char *name, int prio)
{
// 清零,都是在获取页以后清零,在回收页的时候不会清零
memset(pthread, 0, sizeof(*pthread));
// 设置任务的名字
strcpy(pthread->name, name);
// 每一个新建的任务都是就绪态,然后放入就绪队列中
// 但是 第一个执行流特殊,第一个执行流为自己创建PCB结构的时候
// 他已经在运行了,所以是设置为TASK_RUNNING
if (pthread == main_thread)
{
pthread->status = TASK_RUNNING;
}
else
{
pthread->status = TASK_READY;
}
// 设置该任务的栈为栈顶,后面会在 thread_create 中跳过cpu压栈和
// 中断历程的压栈数据
pthread->self_kstack = (uint32_t *)((uint32_t)pthread + PG_SIZE);
// 设置任务的优先级
pthread->priority = prio;
// 每个每次换上cpu能够执行的滴答数等于其优先级
pthread->ticks = prio;
// 该任务从开始运行,到某个时刻一共运行的嘀嗒数
pthread->elapsed_ticks = 0;
// 因为 stack_magic 字段后面到该页的结束都是该任务的内核栈
// 因此为了防止栈溢出以后破坏PCB结构,因此需要这魔数
pthread->stack_magic = 0x19870916;
}
// 任务初始化流程,先分配一页,然后初始化该页,再初始化要执行的函数
struct TaskStruct *thread_start(char *name, int prio, ThreadFunc func, void *func_arg)
{
// 1. 首先是去获取一页,作为存储PCB结构的页
struct TaskStruct *thread = get_kernel_pages(1);
// 2. 然后初始化该PCB结构
init_thread(thread, name, prio);
// 3. 初始化需要执行的任务
thread_create(thread, func, func_arg);
// 最后添加到全部任务队列和已就绪任务队列
ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));
list_append(&thread_ready_list, &thread->general_tag);
ASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));
list_append(&thread_all_list, &thread->all_list_tag);
return thread;
}
// 为第一个执行流创建PCB结构
static void make_main_thread(void)
{
// 第一个执行流的栈是0xc0009f00,因此所在的页就是0xc0009e00
main_thread = running_thread();
init_thread(main_thread, "main", 31);
ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag));
list_append(&thread_all_list, &main_thread->all_list_tag);
}
void thread_init()
{
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
make_main_thread();
put_str("thread_init done\n");
}
// 任务调度函数
void schedule()
{
ASSERT(intr_get_status() == INTR_OFF);
struct TaskStruct *cur = running_thread();
// 如果一个任务被阻塞,那么在进入 schedule 之前就会被设置为 TASK_BLOCKED
// 因此如果在 schedule 中是 TASK_RUNNING ,那么证明时间片用完了
if (cur->status == TASK_RUNNING)
{
// 时间片用完了,那么直接加入到就绪队列中,然后重新设置嘀嗒数(时间片)
ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
list_append(&thread_ready_list, &cur->general_tag);
cur->ticks = cur->priority;
// 并设置为 TASK_READY 表示随时可以执行
cur->status = TASK_READY;
}
else
{
// 如果需要等待资源,那么需要另外处理
}
ASSERT(!list_empty(&thread_ready_list));
// 取一个任务,thread_tag 是要换上cpu的那个任务的PCB的general_tag字段的指针
thread_tag = NULL;
thread_tag = list_pop(&thread_ready_list);
// 根据其 thread_tag 计算该任务的PCB
struct TaskStruct *next = elem2entry(struct TaskStruct, general_tag, thread_tag);
// 该任务设置为 TASK_RUNNING
next->status = TASK_RUNNING;
// 调用 switch_to ,去切换esp
switch_to(cur, next);
}
4.4 timer.h/timer.h
#ifndef __DEVICE_TIME_H
#define __DEVICE_TIME_H
#include "libint.h"
void timer_init(void);
#endif
#include "timer.h"
#include "io.h"
#include "print.h"
#include "interrupt.h"
#include "thread.h"
#include "debug.h"
#define IRQ0_FREQUENCY 100
#define INPUT_FREQUENCY 1193180
#define COUNTER0_VALUE INPUT_FREQUENCY / IRQ0_FREQUENCY
#define CONTRER0_PORT 0x40
#define COUNTER0_NO 0
#define COUNTER_MODE 2
#define READ_WRITE_LATCH 3
#define PIT_CONTROL_PORT 0x43
// ticks是内核自中断开启以来总共的嘀嗒数
uint32_t ticks;
static void frequency_set(uint8_t counter_port,
uint8_t counter_no,
uint8_t rwl,
uint8_t counter_mode,
uint16_t counter_value)
{
outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
outb(counter_port, (uint8_t)counter_value);
outb(counter_port, (uint8_t)counter_value >> 8);
}
// 时钟中断处理函数
static void intr_timer_handler(void)
{
struct TaskStruct *cur_thread = running_thread();
// 检查是 内核栈是否溢出
ASSERT(cur_thread->stack_magic == 0x19870916);
// 该任务从开始执行到现在所执行的嘀嗒数
cur_thread->elapsed_ticks++;
// 计算机从开机到现在所执行的滴答数
ticks++;
// 如果时间片(嘀嗒数)用完,那么需要切换。否则嘀嗒数--,
// 退出中断处理函数
if (cur_thread->ticks == 0)
{
schedule();
}
else
{
cur_thread->ticks--;
}
}
// 初始化 timer
void timer_init()
{
put_str("timer_init start\n");
// 初始化8253
frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
// 为 0x20 时钟中断,设置新的中断处理函数
register_handler(0x20, intr_timer_handler);
put_str("timer_init done\n");
}
4.5 init.h/init.c
#ifndef __KERNEL_INIT_H
#define __KERNEL_INIT_H
void init_all(void);
#endif
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "memory.h"
#include "timer.h"
#include "thread.h"
/*负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); //初始化中断
mem_init();
thread_init();
timer_init();
}
4.6 idt.asm
[bits 32]
; %define用于定义文本替换标号,类似于C语言里面常用的宏替换。
; equ用于 对标号赋值,equ可放在程序中间,而%define则只能用于程序开头。
%define ERROR_CODE nop
%define ZERO push 0
; 引用外部函数
extern put_str
extern idt_table
section .data
; 中断处理程序中打印的字符串
intr_str db "interrupt occur!",0xa,0
; 暴露给外部的接口,没有参数
global idt_entry_table
idt_entry_table:
; 模板,一共两个参数,第一个参数是中断的编号,第2个参数,根据需要压入一个0
%macro VECTOR 2
section .text
; %1 表示第一个参数,直接替换
intr%1entry:
%2
push ds
push es
push fs
push gs
pushad ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
push %1 ;压栈中断号,作为 idt_handle 的参数
; 手动模式下,需要主动的向主片和从片发送EOI信号
mov al,0x20
out 0xa0,al
out 0x20,al
call [idt_table+%1*4]
add esp,4 ;跳过参数
popad
pop gs
pop fs
pop es
pop ds
add esp,4 ;跳过 error_code
iretd
section .data
dd intr%1entry
%endmacro
VECTOR 0x00,ZERO
VECTOR 0x01,ZERO
VECTOR 0x02,ZERO
VECTOR 0x03,ZERO
VECTOR 0x04,ZERO
VECTOR 0x05,ZERO
VECTOR 0x06,ZERO
VECTOR 0x07,ZERO
VECTOR 0x08,ERROR_CODE
VECTOR 0x09,ZERO
VECTOR 0x0a,ERROR_CODE
VECTOR 0x0b,ERROR_CODE
VECTOR 0x0c,ZERO
VECTOR 0x0d,ERROR_CODE
VECTOR 0x0e,ERROR_CODE
VECTOR 0x0f,ZERO
VECTOR 0x10,ZERO
VECTOR 0x11,ERROR_CODE
VECTOR 0x12,ZERO
VECTOR 0x13,ZERO
VECTOR 0x14,ZERO
VECTOR 0x15,ZERO
VECTOR 0x16,ZERO
VECTOR 0x17,ZERO
VECTOR 0x18,ERROR_CODE
VECTOR 0x19,ZERO
VECTOR 0x1a,ERROR_CODE
VECTOR 0x1b,ERROR_CODE
VECTOR 0x1c,ZERO
VECTOR 0x1d,ERROR_CODE
VECTOR 0x1e,ERROR_CODE
VECTOR 0x1f,ZERO
VECTOR 0x20,ZERO
4.7 list.h/list.c
#ifndef __LIB_KERNEL_LIST_H
#define __LIB_KERNEL_LIST_H
#include "global.h"
#define offset( struct_type, member ) ( int )( &( ( struct_type* )0 )->member )
#define elem2entry( struct_type, struct_member_name, elem_ptr ) \
( struct_type* )( ( int )elem_ptr - offset( struct_type, struct_member_name ) )
/********** 定义链表结点成员结构 ***********
*结点中不需要数据成元,只要求前驱和后继结点指针*/
struct ListElem
{
struct ListElem* prev; // 前躯结点
struct ListElem* next; // 后继结点
};
/* 链表结构,用来实现队列 */
struct List
{
/* head是队首,是固定不变的,不是第1个元素,第1个元素为head.next */
struct ListElem head;
/* tail是队尾,同样是固定不变的 */
struct ListElem tail;
};
/* 自定义函数类型function,用于在list_traversal中做回调函数 */
typedef bool( Function )( struct ListElem*, int arg );
void list_init( struct List* );
void list_insert_before( struct ListElem* before, struct ListElem* elem );
void list_push( struct List* plist, struct ListElem* elem );
void list_iterate( struct List* plist );
void list_append( struct List* plist, struct ListElem* elem );
void list_remove( struct ListElem* pelem );
struct ListElem* list_pop( struct List* plist );
bool list_empty( struct List* plist );
uint32_t list_len( struct List* plist );
struct ListElem* list_traversal( struct List* plist, Function func, int arg );
bool elem_find( struct List* plist, struct ListElem* obj_elem );
#endif
#include "list.h"
#include "interrupt.h"
/* 初始化双向链表list */
void list_init( struct List* list )
{
list->head.prev = NULL;
list->head.next = &list->tail;
list->tail.prev = &list->head;
list->tail.next = NULL;
}
/* 把链表元素elem插入在元素before之前 */
void list_insert_before( struct ListElem* before, struct ListElem* elem )
{
enum IntrStatus old_status = intr_disable();
/* 将before前驱元素的后继元素更新为elem, 暂时使before脱离链表*/
before->prev->next = elem;
/* 更新elem自己的前驱结点为before的前驱,
* 更新elem自己的后继结点为before, 于是before又回到链表 */
elem->prev = before->prev;
elem->next = before;
/* 更新before的前驱结点为elem */
before->prev = elem;
intr_set_status( old_status );
}
/* 添加元素到列表队首,类似栈push操作 */
void list_push( struct List* plist, struct ListElem* elem )
{
list_insert_before( plist->head.next, elem ); // 在队头插入elem
}
/* 追加元素到链表队尾,类似队列的先进先出操作 */
void list_append( struct List* plist, struct ListElem* elem )
{
list_insert_before( &plist->tail, elem ); // 在队尾的前面插入
}
/* 使元素pelem脱离链表 */
void list_remove( struct ListElem* pelem )
{
enum IntrStatus old_status = intr_disable();
pelem->prev->next = pelem->next;
pelem->next->prev = pelem->prev;
intr_set_status( old_status );
}
/* 将链表第一个元素弹出并返回,类似栈的pop操作 */
struct ListElem* list_pop( struct List* plist )
{
struct ListElem* elem = plist->head.next;
list_remove( elem );
return elem;
}
/* 从链表中查找元素obj_elem,成功时返回true,失败时返回false */
bool elem_find( struct List* plist, struct ListElem* obj_elem )
{
struct ListElem* elem = plist->head.next;
while ( elem != &plist->tail )
{
if ( elem == obj_elem )
{
return true;
}
elem = elem->next;
}
return false;
}
/* 把列表plist中的每个元素elem和arg传给回调函数func,
* arg给func用来判断elem是否符合条件.
* 本函数的功能是遍历列表内所有元素,逐个判断是否有符合条件的元素。
* 找到符合条件的元素返回元素指针,否则返回NULL. */
struct ListElem* list_traversal( struct List* plist, Function func, int arg )
{
struct ListElem* elem = plist->head.next;
/* 如果队列为空,就必然没有符合条件的结点,故直接返回NULL */
if ( list_empty( plist ) )
{
return NULL;
}
while ( elem != &plist->tail )
{
if ( func( elem, arg ) )
{ // func返回ture则认为该元素在回调函数中符合条件,命中,故停止继续遍历
return elem;
} // 若回调函数func返回true,则继续遍历
elem = elem->next;
}
return NULL;
}
/* 返回链表长度 */
uint32_t list_len( struct List* plist )
{
struct ListElem* elem = plist->head.next;
uint32_t length = 0;
while ( elem != &plist->tail )
{
length++;
elem = elem->next;
}
return length;
}
/* 判断链表是否为空,空时返回true,否则返回false */
bool list_empty( struct List* plist )
{ // 判断队列是否为空
return ( plist->head.next == &plist->tail ? true : false );
}
4.8 main.c
#include "print.h"
#include "init.h"
#include "debug.h"
#include "interrupt.h"
#include "memory.h"
#include "thread.h"
// 这里一定要先声明,后面定义
// 不然会出错,我也不知道为啥,应该是因为改变了地址?
// 就是在ld中
void k_thread_a(void *);
void k_thread_b(void *);
int main(int argc, char const *argv[])
{
set_cursor(880);
put_char('k');
put_char('e');
put_char('r');
put_char('n');
put_char('e');
put_char('l');
put_char('\n');
put_char('\r');
put_char('1');
put_char('2');
put_char('\b');
put_char('3');
put_str("\n put_char\n");
init_all();
put_str("interrupt on\n");
void *addr = get_kernel_pages(3);
put_str("\n get_kernel_page start vaddr is ");
put_int((uint32_t)addr);
put_str("\n");
// 改变执行流
thread_start("k_thread_b", 8, k_thread_b, "argB ");
thread_start("k_thread_a", 31, k_thread_a, "argA1 ");
intr_enable(); // 打开中断,使时钟中断起作用
while (1)
{
put_str("main ");
}
return 0;
}
/* 在线程中运行的函数 */
void k_thread_a(void *arg)
{
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char *para = arg;
while (1)
{
put_str(para);
}
}
/* 在线程中运行的函数 */
void k_thread_b(void *arg)
{
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char *para = arg;
while (1)
{
put_str(para);
// put_str("b ");
}
}
4.8 makefile
BUILD_DIR=./build
AS=nasm
NASM_ELF=-f elf
INCLUDE=-I./lib -I ./lib/kernel -I./kernel -I./thread/ -I./device
ASINCLUDE=-I./boot/include/
CFLAGS=-m32 -fno-builtin
LDFLAGS=-Ttext 0xc0001500 -m elf_i386 -e main
CC=gcc
# 注意这里: $(BUILD_DIR)/kernel.o 一定要放在第一个上,因此,这个变量是为连接器准备的.如果不放在第一项,保证错
OBJ=$(BUILD_DIR)/kernel.o $(BUILD_DIR)/print.o $(BUILD_DIR)/idt.o $(BUILD_DIR)/debug.o \
$(BUILD_DIR)/interrupt.o $(BUILD_DIR)/init.o $(BUILD_DIR)/memory.o $(BUILD_DIR)/string.o \
$(BUILD_DIR)/bitmap.o $(BUILD_DIR)/thread.o $(BUILD_DIR)/list.o $(BUILD_DIR)/switch.o \
$(BUILD_DIR)/timer.o
# 最终要生成的文件
all: $(BUILD_DIR)/kernel.bin $(BUILD_DIR)/loader.bin $(BUILD_DIR)/mbr.bin
# 编译 并刻录 loader.bin
$(BUILD_DIR)/loader.bin: ./boot/loader.asm
$(AS) -o [email protected] $^ $(ASINCLUDE)
dd if=$(BUILD_DIR)/loader.bin of=./hd60m.img bs=512 count=4 seek=2 conv=notrunc
# 编译 并刻录 mbr.bin
$(BUILD_DIR)/mbr.bin: ./boot/mbr.asm
$(AS) -o [email protected] $^ $(ASINCLUDE)
dd if=$(BUILD_DIR)/mbr.bin of=./hd60m.img bs=512 count=1 conv=notrunc
# 编译 print.asm
$(BUILD_DIR)/print.o:./lib/kernel/print.asm
$(AS) $(NASM_ELF) -o [email protected] $^ $(ASINCLUDE)
# 编译 idt.asm
$(BUILD_DIR)/idt.o:./kernel/idt.asm
$(AS) $(NASM_ELF) -o [email protected] $^ $(ASINCLUDE)
# 编译 interrupt.c
$(BUILD_DIR)/interrupt.o:./kernel/interrupt.c
$(CC) -o [email protected] $(CFLAGS) -fno-stack-protector -c $^ $(INCLUDE)
# 编译 debug.c
$(BUILD_DIR)/debug.o:./kernel/debug.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 编译 init.c
$(BUILD_DIR)/init.o:./kernel/init.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 编译 kernel/memory.c
$(BUILD_DIR)/memory.o:./kernel/memory.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 编译 lib/kernel/string.c
$(BUILD_DIR)/string.o:./lib/kernel/string.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 编译 lib/kernel/bitmap.c
$(BUILD_DIR)/bitmap.o:./lib/kernel/bitmap.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 编译 lib/kernel/list.c
$(BUILD_DIR)/list.o:./lib/kernel/list.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 编译 thread/thread.c
$(BUILD_DIR)/thread.o:./thread/thread.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 编译 switch.asm
$(BUILD_DIR)/switch.o:./thread/switch.asm
$(AS) $(NASM_ELF) -o [email protected] $^ $(ASINCLUDE)
# 编译 main.c
$(BUILD_DIR)/timer.o:./device/timer.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 编译 main.c
$(BUILD_DIR)/kernel.o:./kernel/main.c
$(CC) -o [email protected] $(CFLAGS) -c $^ $(INCLUDE)
# 最终链接
$(BUILD_DIR)/kernel.bin:$(OBJ)
$(LD) -Ttext 0xc0001500 -m elf_i386 -e main -o ./build/kernel.bin $(OBJ)
dd if=./build/kernel.bin of=./hd60m.img bs=512 count=40 seek=9 conv=notrunc
clean:
rm -rf ./build/*
6 运行
为了验证新任务第一次运行实际上是属于中断处理函数部分,因此我们在kernel_thread()
中,加一下的任意断点
static void kernel_thread(ThreadFunc *func, void *func_arg)
{
BREAK();
// 在中断处理函数中关中断,然后切换完成后开中断
intr_enable();
BREAK();
func(func_arg);
}
以上是关于第09章下 任务调度的主要内容,如果未能解决你的问题,请参考以下文章
STM32H7第14章 ThreadX调度锁,任务锁和中断锁(调度阀值)