配合视频学习效果更佳!
第一节:https://www.bilibili.com/video/BV1ho4y1J7Pi/?vd_source=701807c4f8684b13e922d0a8b116af31
第二节:https://www.bilibili.com/video/BV1MM4y1n7uV/?vd_source=701807c4f8684b13e922d0a8b116af31
第三节:https://www.bilibili.com/video/BV1Pg4y1K7TS/?vd_source=701807c4f8684b13e922d0a8b116af31
末尾GP异常:https://www.bilibili.com/video/BV18P411q7hg/?vd_source=701807c4f8684b13e922d0a8b116af31
程序是指静态的、存储在文件系统上、尚未运行的指令代码,比如说一个编译好的存储在磁盘中的程序。进程是指正在运行中的程序,程序必须在获得运行时所需要的各类资源后才能成为进程,资源包括进程所使用的栈、寄存器、内存、页表等。线程,就是最基本的调度单元,能够以身份独立上处理器运行,也就是一个程序中可以独立的,且正在运行的代码块。线程作为调度单元,一个进程线程越多,那么它上处理器的频率就越高,执行就越快。进程=线程+资源,因为线程运行脱离不了进程,进程里面有线程的运行的资源。线程是调度的单元,进程是分配资源的单元,同一个进程的多个线程享受这个进程的共同资源,如内存,页表等。
把需要等待外界条件的才能运行的进程状态叫做阻塞态,进程可以随时准备运行的状态成为就绪态,把正在处理器上运行的进程的状态成为运行态。
操作系统为每个进程提供了一个PCB,进程控制块,它就是进程的身份证。每个PCB放到一张表格中维护,这就是进程表。调度器可以根据这张表选择上处理器运行的进程。
一个基本的PCB其结构如图所示
本章我们最终要实现的就是一个多线程并行的效果,也就是如果我创建了两个线程A和B,A线程内容就是循环打印A,B线程是循环打印B。那么最终呈现在屏幕上的效果就是,一会打印一堆A,一会打印一堆B,如此循环往复交替执行。
接下来,我们首先实现这一切的基础,创建并运行内核线程。
剖析409,410,414,thread.c代码(核心):
1、代码功能
在内核中实现创建并运行线程
2、实现原理
通过pcb我们可以管理进程、线程的运行。所以,对于进程、线程的运行管理(是否运行、运行多长时间等问题),简化为对于pcb的管理。通过pcb,我们可以找到进程、线程的栈。在栈中,找到实际要运行的线程函数的地址,然后去执行。
3、代码逻辑
A、创建线程信息(包含管理信息与运行信息)
B、执行线程
4、怎么写代码?
A、建立核心数据结构,task_struct
,用于记录我们要创建的线程的管理信息;定义核心数据结构thread_stack
,用于记录线程的运行信息
B、写出函数init_thread
,来初始化线程pcb中的管理信息
C、写出函数thread_create
,来初始化线程栈中的函数运行信息
D、写出函数kernel_thread
作为线程启动器来启动真正要运行的函数,并将其地址放在thread_stack
中,也就是线程的栈中
E、封装BCD成为一个函数thread_create
,其内部加入通过pcb中的satck信息找到线程栈,然后ret弹出栈顶地址进入执行的汇编代码
5、 **代码实现 **
myos/thread/thread.h
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"
//定义一种叫thread_fun的函数类型,该类型返回值是空,参数是一个地址(这个地址用来指向自己的参数)。
//这样定义,这个类型就能够具有很大的通用性,很多函数都是这个类型
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,返回void的函数指针(*epi的*决定了这是个指针),
//该函数传入的参数是一个thread_func类型的函数指针与函数的参数地址
void (*eip) (thread_func* func, void* func_arg);
//以下三条是模仿call进入thread_start执行的栈内布局构建的,call进入就会压入参数与返回地址,因为我们是ret进入kernel_thread执行的
//要想让kernel_thread正常执行,就必须人为给它造返回地址,参数
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; //如果线程的栈无限生长,总会覆盖地pcb的信息,那么需要定义个边界数来检测是否栈已经到了PCB的边界
};
#endif
代码实现如下: myos/thread/thread.c
#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);
}
/*用于根据传入的线程的pcb地址、要运行的函数地址、函数的参数地址来初始化线程栈中的运行信息,核心就是填入要运行的函数地址与参数 */
void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) {
/* 先预留中断使用栈的空间,可见thread.h中定义的结构 */
//pthread->self_kstack -= sizeof(struct intr_stack); //-=结果是sizeof(struct intr_stack)的4倍
//self_kstack类型为uint32_t*,也就是一个明确指向uint32_t类型值的地址,那么加减操作,都是会是sizeof(uint32_t) = 4 的倍数
pthread->self_kstack = (uint32_t*)((int)(pthread->self_kstack) - sizeof(struct intr_stack));
/* 再留出线程栈空间,可见thread.h中定义 */
//pthread->self_kstack -= sizeof(struct thread_stack);
pthread->self_kstack = (uint32_t*)((int)(pthread->self_kstack) - sizeof(struct thread_stack));
struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack; //我们已经留出了线程栈的空间,现在将栈顶变成一个线程栈结构体
//指针,方便我们提前布置数据达到我们想要的目的
kthread_stack->eip = kernel_thread; //我们将线程的栈顶指向这里,并ret,就能直接跳入线程启动器开始执行。
//为什么这里我不能直接填传入进来的func,这也是函数地址啊,为什么还非要经过一个启动器呢?其实是可以不经过线程启动器的
//因为用不着,所以不用初始化这个返回地址kthread_stack->unused_retaddr
kthread_stack->function = function; //将线程启动器(thread_start)需要运行的函数地址放入线程栈中
kthread_stack->func_arg = func_arg; //将线程启动器(thread_start)需要运行的函数所需要的参数地址放入线程栈中
kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;
}
/* 初始化线程基本信息 , pcb中存储的是线程的管理信息,此函数用于根据传入的pcb的地址,线程的名字等来初始化线程的管理信息*/
void init_thread(struct task_struct* pthread, char* name, int prio) {
memset(pthread, 0, sizeof(*pthread)); //把pcb初始化为0
strcpy(pthread->name, name); //将传入的线程的名字填入线程的pcb中
pthread->status = TASK_RUNNING; //这个函数是创建线程的一部分,自然线程的状态就是运行态
pthread->priority = prio;
/* self_kstack是线程自己在内核态下使用的栈顶地址 */
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE); //本操作系统比较简单,线程不会太大,就将线程栈顶定义为pcb地址
//+4096的地方,这样就留了一页给线程的信息(包含管理信息与运行信息)空间
pthread->stack_magic = 0x19870916; // /定义的边界数字,随便选的数字来判断线程的栈是否已经生长到覆盖pcb信息了
}
/* 创建一优先级为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); //为线程的pcb申请4K空间的起始地址
init_thread(thread, name, prio); //初始化线程的pcb
thread_create(thread, function, func_arg); //初始化线程的线程栈
//我们task_struct->self_kstack指向thread_stack的起始位置,然后pop升栈,
//到了通过线程启动器来的地址,ret进入去运行真正的实际函数
//通过ret指令进入,原因:1、函数地址与参数可以放入栈中统一管理;2、ret指令可以直接从栈顶取地址跳入执行
asm volatile ("movl %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; ret" : : "g" (thread->self_kstack) : "memory");
return thread;
}
6、其他代码详解查看书p410
声明函数(myos/kernel/thread.h)
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);
测试代码 myos/kernel/main.c
#include "print.h"
#include "init.h"
#include "thread.h"
void k_thread_a(void*);
int main(void) {
put_str("I am kernel\n");
init_all();
thread_start("k_thread_a", 31, k_thread_a, "argA ");
while(1);
return 0;
}
/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
char* para = arg;
while(1) {
int i = 9999999;
while(i--);
put_str(para);
}
}
以上仅仅是线程的创建与进入,我们要实现依靠线程的pcb之间形成的链表来实现管理与调度,pcb之间形成的链表是为了通过一个pcb顺利找到下一个pcb,因为我们会在task_struct
中插入一个双向链表,为了实现这样的数据结构,我们接下来实现一大堆与链表有关的数据结构与函数。
创建双链表的管理节点与普通节点数据结构 (myos/lib/kernel/list.h)
#ifndef __LIB_KERNEL_LIST_H
#define __LIB_KERNEL_LIST_H
#include "global.h"
/********** 定义链表结点成员结构 ***********
*结点中不需要数据成元,只要求前驱和后继结点指针*/
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的函数类型,返回值是int,参数是链表结点指针与一个整形值
typedef bool (function)(struct list_elem*, int arg);
#endif
写有关双链表的函数
myos/lib/kernel/list.c 代码剖析略
#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(); //未来这个链表结点插入是用于修改task_struck队列的,这是个公共资源,所以需要不被切换走
/* 将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操作,添加结点到链表队首,类似于push操作, 参数1是链表的管理结点,参数2是一个新结点 */
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);
}
函数声明与通过结构体成员计算整个结构体地址的宏 myos/lib/kernel/list.h
//用于计算一个结构体成员在结构体中的偏移量
#define offset(struct_type,member_name) (int)(&(((struct_type*)0)->member_name))
//用于通过一个结构体成员地址计算出整个结构体的起始地址
#define member_to_entry(struct_type,member_name,member_ptr) (struct_type*)((int)member_ptr-offset(struct_type,member_name))
void list_init(struct list* list);
void list_insert(struct list* link,struct list* new_link);
void list_push(struct list* list, struct list* new_link);
void list_append(struct list* list,struct list* new_link);
void list_remove(struct list* link);
struct list* list_pop(struct list* list);
int list_find(struct list* list, struct list* link);
int list_empty(struct list* list);
uint32_t list_len(struct list* list);
struct list* list_traversal(struct list* list,function func,int arg);
现在我们来实现多线程的运行
421、422、423、426、427、428、429、432、433、434、435、析代码:thread.h、thread.c、init.c、pirnt.S、interrup.c、timer.c、swith .S、main.c:
1、代码功能
实现多线程的轮询调度运行
2、实现原理
线程的pcb中存储着线程栈的位置,而线程栈中又存储着线程的运行所需要运行的一系列信息,通过这些信息,我们可以进入线程执行。在线程执行过程中,每一次时钟中断都会修改线程pcb中的允许运行的时间值,当一个线程的所允许被执行的时间归零。那么就执行调度,就是从就绪队列中找到下一个线程的pcb,进而找到线程栈中的运行信息,并进入执行。
3、代码逻辑
内核多线程的轮询调度,核心就4个:
- 多线程要形成队列,调度是从队列中挑选pcb,通过pcb去运行线程;
- main要把自己初始化成主线程与其他线程参与轮询调度;
- 用时钟中断来打断线程的运行,然后统计线程运行的时间,由时钟中断来决定是否调度切换;
- 有切换机制来完成线程之间的切换;
4、代码干了啥(主要)?
1、thread.h
- task_struct增加时间片字段,链表字段
2、thread.c
- 增加主线程的pcb;
- 增加管理所有线程pcb的队列;
- 增加管理所有就绪线程的队列;
- 写出通过当前栈值获得pcb的函数running_thread;
- 线程启动器函数kernel_thread增加打开中断部分代码;
- 初始化线程管理信息(pcb)的函数init_thread增加对task_struct增加字段的初始化代码;
- 创建线程函数thread_start增加加入线程pcb进入所有队列与就绪队列的代码并删除ret进入线程代码(因为我们要用切换函数schedule来选择线程运行,而不是让线程创建函数创建完毕后直接运行);
- 增加函数make_main_thread用于初始化主线程;
- 增加切换函数schedule;
- 增加函数thread_init用于初始化主线程;
3、init.c
- 将thread_init函数封装进入总初始化函数,并且要调换几个初始化函数的调用顺序
4、print.S
- 增加用于设定光标位置的函数set_cursor
5、interrupt.c
- 修改通用中断处理函数general_intr_handler,增加修改光标位置打印错误新的的代码,原因是多线程切换引发的同步问题,有时候会引发光标值超过允许范围,这个范围是显存段段描述符的界限决定的,如果这时候由光标值超限引发的中断,然后调用general_intr_handler去打印信息,将再次导致异常,那么就不会输出错误信息;
- 增加中断处理函数注册函数register_handler;
6、timer.c
- 增加全局变量ticks用于记录自时钟中断启动以来发生了多少次时钟中断;
- 增加时钟中断处理函数intr_timer_handler,其主要功能就是每发生一次时钟中断,就增加一次ticks与当前正在运行的线程的pcb中的时间,如果时间到期,那么就运行切换程序schedule进行切换;
- 在timer.c中增加注册时钟中断处理函数的代码
7、switch.S
- 里面写入根据schedule函数传入的当前正在运行的线程pcb与下一个要运行的线程pcb完成切换的代码
5、代码实现如下:
myos/thread/thread.h
#include "list.h"
struct task_struct {
uint32_t* self_kstack; // 用于存储线程的栈顶位置,栈顶放着线程要用到的运行信息
enum task_status status;
uint8_t priority; // 线程优先级
char name[16]; //用于存储自己的线程的名字
uint8_t ticks; //线程允许上处理器运行还剩下的滴答值,因为priority不能改变,所以要在其之外另行定义一个值来倒计时
uint32_t elapsed_ticks; //此任务自上cpu运行后至今占用了多少cpu嘀嗒数, 也就是此任务执行了多久*/
struct list_elem general_tag; //general_tag的作用是用于线程在一般的队列(如就绪队列或者等待队列)中的结点
struct list_elem all_list_tag; //all_list_tag的作用是用于线程队列thread_all_list(这个队列用于管理所有线程)中的结点
uint32_t* pgdir; // 进程自己页表的虚拟地址
uint32_t stack_magic; //如果线程的栈无限生长,总会覆盖地pcb的信息,那么需要定义个边界数来检测是否栈已经到了PCB的边界
};
myos/thread/thread.c
#include "debug.h"
#include "interrupt.h"
#include "print.h"
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);
}
/* 初始化线程基本信息 , pcb中存储的是线程的管理信息,此函数用于根据传入的pcb的地址,线程的名字等来初始化线程的管理信息*/
void init_thread(struct task_struct* pthread, char* name, int prio) {
memset(pthread, 0, sizeof(*pthread)); //把pcb初始化为0
strcpy(pthread->name, name); //将传入的线程的名字填入线程的pcb中
if(pthread == main_thread){
pthread->status = TASK_RUNNING; //由于把main函数也封装成一个线程,并且它一直是运行的,故将其直接设为TASK_RUNNING */
}
else{
pthread->status = TASK_READY;
}
pthread->priority = prio;
/* self_kstack是线程自己在内核态下使用的栈顶地址 */
pthread->ticks = prio;
pthread->elapsed_ticks = 0;
pthread->pgdir = NULL; //线程没有自己的地址空间,进程的pcb这一项才有用,指向自己的页表虚拟地址
pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE); //本操作系统比较简单,线程不会太大,就将线程栈顶定义为pcb地址
//+4096的地方,这样就留了一页给线程的信息(包含管理信息与运行信息)空间
pthread->stack_magic = 0x19870916; // /定义的边界数字,随便选的数字来判断线程的栈是否已经生长到覆盖pcb信息了
}
/* 创建一优先级为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); //为线程的pcb申请4K空间的起始地址
init_thread(thread, name, prio); //初始化线程的pcb
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");
}
myos/thread/thread.h
struct task_struct* running_thread(void);
void schedule(void);
void thread_init(void);
myos/kernel/init.c
#include "init.h"
#include "print.h"
#include "interrupt.h"
#include "timer.h"
#include "memory.h"
#include "thread.h"
/*负责初始化所有模块 */
void init_all() {
put_str("init_all\n");
idt_init(); //初始化中断
mem_init(); // 初始化内存管理系统
thread_init(); // 初始化线程相关结构
timer_init();
}
myos/lib/kernelprint.S(添加新的代码块,而不是修改原有的.set_cursor
)
global set_cursor
set_cursor:
pushad
mov bx, [esp+36]
;;;;;;; 1 先设置高8位 ;;;;;;;;
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高8位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口0x3d5来获得或设置光标位置
mov al, bh
out dx, al
;;;;;;; 2 再设置低8位 ;;;;;;;;;
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
mov al, bl
out dx, al
popad
ret
myos/lib/print.h
void set_cursor(uint32_t cursor_pos);
myos/kernel/interrupt.c
/* 通用的中断处理函数,用于初始化,一般用在异常出现时的处理 */
static void general_intr_handler(uint8_t vec_nr) {
if (vec_nr == 0x27 || vec_nr == 0x2f) { //伪中断向量,无需处理。详见书p337
return;
}
/* 将光标置为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);
}
/* 在中断处理程序数组第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;
}
myos/kernel/interrupt.h
void register_handler(uint8_t vector_no, intr_handler function);
myos/device/timer.c
#include "interrupt.h"
#include "thread.h"
#include "debug.h"
uint32_t ticks; // ticks是内核自中断开启以来总共的嘀嗒数
/* 时钟的中断处理函数 */
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");
}
myos/thread/switch.S
[bits 32]
section .text
global switch_to
switch_to:
;栈中此处是返回地址
push esi ;这4条就是对应压入线程栈中预留的ABI标准要求保存的,esp会保存在其他地方
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
myos/kernel/main.c
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
void k_thread_a(void*);
void k_thread_b(void*);
int main(void) {
put_str("I am kernel\n");
init_all();
thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 8, k_thread_b, "argB ");
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);
}
}
由于多线程引起的同步问题,会让光标值位置超过显存段段描述符规定的界限而引发错误,所以我们可以在执行线程时候关闭中断,然后运行完毕之后打开中断,以确保线程的原子执行,来杜绝此类问题
修改后的myos/kernel/main.c
#include "print.h"
#include "init.h"
#include "thread.h"
#include "interrupt.h"
void k_thread_a(void*);
void k_thread_b(void*);
int main(void) {
put_str("I am kernel\n");
init_all();
int i = 999999;
thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 31, k_thread_b, "argB ");
intr_enable(); // 打开中断,使时钟中断起作用
while(1)
{
while(i--);
i=999999;
intr_disable();
put_str("main ");
intr_enable();
}
return 0;
}
/* 在线程中运行的函数 */
void k_thread_a(void* arg) {
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
int i=9999999;
char* tmp = arg;
while(1)
{
while(i--);
i=999999;
intr_disable();
put_str(tmp);
intr_enable();
}
}
/* 在线程中运行的函数 */
void k_thread_b(void* arg) {
/* 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 */
int i=9999999;
char* tmp = arg;
while(1)
{
while(i--);
i=999999;
intr_disable();
put_str(tmp);
intr_enable();
}
}