线程是完成一定功能的函数,但不是所有的函数都可以被称为线程。一个函数只有在给出其线程描述符及线程堆栈的情况下,才可以被称为线程,才能够被调度运行。本节先介绍线程的三要素(线程函数、线程堆栈和线程描述符),然后介绍线程的四种状态(终止态、阻塞态、就绪态和激活态),最后介绍线程的三种基本形式(单次执行、周期执行和资源驱动)。
从线程的存储结构上看,线程由线程函数、线程堆栈和线程描述符三个部分组成,这三个组成部分也称为线程的三要素。线程函数就是线程要完成具体功能的程序;每个线程拥有自己独立的线程堆栈空间,用于保存线程在调度时的上下文信息及线程内部使用的局部变量;线程描述符是关联线程属性的程序控制块,用于记录线程的各个属性。
一个线程对应一段函数代码,完成一定功能,可被称为线程函数。从代码上看,线程函数与一般函数并无区别,被编译链接生成机器码之后,一般存储在Flash中。但是从线程自身视角来看,线程认为CPU就是属于它自己的,并不知道还有其他线程存在。线程函数也不是用来被其他函数直接调用的,而是由实时操作系统内核调度运行的。要使线程函数能够被实时操作系统内核调度运行,必须先登记线程函数,设置线程优先级、堆栈大小,给线程编号等,不然当运行多个线程时,实时操作系统内核无法知道先运行哪个线程。由于任何时刻只能有一个线程在运行(处于激活态),因此当实时操作系统内核通过调度使一个线程运行时,之前运行的线程就会退出激活态。CPU被处于激活态的线程独占,从这个角度来看,线程函数与无操作系统中的“main”函数性质相近,一般被设计为永久循环,认为线程一直在执行,永远独占处理器。但也有一些特殊性,这将在第7章中讨论。
线程堆栈是独立于线程函数之外的RAM,是按照先进后出策略组织的一段连续存储空间,是实时操作系统中线程概念的重要组成部分。在实时操作系统中,每个线程都有自己私有的堆栈空间。在线程的运行过程中,堆栈用于保存线程的上下文、线程运行过程中的局部变量。此外,当线程调用普通函数时,它还会为线程保存返回地址等参数变量。
虽然前面已经简要描述过线程的上下文的概念,但是这里还要多说几句,以便对线程堆栈用于保存线程上下文有充分的认识。在多线程系统中,每个线程都认为CPU寄存器是自己的。当一个线程正在运行,实时操作系统内核决定不让该线程继续运行,而转去运行别的线程时,就要把CPU的当前状态保存在属于该线程的线程堆栈中;当实时操作系统内核再次决定让其运行时,就从该线程的线程堆栈中恢复原来的CPU状态,就像未被暂停过一样。
在系统资源充裕的情况下,可分配尽量多的堆栈空间,可以是K数量级的(如常用1024字节),但若是系统资源受限,就得精打细算了,具体的数值要根据线程的执行内容确定。线程堆栈的组织及使用由系统维护,对于用户而言,只要在创建线程时指定其大小即可。
在创建线程时,系统会为每个线程创建一个唯一的线程描述符(Task Descriptor,TD),它相当于线程在实时操作系统中的“身份证”,实时操作系统就是通过线程描述符来管理线程和查询线程信息的。虽然在不同操作系统中,线程描述符的名称不同,但含义相同,例如在Mbed OS中被称为线程控制块(Thread Control Block,TCB),在μC/OS中被称任务控制块(Task Control Block,TCB),在Linux中被称为进程控制块(Process Control Block,PCB)。线程函数只有配备了线程描述符才能被实时操作系统内核调度,未配备线程描述符、驻留在Flash中的线程函数代码只是通常意义上的函数,不会被实时操作系统内核调度。
多个线程的线程描述符被组成链表,存储在RAM中。每个线程描述符中都包含指向前一个节点的指针、指向后一个节点的指针、线程状态、线程优先级、线程堆栈指针、线程函数指针(指向线程函数)等字段,实时操作系统内核通过线程描述符来执行线程。
在实时操作系统中,一般情况下使用列表来维护线程描述符,使用就绪列表管理就绪的线程,使用延时列表管理延时等待的线程,使用条件阻塞列表管理因等待事件、消息等而阻塞的线程。在Mbed OS中,还提供了一个等待列表来管理因等待事件、消息等而阻塞的线程。当实时操作系统内核调度线程时,可以通过就绪列表的头节点查找链表,获取就绪列表上所有线程描述符的信息。
实时操作系统中的线程一般有四种状态,分别为终止态、阻塞态、就绪态和激活态。在任一时刻,线程被创建后所处的状态一定是以上四种状态之一。
(1)终止态(Terminated,Inactive):线程执行已经完成或被删除,不再需要使用CPU。
(2)阻塞态(Blocked):又可称为挂起态。线程未准备好,不能被激活,因为该线程需要等待一段时间或某些情况发生;当等待时间到或等待的情况发生时,该线程才变为就绪态。处于阻塞态的线程描述符存放于阻塞列表或延时列表中。
(3)就绪态(Ready):线程已经准备好可以被激活,但未进入激活态,因为其优先级等于或低于当前正在运行的线程,一旦获取CPU的使用权就可以进入激活态。处于就绪态的线程描述符存放于就绪列表中。
(4)激活态(Active,Running):又称为运行态,该线程正在运行中,线程拥有CPU使用权。
如果一个激活态的线程变为阻塞态,那么实时操作系统将执行线程切换操作,从就绪列表中选择优先级最高的线程进入激活态,若有多个具有相同优先级的线程处于就绪态,则就绪列表中的首个线程先被激活。也就是说,每个就绪列表中相同优先级的线程是按先进先出(First In First Out,FIFO)的策略进行调度的。
在一些操作系统中,还把线程分为中断态和休眠态。对于被中断的线程,实时操作系统把它归为中断态;休眠态是指该线程的相关资源虽然仍驻留在内存中,但不被实时操作系统内核调度的一种状态,其实它就是一种终止的状态。
实时操作系统线程的四种状态是动态转换的,有的情况是由系统调度自动完成的,有的情况是由用户调用某个系统函数完成的,还有的情况是等待某个条件满足后完成的。线程的四种状态转换关系图如图1-1所示。
图1-1 线程的四种状态转换关系图
1)终止态转为就绪态
终止态转为就绪态(图1-1中的①线):线程准备重新运行,根据线程优先级进入就绪态。例如,在Mbed OS中,调用svcRtxThreadNew()函数再次创建线程。
2)阻塞态转为就绪态、终止态
阻塞态转为就绪态(图1-1中的②线):阻塞条件被解除,如中断服务程序或其他线程运行时释放了线程等待的信号量,从而使线程再次进入就绪态;延时列表中的线程延时到达唤醒的时刻。例如,在Mbed OS中,会自动调用svcRtxThreadResume()函数。
阻塞态转为终止态(图1-1中的⑥线):如在Mbed OS中,调用svcRtxThreadTerminate()函数。
3)就绪态转为激活态、终止态
就绪态转为激活态(图1-1中的③线):就绪线程被调度而获得了CPU资源进入运行;也可以直接调用函数进入激活态。例如,在Mbed OS中,调用svcRtxThreadYield()函数。
就绪态转为终止态(图1-1中的⑧线):如在Mbed OS中,调用svcRtxThreadTerminate()函数。
4)激活态转为就绪态、阻塞态、终止态
激活态转为就绪态(图1-1中的④线):正在执行的线程被高优先级线程抢占后进入就绪列表;或使用时间片轮询调度策略时,时间片耗尽,正在执行的线程让出CPU;或被外部事件中断。
激活态转为阻塞态(图1-1中的⑤线):正在执行的线程等待信号量、等待事件或者等待I/O资源等,如在Mbed OS中,调用svcRtxThreadSuspend()函数。
激活态转为终止态(图1-1中的⑦线):如在Mbed OS中,调用svcRtxThreadExit()函数。
线程函数一般分为两个部分:初始化部分和线程体部分。初始化部分实现对变量的定义、初始化及设备的打开等,线程体部分负责完成该线程的基本功能。线程的一般结构如下:
线程的基本形式主要有单次执行线程、周期执行线程和资源驱动线程三种,下面介绍这三种线程的结构特点。
单次执行线程是指线程在创建完之后只会被执行一次,执行完成后就会被销毁或阻塞的线程。其线程函数结构如下:
单次执行线程由三部分组成:线程函数初始化、线程函数执行和线程函数销毁或阻塞。线程函数初始化包括对变量的定义和赋值、打开需要使用的设备等;线程函数的执行是该线程的基本功能实现;线程函数的销毁或阻塞,即调用线程销毁或阻塞函数将自己从线程列表中删除。销毁与阻塞的区别在于销毁除了停止线程的运行,还将回收该线程所占用的所有资源,如堆栈空间等;而阻塞只是将线程描述符中的状态设置为阻塞态而已。举例来说,在水质监测系统中,主线程需要创建传感器采集数据线程和处理线程,需要对小灯、串口、传感器等外设进行初始化,当启动完传感器采集数据线程和处理线程后,就阻塞主线程,然后实时操作系统内核开始线程调度,此时的主线程就是单次执行线程。
周期执行线程是指需要按照一定周期执行的线程。其线程函数结构如下:
初始化部分同单次执行线程一样,包括对变量的定义和赋值、打开需要使用的设备等。与单次执行线程不同的是,周期执行线程的函数体内存在永久循环部分,由于该线程需要按照一定周期执行,因此在该线程内一般会调用延时函数,使线程转入阻塞态,进入延时列表中。当延时时间到时,线程就会转入就绪态,进入就绪列表中。举例来说,在水质监测系统中,我们需要得到被监测水域的酸碱度和各种离子的浓度,但是不需要时时刻刻都在监测数据,因为这些物理量的变化比较缓慢,所以使用传感器采集数据时可以调用延时函数,每隔半个小时采集一次数据,此时的物理量采集线程就是典型的周期执行线程。
除了上面介绍的两种线程类型,还有一种线程形式,那就是资源驱动线程。这里的资源主要指线程同步与通信中的事件、信号量、互斥量等。这种类型的线程比较特殊,它是操作系统特有的线程类型,因为只有在操作系统下才会出现资源共享使用的问题,同时引出操作系统中另一个主要问题,那就是线程同步与通信。该线程与周期执行线程的区别在于它的执行时间不是确定的,只有当它所要等待的资源可用时,它才会转入就绪态,否则被加入等待该资源的阻塞列表中。资源驱动线程的函数结构如下:
初始化部分和线程体部分与之前两种类型的线程类似,主要区别就是在线程体执行之前会调用等待资源函数,以等待资源实现线程体部分的功能。仍以水质监测系统为例,数据处理是在物理量采集完成后才能进行的操作,所以在系统中使用一个信号量用于两个线程之间的同步,当物理量采集线程完成时就会释放这个信号量,而数据处理线程一直在等待这个信号量,当等到这个信号量时,就可以进行下一步的操作。系统中的数据处理线程就是一个典型的资源驱动线程。
以上就是三种线程基本形式的介绍,其中周期执行线程和资源驱动线程从本质上来讲可以归结为一种,也就是资源驱动线程。因为时间也是操作系统的一种资源,只不过时间是一种特殊的资源,特殊在该资源是整个操作系统的实现基础,系统中大部分函数都是基于时间这一资源的,所以在分类中将周期执行线程单独作为一类。