[TOC]

UCOS-Ⅲ:信号量

一、信号量基本概念

信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问(临界资源指同一时刻只能有有限个访问),常用于协助一组相互竞争的任务来访问临界资源。运行机制可以理解为:信号量是一个正值,代表资源的可访问数目,当有任务访问时,这个数目减一,任务访问完成时,任务访问结束,释放他,让他加一,信号量为0时,其他任务则不能获取他,选择退出或者等待挂起,直到有信号量释放后,按照优先级来获取信号量,获取后就绪,其运行流程大概如下图:

信号量 (1)

UCOS中信号量是内核对象,通过数据类型OS_SEM 定义,OS_SEM 源于结构体os_sem,UCOS中-Ⅲ的信号量相关的代码都被放在OS_SEM.C 中,通过设置OS_CFG.H 中的OS_CFG_SEM_EN 为1 使能信号量

1
2
3
4
5
                                             /* ----------------------------- SEMAPHORES ---------------------------- */
#define OS_CFG_SEM_EN 1u //使能或禁用多值信号量
#define OS_CFG_SEM_DEL_EN 1u //使能或禁用 OSSemDel() 函数
#define OS_CFG_SEM_PEND_ABORT_EN 1u //使能或禁用 OSSemPendAbort() 函数
#define OS_CFG_SEM_SET_EN 1u //使能或禁用 OSSemSet() 函数

信号量的结构体如下:

1
2
3
4
5
6
7
8
9
struct  os_sem {                                            /* Semaphore                                              */
/* ------------------ GENERIC MEMBERS ------------------ */
OS_OBJ_TYPE Type; /* Should be set to OS_OBJ_TYPE_SEM */
CPU_CHAR *NamePtr; /* Pointer to Semaphore Name (NUL terminated ASCII) */
OS_PEND_LIST PendList; /* List of tasks waiting on semaphore */
/* ------------------ SPECIFIC MEMBERS ------------------ */
OS_SEM_CTR Ctr;
CPU_TS TS;
};

第一个变量是“Type”域:表明UCOS识别所定义的是一个信号量。其它的内核对象也有“Type”域作为结构体的第一个变量。如果函数要调用一个内核对象,UCOS会检测所调用的内核对象的数据类型是否对应。例如,如果需要传递一个消息队列OS_Q 给函数,但实际传递的是一个信号量OS_SEM,UCOS就会检测出这是一个无效的参数,并返回错误代号

第二个指针指向内核对象的名字:每个内核对象都可以被赋予一个名字,名字有ASCII 字符串组成,但必须以空字符结尾。

第三个等待列表PendList:若有多个任务等待信号量,信号量就会将这些任务放入其挂起队列中。

第四个包含一个信号量计数值:信号量计数值可以定义为8 位,16 位,或32 位,取决于OS_TYPE.H 中的OS_SEM_CTR是如何被定义的,这个值用于判断信号量可用的资源数目(此值的上限大于1为多值信号量,只为0或者1则为二值信号量,UCOS-Ⅲ没有对这两个多做区分,全看如何使用)

第五个CPU_TS时间戳:信号量中包含了一个时间戳变量,存储了上一次信号量被提交时的时间戳。当信号量被提交时,CPU 的时间戳被读取并存在信号量的时间戳变量中,当OSSemPend()被调用时就能读取这个时间戳变量。

用户代码也不能直接访问信号量中的变量。必须通过UCOS-III 提供的API来进行访问!

二、调用API

UCOS中信号量的调用API有以下四个API,分别为创建、删除、获取、释放信号量

2.1 创建信号量函数OSSemCreate()

OSSemCreate()函数进行创建一个信号量,跟消息队列的创建差不多,我们知道,其实这里的“创建信号量”指的就是对内核对象(信号量)的一些初始化。

函数入口:

1
2
3
4
void  OSSemCreate (OS_SEM      *p_sem,  //多值信号量控制块指针
CPU_CHAR *p_name, //多值信号量名称
OS_SEM_CTR cnt, //资源数目或事件是否发生标志
OS_ERR *p_err) //返回错误类型

介绍:

p_sem 指向信号量变量的指针。
p_name 指向信号量变量名字字符串的指针。
cnt 信号量的初始值,用作资源保护的信号量这个值通常跟资源的数量相同,用做标志事件发生的信号量这个值设置为0,标志事情还没有发生。
p_err 指向返回错误类型的指针。

p_err返回错误标志,其具体的返回值对应的情景如下

错误返回值 错误类型
OS_ERR_CREATE_ISR 在中断中创建信号量是不被允许的,返回错误。
OS_ERR_ILLEGAL_CREATE_RUN_TIME 在定义OSSafetyCriticalStartFlag 为DEF_TRUE 后就不运行创建任何内核对象。
OS_ERR_NAME 参数p_name 是个空指针。
OS_ERR_OBJ_CREATED 信号量已经被创建(不过函数中并没有涉及到这个错误的代码)
OS_ERR_OBJ_PTR_NULL 参数p_sem 是个空指针。
OS_ERR_OBJ_TYPE 参数p_sem 被初始化为别的内核对象了。
OS_ERR_NONE 无错误,继续执行

使用实例:

1
2
3
4
5
6
7
8
9
10
11
12
//定义结构体
OS_SEM SemOfKey; //标志KEY1是否被按下的多值信号量

//使用前调用API初始化
任务体
{
/* 创建多值信号量 SemOfKey */
OSSemCreate((OS_SEM *)&SemOfKey, //指向信号量变量的指针
(CPU_CHAR *)"SemOfKey", //信号量的名字
(OS_SEM_CTR )5, //表示现有资源数目
(OS_ERR *)&err); //错误类型
}

2.2 信号量删除函数OSSemDel()

OSSemDel()用于删除一个信号量,信号量删除函数是根据信号量结构(信号量句柄)直接删除的,删除之后这个信号量的所有信息都会被系统清空,而且不能再次使用这个信号量了,但是需要注意的是,如果某个信号量没有被定义,那也是无法被删除的,如果有任务阻塞在该信号量上,那么尽量不要删除该信号量。使用之前首先要将OS_CFG_SEM_DEL_EN 这个宏置1,注意调用这个函数后,之前用信号量保护的资源将不再得到保护。

函数入口:

1
2
3
OS_OBJ_QTY  OSSemDel (OS_SEM  *p_sem,  //多值信号量指针
OS_OPT opt, //选项
OS_ERR *p_err) //返回错误类型
参数名称 参数作用
p_sem 指向信号量变量的指针。
opt 删除信号量时候的选项,有以下两个选择。
p_err 指向返回错误类型的指针,有以下几种可能。

参数选项选择

选项 作用
OS_OPT_DEL_NO_PEND 当信号量的等待列表上面没有相应的任务的时候才删除信号量。
OS_OPT_DEL_ALWAYS 不管信号量的等待列表是否有相应的任务都删除信号量。

错误类型

错误返回值 错误类型
OS_ERR_DEL_ISR 企图在中断中删除信号量。
OS_ERR_OBJ_PTR_NULL 参数p_sem 是空指针。
OS_ERR_OBJ_TYPE 参数p_sem 指向的内核变量类型不是信号量
OS_ERR_OPT_INVALID opt 在给出的选项之外
OS_ERR_TASK_WAITING 在选项opt 是OS_OPT_DEL_NO_PEND 的时候,并且信号量等待列表上有等待的任务。

同时该函数有一个返回值,返回值的含义为:

删除信号量的时候,会将信号量等待列表上的任务脱离该信号量的等待列表。返回值表示的就是脱离等待列表的任务个数。

使用实例

1
2
3
4
5
6
OS_SEM SemOfKey;; //声明信号量

/* 删除信号量 sem*/
OSSemDel ((OS_SEM *)&SemOfKey, //指向信号量的指针
OS_OPT_DEL_NO_PEND,
(OS_ERR *)&err); //返回错误类型

2.3 信号量释放函数OSSemPost()

当信号量有值的时候,任务才能获取信号量,有两个方式使信号量有值,一个是在创建的时候进行初始化,将它可用的信号量个数设置一个初始值;如果该信号量用作二值信号量,那么我们在创建信号量的时候其初始值的范围是01,假如初始值为1个可用的信号量的话,被获取一次就变得无效了,那就需要我们释放信号量,uCOS 提供了信号量释放函数,每调用一次该函数就释放一个信号量。UCOS可以一直释放信号量,但如果用作二值信号量的话,一直释放信号量就达不到同步或者互斥访问的效果,虽然说uCOS 的信号量是允许一直释放的,但是,信号量的范围还需我们用户自己根据需求进行决定,当用作二值信号量的时候,必须确保其可用值在01 范围内;而用作计数信号量的话,其范围是由用户根据实际情况来决定的

函数入口:

1
2
3
OS_SEM_CTR  OSSemPost (OS_SEM  *p_sem,    //多值信号量控制块指针
OS_OPT opt, //选项
OS_ERR *p_err) //返回错误类型
参数名称 参数作用
p_sem 指向要提交的信号量的指针
opt 发布信号时的选项,可能有以下几个选项
p_err 指向返回错误类型的指针,错误的类型如下。(只列了必要部分)

选项列表:

选项 功能
OS_OPT_POST_1 发布给信号量等待列表中优先级最高的任务。
OS_OPT_POST_ALL 发布给信号量等待列表中所有的任务。
OS_OPT_POST_NO_SCHED 提交信号量之后要不要进行任务调度,默认是要进行任务调度的,选择该选项可能的原因是想继续运行当前任务,因为发布信号量可能让那些等待信号量的任务就绪,这个选项没有进行任务调度,发布完信号量当前任务还是继续运行。当任务想发布多个信号量,最后同时调度的话也可以用这个选项。可以跟上面两个选项之一相与做为参数。

错误值:

OS_ERR_SEM_OVF 信号量计数值已经达到最大范围了,这次提交会引起信号量计数值溢出。

返回值:

信号量计数值

使用实例:

1
2
3
4
5
OS_SEM SemOfKey; //标志KEY1 是否被按下的信号量

OSSemPost((OS_SEM *)&SemOfKey, //发布SemOfKey
(OS_OPT )OS_OPT_POST_ALL, //发布给所有等待任务
(OS_ERR *)&err); //返回错误类型

2.4 信号量获取函数OSSemPend()

当任务获取了某个信号量的时候,该信号量的可用个数就减一,当它减到0 的时候,任务就无法再获取了,并且获取的任务会进入阻塞态(假如用户指定了阻塞超时时间的话)。如果某个信号量中当前拥有1 个可用的信号量的话,被获取一次就变得无效了,那么此时另外一个任务获取该信号量的时候,就会无法获取成功,该任务便会进入阻塞态,阻塞时间由用户指定。uCOS 支持系统中多个任务获取同一个信号量,假如信号量中已有多个任务在等待,那么这些任务会按照优先级顺序进行排列,如果信号量在释放的时候选择只释放给一个任务,那么在所有等待任务中最高优先级的任务优先获得信号量,而如果信号量在释放的时候选择释放给所有任务,则所有等待的任务都会获取到信号量

函数入口:

1
2
3
4
5
OS_SEM_CTR  OSSemPend (OS_SEM   *p_sem,   //多值信号量指针
OS_TICK timeout, //等待超时时间
OS_OPT opt, //选项
CPU_TS *p_ts, //等到信号量时的时间戳
OS_ERR *p_err) //返回错误类型

参数:

参数 作用
p_sem 指向要获取的信号量变量的指针。
opt 可能是以下几个选项之一。
timeout 这个参数是设置的是获取不到信号量的时候等待的时间。如果这个值为0,表示一直等待下去,如果这个值不为0,则最多等待timeout 个时钟节拍。
p_ts 指向等待的信号量被删除,等待被强制停止,等待超时等情况时的时间戳的指针。
p_err 指向返回错误类型的指针,有以下几种类型。

功能选项:

功能 作用
OS_OPT_PEND_BLOCKING 如果不能即刻获得信号量,选项表示要继续等待。
OS_OPT_PEND_NON_BLOCKING 如果不能即刻获得信号量,选项表示不等待信号量。

错误类型:

错误 类型
OS_ERR_OBJ_DEL 信号量已经被删除了。
OS_ERR_OBJ_PTR_NULL 输入的信号量变量指针是空类型。
OS_ERR_OBJ_TYPE p_sem 指向的变量内核对象类型不是信号量。
OS_ERR_OPT_INVALID 参数opt 不符合要求。
OS_ERR_PEND_ABORT 等待过程,其他的任务调用了函数OSSemPendAbort 强制取消等待。
OS_ERR_PEND_ISR 企图在中断中等待信号量。
OS_ERR_PEND_WOULD_BLOCK 开始获取不到信号量,且没有要求等待。
OS_ERR_SCHED_LOCKED 调度器被锁住。
OS_ERR_STATUS_INVALID 系统出错,导致任务控制块的元素PendStatus 不在可能的范围内。
OS_ERR_TIMEOUT 等待超时。
OS_ERR_NONE 成功获取

返回值:

信号量计数值

使用实例:

1
2
3
4
5
6
7
8
9
10
11
ctr = OSSemPend ((OS_SEM   *)&SemOfKey,               //等待该信号量 SemOfKey
(OS_TICK )0, //下面选择不等待,该参无效
(OS_OPT )OS_OPT_PEND_NON_BLOCKING,//如果没信号量可用不等待
(CPU_TS *)0, //不获取时间戳
(OS_ERR *)&err); //返回错误类型

if(err == OS_ERR_NONE)
{
//获取成功
}

三、信号量BUG-优先级反转

优先级反转是实时系统中的一个常见问题,存在于基于优先级的抢占式内核中,优先级反转的原理如下:

下图有三个调度任务L、M、H,任务H 的优先级高于任务M,任务M 的优先级高于任务L

image-20210309200306888

任务L最开始运行的时候获取信号量,之后任务H开始执行,抢占了任务L,在任务H运行的时候,刚好需要获取信号量,但此时信号量还在任务L的手里,于是任务H进入挂起队列,任务L继续运行,在任务L没有释放信号量的时候,任务M过来抢占L运行,在任务L释放信号量时,任务H才继续执行,若任务M 需要执行很长时间,则任务H 会被延迟很长时间才执行,这叫做优先级反转。

解决方法是临时提高任务L的优先级,这一内容我们下一节互斥量再分析

四、使用实例

功能:信号量管理停车位资源,按键2按下释放信号量,停车位+1,串口打印数目,按键1按下获取信号量,停车位-1,串口显示是否获取成功

启动任务创建信号量

1
2
3
4
5
/* 创建多值信号量 SemOfKey */
OSSemCreate((OS_SEM *)&SemOfKey, //指向信号量变量的指针
(CPU_CHAR *)"SemOfKey", //信号量的名字
(OS_SEM_CTR )5, //表示现有资源数目
(OS_ERR *)&err); //错误类型

再创建两个任务

任务一主体:

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
/*
*********************************************************************************************************
* KEY1 TASK
*********************************************************************************************************
*/
static void AppTaskKey1 ( void * p_arg )
{
OS_ERR err;
OS_SEM_CTR ctr;
CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必需该宏,该宏声明和定义一个局部变
//量,用于保存关中断前的 CPU 状态寄存器 SR(临界段关中断只需保存SR)
//,开中断时将该值还原。
uint8_t ucKey1Press = 0;


(void)p_arg;


while (DEF_TRUE) { //任务体
if( Key_Scan ( macKEY1_GPIO_PORT, macKEY1_GPIO_PIN, 1, & ucKey1Press ) ) //如果KEY1被按下
{
ctr = OSSemPend ((OS_SEM *)&SemOfKey, //等待该信号量 SemOfKey
(OS_TICK )0, //下面选择不等待,该参无效
(OS_OPT )OS_OPT_PEND_NON_BLOCKING,//如果没信号量可用不等待
(CPU_TS *)0, //不获取时间戳
(OS_ERR *)&err); //返回错误类型

OS_CRITICAL_ENTER(); //进入临界段

if ( err == OS_ERR_NONE )
printf ( "\r\nKEY1被按下:成功申请到停车位,剩下%d个停车位。\r\n", ctr );
else if ( err == OS_ERR_PEND_WOULD_BLOCK )
printf ( "\r\nKEY1被按下:不好意思,现在停车场已满,请等待!\r\n" );

OS_CRITICAL_EXIT();

}

OSTimeDlyHMSM ( 0, 0, 0, 20, OS_OPT_TIME_DLY, & err ); //每20ms扫描一次

}

}

任务二主体:

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
static  void  AppTaskKey2 ( void * p_arg )
{
OS_ERR err;
OS_SEM_CTR ctr;
CPU_SR_ALLOC(); //使用到临界段(在关/开中断时)时必需该宏,该宏声明和定义一个局部变
//量,用于保存关中断前的 CPU 状态寄存器 SR(临界段关中断只需保存SR)
//,开中断时将该值还原。
uint8_t ucKey2Press = 0;


(void)p_arg;


while (DEF_TRUE) { //任务体
if( Key_Scan ( macKEY2_GPIO_PORT, macKEY2_GPIO_PIN, 1, & ucKey2Press ) ) //如果KEY2被按下
{
ctr = OSSemPost((OS_SEM *)&SemOfKey, //发布SemOfKey
(OS_OPT )OS_OPT_POST_ALL, //发布给所有等待任务
(OS_ERR *)&err); //返回错误类型

OS_CRITICAL_ENTER(); //进入临界段

printf ( "\r\nKEY2被按下:释放1个停车位,剩下%d个停车位。\r\n", ctr );

OS_CRITICAL_EXIT();

}

OSTimeDlyHMSM ( 0, 0, 0, 20, OS_OPT_TIME_DLY, & err ); //每20ms扫描一次

}

}

串口现象:

20210309205107

wechat