当前位置: 首页 > news >正文

《操作系统真象还原》 第九章 第二部分

一、实现多线程调度

在上一部分我们介绍了如何创建线程,以及内核中所用到的数据结构双向链表,这部分我们使用双向链表结构来实现多线程切换。

在thread.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的边界
};

在该文件中主要就是task_thread结构体中添加一些属性,ticks表示该线程执行的时间,elapsed_ticks表示该线程在处理器总执行时间,general_tag是在就绪队列中所存放的结点,all_list_tag是在全部线程队列中所存放的结点。

在thread.c文件中添加代码

#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"#include "debug.h"
#include "interrupt.h"
#include "print.h"#define PG_SIZE 4096struct 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地址、要运行的函数地址、函数的参数地址来初始化线程栈中的运行信息,核心就是填入要运行的函数地址与参数 */
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_retaddrkthread_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初始化为0strcpy(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);                     //初始化线程的pcbthread_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");
}

开头分别定义了main_thread主线程的PCB,以及定义了两个队列,一个为保存已经就绪的队列,另一个用于保存全部线程的队列,switch_to为switch.s文件中使用汇编语言所定义的函数用于切换线程,running_thread()可返回当前正在运行线程的栈顶指针,init_thread()初始化线程中添加了判断当前线程是否是主线程,如果是主线程,修改主线程的状态,thread_start()函数中创建完线程添加了将线程加入到就绪队列和全部线程队列中,make_main_thread()主要是为main线程的PCB初始化信息,schedule()函数主要用于判断当前线程的时间片是否到时,如果到时,给当前线程状态修改为就绪状态,并且将该线程添加到就绪队列最后。

在thread文件夹中创建switch.s文件

[bits 32]
section .text
global switch_to
switch_to:;栈中此处是返回地址	       push esi                      	;这4条就是对应压入线程栈中预留的ABI标准要求保存的,esp会保存在其他地方push edipush ebxpush ebpmov 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 ebppop ebxpop edipop esiret				 				; 返回到上面switch_to下面的那句注释的返回地址,; 未由中断进入,第一次执行时会返回到kernel_thread

该文件主要用于定义switch_to函数实现真正的切换进程。由schedule()函数调用。

在interrupt.c文件中添加代码

/* 通用的中断处理函数,用于初始化,一般用在异常出现时的处理 */
static void general_intr_handler(uint8_t vec_nr) {if (vec_nr == 0x27 || vec_nr == 0x2f) {	//伪中断向量,无需处理。详见书p337return;		}/* 将光标置为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; 
}

添加了general_intr_handler()中断通用处理函数主要用于打印异常信息,register_handler()函数就是根据中断向量号注册相应的中断处理函数。

在interrupt.h中添加函数声明

void register_handler(uint8_t vector_no, intr_handler function);

新建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) {	  // 若进程时间片用完就开始调度新的进程上cpuschedule(); } else {				  // 将当前进程的时间片-1cur_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()函数完成线程调度。

修改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();  
}

编写main函数进行测试

#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();}
}

测试结果就会看到main线程和其它两个线程交换打印字符

二、主线程切换到线程A的过程

        当时钟中断发生时,会调用时钟中断处理函数,若当前时间片还没到,则继续执行,否则调用thread.c文件中的schedule()任务调度函数,给当前线程重新赋予时间滴答数并添加到就绪队列的末尾。调用switch.s中定义的switch_to()函数完成线程栈的切换。

每次主线程调用函数都会给自己的栈中压入返回地址,当调用switch_to函数时,主线程的栈如下图所示:

一次从未执行过的线程栈为:

当执行完switch_to函数时,就会把esp指针从主线程的self_kstack指针跳到线程A的self_kstack指针,然后连续出四次栈,就会执行eip,执行ret指令,就会执行线程A所指向的函数,当线程A的时间片执行结束后,线程A的栈就会像主线程的栈一样压入一系列调用函数的返回地址,当esp指针重新指向主线程的self_kstack时,依旧是连续出四次栈,从switch_to函数中一层一层返回到之前执行到的位置继续执行。上图只是为了更好地理解线程切换,并非实际栈中内容。

http://www.dtcms.com/a/435528.html

相关文章:

  • 网站开发服务器的选择wordpress自动添加视频
  • 外贸网站源码自己建站模板
  • 网站开发制作公司宁波商城网站开发设计
  • 做网站的主要内容公司图标设计logo
  • 郑州七彩网站建设公司网络架构设计方案
  • 408之二叉树(一)
  • 阳泉市住房保障和城乡建设管理局网站建设协会网站的公司
  • 【密码学实战】openHiTLS CRL命令行:证书吊销列表
  • 烟台网站建设方案报价国开行网站毕业申请怎么做
  • js中异步回调函数的执行机制与事件循环
  • 创造与魔法官方网站做自己喜欢的事购物商城起名
  • 自己搞网站建设企业网站更新什么内容
  • 解决一个C# 在Framework 4.5反序列化的问题
  • 营销导向网站建设流程电脑配件网站建设
  • 网站搭建素材群会计培训班的费用是多少
  • 建设银行短信带网站江苏省住房和城乡建设厅网站首页
  • 哪个网站最好wordpress找回密码收不到邮件
  • 哈希表(散列表)介绍及实现
  • 一个专门做ppt的网站吗注册域名需要实名认证吗
  • 做网站外包工作怎么样visual composer for wordpress
  • 平面设计鉴赏网站关于加强网站建设
  • Spring Boot 热部署配置
  • 成都网站设计公司南宁seo按天收费
  • 自适应微网站开发专业集团门户网站建设企业
  • PCIe协议之低功耗篇之 理论深度学习(三)
  • 广州帮人网站建设广州网站建设需要多少费用
  • 在县城怎么做网站公司网络域名侵权十大案例
  • 佛山专业做淘宝网站推广住房与城乡建设局网站
  • 康复实训室介绍:告别“假人”模型,在沉浸式环境中锻造康复精英的黄埔军校
  • C语言小白实现多功能计算器的艰难历程