提要

上一篇文章中讲述了线程机制原理,这篇则是根据线程机制的原理简单实现线程,并在初始化线程后实现简单的线程调度。

非常简单的构建线程,PCB的结构很简单,线程栈也很小。线程调度的实现相对于大型操作系统来说也很简单,是在现有的条件下实现的简单线程调度。这里没有提到进程结构体,但用到了task_struct的命名方式,是因为后续实现用户进程也是通过线程实现的,到时候只是在现有的结构上增加结构体成员变量,进程和线程的区别只是有无资源。做这些的目的是为了深入理解线程和线程调度的原理。

线程初始化

task_struct结构体中的成员:

线程和线程调度的简单实现

该结构体中声明了一些PCB的一些信息,用来记录线程的信息。

初始化task_struct结构体中的成员:

线程和线程调度的简单实现

此时PCB的情况如下图所示:

线程和线程调度的简单实现

初始化线程栈

线程栈结构体:

线程和线程调度的简单实现

线程栈是构建线程非常重要的结构体。线程第一次运行时eip指向kernel_thread。在第一次之后,该eip记录发生任务切换时指令地址,保证任务再次运行的时候能够继续运行。ebp,ebx,edi,esi是ABI要求的需要保存的寄存器(ABI介绍可查阅LINUX系统编程)。

仅第一次上CPU使用的内容是线程运行函数kernel_thead的参数,function是要封装函数的地址,func_arg是function函数运行时的参数地址。组合起来就是funciton(func_arg),该函数就是我们真正想要运行的函数。

汇编中调用另一个程序段,通过call指令,通过ret指令返回。因为使用C语言实现的内核,函数的参数由主调函数确定。这里要说一下call指令必须要通过ret指令返回,但是ret指令并不需要绑定call使用。ret指令的作用就是读取栈中的返回地址加载到eip,这样程序计数器的方向就发生了改变。在后面kernel_thread(function, func_arg)中kernel_thread是直接通过switch_to函数返回的(直接执行ret指令,不是通过call指令调用),kernel_thread函数主要是要执行funciton(func_arg)函数,结构体中eip这个函数指针让switch_to函数知道要去执行kernel_thread函数,该函数里要去调用另外一个函数function,结构体中为kernel_therad的栈空间构造好它需要的参数。因为要符合C语言函数调用的标准这里需要一个unused_ret作占位用。可以理解为在平时是程序员为函数确定函数调用的参数,现在需要内核为函数确定函数调用的参数。unused_ret是为了符合C语言函数调用的规则,function和func_arg是函数调用的参数。

初始化线程栈:

线程和线程调度的简单实现

函数说明:

初始化线程栈。任务调度是由中断驱动的,现在PCB的顶端一定是中断处理程序要保存的上下文,所以先给中断栈留出位置。然后留出线程栈的空间,因为栈内存中是高地址到低地址扩展的,结构体定义是从低地址到高地址定义。然后进行初始化,让self_kstack指向现在的栈顶。eip指向要kernel_thread,这是线程要封装的函数的入口。function和func_arg分别指向kernel_thread要调用的函数和函数的参数。然后初始化结构体中定义的寄存器的值,线程第一次运行时这些寄存器的值应该为0。此时的PCB如下图所示。intr_stack是中断栈,这里就不展开了,里面保存了所有通用寄存器。图中的顶端是为线程PCB申请的一页内存的顶端。

线程和线程调度的简单实现

创建线程

线程和线程调度的简单实现

函数说明:

PCB需要在内存存储,这样才能根据PCB进行任务调度,所以先申请一页的内存用于保存线程的PCB,这里的PCB比较小申请一页就行了。然后调用上面介绍的函数,进行初始化,经过初始化后,线程已经构建完成了。现在将PCB保存在就绪队列中和全部任务队列中,根据任务队列找到要调度的任务。就绪队列保存可以上CPU运行的线程,全部任务队列保存内核中所有的线程。只有就绪队列中的线程才可以直接上CPU运行。

