GoupOS线程管理

GoupOS线程管理

1. 需要线程管理的原因

在日常生活中,我们要解决个大问题,般会将它分解成多个简单的、容易解决的小问题,小问题逐个被解决,大问题也就随之解决了。在多线程操作系统中,同样需要开发人员把个复杂的应用分解成多个小的、可调度的、序列化的程序单元,当合理地划分任务并正确地执行时,这种设计就能够让系统满足实时系统的性能及时间的要求。例如让嵌入式系统执行这样的任务,即系统通过传感器采集数据,并通过显示屏将数据显示出来,在多线程实时系统中,可以将该任务分解成两个子任务,如下图所示,任务l不间断地读取传感器数据,并将数据写到共享内存中,任务2周期性地从共享内存中读取数据,并 将传感器数据输出到显示屏上。
传感器数据接收任务与显示任务的切换执行.png
在GoupOS中,与上述子任务对应的程序实体就是线程。线程是实现任务的载体,是GoupOS中最基本的调度单位,它描述了个任务执行的运行环境,也描述了该任务所处的优先等级。重要的任务可设置相对较高的优先级,非重要的任务可以设置较低的优先级,不同的任务还可以设置相同的优先级,轮流运行。
当线程运行时,它会认为自己是以独占CPU的方式在运行,线程执行时的运行环境称为上下文。

2. 线程管理的功能特点

GoupOS线程管理的主要功能是对线程进行管理和调度。系统中共存在两类线程,分别是系统线程和用户线程,系统线程是由GoupOS内核创建的线程,用户线程是由应用程序创建的线程。这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,线程对象也会被从对象容器中删除。每个线程都有重要的属性,如线程控制块、线程栈、入口函数等。
GoupOS的线程调度器是抢占式的,其主要工作就是从就绪线程列表中查找最高优先级线程,保证优先级最高的线程能够被运行,优先级最高的任务一旦就绪,总能得到CPU一的使用权。
当有高优先级线程满足条件后,低优先级线程运行权就被剥夺了,或者说让出了,高优先级的线程会立刻得到CPU的使用权。
如果是中断服务程序使一个高优先级的线程满足运行条件,则中断完成时,被中断的线程挂起,优先级高的线程开始运行。
当调度器调度线程切换时,先将当前线程上下文保存起来,当再切回到这个线程时,线程调度器将恢复该线程的上下文信息。

3. 线程工作机制

在GoupOS中,线程控制块由结构体struct _tTask表示。线程控制块是操作系统用于管理线程的数据结构,它会存放线程的一些信息,例如优先级、线程名称、线程状态等,包含线程与线程之间连接用的链表结构、线程等待事件集合等,详细定义如下:

os_tTask.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*任务结构体*/
typedef struct _tTask
{
/*<!指向堆栈指针*/
tTaskStack *stack;
char taskname[OS_TASKNAME_LENGTH]; /*<!任务的名字*/
uint8_t taskname_length; /*<!任务名字长度*/
/*<!任务队列,当前保留,暂时没使用,使用方法是将所有任务链接在一起*/
tNode tasknode;
/*<!任务ID号,为每个任务分配一个ID号,当前保留,暂时没有使用*/
uint32_t taskID;
/*<!优先级队列链表节点,放在就绪表中*/
tNode linkNode;
uint32_t TaskRunTimeslice; /*<!任务运行的时间片*/
/*<!任务的时间片,保存者当前任务运行多长的时间*/
uint32_t TaskTimeslice;
/*<!添加挂起计数器 被挂起的次数(暂停计数器,被暂停运行的次数)*/
uint32_t suspendCount;
uint32_t delayTicks; /*<!任务延时计时器*/
tNode delayNode; /*<!添加延时节点*/
uint32_t prio; /*<!任务的优先级*/
/*<!任务状态字段(指示是否处于延时状态)*/
uint32_t state;
uint32_t *stackBase; /*<!栈的起始地址*/
uint32_t stacksize; /*<!任务栈总大小*/
/*<!任务被删除时调用的清理函数*/
void (*clean)(void * param);
void *cleanParam; /*<!传递给清理函数的参数*/
/*<!请求删除标志,非0表示请求删除*/
uint8_t requestDeleteFlag;
/*<!添加等待的事件控制块类型数据*/
/*<!因为是指针类型,只要告诉编译器有这么一个类型就可以了,他的大小是指针变量的大小就可以了,
* 编译器就知道怎么编译了,指针分配的空间是固定的,4个字节
*/
tEvent *waitEvent;
/*<!等待事件控制块,存放消息的地方(邮箱中会使用到)*/
void *EventMsg;
uint32_t waitEventResult; /*<!等待事件的结果*/
/*<!保存请求的类型: 置位清零(置位中清零有:任意标志位匹配 、所有标志位匹配)*/
uint32_t waitFlagsType;
/*<!请求的事件标志:等待那些标志出现任务就运行*/
uint32_t eventFlags;
/**线程故障信息,实时性要求不高时,可以开启*/
#if(THREAD_FAULT == 1)
Threadfailure Threadfault;
#endif

}tTask;

