实现内核线程
执行流
执行流是独立的,它的独立性体现在每个执行流都有自己的栈、一套自己的寄存器映像和内存资源,这是 Intel 处理器在硬件上规定的,其实这正是执行流的上下文环境。
进程、线程的状态
进程的身份证—PCB
(1)要加载一个任务上处理器运行,任务由哪来?也就是说,调度器从哪里才能找到该任务?
(2)即使找到了任务,任务要在系统中运行,其所需要的资源从哪里获得?
(3)即使任务已经变成进程运行了,此进程应该运行多久呢?总不能让其独占处理器吧。
(4)即使知道何时将其换下处理器,那当前进程所使用的这一套资源(寄存器内容)应该存在哪里?
(5)进程被换下的原因是什么?下次调度器还能把它换上处理器运行吗?
(6)前面都说过了,进程独享地址空间,它的地址空间在哪里?
操作系统为每个进程提供了一个 PCB,Process Control Block,即程序控制块,它就是进程的身份证,用它来记录与此进程相关的信息,比如进程状态、 PID、优先级等。
进程使用的栈也属于 PCB 的一部分,不过此栈是进程所使用的 0 特权级下内核栈(并不是 3 特权级下的用户梳) 。 栈在 PCB 中,这听上去有点不可思议,但细想一下还是觉得合理的 。 和进程相关的所有资源都应该集中放在一起,这样才方便管理 。
既然内核栈都要放在 PCB 中,那么, PCB 一般都很大,通常以页为单位,咱们系统比较小,PCB 只占一页。顺便说一句,上面所说的“寄存器映像”的位置并不固定,原因就是“寄存器映像”存储到内核栈中,通常情况下进程或线程被中断时,处理器自动在 TSS 中获取内核栈指针,这通常是 PCB的顶端,因此通常情况下“寄存器映像”位于 PCB 的顶端。但有时候进程或线程的上下文,也就是“寄存器映像”并不是在中断发生时保存到栈中的,而是在内核态下工作时,栈指针己经发生了变化时才向栈中保存“寄存器映像”比如线程主动让出处理器,这时候就得保存线程的现场,此时“寄存器映像”必然就不在 PCB 顶端了。提醒一下,内核态未必都是关中断的状态,可以在开中断下执行内核代码,否则就不会接收时钟中断,进而就不会调用调度器,也就无法进行任务调度了,本章介绍相关内容时大伙儿就会体会到。鉴于“寄存器映像”的位置并不固定,我们在 PCB 中还要维护一个“栈指针 ” 成员,它记录0级栈枝顶的位置,借此找到进程或线程的“寄存器映像”。
实现进程的两种方式—内核或用户进程
在用户进程中实现线程有以下优点。
• 线程的调度算法是由用户程序自己实现的,可以根据实现应用情况为某些线程加权调度。
• 将线程的寄存器映像装载到 CPU 时,可以在用户空间完成,即不用陷入到内核态,这样就免去了进入内核时的入战及出战操作。
当然,任何事物都有两方面,用户级线程也会有以下缺点。
• 进程中的某个线程若出现了阻塞(通常是由于系统调用造成的),操作系统不知道进程中存在线程,它以为此进程是传统型进程(单线程进程),因此会将整个进程挂起,即进程中的全部线程都无法运行,得,这下因小失大了。
• 线程未在内核空间中实现,因此对于操作系统来说,调度器的调度单元是整个进程,并不是进程中的线程,所以时钟中断只能影响进程一级的执行流。当时钟中断发生后,操作系统的调度器只能感知到进程一级的调度实体,它要么把处理器交给进程 A,要么交给进程 B,绝不可能交给进程中的某个线程,也就是说,要想让操作系统调度,操作系统得知道它的存在才行,但由于线程在用户空间中实现,线程属于进程自己的“家务事飞操作系统根本不知道它的存在。这就导致了:如果在用户空间中实现线程,但凡进程中的某个线程开始在处理器上执行后,只要该线程不主动让出处理器,此进程中的其他钱程都没机会运行。也就是说,没有保险的机制使线程运行“适时飞即避免单一线程过度使用处理器,而其他线程没有调度的机会。这只能凭借开发人员“人为”地在线程中调用类似 pthread_yield 或 pthread_exit 之类的方法使线程发扬“高风亮节”让出处理器使用权,此类方法通过回调方式触发进程内的线程调度器,让调度器有机会选择进程内的其他线程上处理器运行。重复强调:这里所说的“线程让出处理器使用权”,不是将整个进程的处理器使用权通过操作系统调度器交给其他进程,而是将控制权交给此进程自己的线程调度器,由自己的调度器将处理器使用权交给此进程中的下一个线程,肥水不流外人田嘛。
• 最后,线程在用户空间实现,和在内核空间实现相比,只是在内部调度时少了陷入内核的代价,确实相当于提速,但由于整个进程占据处理器的时间片是有限的,这有限的时间片还要再分给内部的线程,所以每个线程执行的时间片非常非常短暂,再加上进程内线程调度器维护线程表、运行调度算法的时间片消耗,反而抵销了内部调度带来的提速。
在内核空间实现线程
简单的PCB及线程栈的实现
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"
/* 自定义通用函数类型,它将在很多线程函数中做为形参类型 */
typedef void thread_func(void*);
/* 进程或线程的状态 */
enum task_status {
TASK_RUNNING,
TASK_READY,
TASK_BLOCKED,
TASK_WAITING,
TASK_HANGING,
TASK_DIED
};
/*********** 中断栈intr_stack ***********
* 此结构用于中断发生时保护程序(线程或进程)的上下文环境:
* 进程或线程被外部中断或软中断打断时,会按照此结构压入上下文
* 寄存器, intr_exit中的出栈操作是此结构的逆操作
* 此栈在线程自己的内核栈中位置固定,所在页的最顶端
********************************************/
struct intr_stack {
uint32_t vec_no; // kernel.S 宏VECTOR中push %1压入的中断号
uint32_t edi;
uint32_t esi;
uint32_t ebp;
uint32_t esp_dummy; // 虽然pushad把esp也压入,但esp是不断变化的,所以会被popad忽略
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从低特权级进入高特权级时压入 */
uint32_t err_code; // err_code会被压入在eip之后
void (*eip) (void);
uint32_t cs;
uint32_t eflags;
void* esp;
uint32_t ss;
};
/*********** 线程栈thread_stack ***********
* 线程自己的栈,用于存储线程中待执行的函数
* 此结构在线程自己的内核栈中位置不固定,
* 用在switch_to时保存线程环境。
* 实际位置取决于实际运行情况。
******************************************/
struct thread_stack {
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;
/* 线程第一次执行时,eip指向待调用的函数kernel_thread
其它时候,eip是指向switch_to的返回地址*/
void (*eip) (thread_func* func, void* func_arg);
/***** 以下仅供第一次被调度上cpu时使用 ****/
/* 参数unused_ret只为占位置充数为返回地址 */
void (*unused_retaddr);
thread_func* function; // 由Kernel_thread所调用的函数名
void* func_arg; // 由Kernel_thread所调用的函数所需的参数
};
/* 进程或线程的pcb,程序控制块 */
struct task_struct {
uint32_t* self_kstack; // 各内核线程都用自己的内核栈
enum task_status status;
uint8_t priority; // 线程优先级
char name[16];
uint32_t stack_magic; // 用这串数字做栈的边界标记,用于检测栈的溢出
};
void thread_create(struct task_struct* pthread, thread_func function, void* func_arg);
void init_thread(struct task_struct* pthread, char* name, int prio);
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg);
#endif
struct intr_stack 定义了程序的中断栈,无论是进程,还是线程,此结构用于中断发生时保护程序的上下文环境。也就是说,进入中断后,在 kemel.S 中的中断入口程序“intr%1entry”所执行的上下文保护的一系列压栈操作都是压入了此结构中。因此,进程或线程被外部中断或软中断打断时,中断入口程序会按照此结构压入上下文寄存器,所以, kemel.S 中 intr exit 中的出栈操作便是此结构的逆操作 。 初始情况下此栈在线程自己的内核栈中位置固定,在 PCB 所在页的最顶端,每次进入中断时就不一定了,如果进入中断时不涉及到特权级变化,它的位置就会在当前的 esp 之下,否则处理器会从 TSS 中获得新的esp 的值,然后该枝在新的 esp 之下.
函数在执行前,如果该函数有参数的话,调用者一定会按照调用约定,先把参数压到战中。
在 C 语言层面,函数的执行都是由调用者发起调用的,这通过 call指令完成,此指令会在栈中留下返回地址。因此被调用的函数在执行时,会认为调用者己经把返回地址留在战中,而且是在战顶的位置 。 也就是说当进入到被调用函数中执行时,栈中的情形应该如图所示。
线程栈struct thread_stack 有两个作用 。
第 1 个作用是在线程首次运行时,线程栈用于存储创建线程所需的相关数据。和线程有关的数据应该都在该线程的 PCB 中,这样便于线程管理,避免为它们再单独维护数据空间。 创建线程之初,要指定在线程中运行的函数及参数,因此,把它们放在位于 PCB 所在页的高地址处的 0 级战中比较合适,该处就是线程栈的所在地址。
第 2 个作用是用在任务切换函数 switch to 中的,这是线程已经处于正常运行后线程技所体现的作用。为解释清楚这个疑惑,现在不得不告诉大家 switch to 函数是我们用汇编语言实现的,它是被内核调度器函数调用的,因此这里面涉及到主调函数寄存器的保护,就是 ebp、 ebx, edi 和 esi 这 4 个寄存器,前面f 那段英文己经阐述了,它们属于主调函数 (这里是指调度器函数〉 ,咱们要在被调用函数 switch to 中将它们保护起来,也就是将它们保存在校中,这必然涉及到压栈指令 push,用单纯的汇编语言 比 C 语言 内嵌汇编的方式要方便一些,请大伙儿见谅 。
switch to 既然是汇编程序,从其返回时,必然要用到 ret 指令,因此为了同时满足这 2 个作用,或者说是为了让作用 1 “配合”作用 2,我们最初先在线程技中装入适合的返回地址及参数,使作用 2 中switch to 的 ret 指令也满足创建线程时的作用 l 。
线程的实现
#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"
#define PG_SIZE 4096
/* 由kernel_thread去执行function(func_arg) */
static void kernel_thread(thread_func* function, void* func_arg) {
function(func_arg);
}
/* 初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置 */
void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) {
/* 先预留中断使用栈的空间,可见thread.h中定义的结构 */
pthread->self_kstack -= sizeof(struct intr_stack);
/* 再留出线程栈空间,可见thread.h中定义 */
pthread->self_kstack -= sizeof(struct thread_stack);
struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack;
kthread_stack->eip = kernel_thread;
kthread_stack->function = function;
kthread_stack->func_arg = func_arg;
kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;
}
/* 初始化线程基本信息 */
void init_thread(struct task_struct* pthread, char* name, int prio) {
memset(pthread, 0, sizeof(*pthread));
strcpy(pthread->name, name);
pthread->status = TASK_RUNNING;
pthread->priority = prio;
/* self_kstack是线程自己在内核态下使用的栈顶地址 */
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);
pthread->stack_magic = 0x19870916; // 自定义的魔数
}
/* 创建一优先级为prio的线程,线程名为name,线程所执行的函数是function(func_arg) */
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg) {
/* pcb都位于内核空间,包括用户进程的pcb也是在内核空间 */
struct task_struct* thread = get_kernel_pages(1);
init_thread(thread, name, prio);
thread_create(thread, function, func_arg);
asm volatile ("movl %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; ret" : : "g" (thread->self_kstack) : "memory");
return thread;
}
在出read_create 中, pthread->self_kstack -= sizeof (struct intr_stack)是为了预留线程所使用的中断措struct intr stack 的空间,这有两个目的。
(1 )将来线程进入中断后,位于 kemel.S 中的中断代码会通过此战来保存上下文 。(2 )将来实现用户进程时,会将用户进程的初始信息放在中断战中。
kernel thread 并不是通过 call 指令调用的,而是通过 ret 来执行的(一会儿我们继续介绍也read_start 时您就知道 ret 在哪里了),因此无法按照正常的函数调用形式传递 kernel_'也read 所需要的参数,如这样调用是不行的: kemel_thread(function, func_arg),只能将参数放在 kernel_thread 所用的战中,即处理器进入 kernel 也read 函数体时,战顶为返回地址,战顶+4 为参数 function,枝顶峰为参数func_arg,这就是上面三行中标有下画线的两行代码的作用。
核心数据结构,双向链表
#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 list_elem {
struct list_elem* prev; // 前躯结点
struct list_elem* next; // 后继结点
};
/* 链表结构,用来实现队列 */
struct list {
/* head是队首,是固定不变的,不是第1个元素,第1个元素为head.next */
struct list_elem head;
/* tail是队尾,同样是固定不变的 */
struct list_elem tail;
};
/* 自定义函数类型function,用于在list_traversal中做回调函数 */
typedef bool (function)(struct list_elem*, int arg);
void list_init (struct list*);
void list_insert_before(struct list_elem* before, struct list_elem* elem);
void list_push(struct list* plist, struct list_elem* elem);
void list_iterate(struct list* plist);
void list_append(struct list* plist, struct list_elem* elem);
void list_remove(struct list_elem* pelem);
struct list_elem* list_pop(struct list* plist);
bool list_empty(struct list* plist);
uint32_t list_len(struct list* plist);
struct list_elem* list_traversal(struct list* plist, function func, int arg);
bool elem_find(struct list* plist, struct list_elem* 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 list_elem* before, struct list_elem* elem) {
enum intr_status 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 list_elem* elem) {
list_insert_before(plist->head.next, elem); // 在队头插入elem
}
/* 追加元素到链表队尾,类似队列的先进先出操作 */
void list_append(struct list* plist, struct list_elem* elem) {
list_insert_before(&plist->tail, elem); // 在队尾的前面插入
}
/* 使元素pelem脱离链表 */
void list_remove(struct list_elem* pelem) {
enum intr_status old_status = intr_disable();
pelem->prev->next = pelem->next;
pelem->next->prev = pelem->prev;
intr_set_status(old_status);
}
/* 将链表第一个元素弹出并返回,类似栈的pop操作 */
struct list_elem* list_pop(struct list* plist) {
struct list_elem* elem = plist->head.next;
list_remove(elem);
return elem;
}
/* 从链表中查找元素obj_elem,成功时返回true,失败时返回false */
bool elem_find(struct list* plist, struct list_elem* obj_elem) {
struct list_elem* 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 list_elem* list_traversal(struct list* plist, function func, int arg) {
struct list_elem* 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 list_elem* 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);
}
多线程调度
简单优先级调度的基础
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"
#include "list.h"
/* 自定义通用函数类型,它将在很多线程函数中做为形参类型 */
typedef void thread_func(void*);
/* 进程或线程的状态 */
enum task_status {
TASK_RUNNING,
TASK_READY,
TASK_BLOCKED,
TASK_WAITING,
TASK_HANGING,
TASK_DIED
};
/*********** 中断栈intr_stack ***********
* 此结构用于中断发生时保护程序(线程或进程)的上下文环境:
* 进程或线程被外部中断或软中断打断时,会按照此结构压入上下文
* 寄存器, intr_exit中的出栈操作是此结构的逆操作
* 此栈在线程自己的内核栈中位置固定,所在页的最顶端
********************************************/
struct intr_stack {
uint32_t vec_no; // kernel.S 宏VECTOR中push %1压入的中断号
uint32_t edi;
uint32_t esi;
uint32_t ebp;
uint32_t esp_dummy; // 虽然pushad把esp也压入,但esp是不断变化的,所以会被popad忽略
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从低特权级进入高特权级时压入 */
uint32_t err_code; // err_code会被压入在eip之后
void (*eip) (void);
uint32_t cs;
uint32_t eflags;
void* esp;
uint32_t ss;
};
/*********** 线程栈thread_stack ***********
* 线程自己的栈,用于存储线程中待执行的函数
* 此结构在线程自己的内核栈中位置不固定,
* 用在switch_to时保存线程环境。
* 实际位置取决于实际运行情况。
******************************************/
struct thread_stack {
uint32_t ebp;
uint32_t ebx;
uint32_t edi;
uint32_t esi;
/* 线程第一次执行时,eip指向待调用的函数kernel_thread
其它时候,eip是指向switch_to的返回地址*/
void (*eip) (thread_func* func, void* func_arg);
/***** 以下仅供第一次被调度上cpu时使用 ****/
/* 参数unused_ret只为占位置充数为返回地址 */
void (*unused_retaddr);
thread_func* function; // 由Kernel_thread所调用的函数名
void* func_arg; // 由Kernel_thread所调用的函数所需的参数
};
/* 进程或线程的pcb,程序控制块 */
struct task_struct {
uint32_t* self_kstack; // 各内核线程都用自己的内核栈
enum task_status status;
char name[16];
uint8_t priority;
uint8_t ticks; // 每次在处理器上执行的时间嘀嗒数
/* 此任务自上cpu运行后至今占用了多少cpu嘀嗒数,
* 也就是此任务执行了多久*/
uint32_t elapsed_ticks;
/* general_tag的作用是用于线程在一般的队列中的结点 */
struct list_elem general_tag;
/* all_list_tag的作用是用于线程队列thread_all_list中的结点 */
struct list_elem all_list_tag;
uint32_t* pgdir; // 进程自己页表的虚拟地址
uint32_t stack_magic; // 用这串数字做栈的边界标记,用于检测栈的溢出
};
void thread_create(struct task_struct* pthread, thread_func function, void* func_arg);
void init_thread(struct task_struct* pthread, char* name, int prio);
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg);
struct task_struct* running_thread(void);
void schedule(void);
void thread_init(void);
#endif
pgd让是任务自己的页表。线程与进程的最大区别就是进程独享自己的地址空间,即进程有自己的页表,而线程共享所在进程的地址空间,即线程无页表。如果该任务为线程, pgdir 则为 NULL,否则pgdir,会被赋予页表的虚拟地址,注意此处是虚拟地址,页表加载时还是要被转换成物理地址的.
#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "debug.h"
#include "interrupt.h"
#include "print.h"
#include "memory.h"
#define PG_SIZE 4096
struct task_struct* main_thread; // 主线程PCB
struct list thread_ready_list; // 就绪队列
struct list thread_all_list; // 所有任务队列
static struct list_elem* thread_tag;// 用于保存队列中的线程结点
extern void switch_to(struct task_struct* cur, struct task_struct* next);
/* 获取当前线程pcb指针 */
struct task_struct* running_thread() {
uint32_t esp;
asm ("mov %%esp, %0" : "=g" (esp));
/* 取esp整数部分即pcb起始地址 */
return (struct task_struct*)(esp & 0xfffff000);
}
/* 由kernel_thread去执行function(func_arg) */
static void kernel_thread(thread_func* function, void* func_arg) {
/* 执行function前要开中断,避免后面的时钟中断被屏蔽,而无法调度其它线程 */
intr_enable();
function(func_arg);
}
/* 初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置 */
void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) {
/* 先预留中断使用栈的空间,可见thread.h中定义的结构 */
pthread->self_kstack -= sizeof(struct intr_stack);
/* 再留出线程栈空间,可见thread.h中定义 */
pthread->self_kstack -= sizeof(struct thread_stack);
struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack;
kthread_stack->eip = kernel_thread;
kthread_stack->function = function;
kthread_stack->func_arg = func_arg;
kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;
}
/* 初始化线程基本信息 */
void init_thread(struct task_struct* pthread, char* name, int prio) {
memset(pthread, 0, sizeof(*pthread));
strcpy(pthread->name, name);
if (pthread == main_thread) {
/* 由于把main函数也封装成一个线程,并且它一直是运行的,故将其直接设为TASK_RUNNING */
pthread->status = TASK_RUNNING;
} else {
pthread->status = TASK_READY;
}
/* self_kstack是线程自己在内核态下使用的栈顶地址 */
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);
pthread->priority = prio;
pthread->ticks = prio;
pthread->elapsed_ticks = 0;
pthread->pgdir = NULL;
pthread->stack_magic = 0x19870916; // 自定义的魔数
}
/* 创建一优先级为prio的线程,线程名为name,线程所执行的函数是function(func_arg) */
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg) {
/* pcb都位于内核空间,包括用户进程的pcb也是在内核空间 */
struct task_struct* thread = get_kernel_pages(1);
init_thread(thread, name, prio);
thread_create(thread, function, 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;
}
/* 将kernel中的main函数完善为主线程 */
static void make_main_thread(void) {
/* 因为main线程早已运行,咱们在loader.S中进入内核时的mov esp,0xc009f000,
就是为其预留了tcb,地址为0xc009e000,因此不需要通过get_kernel_page另分配一页*/
main_thread = running_thread();
init_thread(main_thread, "main", 31);
/* main函数是当前线程,当前线程不在thread_ready_list中,
* 所以只将其加在thread_all_list中. */
ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag));
list_append(&thread_all_list, &main_thread->all_list_tag);
}
/* 实现任务调度 */
void schedule() {
ASSERT(intr_get_status() == INTR_OFF);
struct task_struct* cur = running_thread();
if (cur->status == TASK_RUNNING) { // 若此线程只是cpu时间片到了,将其加入到就绪队列尾
ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
list_append(&thread_ready_list, &cur->general_tag);
cur->ticks = cur->priority; // 重新将当前线程的ticks再重置为其priority;
cur->status = TASK_READY;
} else {
/* 若此线程需要某事件发生后才能继续上cpu运行,
不需要将其加入队列,因为当前线程不在就绪队列中。*/
}
ASSERT(!list_empty(&thread_ready_list));
thread_tag = NULL; // thread_tag清空
/* 将thread_ready_list队列中的第一个就绪线程弹出,准备将其调度上cpu. */
thread_tag = list_pop(&thread_ready_list);
struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag);
next->status = TASK_RUNNING;
switch_to(cur, next);
}
/* 初始化线程环境 */
void thread_init(void) {
put_str("thread_init start\n");
list_init(&thread_ready_list);
list_init(&thread_all_list);
/* 将当前main函数创建为线程 */
make_main_thread();
put_str("thread_init done\n");
}
任务调度器和任务切换
- 注册时钟中断处理函数
#include "interrupt.h"
#include "stdint.h"
#include "global.h"
#include "io.h"
#include "print.h"
#define PIC_M_CTRL 0x20 // 这里用的可编程中断控制器是8259A,主片的控制端口是0x20
#define PIC_M_DATA 0x21 // 主片的数据端口是0x21
#define PIC_S_CTRL 0xa0 // 从片的控制端口是0xa0
#define PIC_S_DATA 0xa1 // 从片的数据端口是0xa1
#define IDT_DESC_CNT 0x21 // 目前总共支持的中断数
#define EFLAGS_IF 0x00000200 // eflags寄存器中的if位为1
#define GET_EFLAGS(EFLAG_VAR) asm volatile("pushfl; popl %0" : "=g" (EFLAG_VAR))
/*中断门描述符结构体*/
struct gate_desc {
uint16_t func_offset_low_word;
uint16_t selector;
uint8_t dcount; //此项为双字计数字段,是门描述符中的第4字节。此项固定值,不用考虑
uint8_t attribute;
uint16_t func_offset_high_word;
};
// 静态函数声明,非必须
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function);
static struct gate_desc idt[IDT_DESC_CNT]; // idt是中断描述符表,本质上就是个中断门描述符数组
char* intr_name[IDT_DESC_CNT]; // 用于保存异常的名字
intr_handler idt_table[IDT_DESC_CNT]; // 定义中断处理程序数组.在kernel.S中定义的intrXXentry只是中断处理程序的入口,最终调用的是ide_table中的处理程序
extern intr_handler intr_entry_table[IDT_DESC_CNT]; // 声明引用定义在kernel.S中的中断处理函数入口数组
/* 初始化可编程中断控制器8259A */
static void pic_init(void) {
/* 初始化主片 */
outb (PIC_M_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_M_DATA, 0x20); // ICW2: 起始中断向量号为0x20,也就是IR[0-7] 为 0x20 ~ 0x27.
outb (PIC_M_DATA, 0x04); // ICW3: IR2接从片.
outb (PIC_M_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 初始化从片 */
outb (PIC_S_CTRL, 0x11); // ICW1: 边沿触发,级联8259, 需要ICW4.
outb (PIC_S_DATA, 0x28); // ICW2: 起始中断向量号为0x28,也就是IR[8-15] 为 0x28 ~ 0x2F.
outb (PIC_S_DATA, 0x02); // ICW3: 设置从片连接到主片的IR2引脚
outb (PIC_S_DATA, 0x01); // ICW4: 8086模式, 正常EOI
/* 打开主片上IR0,也就是目前只接受时钟产生的中断 */
outb (PIC_M_DATA, 0xfe);
outb (PIC_S_DATA, 0xff);
put_str(" pic_init done\n");
}
/* 创建中断门描述符 */
static void make_idt_desc(struct gate_desc* p_gdesc, uint8_t attr, intr_handler function) {
p_gdesc->func_offset_low_word = (uint32_t)function & 0x0000FFFF;
p_gdesc->selector = SELECTOR_K_CODE;
p_gdesc->dcount = 0;
p_gdesc->attribute = attr;
p_gdesc->func_offset_high_word = ((uint32_t)function & 0xFFFF0000) >> 16;
}
/*初始化中断描述符表*/
static void idt_desc_init(void) {
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
make_idt_desc(&idt[i], IDT_DESC_ATTR_DPL0, intr_entry_table[i]);
}
put_str(" idt_desc_init done\n");
}
/* 通用的中断处理函数,一般用在异常出现时的处理 */
static void general_intr_handler(uint8_t vec_nr) {
if (vec_nr == 0x27 || vec_nr == 0x2f) { // 0x2f是从片8259A上的最后一个irq引脚,保留
return; //IRQ7和IRQ15会产生伪中断(spurious interrupt),无须处理。
}
/* 将光标置为0,从屏幕左上角清出一片打印异常信息的区域,方便阅读 */
set_cursor(0);
int cursor_pos = 0;
while(cursor_pos < 320) {
put_char(' ');
cursor_pos++;
}
set_cursor(0); // 重置光标为屏幕左上角
put_str("!!!!!!! excetion message begin !!!!!!!!\n");
set_cursor(88); // 从第2行第8个字符开始打印
put_str(intr_name[vec_nr]);
if (vec_nr == 14) { // 若为Pagefault,将缺失的地址打印出来并悬停
int page_fault_vaddr = 0;
asm ("movl %%cr2, %0" : "=r" (page_fault_vaddr)); // cr2是存放造成page_fault的地址
put_str("\npage fault addr is ");put_int(page_fault_vaddr);
}
put_str("\n!!!!!!! excetion message end !!!!!!!!\n");
// 能进入中断处理程序就表示已经处在关中断情况下,
// 不会出现调度进程的情况。故下面的死循环不会再被中断。
while(1);
}
/* 完成一般中断处理函数注册及异常名称注册 */
static void exception_init(void) { // 完成一般中断处理函数注册及异常名称注册
int i;
for (i = 0; i < IDT_DESC_CNT; i++) {
/* idt_table数组中的函数是在进入中断后根据中断向量号调用的,
* 见kernel/kernel.S的call [idt_table + %1*4] */
idt_table[i] = general_intr_handler; // 默认为general_intr_handler。
// 以后会由register_handler来注册具体处理函数。
intr_name[i] = "unknown"; // 先统一赋值为unknown
}
intr_name[0] = "#DE Divide Error";
intr_name[1] = "#DB Debug Exception";
intr_name[2] = "NMI Interrupt";
intr_name[3] = "#BP Breakpoint Exception";
intr_name[4] = "#OF Overflow Exception";
intr_name[5] = "#BR BOUND Range Exceeded Exception";
intr_name[6] = "#UD Invalid Opcode Exception";
intr_name[7] = "#NM Device Not Available Exception";
intr_name[8] = "#DF Double Fault Exception";
intr_name[9] = "Coprocessor Segment Overrun";
intr_name[10] = "#TS Invalid TSS Exception";
intr_name[11] = "#NP Segment Not Present";
intr_name[12] = "#SS Stack Fault Exception";
intr_name[13] = "#GP General Protection Exception";
intr_name[14] = "#PF Page-Fault Exception";
// intr_name[15] 第15项是intel保留项,未使用
intr_name[16] = "#MF x87 FPU Floating-Point Error";
intr_name[17] = "#AC Alignment Check Exception";
intr_name[18] = "#MC Machine-Check Exception";
intr_name[19] = "#XF SIMD Floating-Point Exception";
}
/* 开中断并返回开中断前的状态*/
enum intr_status intr_enable() {
enum intr_status old_status;
if (INTR_ON == intr_get_status()) {
old_status = INTR_ON;
return old_status;
} else {
old_status = INTR_OFF;
asm volatile("sti"); // 开中断,sti指令将IF位置1
return old_status;
}
}
/* 关中断,并且返回关中断前的状态 */
enum intr_status intr_disable() {
enum intr_status old_status;
if (INTR_ON == intr_get_status()) {
old_status = INTR_ON;
asm volatile("cli" : : : "memory"); // 关中断,cli指令将IF位置0
return old_status;
} else {
old_status = INTR_OFF;
return old_status;
}
}
/* 将中断状态设置为status */
enum intr_status intr_set_status(enum intr_status status) {
return status & INTR_ON ? intr_enable() : intr_disable();
}
/* 获取当前中断状态 */
enum intr_status intr_get_status() {
uint32_t eflags = 0;
GET_EFLAGS(eflags);
return (EFLAGS_IF & eflags) ? INTR_ON : INTR_OFF;
}
/* 在中断处理程序数组第vector_no个元素中注册安装中断处理程序function */
void register_handler(uint8_t vector_no, intr_handler function) {
/* idt_table数组中的函数是在进入中断后根据中断向量号调用的,
* 见kernel/kernel.S的call [idt_table + %1*4] */
idt_table[vector_no] = function;
}
/*完成有关中断的所有初始化工作*/
void idt_init() {
put_str("idt_init start\n");
idt_desc_init(); // 初始化中断描述符表
exception_init(); // 异常名初始化并注册通常的中断处理函数
pic_init(); // 初始化8259A
/* 加载idt */
uint64_t idt_operand = ((sizeof(idt) - 1) | ((uint64_t)(uint32_t)idt << 16));
asm volatile("lidt %0" : : "m" (idt_operand));
put_str("idt_init done\n");
}
#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
uint32_t ticks; // ticks是内核自中断开启以来总共的嘀嗒数
/* 把操作的计数器counter_no、读写锁属性rwl、计数器模式counter_mode写入模式控制寄存器并赋予初始值counter_value */
static void frequency_set(uint8_t counter_port, \
uint8_t counter_no, \
uint8_t rwl, \
uint8_t counter_mode, \
uint16_t counter_value) {
/* 往控制字寄存器端口0x43中写入控制字 */
outb(PIT_CONTROL_PORT, (uint8_t)(counter_no << 6 | rwl << 4 | counter_mode << 1));
/* 先写入counter_value的低8位 */
outb(counter_port, (uint8_t)counter_value);
/* 再写入counter_value的高8位 */
outb(counter_port, (uint8_t)counter_value >> 8);
}
/* 时钟的中断处理函数 */
static void intr_timer_handler(void) {
struct task_struct* cur_thread = running_thread();
ASSERT(cur_thread->stack_magic == 0x19870916); // 检查栈是否溢出
cur_thread->elapsed_ticks++; // 记录此线程占用的cpu时间嘀
ticks++; //从内核第一次处理时间中断后开始至今的滴哒数,内核态和用户态总共的嘀哒数
if (cur_thread->ticks == 0) { // 若进程时间片用完就开始调度新的进程上cpu
schedule();
} else { // 将当前进程的时间片-1
cur_thread->ticks--;
}
}
/* 初始化PIT8253 */
void timer_init() {
put_str("timer_init start\n");
/* 设置8253的定时周期,也就是发中断的周期 */
frequency_set(CONTRER0_PORT, COUNTER0_NO, READ_WRITE_LATCH, COUNTER_MODE, COUNTER0_VALUE);
register_handler(0x20, intr_timer_handler);
put_str("timer_init done\n");
}
- 实现任务调度器schedule
/* 实现任务调度 */
void schedule() {
ASSERT(intr_get_status() == INTR_OFF);
struct task_struct* cur = running_thread();
if (cur->status == TASK_RUNNING) { // 若此线程只是cpu时间片到了,将其加入到就绪队列尾
ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));
list_append(&thread_ready_list, &cur->general_tag);
cur->ticks = cur->priority; // 重新将当前线程的ticks再重置为其priority;
cur->status = TASK_READY;
} else {
/* 若此线程需要某事件发生后才能继续上cpu运行,
不需要将其加入队列,因为当前线程不在就绪队列中。*/
}
ASSERT(!list_empty(&thread_ready_list));
thread_tag = NULL; // thread_tag清空
/* 将thread_ready_list队列中的第一个就绪线程弹出,准备将其调度上cpu. */
thread_tag = list_pop(&thread_ready_list);
struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag);
next->status = TASK_RUNNING;
switch_to(cur, next);
}
- 实现任务切换函数switch_to
任务在执行过程中会执行用户代码和内核代码,当处理器处于低特权级下执行用户代码时我们称之为用户态,当处理器进入高特权级执行到内核代码时,我们称之为内核态,当处理器从用户代码所在的低特权级过渡到内核代码所在的高特权级时,这称为陷入内核。因此一定要清楚,无论是执行用户代码,还是执行内核代码,这些代码都属于这个完整的程序,即属于当前任务,并不是说当前任务由用户态进入内核态后当前任务就切换成内核了,这样理解是不对的。任务与任务的区别在于执行流一整套的上下文资源,这包括寄存器映像、地址空间、 IO 位图等,在将来介绍任务状态段 TSS 之后,您就会了解这套上下文资源恰恰就是 TSS 结构中的内容,拥有这些资源才称得上是任务。因此,处理器只有被新的上下文资源重新装载后,当前任务才被替换为新的任务,这才叫任务切换。当任务进入内核态时,其上下文资源并未完全替换,只是执行了“更厉害”的代码。这有点像咱们在游戏中打怪,用不同的武器打不同的怪,游戏的人物角色始终没有变。
为什么要保护任务的上下文?
系统中的任务调度,过程中需要保护好任务两层执行流的上下文,这分两部分来完成。
第一部分是进入中断时的保护,这保存的是任务的全部寄存器映像,也就是进入中断前任务所属第一层的状态,这些寄存器映像相当于任务中用户代码的上下文。
这些寄存器是由 kemel.S 中定义的中断处理入口程序 intr°/olen町来保护的,里面是一些 p山h 寄存器的指令,这是由汇编下的宏“%macro VECTOR 2 ”定义的,因此前面在介绍注册中断处理程序时曾称之为中断处理程序“模板飞当把这些寄存器映像恢复到处理器中后,任务便完全退出中断,继续执行自己的代码部分。 换句话说,当恢复寄存器后,如果此任务是用户进程,任务就完全恢复为用户程序继续在用户态下执行,如果此任务是内核线程,任务就完全恢复为另一段被中断执行的内核代码,依然是在内核态下运行。
第二部分是保护内核环境上下文,根据 ABI,除 esp 外,只保护esi、edi 、 ebx 和 ebp 这 4 个寄存器就够了。这 4 个寄存器映像相当于任务中的内核代码的上下文,也就是第二层执行流,此部分只负责恢复第二层的执行流,即恢复为在内核的中断处理程序中继续执行的状态。
虽然内核代码是任务的一部分,但任务不可能老留在内核中做客,毕竟这种中断只是临时的,它还得赶紧回去忙其他事呢,因此内核代码有责任包含使任务回去的“出口气咱们是在内核中实现线程机制的,因此任务切换由内核代码完成,这表示当前任务还未执行到“出口”就会被换下处理器停止执行,“出口”在剩下未执行的代码中,待将来再次被调度到处理器上才会继续执行,才会找到“出口气因此,为了任务在将来再次被换上处理器时能够顺利地找到退出中断的出口,我们更有理由在任务切换前把任务的内核上下文保护好,这样当该任务再次被换上处理器时,才能继续把剩下的内核代码执行完,任务才能找到回去的路,也就是返回到进入中断前的状态,继续执行未完成第一层执行流的工作。我们所提的“出口”,是指 kemel.S 中的 intr_exit,这是退出中断的出口,中断处理完成后,执行流程会通过 jmp intr_exit 跳转到此,此处的指令会用进入中断时保护的寄存器映像装载处理器,从而彻底走出中断,恢复任务。
总结:
( 1 )上下文保护的第一部分负责保存任务进入中断前的全部寄存器,目的是能让任务恢复到中断前 。
(2 )上下文保护的第二部分负责保存这 4 个寄存器 z esi 、 edi 、 ebx 和 ebp ,目的是让任务恢复执行在任务切换发生时剩下尚未执行的内核代码,保证顺利走到退出中断的出口,利用第一部分保护的寄存器环境彻底恢复任务。
[bits 32]
section .text
global switch_to
switch_to:
;栈中此处是返回地址
push esi
push edi
push ebx
push ebp
mov eax, [esp + 20] ; 得到栈中的参数cur, cur = [esp+20]
mov [eax], esp ; 保存栈顶指针esp. task_struct的self_kstack字段,
; self_kstack在task_struct中的偏移为0,
; 所以直接往thread开头处存4字节便可。
;------------------ 以上是备份当前线程的环境,下面是恢复下一个线程的环境 ----------------
mov eax, [esp + 24] ; 得到栈中的参数next, next = [esp+24]
mov esp, [eax] ; pcb的第一个成员是self_kstack成员,用来记录0级栈顶指针,
; 用来上cpu时恢复0级栈,0级栈中保存了进程或线程所有信息,包括3级栈指针
pop ebp
pop ebx
pop edi
pop esi
ret ; 返回到上面switch_to下面的那句注释的返回地址,
; 未由中断进入,第一次执行时会返回到kernel_thread