创建内核线程,实现多线程轮询调度

Q&A

1、什么是线程?线程的产生背景?
线程是任务调度器进行调度的基本单位。
一开始OS内核不支持创建多线程,后来为了满足程序高并发的需求,OS内核开始支持多线程,使用户进程可以通过系统调用使用。

2、进程和线程的区别?
如果程序没有使用多线程,那么运行时整个进程将成为调度的基本单位;如果使用了多线程,则整个进程会分为多个线程供处理器调度。
线程共享进程的地址空间,进程有自己的页表,线程没有页表。

3、在用户空间实现线程和在内核实现线程的优劣比较?
在用户空间实现线程的优点在于(相对内核提供支持):
(1)线程调度算法可修改,可以为某些线程加权调度;
(2)避免因陷入到内核态而切换栈,避免了陷入内核的代价。
劣势在于(相对内核提供支持):
(1)内核调度器感受不到线程的阻塞态,因此无法将处理器控制权交给进程内的其他线程,因此提速不明显

3、多线程的优势与危险?
优势:程序提速
1、建立了多个执行流,能更多占有处理器的运行时间
2、避免因为用户输入等原因阻塞整个进程
危险:线程共享进程的地址空间,可以互相修改数据

4、进程控制块PCB结构:
进程控制块由OS提供,记录进程的状态信息,结构如下:
在这里插入图片描述
寄存器映像:保存寄存器现场;
栈:进程0特权级下的内核栈;
进程(线程)状态:阻塞态、就绪态、运行态;
优先级:体现任务在处理器上执行的时间片长度,优先级越高,时间片越长;
时间片:当时间片为0时,表示应该下CPU
结构体定义:

struct task_struct {
    uint32_t* self_kstack;	 // 各内核线程都用自己的内核栈
    enum task_status status;
    char name[16];
    uint8_t priority;
    uint8_t ticks;	   // 每次在处理器上执行的时间嘀嗒数,也就是时间片
 
//总时钟数
    uint32_t elapsed_ticks;

//线程在队列中的结点
    struct list_elem general_tag;				    

//all_list_tag的作用是用于线程队列thread_all_list中的结点 
    struct list_elem all_list_tag;

    uint32_t* pgdir;              // 进程自己页表的虚拟地址
    uint32_t stack_magic;	 // 用这串数字做栈的边界标记,用于检测栈的溢出
};

5、线程栈结构定义:

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所调用的函数所需的参数
};

6、负责管理线程的数据结构:
双向链表定义的队列:

// 链表结构,用来实现队列 
struct list {
// head是队首,是固定不变的,不是第1个元素,第1个元素为head.next 
   struct list_elem head;
// tail是队尾,同样是固定不变的 
   struct list_elem tail;
};

链表结点定义,结点是线程PCB中定义的tag:

struct list_elem {
   struct list_elem* prev; // 前躯结点
   struct list_elem* next; // 后继结点
};

7、任务调度机制的实现原理:
每发生一次时钟中断,当前运行线程PCB中定义的ticks就减一,当ticks为0时,时钟中断的中断处理程序调用调度器schedule,让调度器选择另一个线程上处理器。

8、线程调度算法:
实现中用的是轮询调度(Round Robin Scheduling),简单来说就是让队列中的线程挨个执行,也需要根据当前线程的几种情况分别判断
(1)如果当前进程的状态是TASK_RUNNING,说明是时间片到期,将ticks重新赋值为优先级prio,将状态改为TASK_READY,并加入到就绪队列的末尾。将就绪队列的头部线程状态设为TASK_RUNNING, 之后通过函数switch_to将新线程的寄存器环境恢复.
(2)如果状态为其他,不需要任何操作。


线程的创建

创建内核线程的顺序逻辑为:

main.c -> thread_start -> init_thread(初始化PCB) -> thread_create(初始化线程栈)
线程栈和PCB的关系:

当前的内核main函数:

int main(void) {
   init_all();
   thread_start("k_thread_a", 31, k_thread_a, "argA ");
   while(1);
   return 0;
}
// 用void*来通用表示参数,被调用的函数知道自己需要什么类型的参数,自己转换再用 

void k_thread_a(void* arg) {     

   char* para = arg;
   while(1) {
      put_str(para);
   }
}

thread_start:

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