其中uint32_t prio是线程创建时指定的线程优先级,它在线程运行过程中是不会改变的。

1. 线程的属性

1. 线程栈

GoupOS线程具有独立的栈,当进行线程切换时,会将当前线程的上下文保存在栈中,当线程要恢复运行时,再从栈中读取上下文信息进行恢复。
线程栈还用来存放函数中的局部变最:函数中的局部变量从线程栈空间中申请;函数中局部变量初始时从寄存器中分配(ARM架构),当该函数再调用另一个函数时,这些局部变量将被放入栈中。
第一次运行线程时,可以以手工的方式构造上下文来设置一些初始环境:入口函数(PC寄存器)、入口参数(RO寄存器)、返回位置(LR寄存器)、当前机器运行状态(CPSR寄存器)。
线程栈的增长方向是与芯片构架密切相关的,目前版本均只支持栈由高地址向低地址增对千ARM Cortex M架构,线程栈的构造如右侧图所示。
线程栈大小可以这样设定:对于资源相对较大的MCU,可以设计较大的线程栈;也可以在初始时设置较大的栈。例如指定大小为1KB或2KB,可以根据系统提供的函数获取栈大概的使用率。
线程栈的构造.png

2. 线程状态

在线程运行过程中,同时间内只允许一个线程在处理器中运行。从运行的过程上划分,线程有多种不同的运行状态,如初始状态、挂起状态、就绪状态等。在GoupOS中,线程包含4种状态,操作系统会自动根据线程运行的情况来动态调整其状态。GoupOS中线程的4种状态如下表所示。
线程的4中状态.png

3. 线程优先级

GoupOS线程的优先级表示线程被调度的优先程度。每个线程都具有优先级, 线程越重要,被赋予的优先级就越高,该线程被调度的可能性就越大。
GoupOS最大支持32个线程优先级(0~32),数值越小的优先级越高,0为最高优先级。对于ARM Cortex-M系列,普遍采用32个优先级。最低优先级默认分配给空。最低优先级默认分配给空闲线程使用,用户一般不使用。在系统中, 如果有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。

4. 时间片

每个线程都有时间片参数,但时间片仅对优先级相同的就绪状态线程有效。 当系统对优先级相同的就绪状态线程采用时间片轮转的调度方式进行调度时, 时间片起到约束线程单次运行时长的作用, 其单位是一个系统节拍(OS Tick)。 假设有2个优先级相同的就绪状态线程A与B, A线程的时间片设置为10, B线程的时间片设置为5, 那么当系统中不存在比A优先级高的就绪状态线程时,系统会在A、B线程间来回切换执行,并且每次对A线程执行10个节拍的时长, 对B线程执行5个节拍的时长,如下图所示。
相同优先级时间片轮转.png

5. 线程入口函数

线程控制块中的task1Entry是线程的入口函数, 它是线程实现预期功能的函数。线程的入口函数有用户设计实现,一般有一下两种代码模式。
(1)无限循环模式:在实时系统中, 线程通常是被动式的。 这是由实时系统的特性所决定的, 实时系统通常总是等待外界事件的发生, 而后进行相应的服务。

1
2
3
4
5
6
7
void task1Entry(void *param)
{
for(;;)
{
/*<! User code */
}
}

线程看似没有什么限制程序执行的因素,似乎所有的操作都可以执行。但是作为一个优先级明确的实时系统,如果一个线程中的程序陷入了死循环,那么比它优先级低的线程都将不能得到执行。所以在实时操作系统中必须注意的一点:线程中不能陷入死循环操作,必须要有让出CPU使用权的动作,如在循环中调用延时函数或者主动挂起。用户设计这种无限循环线程的目的,就是为了让该线程一直被系统循环调度运行,永不删除。
(2)顺序执行或有限次循环模式:简单的顺序语旬、do while()或for()循环等。

6. 线程错误码

