零、项目说明
- 本项目仓库现已公开,地址:GitHub:-HC-OS-操作系统设计项目
- 本项目当前进度:已完成多线程调度基础功能;
一、前置知识点
1.1 执行流
- 过去,计算机只有1个处理器,在其上运行的系统也是单任务操作系统,即不管有多少个任务,任务的执行都是串行的,一个任务彻底执行完成后才能开始下一个任务。假如有任务A,它的执行要耗时一天,而任务B的执行仅需要2分钟,如果任务A先上处理器上运行,似乎会严重滞后其他的任务执行计划。
- 任务就是一段在处理器上运行的程序
- 【由后续知识点引出】任务其实就是执行流,要么是大的执行流——单线程的进程,要么是小的执行流——线程。
- 在处理器数量不变的情况下,多任务操作系统采用了一种称为多道程序设计的方式,使处理器在所有任务之间来回切换,这样就给用户一种所有任务并行运行的错觉,这称为“伪并行”,毕竟在任意时刻,处理器只会执行某一个任务,处理器会落到哪些任务上执行,这是由操作系统中的任务调度器决定的。如下图所示,处理器固定在圆心,任务就像轮盘一样,由任务调度器把任务转动到处理器的箭头处,这就表示上CPU运行。
- 真正的并行是指:多个处理器同时工作。一台计算机的并行能力取决于其物理处理器的数量。
- 这种伪并行的好处是降低了任务的平均响应时间,通俗点说,就是让那些执行时间短的任务不会因为“后到”而不得不等前面“先来”的且执行时间很长的程序执行完后才能获得执行的机会,整体上“显得”快了很多。当然这和调度算法有关,这里所说的调度算法是较为“公正”的时间片轮转算法,也称为轮询。
- 关于任务调度器【多任务系统的核心】
- 简单而言,任务调度器就是操作系统中用于把任务轮流调度上处理器运行的一个软件模块。
- 调度器在内核中维护一个任务表(也称进程表、线程表或调度表),然后按照一定的算法,从任务表中选择一个任务,然后把该任务放到处理器上运行,当任务运行的时间片到期后,再从任务表中找另外一个任务放到处理器上运行,周而复始,让任务表中的所有任务都有机会运行。
- 正是因为有了调度器,多任务操作系统才能得以实现,它是多任务系统的核心,它的好坏直接影响了系统的效率。
- 多任务操作系统的弊端
- 对于所有任务来说,在不考虑阻塞的情况下,无论是在哪种系统上,它们“自身指令”总共的执行时间之和应该是一致的。但是,在多任务系统中,任务切换是软件(任务调度器)完成的,切换工作本身必然要消耗处理器周期,因此所有任务的总共执行时间反而更长了。
- 对于所有任务来说,在不考虑阻塞的情况下,无论是在哪种系统上,它们“自身指令”总共的执行时间之和应该是一致的。但是,在多任务系统中,任务切换是软件(任务调度器)完成的,切换工作本身必然要消耗处理器周期,因此所有任务的总共执行时间反而更长了。
- 关于执行流
- 在不断执行指令的过程中,我们把程序计数器中的下一条指令地址所组成的执行轨迹称为程序的控制执行流。
- 执行流就是一段逻辑上独立的指令区域,是人为给处理器安排的处理单元。
- 指令是具备“能动性”的数据,因此只有指令才有“执行”的能力,它相当于是动作的发出者,由它指导处理器产生相应的行为。指令是由处理器来执行的,它引领处理器“前进”的方向,用“流”来表示处理器中程序计数器的航向,借此比喻处理器依次把此区域中的指令执行完后,所形成的像河流一样曲直不一的执行轨迹、执行路径(由顺序执行指令及跳转指令导致)。
- 执行流对应于代码,大到可以是整个程序文件,即进程,小到可以是一个功能独立的代码块,即函数(线程本质上就是函数)。
- 执行流是独立的,它的独立性体现在每个执行流都有自己的栈、一套自己的寄存器映像和内存资源,这是Intel处理器在硬件上规定的,其实这正是执行流的上下文环境。
- 任何代码块,无论大小都可以独立成为执行流,只要在它运行的时候,我们提前准备好它所依赖的上下文环境就行,这个上下文环境就是它所使用的寄存器映像、栈、内存等资源。
- 在任务调度器的眼里,只有执行流才是调度单元,即处理器上运行的每个任务都是调度器给分配的执行流,只要成为执行流就能够独立上处理器运行,也就是说处理器会专门运行执行流中的指令。
- 这些独立的执行流就是我们要介绍的线程和进程。
- 操作系统软件中所做的任务切换,本质上就是改变了处理器中程序计数器的指向,即改变了处理器的“执行流”。
- 任务只是人为划分的、逻辑上的概念,人们把一个个的执行单元称为任务,我们所说的执行单元就是这些彼此独立的执行流,因此,独立的执行流成了调度器的调度单元,并使之成为了处理器的基本执行单位。
1.2 线程到底是什么
- 作者观点:线程就是运行一段函数的载体
- 在高级语言中,线程是运行函数的另一种方式,也就是说,构建一套线程方法,让函数在此线程中被调用,然后处理器去执行这个函数,因此线程实际的功能就是相当于调用了这个函数,从而让函数执行。
- 线程与普通的函数调用的区别
- 要回答这个问题,这涉及到调度器维护的线程表或进程表了,这属于调度单元的问题。调度器每次安排一个“执行流”上处理器,执行流肯定是独立的,独立的意思就是它有自己的一套寄存器映像,有自己的栈,也就是有独立的上下文件环境。
- 之前在介绍执行流的时候强调过了,其实任何代码块都可以独立,只要在它运行的时候,我们给它准备好它所依赖的上下文环境就行,这个上下文环境就是它所使用的寄存器映像和栈等资源。只要是独立的执行流就可以被调度器视为一个调度单元,就可以享受处理器的单独服务。我们要做的就是:给任何想单独上处理器的代码块准备好它所依赖的上下文环境,从而使其具备独立性,使之成为执行流,即调度单元。
- 在普通的函数调用中:
- 被调用函数是随着该函数所在的调度单元(执行流)一块上处理器运行的,这个调度单元也许是整个进程,也可能是其他的线程,总之是混在更大的执行流中被“夹杂着、稍带着”执行的,甚至有可能还未执行到此函数它的时间片就到了,从而被换下了处理器,处理器毕竟不是专门去执行此函数,说是被顺便执行一点不夸张,因为调度单元是被调度器安排上处理器的,此函数不是单独的调度单元,调度器“眼里看不到此函数”,当然不能专门运行它了。
- 线程
- 线程是一套机制,此机制可以为一般的代码块创造它所依赖的上下文环境,从而让代码块具有独立性,因此在原理上线程能使一段函数成为调度单元(或称为执行流),使函数能被调度器“认可”,从而能够被专门调度到处理器上执行。这样,函数就可以被加入到线程表中作为调度器的调度单元,从而有机会单独获得处理器资源,也就是说,处理器不是把线程中调用的函数和其他指令混在一块执行的,或者说处理器不是在执行整个进程时顺便执行了该函数,而是单独、专门执行了此函数。
- 总之,在线程中调用函数是让所运行的函数能够以调度单元的身份独立上处理器运行,当函数可以独立运行时,就会有更大的好处,那就是可以让程序中的多个函数(执行流)以并行的方式运行(当然是伪并行),为程序提速。
1.3 进程与线程的关系、区别简述
- 程序是指静态的、存储在文件系统上、尚未运行的指令代码,它是实际运行时程序的映像。
- 进程是指正在运行的程序,即进行中的程序,程序必须在获得运行所需要的各类资源后才能成为进程,资源包括进程所使用的栈,使用的寄存器等。
- 对于处理器来说,进程是一种控制流集合,集合中至少包含一条执行流,执行流之间是相互独立的,但它们共享进程的所有资源,它们是处理器的执行单位,或者称为调度单位,它们就是线程。可以认为,线程是在进程基础之上的二次并发。
- 按照进程中线程数量划分,进程分为单线程进程和多线程进程两种。我们平时所写的程序,如果其中未“显式”创建线程,它就属于单线程进程,这就是我们平时所指的“传统型”的进程,否则就属于多线程进程。
- 线程和进程比,进程拥有整个地址空间,从而拥有全部资源,线程没有自己的地址空间,因此没有任何属于自己的资源,需要借助进程的资源“生存”,所以线程被称为轻量级进程。进程和线程都是执行流,它们都具备独立寄存器资源和独立的栈空间,因此线程也可以像进程那样调用其他函数。
- 线程仅仅是个执行流,并不是什么高深莫测的东西,它只是被一些线程实现机制增加了神秘感。
- 比如像POSIX线程库中的pthread_create函数,它的功能是用来创建线程,传给此函数的第三个实参必须是一个事先定义好的函数,这个作为参数的函数就是我们所说的代码块,也就是前面所解释的“执行流”。可见,线程创建函数pthread_create仅仅是创建执行流(创建线程)的一种方式而已。
- 在显式创建了线程之后,任务调度器就可以把它对应的代码块从进程中分离出来单独调度上处理器执行了,否则调度器会把整个进程当成一个大的执行流,也可以说是把整个进程当成一个线程,从头到尾依次执行下去。
- 利用线程提速的原理
- 利用线程提速,原理之一就是实现多个执行流的伪并行。
- 该提速的原理很简单,就是想办法让处理器多执行自己进程中的代码,这样进程执行完成得就快。
- 假如线程是在内核中实现,此时系统中一共有 2 个任务,进程 A 和进程B,进程 A 为了提速,创建了 3 个线程,任务调度器中便有了 4 个执行流(不包括主线程),其中有 3 个都属于进程A,也就是说这调度器把所有任务调度一圈后,进程 A 相当于被处理器执行了三次,而进程 B 只在处理器上运行了一次,进程 A 当然执行得快了,这就是线程提速的原理之一。
- 线程另一个提速的原理是避免了阻塞整个进程,
- 比如当进程因等待用户输入而暂时无法继续运行时,此时操作系统会把整个进程挂起,也就是将其从就绪队列中去除,这样便无法获得执行的机会,等用户输入完成,可以继续执行后,操作系统再将其加入到就绪队列,这样调度器才会重新调度它上处理器运行。
- 然而,并不是进程中所有的部分都依赖于用户的输入,对于那些不依赖于用户输入的代码块,可以为其单独创建一线程来“并行”执行,这样进程的某个执行流阻塞于用户输入时,此进程的另一线程还能运行,还能继续做其他事,相当于给进程提速了。因此,通常程序员写程序时会把整个任务划分成几个独立的部分,每一部分就用线程来完成,各部分是独立无依赖的,因此这几个线程就可以“并行”运行【独立无依赖是关键】。
- 利用线程提速,原理之一就是实现多个执行流的伪并行。
- 进程与线程概念的出现概念解释【重点】
- 最初进程中只有一条执行流,大家的想法是程序就应该沿着这条路执行下去,谁也不会给“理所当然”的事情起个名字,只是后来为了让程序提速,进程中的执行流变成两条以上了,为了强调进程中包含不同的程序流(执行流),这才出现了线程的概念。其实在处理器上运行的执行流都是“人为划分的”“逻辑上独立的”程序段,本质上都是一段代码区域,只不过线程是纯粹的执行部分,它运行所需要的资源存储在进程这个大房子中,进程中包含此进程中所有线程使用的资源,因此线程依赖于进程,存在于进程之中,用表达式来表示:进程 = 线程 + 资源。
- 举个例子:比如在饭店里,只要有人点菜,厨房就要开始忙活。厨房就相当于进程,里面有食材和烹饪的锅具等,这些都是资源,在厨房中工作的人有厨师、配菜员、餐具清洁员等,他们都是进程中的线程。比如客人点了一盘鱼香肉丝,厨房中各类角色就要开始并行工作,配菜员开始准备食材,厨师负责烹饪,配菜员和厨师这两个线程是各干各的,但他们只能在厨房里工作,他们出了厨房后,什么都干不了,毕竟他们工作时所用的资源,即食材、锅具等都在厨房里,但他们每个人确实都可以分开工作,都是单独的执行流,最终做出鱼香肉丝的是这些具有能动性的人,而不是锅具食材等静态资源。
- 由于各个进程都拥有自己的虚拟地址空间,正常情况下它们彼此无法访问到对方的内部,因为进程之间的安全性是由操作系统的分页机制来保证的,只要操作系统不要把相同的物理页分配给多个进程就行了。
- 【重点再次强调】
- 只有线程才具备能动性,它才是处理器的执行单元,因此它是调度器眼中的调度单位。
- 进程只是个资源整合体,它将进程中所有线程运行时用到资源收集在一起,供进程中的所有线程使用,真正上处理器上运行的其实都叫线程,进程中的线程才是一个个的执行实体、执行流,因此,经调度器送上处理器执行的程序都是线程。
- 即使进程中未显式创建线程,进程中总会有一个向下执行的方向,即执行流,该进程也即单线程进程;如果进程中显式创建了多个线程,此进程称为多线程进程。总之线程属于进程之内,进程内必有线程。也就是说,任何进程都有自己的执行流,如果只有一个执行流,该执行流可以称为主线程,因为其他新的线程也要通过此主线程创建。
- 进程与线程的关系:进程 = 线程 + 资源
- 【总结】
- 线程是什么————具有能动性、执行力、独立的代码块
- 执行流、调度单位、运行实体等概念都是针对线程而言的,线程才是解决问题的思路、步骤,它是具有能动性的指令,因此只有它才能上处理器运行,即一切执行流其实都是线程,因为任何时候进程中都至少存在一个线程。
- 进程是什么————进程 = 线程 + 资源,根据进程内线程的数量,进程可分为:单线程进程、多线程进程
1.4 进程、线程的状态
- 操作系统把进程“执行过程”中所经历的不同阶段按状态归为几类【注意,强调的是“执行过程”,意为进程的状态描述的是进程中有关“动作”的执行流部分,即线程,而不包括静止的资源部分】
- 把需要等待外界条件的状态称为“阻塞态”;
- 把外界条件成立时,进程可以随时准备运行的状态称为“就绪态”;
- 把正在处理器上运行的进程的状态称为“运行态”;
- 进程有哪些状态,取决于操作系统对进程的管理方法,这没有定律,我们也可以创造一套自己的进程状态。
- 以上虽然是以进程举例,但调度器的调度单位是执行流,“状态”描述的也是执行流,而“状态”又主要是给调度器用的,因此“状态”是对所有执行流而言的概念,这里所说的进程状态其实就是指单线程进程中线程的状态,归根结底,状态是描述线程的。
1.5 进程的身份证——PCB【进程的核心】
- 操作系统为每个进程提供了一个PCB【Process Control Block】即程序控制块,它就是进程的身份证,用它来记录与此进程相关的信息,比如进程状态、PID、优先级等。一般PCB的结构如下图所示:PCB没有具体的格式,其实际格式取决于操作系统的功能复杂度,以下只是列出了基本该有的内容
- 每个进程都有自己的PCB,所有PCB放到一张表格中维护,这就是进程表,PCB就成了进程表中的“项”,因此,PCB又可称为进程表项,调度器可以根据这张表选择上处理器运行的进程。
1.6 实现线程的两种方式———内核或用户进程
- 线程的实现就有两种方式:要么由操作系统原生支持,用户进程通过系统调用使用线程;要么操作系统不支持线程,由进程自己想办法解决。因此,线程要么在 0 特权级的内核空间中实现,要么在 3 特权级的用户空间实现。
- 强调一下,这里所说的“在 0 特权级的内核空间中实现线程”,只是说线程机制由内核来提供,并不是说线程中所运行的代码也必须是 0 特权级的内核级代码,也可以是 3 特权级的用户级代码,内核毕竟是为用户进程提供服务的。
- 而“在 3 特权级的用户空间实现线程”,是指线程机制由用户进程自己提供,相当于用户进程除了负责业务外,还要在进程中实现线程调度器,这样一来程序员负担比较重,所以通常情况下很少有程序员愿意在进程中写线程机制,故标准库便提供了用户级线程库,程序员直接使用标准线程库就行了。
- 线程仅仅是个执行流,在用户空间,还是在内核空间实现它,最大的区别就是线程表在哪里,由谁来调度它上处理器。
- 如果线程在用户空间中实现,线程表就在用户进程中,用户进程就要专门写个线程用作线程调度器,由它来调度进程内部的其他线程;
- 如果线程在内核空间中实现,线程表就在内核中,该线程就会由操作系统的调度器统一调度,无论该线程属于内核,还是用户进程。
- 【在用户空间中实现线程(线程机制由用户进程提供)】
- 注意,咱们这里讨论的是线程只由用户进程来实现,操作系统中无线程机制。
- 在用户空间中实现线程的好处是可移植性强,由于是用户级的实现,所以在不支持线程的操作系统上也可以写出完美支持线程的用户程序。
- 在用户空间中实现线程,操作系统不会意识到线程的存在,因为操作系统调度器只会以整个进程的方式调度,将处理器的使用权交给这个进程,由进程中的调度器自己去协调分配处理器时间。
- 无论线程在哪里实现,目的都是要到处理器上运行,因此必然要考虑到线程调度的问题,这涉及到调度器及线程表。
- 如果在用户空间中实现线程,用户线程就要肩负起调度器的责任,因此除了要实现进程内的线程调度器外,还要自己在进程内维护线程表。
- 用户进程中,很少有人亲自写线程调度器,因此,一般是某个权威机构发布个用户级线程包,也就是线程库,开发人员在用户进程中调用此包中的方法去创建线程、结束线程等。线程包中一定存在着线程调度器,而且,线程包中的方法都会与此线程调度器有调用关系,这样当有新线程产生或有线程退出时,线程调度器才会被调用,从而在内部维护的线程表中找出下一个线程上处理器运行。
- 在用户进程中实现线程有以下优点:
- 线程的调度算法是由用户程序自己实现的,可以根据实现应用情况为某些线程加权调度;
- 将线程的寄存器映像装载到CPU时,可以在用户空间完成,即不用陷入到内核态,这样就免去了进入内核时的入栈及出栈操作。
- 在用户进程中实现线程有以下优缺点:
- 进程中的某个线程若出现了阻塞(通常是由于系统调用造成的),操作系统不知道进程中存在线程,它以为此进程是传统型进程(单线程进程),因此会将整个进程挂起,即进程中的全部线程都会无法运行。
- 最后,线程在用户空间实现,和在内核空间实现相比,只是在内部调度时少了陷入内核的代价,确实相当于提速,但由于整个进程占据处理器的时间片是有限的,这有限的时间片还要再分给内部的线程,所以每个线程执行的时间片非常非常短暂,再加上进程内线程调度器维护线程表、运行调度算法的时间片消耗,反而抵销了内部调度带来的提速。
- 【在内核空间中实现线程(线程机制由内核提供)】
- 注意,这里所说的“实现线程”是指由内核提供原生线程机制,用户进程中不再单独实现。
- 在内核空间中实现线程有以下优点:
- 相比在用户空间中实现线程,内核提供的线程相当于让进程多占了处理器资源。
- 当进程中的某一线程阻塞后,由于线程是由内核空间实现的,操作系统认识线程,所以就只会阻塞这一个线程,此线程所在进程内的其他线程将不受影响,这又相当于提速了。
- 在内核空间中实现线程有以下缺点:
- 用户进程需要通过系统调用陷入内核,这多少增加了一些现场保护的栈操作,这还是会消耗一些处理器时间。
二、在内核空间实现线程
2.1 简单的 PCB 及线程栈的实现
2.1.1 代码介绍
- 本节实现PCB的代码定义及中断栈、线程栈的定义
- 【附加知识点】
- void 是一个关键字,表示“无类型”或“空类型”。
- (void *)是一种特殊的指针类型,可以指向任何类型的数据。
2.1.2 代码详情
thread.h
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"/*will be used as parameter in create-thread function*/
typedef void thread_func(void*);/*the status of process or thread.*/
enum task_status {TASK_RUNNING,TASK_READY,TASK_BLOCKED,TASK_WAITING,TASK_HANGING,TASK_DIED
};/*This structure is used to store environment of process or thread when interrupt occured.*/
struct intr_stack {uint32_t vec_no; //vector numberuint32_t edi;uint32_t esi;uint32_t ebp;uint32_t esp_dummy; //popad will igonre esp.uint32_t ebx;uint32_t edx;uint32_t ecx;uint32_t eax;uint32_t gs;uint32_t fs;uint32_t es;uint32_t ds;/*Sitiuation: when interrupt occured, the privilige level has changed*/uint32_t err_code;void (*eip) (void);uint32_t cs;uint32_t eflags;void* esp;uint32_t ss;
};/*thread stack*/
struct thread_stack {uint32_t ebp;uint32_t ebx;uint32_t edi;uint32_t esi;/*When thread first run,eip point to kernel_thread(), other time, eip point to the return address of suitch_to().*/void (*eip)(thread_func* func, void* func_arg); //seem to declare an function?void (*unused_retaddr); //placeholdingthread_func* function;void* func_arg;
};/*process or thread's PCB(program control block)*/
struct task_struct {uint32_t* self_kstack;enum task_status status;uint8_t priority;char name[16];uint32_t stack_magic; //the boundary of stack.
};struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg);#endif
2.2 单线程的创建实现
2.2.1 代码介绍
- 在构建好PCB结构体基础上,我们通过分配页空间创建PCB,接着初始化PCB信息,随后初始化线程栈信息,最后利用汇编语言(ret指令)重定位eip寄存器,使其执行目标函数,完成内核空间中线程的创建及运行。
2.2.2 代码详情
thread.c
#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"#define PG_SIZE 4096static void kernel_thread(thread_func* function, void* func_arg) {function(func_arg);
}/*initlalize thread_stack*/
void thread_create(struct task_struct* pthread,thread_func function, void* func_arg) { //task_struct: struct of PCB/*reserved space of intr_stack*/pthread->self_kstack -= sizeof(struct intr_stack);/*reserved space of thread_stack*/pthread->self_kstack -= sizeof(struct thread_stack);struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack; //at this time, self_kstack(also is kthread_stack) is pointed to the structure of thread_stack.kthread_stack->eip = kernel_thread;kthread_stack->function = function;kthread_stack->func_arg = func_arg;kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;
}/*initialize the base information of thread.
you can say initialize 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;pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);pthread->stack_magic = 0x12345678;
}/*create an thread which priority is prio,name is name.*/
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg) {struct task_struct* thread = get_kernel_pages(1); //use one page to create an PCBinit_thread(thread, name, prio); //initialize PCB by base informationthread_create(thread, function, func_arg); //initialize thread_stack in PCBasm volatile ("movl %0, %%esp; pop %%ebp; pop %%ebx; pop %%edi; pop %%esi; ret": : "g"(thread->self_kstack) : "memory"); //self_kstack is pointed to the TOP of PCB(also is botton of intr_stack.)return thread;
}
2.3 双向链表及其操作函数定义
2.3.1 代码介绍
- 在内核中要用到队列,比如进程的就绪队列、锁的等待队列等,为了维护内核中的各种队列,本节要实现自己的链表——双向链表。
2.3.2 代码详情
list.h
#ifndef __LIB_KERNEL_LIST_H
#define __LIB_KERNEL_LIST_H
#include "global.h"
#include "stdbool.h"#define NULL ((void*)0)#define offset(struct_type, member) (int)(&((struct_type*)0)->member)
#define elem2entry(struct_type, struct_member_name, elem_ptr) (struct_type*)((int)elem_ptr - offset(struct_type, struct_member_name))/*define the structure of list element*/
struct list_elem {struct list_elem* prev; //preview nodestruct list_elem* next; //next node
};/*define the structure of list, it will be used to achieve queue.*/
struct list {struct list_elem head;struct list_elem tail;
};/*define callback function*/
typedef bool (function)(struct list_elem*, int arg);void list_init(struct list* list);
void list_insert_before(struct list_elem* before, struct list_elem* elem);
void list_push(struct list* plist, struct list_elem* elem);
void list_append(struct list* plist, struct list_elem* elem);
void list_remove(struct list_elem* pelem);
struct list_elem* list_pop(struct list* plist);
bool elem_find(struct list* plist, struct list_elem* obj_elem);
bool list_empty(struct list* plist);
uint32_t list_len(struct list* plist);
struct list_elem* list_traversal(struct list* plist, function func, int arg);#endif
list.c
#include "list.h"
#include "interrupt.h"
#include "stdbool.h"
#include "stdint.h"void list_init(struct list* list) { //pointer use "->" to visit member, variable use "." to visit member.list->head.prev = NULL;list->head.next = &list->tail;list->tail.prev = &list->head;list->tail.next = NULL;
}void list_insert_before(struct list_elem* before, struct list_elem* elem) {enum intr_status old_status = intr_disable(); //make sure an atomic operation.elem->prev = before->prev;elem->next = before;elem->prev->next = elem;before->prev = elem;intr_set_status(old_status);
}void list_push(struct list* plist, struct list_elem* elem) {list_insert_before(plist->head.next, elem);
}void list_append(struct list* plist, struct list_elem* elem) {list_insert_before(&plist->tail, elem);
}void list_remove(struct list_elem* pelem) {enum intr_status old_status = intr_disable(); //make sure an atomic operation.pelem->prev->next = pelem->next;pelem->next->prev = pelem->prev;intr_set_status(old_status);
}struct list_elem* list_pop(struct list* plist) {struct list_elem* elem = plist->head.next;list_remove(elem);return elem;
}bool elem_find(struct list* plist, struct list_elem* obj_elem) {struct list_elem* elem = plist->head.next;while(elem != &plist->tail) {if(elem == obj_elem) {return true;}elem = elem->next;}return false;
}bool list_empty(struct list* plist) {return (plist->head.next == &plist->tail ? true : false);
}uint32_t list_len(struct list* plist) {struct list_elem* elem = plist->head.next;uint32_t length = 0;while(elem != &plist->tail) {length++;elem = elem->next;}return length;
}/*find an element which is eligible*/
struct list_elem* list_traversal(struct list* plist, function func, int arg) {struct list_elem* elem = plist->head.next;if(list_empty(plist)) {return NULL;}while(elem != &plist->tail) {if(func(elem, arg)) {return elem;}elem = elem->next;}return NULL;
}
2.4 多线程调度实现
- 目标:完成线程的轮询调度
2.4.1 代码说明
- 关于 general_tag 和 all_list_tag 的说明。
- 线程在内存中的位置是散落的,由不同的链表将它们各自的 general_tag 和 all_list_tag 串联起来,从而形成队列。线程在队列中的组织结构如下图所示。
- 线程在内存中的位置是散落的,由不同的链表将它们各自的 general_tag 和 all_list_tag 串联起来,从而形成队列。线程在队列中的组织结构如下图所示。
2.4.2 代码详情
timer.c:时钟中断处理函数
static void intr_timer_handler(void) {struct task_struct* cur_thread = running_thread();ASSERT(cur_thread->stack_magic == 0x12345678);cur_thread->elapsed_ticks++;ticks++; //record all ticks from first clock interrupt to now.if(cur_thread->ticks == 0) {schedule();}else {cur_thread->ticks--;}
}
thread.c: 任务调度器schedule
void schedule() {//done current threadASSERT(intr_get_status() == INTR_OFF);struct task_struct* cur = running_thread();if(cur->status == TASK_RUNNING) {ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));list_append(&thread_ready_list, &cur->general_tag);cur->ticks = cur->priority;cur->status = TASK_READY;}else {//temporary empty}//change new thread which is the first element in thread_ready_list.ASSERT(!list_empty(&thread_ready_list));thread_tag = NULL;thread_tag = list_pop(&thread_ready_list);struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag); //general_tag is the name of member.next->status = TASK_RUNNING;switch_to(cur, next);
}
switch.S: 任务切换函数
[bits 32]
section .text
global switch_to
switch_to:;now, esp register point to the switch_to()'s return addresspush esipush edipush ebxpush ebpmov eax, [esp + 20] ;get the pointer of curmov [eax], esp ;now, [eax] is the structure of task_struct's first element,yes, it is self_kstackmov eax, [esp + 24] ;get the pointer of nextmov esp, [eax] ;point to the botton of thread_stack.pop ebppop ebxpop edipop esiret ;to execute objective function.
thread.h
#ifndef __THREAD_THREAD_H
#define __THREAD_THREAD_H
#include "stdint.h"
#include "list.h"/*will be used as parameter in create-thread function*/
typedef void thread_func(void*);/*the status of process or thread.*/
enum task_status {TASK_RUNNING,TASK_READY,TASK_BLOCKED,TASK_WAITING,TASK_HANGING,TASK_DIED
};/*This structure is used to store environment of process or thread when interrupt occured.*/
struct intr_stack { //just define this structure, operating system can use this space arbitrarily.uint32_t vec_no; //vector numberuint32_t edi;uint32_t esi;uint32_t ebp;uint32_t esp_dummy; //popad will igonre esp.uint32_t ebx;uint32_t edx;uint32_t ecx;uint32_t eax;uint32_t gs;uint32_t fs;uint32_t es;uint32_t ds;/*Sitiuation: when interrupt occured, the privilige level has changed*/uint32_t err_code;void (*eip) (void);uint32_t cs;uint32_t eflags;void* esp;uint32_t ss;
};/*thread stack*/
struct thread_stack {uint32_t ebp;uint32_t ebx;uint32_t edi;uint32_t esi;/*When thread first run,eip point to kernel_thread(), other time, eip point to the return address of suitch_to().*/void (*eip)(thread_func* func, void* func_arg); //"eip" is just a parameter name.void (*unused_retaddr); //placeholdingthread_func* function;void* func_arg;
};/*process or thread's PCB(program control block)*/
struct task_struct {uint32_t* self_kstack;enum task_status status;char name[16];uint8_t priority;uint8_t ticks; //the piece of time (task running in CPU), one clock interrupt occured, ticks--.uint32_t elapsed_ticks; //elapsed: have used.struct list_elem general_tag;struct list_elem all_list_tag;uint32_t* pgdir; //task is process, pgdir is the address of page, task is thread, pgdir is NULL.uint32_t stack_magic; //the boundary of stack,clock interrupt function will judge this value.
};void thread_create(struct task_struct* pthread,thread_func function, void* func_arg);
void init_thread(struct task_struct* pthread, char* name, int prio);
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg);
struct task_struct* running_thread(void);
void thread_init(void);
void schedule(void);#endif
thread.c
#include "thread.h"
#include "stdint.h"
#include "string.h"
#include "global.h"
#include "memory.h"
#include "list.h"
#include "interrupt.h"
#include "print.h"
#include "debug.h"#define PG_SIZE 4096struct task_struct* main_thread; //main thread's PCB
struct list thread_ready_list; //the list of ready, ready queue.
struct list thread_all_list; //the list of all task,it is a list include all thread we have created.
/*we use PCB to manage thread, so we need to translate thread's tag to thread's PCB.*/
static struct list_elem* thread_tag; //Be used to store thread's tag when we want to translate. extern void switch_to(struct task_struct* cur, struct task_struct* next);/*get the PCB pointer of current thread*/
struct task_struct* running_thread() {uint32_t esp;asm ("mov %%esp, %0" : "=g"(esp));/*get the start address of PCB*/return (struct task_struct*)(esp & 0xfffff000);
}static void kernel_thread(thread_func* function, void* func_arg) {/*after enter interrupt, CPU will close interrupt automatically,but our task's schedule is based on clock interrupt,so we need enable interrupt.*/intr_enable(); //set IF bit to 1,CPU begin to receive clock interrupt.function(func_arg);
}/*initlalize thread_stack*/
void thread_create(struct task_struct* pthread,thread_func function, void* func_arg) { //task_struct: struct of PCB/*reserved space of intr_stack*/pthread->self_kstack -= sizeof(struct intr_stack);/*reserved space of thread_stack*/pthread->self_kstack -= sizeof(struct thread_stack);struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack; //at this time, self_kstack(also is kthread_stack) is pointed to the structure of thread_stack.kthread_stack->eip = kernel_thread;kthread_stack->function = function;kthread_stack->func_arg = func_arg;kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;
}/*initialize the base information of thread.
you can say initialize PCB*/
void init_thread(struct task_struct* pthread, char* name, int prio) {memset(pthread, 0, sizeof(*pthread)); //the value of sizeof(*pthread) is just a structure's size of task_struct instead of a page's size, so don't warry about return address.pthread->self_kstack = (uint32_t*)((uint32_t)pthread + PG_SIZE);if(pthread == main_thread) {pthread->status = TASK_RUNNING;}else {pthread->status = TASK_READY;}strcpy(pthread->name, name);pthread->priority = prio;pthread->ticks = prio;pthread->elapsed_ticks = 0;pthread->pgdir = NULL;pthread->stack_magic = 0x12345678;
}/*The entry of creating thread: create an thread which priority is prio,name is name.*/
struct task_struct* thread_start(char* name, int prio, thread_func function, void* func_arg) {struct task_struct* thread = get_kernel_pages(1); //use one page to create an PCBinit_thread(thread, name, prio); //initialize PCB by base informationthread_create(thread, function, func_arg); //initialize thread_stack in PCB/*Pay attention: we use general_tag to represent PCB in thread_ready_list*/ASSERT(!elem_find(&thread_ready_list, &thread->general_tag));list_append(&thread_ready_list, &thread->general_tag);ASSERT(elem_find(&thread_ready_list, &thread->general_tag)); //locate the mistakeASSERT(!elem_find(&thread_all_list, &thread->all_list_tag));list_append(&thread_all_list, &thread->all_list_tag);return thread;
}/*give main()(is main thread^^) an identity card(yes, is PCB)*/
static void make_main_thread(void) {/*when we enter kernel, we make esp to 0xc009f000,it point to the botton of stack in PCB which is used by main_thread, actually, we have already reserved main_thread's PCB in 0xc009e000, so, we don't need to use get_kernel_page() function to create PCB.*/main_thread = running_thread();init_thread(main_thread, "main", 31);/*this thread is already running, so we don't need to put it to thread_ready_list.*/ASSERT(!elem_find(&thread_all_list, &main_thread->all_list_tag));list_append(&thread_all_list, &main_thread->all_list_tag);
}void schedule() {//done current threadASSERT(intr_get_status() == INTR_OFF);struct task_struct* cur = running_thread();if(cur->status == TASK_RUNNING) {ASSERT(!elem_find(&thread_ready_list, &cur->general_tag));list_append(&thread_ready_list, &cur->general_tag);cur->ticks = cur->priority;cur->status = TASK_READY;}else {//temporary empty}//change new thread which is the first element in thread_ready_list.ASSERT(!list_empty(&thread_ready_list));thread_tag = NULL;thread_tag = list_pop(&thread_ready_list);struct task_struct* next = elem2entry(struct task_struct, general_tag, thread_tag); //general_tag is the name of member.next->status = TASK_RUNNING;switch_to(cur, next);
}/*initial the environment of thread*/
void thread_init(void) {put_str("thread_init start\n");list_init(&thread_ready_list);list_init(&thread_all_list);/*create current main() function to main thread.*/make_main_thread();put_str("thread_init done\n");
}