时钟中断处理程序

线程和线程调度的简单实现

函数说明:

这里任务调度是通过时钟中断实现的,所以要注册相应的时钟中断处理程序。时钟中断处理程序先获取当前正在运行的现场,确定线程的PCB是完整的没有被破坏。每次时钟中断是一个滴答,当前线程的elapsed_ticks用来记录总共占用CPU的时间。线程ticks记录一次上CPU运行的时间,这个时间不为0说明当前线程还可以继续使用CPU,只需要-1即可,当这个时间为0时,需要被换下处理器了,此时就调用schedule()函数。

调度函数schedule

线程和线程调度的简单实现

函数说明:

该调度函数主要是将当前正在运行线程的状态变为就绪态,然后加入到就绪队列中,然后从就绪队列中出队一个线程,进行线程调度。当前线程为cur,马上要上CPU运行的线程状态变为运行态,记录在next中,然后调用switch_to(cur, next)完成调度。如果当前线程因为某些原因不是运行态(比如线程阻塞),则不加入就绪队列参与调度。

线程替换switch_to

switch_to是一个汇编程序,使用汇编是因为只是为了实现转换没有其他复杂的内容,就不使用内嵌汇编了。switch_to是通过schedule()调用的,参数为cur,next。cur为当前要被换下CPU的线程,next为要换上CPU运行的线程。此时栈中的情况如下图。只关心switch_to有关的内容,然后给出switch_to函数。

线程和线程调度的简单实现

线程和线程调度的简单实现

函数说明:

esp+20的位置存储的是线程cur,esp+24的位置存储的是线程next。现在将cur取出存入eax临时保存,eax现在的值为当前线程PCB的起始地址,PCB的起始地址为self_stack,将当前esp的值存入当前线程的self_stack保存起来。

然后从栈esp+24中取出线程next存入eax,现在eax的值为线程PCB的起始地址,PCB的起始地址保存着线程next的栈顶self_stack,将该栈顶存入esp,此时线程栈交换完成。

然后进行返回,如果线程正在运行的线程是第一次运行,将上面初始化的线程栈再次放到这以方便看。

线程和线程调度的简单实现

这是交换栈后新线程(next)的栈,经过pop ebp,ebx,edi,esi后,esp指向了eip,然后switch_to执行最后一条指令ret,读取eip的值,此时eip的值为函数kernel_thread的地址,之前说过ret指令并不需要绑定call指令使用,ret指令将esp指向的内存保存进eip使程序计数器cs:eip的方向发生改变。现在eip的值为kernel_thread的地址,就是说现在要去执行kernel_thread。将上面的kernel_thread角度的栈放到下面方便看:

线程和线程调度的简单实现

开始执行kernel_thread函数。这样next线程中我们要执行的function(func_arg)就得以执行了,实现多个线程运行。

kernel_thread函数:

线程和线程调度的简单实现

如果当前线程不是第一次执行,那么上面ret之前栈中的eip就是调用switch_to的返回地址,这里是schedule()调用的switch_to,所以eip指向的返回地址在schedule中。

每当发生时钟中断时,就会进行上述的情况,往复进行实现了多线程调度。

参考书籍:

《操作系统真相还原》-- 郑钢

《LINUX系统编程》-- ROBERT LOVE

文章转自:小组18级成员--胡庆伟

原文地址:https://blog.csdn.net/qq_43769572/article/details/105576485

相关文章:

  • 2021-07-02
  • 2022-12-23
  • 2021-08-07
  • 2021-08-20
  • 2021-09-15
  • 2022-02-26
  • 2022-12-23
  • 2022-12-23
猜你喜欢
  • 2021-04-18
  • 2021-09-28
  • 2022-01-18
  • 2021-08-29
  • 2021-10-07
相关资源
相似解决方案