一个线程就是一个执行场景,错误码是与执行环境密切相关的,所以为每个线程配备了一个变量,用于保存错误码。线程的错误码有以下几种:

os_tEventConBlock_h__
1
2
3
4
5
6
7
8
9
10
11
12
/**
* @brief (等待事件的)错误码
*/
typedef enum _tError
{
tErrorNoError = 0, /*<!无错误*/
tErrorTimeout = 1, /*<!任务超时*/
tErrorResourceUnavaliable = 2, /*<!错误,资源不可用*/
tErrorDel = 3, /*<!信号量被删除 */
tErrorResourceFull = 4, /*<!错误,资源不可用*/
tErrorOwner = 5, /*<!拥有者错误*/
} tError;

2. 线程状态切换

GoupOS提供一系列的操作系统调用接口,使得线程的状态在这几种状态之间来回切换。几种状态之间的转换关系如下图所示。
程状态转换图.png

线程状态转换的基本函数
任务创建函数 tTaskInit();
任务延时函数 tTaskDelay ();
任务挂起函数 tTaskSuspend();
任务唤醒函数 tTaskwakeUp();
任务等待事件函数 tEventwait();
任务获取事件函数 tEventWakeUp();
tEventWakeUpTask();
任务删除函数 tTaskForceDelete();
tTaskRequestDelete();
tTaskIsRequestedDeleted();
tTaskDeleteSelf();

线程通过调用函数tTaskInit()进入初始状态;初始完成后进入就绪状态;就绪状态的线程被调度器调度后进入运行状态;当处于运行状态的线程调用tTaskDelay()、tSemWait()、tMutexChokeObtainlock()等函数或者获取不到资源时,将进入挂起状态;处于挂起状态 的线程,如果等待超时依然未能获得资源或由千其他线程释放了资源,它将返回到就绪状态。挂起状态的线程.,如果调用tSemDestroy()、tMutexTaskDelete()函数,将更改为关闭状态。
注意:GoupOS实际上线程并不存在运行状态,就绪状态和运行状态是等同的。

3. 系统线程

前文中已提到,系统线程是指由系统创建的线程,用户线程是由用户程序调用线程管理接口创建的线程,在GoupOS内核中的系统线程有空闪线程和主线程。

1. 空闲线程

空闲线程是系统创建的最低优先级的线程,线程状态永远为就绪状态。当系统中无其就绪线程存在时,调度器将调度到空闲线程,它通常是一个死循环,且永远不能被挂起。另外,空闲线程在GoupOS中也有它的特殊用途。
空闲线程也提供了接口来运行用户设置的钩子函数在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗、喂狗等工作。

4. 线程的管理方式

程状态转换图描述了线程的相关操作,包括创建/初始化线程、启动线程、运行线程、删除线程。使用tTaskInit()初始化一个静态线程,静态线程由用户分配栈空间。

1. 创建和删除线程

线程的初始化可以使用下面的函数接口完成, 它用于初始化线程对象,线程的线程句柄(或者说线程控制块指针)、线程栈由用户提供。线程控制块、线程运行栈一般都设置为全局变量, 在编译时就被确定和被分配处理, 内核不负责分配内存空间。需要注意的是,用户提供的栈首地址需进行系统对齐(例如ARM上需要进行4字节对齐)。线程创建接口的参数和返回值如下表所示。

os_tTask_h__
1
void tTaskInit(	char *taskname,  uint16_t taskname_legnth,tTask* task,  void (*entry)(void *),void *param,uint32_t prio,tTaskStack *stack,uint32_t stacksize,uint32_t tTaskRTime);
任务初始化tTaskInit()的输入参数列表s
参数 描述
taskname 线程的名称;线程名称的最大长度由GoupOSconfig.h中的宏OS_TASKNAME_LENGTH指定,多余部分会被自动截掉
taskname_legnth 任务名称长度,为了保证线程名称安全,当线程名称中有特殊字符时候,通过指定名称长度,保证特殊字符也再保存,超过名字宏OS_TASKNAME_LENGTH指定长度后,多余部分会被自动截断
task 线程对象,每个线程都有一个自己的线程对象管理线程数据(线程旬柄。线程旬柄由用户提供, 指向对应的线程控制块内存地址)
entry 线程入口函数
param 线程入口函数参数
prio 线程的优先级。优先级范围取决于系统配置情况(GoupOSconfig.h中的TINYOS_PRO_COUNT宏定义),目前支持1024个优先级,可以修改。数值越小优先级越高,0代表最高优先级
stack 线程栈起始地址,该地址为栈底地址;为数组的首地址
stacksize 线程栈大小,单位是系统位宽除以8(单位是字,32位系统中1个字等于4字节);例如:32位/8则单位为4个字节为一个单位。
tTaskRTime 线程的时间片大小,时间片的单位时操作系统的时钟节拍。当系统中存在相同优先级的线程时,并且没有更高优先级任务,这个参数指定的线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪状态的同优先级线程运行。

