操作系统课程设计指导书2015(2)

2019-04-16 22:41

2.2 线程描述

2.2.1线程基本概念

在一些多任务的环境下,用户可以同时运行多个完整的程序。例如,在UNIX环境下,你可以用CC命令编译一个C程序,并把它作为一个后台进程运行(只需在命令行后加上字符‘&’);在前台,你又可以做其他的事情,比如,编辑另一个文件。我们把这种系统称为基于进程的多任务系统。另外有一种多任务系统,在其下,一个程序的多个部分可同时运行,我们把这种环境下的任务,即程序的每个部分叫做线程,称这种系统为基于线程的多任务系统。在这种环境下,处理机的调度单位为线程,它们共享整个进程的资源,还拥有一些自己的私有资源。我们将通过本课程设计实现多个线程的并发执行。

线程,有时也叫做轻进程(lightweight process),是CPU调度的基本单位,每个线程有自己的一个指令计数器、一组寄存器和一个私有堆栈。而代码段、数据段以及操作系统的其它资源(如打开的文件)是由一组线程共享的,这一组线程组成一个Task(传统的进程,即heavyweight process相当于只有一个线程的Task)。

在许多方面,对线程的操作类似于进程:线程可处于就绪、阻塞、执行三种状态之一;线程可共享CPU, 在单机系统中,任何时刻最多只能有一个线程处于执行状态;一个Task中的多个线程可并发执行。但与进程不同,一个Task中的多个线程并不互相独立,因为,所有线程均可访问所属Task的地址空间的任一单元,所以,一个线程读写其它线程的私有堆栈是十分容易的,即系统不提供线程间的保护。

注:上面讲的Task是指一个完整的作业,其中可包括多个线程,与本课程设计中所讲的多任务中的任务(系统中可并发执行的部分,如线程或进程)含义不同,除此之外,本课程设计中所提到的Task或任务均代表后者。 线程的切换只需切换寄存器组的值,而不需做有关内存管理方面的工作,实现起来也就比较简单。

2.2.2 线程控制块

与进程类似,基于线程的多任务系统中的任务,即线程,它不单是指静态的、可并发执行的程序段本身,其实也是一个动态的概念,是指可并发执行的程序段及其执行过程。因此,我们要用一个类似于进程控制块PCB的数据结构——线程控制块TCB,来记录有关描述线程情况和控制线程运行所需的全部信息,具体来说,在一个TCB中主要应包括以下几方面的信息:

1.有关线程私有堆栈的信息

在线程调度的过程中,为了保护线程的现场信息,每个线程都必须有自己的私有堆栈。我们把被切换线程的现场信息,包括目前各寄存器的值和下一条指令的地址都保存在它的堆栈中,再从新线程的私有堆栈中恢复出一组新值来布置系统的寄存器,并从私有堆栈中得到新线程的下一条指令地址。另外,每个线程中用到的局部变量也是存放在它自己的私有堆栈中的。因此,在TCB 中必须有线程的私有堆栈的信息,包括它在内存的起始地址、 堆栈的栈顶指针的段地址和偏移等信息。

DOS中内存的地址是20位的,而且DOS的内存管理采用分段的方式,每个段的基址的低4位必须为0,指令和数据的逻辑地址可用两个16位的整数来描述,即:段地址seg和段内偏移off,其中段地址seg中有段基址的高16位,故逻辑地址seg:off对应的物理地址

- 6 -

为seg×24+off。C语言经常用指针来描述一个地址,Turbo C提供了三个宏函数用来实现指针方式到段地址、偏移地址方式的相互转换:若P为一个指针,则可通过FP_SEG(p)得到该地址的段地址,FP_OFF(p)得到该地址的段内偏移;若seg为一个地址的段地址,off为其段内偏移,则可通过MK_FP(seg,off)得到对应的指针。

2.有关线程的状态的信息

在基于线程的多任务系统中,一个线程的状态在它的生命周期中是在不断地变化的,在此,我们把线程的主要状态划分为:就绪、执行、阻塞和终止态。如果,一个线程拥有CPU,我们就说它处于执行态;如果它现在虽不在执行,但一旦获得CPU 就可执行,我们就说它处于就绪态;如果它在等待CPU以外的其他资源,则说它处于阻塞状态;如果线程所对应的程序段已运行完毕,则它处于终止状态。因此,在TCB中要设置一状态字段,用来记录各线程的现行状态。

3.线程的标识符

线程标识符用于惟一地标识一个线程,与进程一样,通常一个线程有两个标识符: (1)外部标识符:它由创建者提供,通常是一个由字母、数字组成的字符串,记录在线程的TCB中。