init_thread,初始化PCB:

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;	  // 自定义的魔数
}

thread_create,初始化线程栈:

//void* 是通用指针类型,可以接受任何类型的赋值
void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) {
   //预留线程中断栈的空间
   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;
/*
kernel_thread定义:
static void kernel_thread(thread_func* function, void* func_arg) {
   function(func_arg); 
}
*/
   kthread_stack->function = function;
   kthread_stack->func_arg = func_arg;
   kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;
}


线程调度的实现

1、注册时钟中断处理函数
时钟中断发生时,检查当前运行线程的时间片是否为0,如果为0则调用schedule切换线程,如果不为0,则线程的时间片减1

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

缺页异常的处理:当虚拟地址对应的物理地址不存在,虚拟地址会存放在控制寄存器CR2中,处理函数将CR2中的地址存放在变量page_fault_vaddr中,并在屏幕上打印显示

/* 通用的中断处理函数,一般用在异常出现时的处理 */
static void general_intr_handler(uint8_t 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);
}

2、调度器schedule的实现

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

因为从就绪队列中弹出的只是线程的tag,所以需要将其转换为PCB:

//将地址elem_ptr转换为struct_type类型的指针
//转换方法:tag地址减去在PCB中的偏移量,再通过强制类型转换得到struct_type类型的指针
#define elem2entry(struct_type, struct_member_name, elem_ptr) \
	 (struct_type*)((int)elem_ptr - offset(struct_type, struct_member_name))

3、switch_to的实现
一般中断处理执行过程:
在这里插入图片描述
中断处理过程中的上下文保护:
1、保护用户程序上下文,就是保存当前的全部寄存器映像

intr%1entry:		 ; 每个中断处理程序都要压入中断向量号
   %2				 ; 中断若有错误码会压在eip后面 
; 以下是保存上下文环境
   push ds
   push es
   push fs
   push gs
   pushad			 ; PUSHAD指令压入32位寄存器,其入栈顺序是: EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

恢复用户程序上下文:

intr_exit:	     
   add esp, 4			   ; 跳过中断号
   popad
   pop gs
   pop fs
   pop es
   pop ds
   add esp, 4			   ; 跳过error_code
   iretd                   ;中断返回

2、第二部分是保护内核程序上下文,ABI(Application Binary Interface)标准规定只保存esi,edi,ebx和ebp这四个寄存器即可,由switch_to函数负责实现,接收两个参数,第一个参数是当前线程,第二个参数是待切换的线程
任务切换时的栈:
在这里插入图片描述
switch_to:

[bits 32]
section .text
global switch_to
switch_to:
  ;保护ABI标准规定的四个寄存器   
   push esi
   push edi
   push ebx
   push ebp

   mov eax, [esp + 20]		 ; 得到栈中的参数cur, 当前线程的PCB地址
   mov [eax], esp            ; 保存栈顶指针esp. task_struct的self_kstack字段,
			            	 ; self_kstack在task_struct中的偏移为0,
				             ; 所以直接往thread开头处存4字节便可。
;--------------------------- 恢复下一个线程的环境  -----------------------------
   mov eax, [esp + 24]		 ; 得到栈中的参数next,切换目标线程的地址
   mov esp, [eax]		 ; 使esp指向next线程的栈指针
				 ; 用来上cpu时恢复0级栈,0级栈中保存了进程或线程所有信息,包括3级栈指针
				 ;接下来pop的是在新线程的栈中
   pop ebp
   pop ebx
   pop edi
   pop esi
   ret	    	 ;执行next线程的代码


启用线程切换

1、创建就绪队列和全部队列,添加当前main线程:

void thread_init(void) {
//创建就绪队列
   list_init(&thread_ready_list);
//创建全部队列
   list_init(&thread_all_list);
// 将当前main函数创建为线程 
   make_main_thread();
}

2、创建多个线程

int main(void) {
   init_all();
//线程k_thread_a打印argA
   thread_start("k_thread_a", 31, k_thread_a, "argA ");
//线程k_thread_b打印argB
   thread_start("k_thread_b", 8, k_thread_b, "argB ");

   intr_enable();	// 打开中断,使时钟中断起作用
//主线程打印Main
   while(1) {
      put_str("Main ");
   };
   return 0;
}

3、线程切换效果
在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值