线程通过该函数后,就会直接创建好,并且进入就绪态等待CPU的调用。

2. 获取线程信息

在程序运行过程中一段相同的代码可能会被多个线程执行,在执行的时候可以通过下面的函数接口获得当前执行的线程句柄(线程任务)如下表

os_tTask_h__
1
2
void tTaskGetInfo(tTask* task,tTaskInfo * taskinfo);
void tTaskGetCurrentInfo(tTaskInfo * taskinfo);
任务初始化tTaskInit()的输入参数列表s
返回数据 描述
tTaskInfo * taskinfo 线程信息结构体

3. 线程睡眠

在实际应用中,我们有时需要让当前运行的线程延迟一段时间,即在指定的时间到达后重新运行,这就叫做“线程睡眠”,线程睡眠可以使用以下函数接口

os_tTime.h
1
void  tTaskDelay (uint32_t delay);

调用它们可以使当前线程挂起一段指定的时间,当这个时间过后,线程会被唤醒并再次进入就绪状态。这个函数接受一个参数,该参数指定线程的休眠时间。 线程睡眠接口的参数如下表

任务初始化tTaskInit()的输入参数列表
参数 描述
delay 线程睡眠的时间,传入的参数睡眠(或延时)时间等于:delay*系统时钟节拍。具体时间,需要知道设置的时钟节拍最短时间是多少,然后乘上延时数。

4. 挂起和恢复线程

当线程调用tTaskDelay()时,线程将主动挂起;当调用tMutexChokeObtainlock()、tSemWait()等函数时,资源不可使用也将导致线程挂起。处于挂起状态的线程?如果其等待的资源超时(超过其设定的等待时间),那么该线程将不再等待这些资源,而是返回到就绪状态;而是返回到就绪状态,或者当其他线程释放掉该线程所等待的资源时,该线程也会返回到就绪状态。
线程挂起使用下面的函数接口:

os_tTask.h
1
2
void tTaskSuspend(tTask * task);
void CurrentTaskSuspend(void);

线程挂起接口的参数和返回值见下表

tTaskSuspend()/CurrentTaskSuspend()的输入参数
参数 描述
task 线程任务(线程句柄)

注意:通常不应该使用这个函数来挂起线程本身,如果确实需要采用tTaskSuspend()、CurrentTaskSuspend()函数挂起当前任务,需要再调用tTaskwakeUp()函数唤醒任务。用户只需要了解该接口的作用,不推荐使用该接口。
恢复线程就是让挂起的线程重新进入就绪状态,并将线程放入系统的就绪队列中;如果被恢复线程在所有就绪状态线程中位于最高优先级链表的第一位,那么系统将进行线程上下文的切换。线程恢复使用下面的函数接口:

os_tTask.h
1
void tTaskwakeUp(tTask *task)

线程恢复接口的参数和返回值见下表

tTaskwakeUp()的输入参数和返回值
参数 描述
task 线程任务(线程句柄)

5. 设置和删除空闲钩子

空闲钩子函数是空闲线程的钩子函数,如果设置了空闲钩子函数,就可以在系统执行空闲线程时自动执行空闲钩子函数来做一些其他事情,比如系统指示灯。设置空闲任务钩子的接口:

os_Hooks.h
1
void Hooksidle(void)

向钩子函数内部添加函数功能。

6. 设置调度锁钩子

在整个系统运行时,系统都处于线程运行、中断触发-响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可能会想知道在某个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用:

os_Hooks.h
1
void HooksTaskSwitch(tTask * from,tTask * to);

设置调度器钩子函数的输入参数如下表所示。(该部分功能正在完善,此处暂时不要使用)

HooksTaskSwitch()的输入参数和返回值
参数 描述
返回值 描述

5. 线程应用示例

下面给出在Keil模拟器环境下的应用示例,所有应用示例均在Keil模拟器环境下运行。

1. 创建线程示例

下面的例子创建两个线程,代码示例:

线程创建运行示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include "GoupOSinclude.h"   //系统头文件