(2)内部标识符,它通常是一个整数,由多任务系统在创建线程时设置。在本课程设计中,我们在多任务系统的初始化过程中,设置了一个struct类型的TCB数组来统一为各新建线程提供空白TCB,为了简单起见,我们可以隐含地用各线程所分配到的TCB在整个TCB数组中的下标来表示该线程的内部标识符,所以不需要再专门记录在TCB中了。

4.其它信息

TCB中记录的信息量可随系统的复杂情况而变化,如当采用优先权算法进行调度时,在TCB中还必须设置优先权字段;当TCB要按某种方式排队时,在其中必须设置一链接指针字段;当必须唤醒因某种原因而阻塞的相关线程时,则必须设置阻塞原因字段;在使用消息缓冲队列机制实现线程通信时,则必须设置通信机制需要的字段,如接收线程的消息队列队首指针、消息队列的互斥信号量和资源信号量等。

用C语言来描述,一个最简单的TCB的数据结构可以表示如下: /* 状态码常量定义 */ /* null 0 not assigned */

#define FINISHED 0 /*表示线程处于终止态或TCB是空白状态*/ #define RUNNING 1 /*表示线程处于运行态*/ #define READY 2 /*表示线程处于就绪态*/ #define BLOCKED 3 /*表示线程处于阻塞态*/

struct TCB{

unsigned char *stack; /* 线程堆栈的起始地址 */ unsigned ss; /* 堆栈段址 */ unsigned sp; /* 堆栈指针 */

char state; /* 线程状态 ,取值可以是FINISHED、RUNNING、READY、BLOCKED*/ char name[10]; /* 线程的外部标识符 */

} tcb[NTCB]; /*NTCB是系统允许的最多任务数*/

- 7 -

2.3 线程的创建和撤消

2.3.1 线程的创建

在创建一个新线程时,线程的创建者必需提供一些信息,如线程的外部标识符、线程所需的私有堆栈空间的大小、与线程所对应的程序段的入口地址的有关信息(这里假设一个线程执行程序里的一个函数,所以创建者只需提供线程要执行的函数的函数名即可)。

1.线程创建函数格式说明

(1)函数申明原型: typedef int (far *codeptr)(void); /*定义了一个函数指针类型*/

Int create(char *name,codeptr code,int stck) ;

(2)函数功能描述:在main()函数中调用,创建一个新线程,让其执行code开始的代码。

(3)输入:

name:新创建线程的外部标识符;

code:新创建线程要执行的代码的入口地址,此处用函数名作为传入地址; stck: 新创建线程的私有堆栈的长度。

(4)输出:新创建线程的内部标识符,若创建失败,返回-1

2. 函数实现的算法描述

在创建一个线程时主要应完成以下工作:

(1) 为新线程分配一个空闲的线程控制块TCB,该TCB 的数组下标即为新线程的内部标识符。如果没有空闲的TCB,则返回-1,创建失败。

(2) 为新线程的私有堆栈分配内存空间(因为同一进程的多个线程共享该进程的程序段和数据段空间,所以创建线程时不必象创建进程那样再为程序段和数据段分配内存空间)。

(3) 初始化新线程的私有堆栈,即按CPU 调度时现场信息的保存格式布置堆栈,这一点是非常重要的,因为当CPU首次调度该线程运行时,CPU中的SS寄存器和SP寄存器将指向该线程的私有堆栈,并从该堆栈中获得线程运行的正确的指令地址和其它现场信息。新线程的首次执行是从对应函数的入口开始的;而且,执行时CPU的寄存器ES、DS应置上恰当的值;Flags 寄存器的允许中断位也应置上1,这样,线程执行过程中才允许硬中断(如时钟中断)发生并及时响应中断;其它寄存器(AX、BX、CX、DX、SI、DI、BP)的值只在线程执行过程中才有意义,它们的初值可为任意值。初始化工作完成后堆栈中各信息项的值及其相应位置如图2-1b所示。

为了方便堆栈的初始化工作,我们可以按照堆栈中的内容设计一个以下的数据结构: struct int_regs {

unsigned bp,di,si,ds,es,dx,cx,bx,ax,ip,cs,flags,off,seg; };

然后用一个指向该数据结构的指针给堆栈赋值。

(4) 初始化线程控制块,即填入线程的外部标识符,设置好线程私有堆栈的始址、段址和栈顶指针,将线程的状态置成就绪态READY,如图2-1a所示。

另外,如果线程调度算法是按优先权方式进行CPU调度,则需在TCB中置上新线程的优先权信息(初始优先数可由用户提供);若TCB的组织方式是按某种方式拉链,系统设置

- 8 -

