线程的创建
实现可以分成上层条件和底层数据结构和算法,那么创建一个线程是什么意思呢?创建一个线程实际上就是让一个函数能够成为一条执行流供CPU直接执行。因此线程最重要的就是要执行的函数及其参数,又因为线程涉及调度等问题,所以需要优先级和名字下面便是创建一个线程所需要传递的条件。
- 线程名字
- 线程的优先级
- 线程要执行的函数
- 此函数所需要的参数
咱们已经知道了创建线程的上传需求了:把一个函数变成一个可供CPU直接执行的执行流。
那么函数和执行流之间的差别是什么呢?
- 是上下文!
上下文是什么?
- 上下文就是每个线程所需要寄存器和栈。
中断时用intr_stack 来保护线程的上下文环境 , 初始时放在PCB的最顶端,实现用户进程时,会将用户进程的初始信息放在中断栈中。
以下就是PCB的大体结构,只要记住这个结构,那线程和进程理解起来就轻松很多了。
内核线程的创建
因为要使用线程这个概念了,咱们得先初始化线程环境
thread_init(void);
- 等待队列和全局队列的初始化
- 将main创建为主线程
其中有一点要注意**:main函数已经在运行了,所以只能将其加入到全局队列中去。**
其他线程的创建
接下来咱们继续创建其他的线程。
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg) ;
这函数创建一优先级为prio的线程,线程名为name,线程所执行的函数是function(func_arg)
- 创建和初始化PCB
- get_kernel_pages(1); 申请线程的PCB
- init_thread(thread,name,prio); 初始化线程基本信息
- thread_create(thread, function , func_arg);初始化线程栈,将待执行的函数和参数放到thread_stack中相应的位置
- 将此线程加入到等待队列和全部队列中。
这里要注意的是:
- 所有线程or进程的PCB都是在内核中的,所以得申请内核页
- 线程初始化的时候,由于main是一直运行的,所以要特判一下
线程的调度
线程可以说是函数的加强版,线程也有着自己的线程栈,(和函数有自己的栈类似)下面就是线程栈的定义
thread_stack
- 线程首次运行时,用于存储创建线程所需的相关数据
- 存储switch_to中需要的ebp,ebx,edi,esi
线程需要的数据结构基本上上完了,下面我们看下线程调度的过程
调度过程
- 时钟中断处理函数
- 调度器schedule
- 任务切换函数switch_to
下面逐个介绍上面的每一步:
- 时钟中断处理函数
每次时钟中断就检测当前进程的时间片(ticks)是否用完,用完就调用,否则ticks自减
- intr_timer_handler
- running_thread 获取当前运行的线程的PCB 赋给cur_thread
- 如果ticks == 0 就 schedule 否则ticks –
- 调度器schedule
用来将当前线程换下处理器,并在就绪队列中找出下个可以运行的程序,将其换上处理器。
- 若当前线程是在运行的,就变成TASK_READY,否则继续阻塞
- 将队列上的第一个就绪线程弹出,置为TASK_RUNNING,并switch_to
- 实现任务切换函数switch_to
switch_to(cur,next), 将任务由cur换成next
- 根据ABI保存,esi,edi,ebx,ebp
- 换上新环境
这里的上下文转换十分巧妙:因为PCB第一个元素就是线程栈的地址,所以直接把next赋给sp,再pop就完成了上下文环境的切换。
要结合PCB,线程栈,去看这个切换就会清晰明了
通过上面的切换步骤,我们可以发现线程调度发生了两次上下文保护
-
每次调用中断处理函数(intr_timer_handler)都要保护一次上下文环境(push所有寄存器),这是为了能让任务恢复到中断前
-
switch_to,保存了ebp,ebx,edi,esi , 这是为了让任务恢复执行在任务切换发生时剩下的尚未执行的内核代码,保证任务能顺利走到退出中断的出口。
执行结果
main,argA,argB三个线程执行的情况如下:
GP异常了
哈哈哈咱们学过操作系统,这三个线程的临界区根本就没有设置保护机制,三个线程都随时可以对显示器操作,这种竞争肯定是会引发错误的。
64位下的源代码Makefile脚本修改
https://blog.csdn.net/qq_45923646/article/details/120273571