tTaskStack task1Env[1024]; /*线程1的栈*/
tTaskStack task2Env[1024]; /*线程2的栈*/

tTask tTask1; /*线程1句柄*/
tTask tTask2; /*线程2句柄*/

int i = 0;
int a = 0;

/*<!线程1*/
void task1Entry(void *param)
{
tSemInit("test",sizeof("test"),&sem,0,0);
for(;;)
{
i ++;
tTaskDelay(1);
i ++;
tTaskDelay(1);
}
}
/*<!线程2*/
void task2Entry(void *param)
{
for(;;)
{
a ++;
tTaskDelay(1);
a ++;
tTaskDelay(1);
}
}

/*用户任务初始化程序*/
void AppTaskInit(void)
{
tTaskInit("task1",sizeof("task1"),&tTask1,task1Entry,(void *)0,0,task1Env,1024,10);
tTaskInit("task2",sizeof("task2"),&tTask2,task2Entry,(void *)0,9,task2Env,1024,4);
}

int main(void)
{
Goup_os_Init();
/*<!初始化APP相关配置*/
AppTaskInit();
/*<!启动系统*/
StartSystem();
while(1)
{
}
// return 0;
}

程序运行效果.png

2. 线程时间片轮转调度示例

下面的例子创建两个线程,示例如下,i和a按时间片的增加

线程创建运行示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include "GoupOSinclude.h"   //系统头文件

tTaskStack task1Env[1024]; /*线程1的栈*/
tTaskStack task2Env[1024]; /*线程2的栈*/

tTask tTask1; /*线程1句柄*/
tTask tTask2; /*线程2句柄*/

int i = 0;
int a = 0;

/*<!线程1*/
void task1Entry(void *param)
{
tSemInit("test",sizeof("test"),&sem,0,0);
for(;;)
{
i ++;
tTaskDelay(1);
i ++;
tTaskDelay(1);
}
}
/*<!线程2*/
void task2Entry(void *param)
{
for(;;)
{
a ++;
tTaskDelay(1);
a ++;
tTaskDelay(1);
}
}

/*用户任务初始化程序*/
void AppTaskInit(void)
{
tTaskInit("task1",sizeof("task1"),&tTask1,task1Entry,(void *)0,0,task1Env,1024,10);
tTaskInit("task2",sizeof("task2"),&tTask2,task2Entry,(void *)0,9,task2Env,1024,4);
}

int main(void)
{
Goup_os_Init();
/*<!初始化APP相关配置*/
AppTaskInit();
/*<!启动系统*/
StartSystem();
while(1)
{
}
// return 0;
}

程序运行效果.png

3. 线程调度器钩子示例

在线程进行调度切换时,会执行调度,我们可以通过设置一个调度器钩子,在线程切换时做一些额外的事情,下面的例子是在调度器钩子函数中变量的增加,代码如下所示。

调度器钩子函数使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include "GoupOSinclude.h"   //系统头文件

tTaskStack task1Env[1024]; /*线程1的栈*/
tTaskStack task2Env[1024]; /*线程2的栈*/

tTask tTask1; /*线程1句柄*/
tTask tTask2; /*线程2句柄*/

int i = 0;
int a = 0;

/*<!线程1*/
void task1Entry(void *param)
{
tSemInit("test",sizeof("test"),&sem,0,0);
for(;;)
{
i ++;
tTaskDelay(1);
i ++;
tTaskDelay(1);
}
}
/*<!线程2*/
void task2Entry(void *param)
{
for(;;)
{
a ++;
tTaskDelay(1);
a ++;
tTaskDelay(1);
}
}

/**
* @brief 任务切换的钩子函数
* 函数只是定义在这里,具体实现需要用户实现
* @param from 任务结构体
* @param to
*/
void HooksTaskSwitch(tTask * from,tTask * to)
{
extern uint32_t hooksSwitch_count;
hooksSwitch_count++;
}



/*用户任务初始化程序*/
void AppTaskInit(void)
{
tTaskInit("task1",sizeof("task1"),&tTask1,task1Entry,(void *)0,0,task1Env,1024,10);
tTaskInit("task2",sizeof("task2"),&tTask2,task2Entry,(void *)0,9,task2Env,1024,4);
}

int main(void)
{
Goup_os_Init();
/*<!初始化APP相关配置*/
AppTaskInit();
/*<!启动系统*/
StartSystem();
while(1)
{
}
// return 0;
}

调度器钩子函数使用示例.png