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

一、实现多线程调度

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

在thread.h文件中添加代码

cpp 复制代码
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文件中添加代码

cpp 复制代码
#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 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); 
}

/*用于根据传入的线程的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中

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

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

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

bash 复制代码
[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

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

在interrupt.c文件中添加代码

cpp 复制代码
/* 通用的中断处理函数,用于初始化,一般用在异常出现时的处理 */
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; 
}

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

在interrupt.h中添加函数声明

cpp 复制代码
void register_handler(uint8_t vector_no, intr_handler function);

新建device文件夹。创建timer.c文件

cpp 复制代码
#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");
}

该文件中定义了时钟中断处理函数,这也是线程切换的起点,发生时钟异常时就会调用该函数,先判断当前线程时间片是否到期,如果到期了就会调用schedule()函数完成线程调度。

修改init.c文件

cpp 复制代码
#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函数进行测试

cpp 复制代码
#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函数中一层一层返回到之前执行到的位置继续执行。上图只是为了更好地理解线程切换,并非实际栈中内容。

相关推荐
xybDIY1 小时前
亚马逊云 Organizations 组织 Link 账号关联与解绑自动化解决方案
运维·自动化·云计算·aws
倪某某1 小时前
阿里云无影GPU部署WAN2.2模型
阿里云·云计算
倪某某2 小时前
阿里云ECS GPU部署WAN2.2
人工智能·阿里云·云计算
小白考证进阶中4 小时前
阿里云ACA认证常见问题答疑
阿里云·大模型·云计算·阿里云aca证书·阿里云aca·aca认证·入门证书
Web极客码4 小时前
释放WordPress磁盘空间并减少Inode使用量
服务器·数据库·ubuntu
可爱又迷人的反派角色“yang”5 小时前
k8s(四)
linux·网络·云原生·容器·kubernetes·云计算
朝阳5815 小时前
树莓派 Ubuntu 系统登录问题完整指南:解决 Permission denied (publickey)错误
linux·运维·ubuntu
可爱又迷人的反派角色“yang”5 小时前
k8s(二)
linux·运维·docker·云原生·容器·kubernetes·云计算
oMcLin6 小时前
如何在 Ubuntu 22.04 上部署并优化 Jenkins 2.x 流水线,提升持续集成与自动化测试的效率?
ubuntu·ci/cd·jenkins
rfidunion6 小时前
ubuntu下使用qemu模拟ARM(一)-------安装samba服务器
服务器·arm开发·ubuntu