了线程就绪队列,则还需将新线程的TCB插入就绪队列;如果要实现通信,还需要将线程的消息队列队首指针设置为Null、消息队列的互斥信号量和资源信号量分别设置为{1,Null}和{0,Null}

(5) 最后,返回新线程的内部标识符。

在Turbo C的small编译模式下,调用create(\创建一个对应于函数f1()的线程后新线程的内存映象如图2-1所示。

线程私有堆栈空间59ba: 63eTCB 集TCB(0)???sp新线程初始现场信息BPDISIDS: 59baES: 59baDXCXBXAXIP: 879CS: 57f7Flags:200off: 466seg: 57f759ba: a22函数f1( )57f7: 879TCB(i)stack: 63ess: 59basp: a22state:READYname:”f1\???函数over( )57f7: 466F1()返址TCB(NTCB-1)图a图b59ba: a3e

图2-1 对应函数f1()的新线程的内存映像

2.3.2 线程的撤消

引起线程撤销的原因主要有两个:一是系统或用户要求撤销某个线程;二是当前线程所对应的函数已经运行完成。对于第一种情况比较简单,只需调用线程撤销原语将指定线程撤销即可;对于第二钟情况,首先必须自动调用线程撤销原语撤销当前已经运行完成的线程,然后还需要自动地重新进行CPU调度。

1. 线程撤销函数设计:

(1)函数申明原型:void destroy(int id); (2)功能:撤销内部标识符为id的指定线程。 (3)输入:

id:将要被撤销的线程的内部标识符。

- 9 -

(4)输出:无

(5)函数实现的算法描述:

撤销线程所要完成的工作比较简单,主要是将线程所占据的资源归还给系统。在操作系统原理中已经介绍了线程本身基本不占据资源,它与同进程的其他线程共享该进程的代码段和数据段空间;但是线程作为一个可以独立调度和运行的基本单元也拥有一些必不可少的资源,如线程控制块TCB和私有堆栈。所以撤销线程所要做的事情主要就是两个:

(1)将线程的私有堆栈所占的内存空间归还给系统; (2)将线程控制块TCB的各成员变量进行初始化操作。

2. 撤销线程并重新进行调度

前面提到如果是因为当前线程运行完成而引起线程撤消,则系统应能自动撤消该线程,并重新进行CPU调度。我们可以设置一个称为over()的函数来完成这个工作,该函数需要顺序做两件事情:首先调用destroy()撤销当前线程,然后重新进行CPU调度。所以现在关键的问题是在当前线程运行完成后CPU应能自动转去执行over(),这可通过在创建线程时进行一些相关的处理来实现:在进行堆栈初始化时可预先将over( ) 的入口地址压入线程的私有堆栈中,如前面图2-3b所示;这样,当线程所对应的函数正常结束时,over()函数的入口地址将作为函数的返回地址被弹出至CPU的CS、IP寄存器,从而使CPU的控制权自动转向over()去执行。

2.4 线程调度设计

2.4.1 CPU调度中的关键问题

CPU调度所要做的事情是保护旧线程的现场、找到新线程、恢复新线程的现场、并把处理机交给新线程让它执行。其中,找一新线程是比较容易实现的,只需按某种线程调度算法从所有处于就绪状态的线程中选择一个即可;剩余的问题——旧线程的现场保护和新线程的现场恢复、CPU 控制权的转移才是CPU调度的关键,它们是通过堆栈的切换来实现的。在介绍堆栈切换的内容之前,我们先来看看函数调用和进行中断处理时控制转移的情况。

1.函数调用时的控制转移情况

在执行函数调用指令时,系统会自动地先将主调函数的下一条指令的地址(在CS:IP中)压入堆栈,然后把被调函数的入口地址装入CS和IP 寄存器(段内函数调用只需压入和装配IP),控制就从主调函数转向被调函数;当执行函数返回指令时,系统将当前堆栈的栈顶的两个字(主调函数下一条指令的地址)弹出并送到IP和CS中(段内函数返回只需弹出一个字送到IP中),控制就从被调函数返回到主调函数。

例如,我们编写了一个main()函数和一个f1()函数,在main()中调用f1()。程序的设计及调用返回关系如图2-2所示:

- 10 -


操作系统课程设计指导书2015(2).doc 将本文的Word文档下载到电脑 下载失败或者文档不完整,请联系客服人员解决!

下一篇:计算机网络课后习题答案(仅供参考)

相关阅读
本类排行
× 注册会员免费下载(下载后可以自由复制和排版)

马上注册会员

注:下载文档有可能“只有目录或者内容不全”等情况,请下载之前注意辨别,如果您已付费且无法下载或内容有问题,请联系我们协助你处理。
微信: QQ: