UCOS-Ⅲ:信号量
@[toc]
UCOS-Ⅲ:信号量
一、信号量基本概念
信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务之间同步或临界资源的互斥访问**(临界资源指同一时刻只能有有限个访问)**,常用于协助一组相互竞争的任务来访问临界资源。运行机制可以理解为:信号量是一个正值,代表资源的可访问数目,当有任务访问时,这个数目减一,任务访问完成时,任务访问结束,释放他,让他加一,信号量为0时,其他任务则不能获取他,选择退出或者等待挂起,直到有信号量释放后,按照优先级来获取信号量,获取后就绪,其运行流程大概如下图:
UCOS中信号量是内核对象,通过数据类型OS_SEM 定义,OS_SEM 源于结构体os_sem,UCOS中-Ⅲ的信号量相关的代码都被放在OS_SEM.C 中,通过设置OS_CFG.H 中的OS_CFG_SEM_EN 为1 使能信号量
/* ----------------------------- 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() 函数
信号量的结构体如下:
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()函数进行创建一个信号量,跟消息队列的创建差不多,我们知道,其实这里的“创建信号量”指的就是对内核对象(信号量)的一些初始化。
函数入口:
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 | 无错误,继续执行 |
使用实例:
//定义结构体
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,注意调用这个函数后,之前用信号量保护的资源将不再得到保护。
函数入口:
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 的时候,并且信号量等待列表上有等待的任务。 |
同时该函数有一个返回值,返回值的含义为:
删除信号量的时候,会将信号量等待列表上的任务脱离该信号量的等待列表。返回值表示的就是脱离等待列表的任务个数。
使用实例
OS_SEM SemOfKey;; //声明信号量
/* 删除信号量 sem*/
OSSemDel ((OS_SEM *)&SemOfKey, //指向信号量的指针
OS_OPT_DEL_NO_PEND,
(OS_ERR *)&err); //返回错误类型
2.3 信号量释放函数OSSemPost()
当信号量有值的时候,任务才能获取信号量,有两个方式使信号量有值,一个是在创建的时候进行初始化,将它可用的信号量个数设置一个初始值;如果该信号量用作二值信号量,那么我们在创建信号量的时候其初始值的范围是0~1,假如初始值为1个可用的信号量的话,被获取一次就变得无效了,那就需要我们释放信号量,uCOS 提供了信号量释放函数,每调用一次该函数就释放一个信号量。UCOS可以一直释放信号量,但如果用作二值信号量的话,一直释放信号量就达不到同步或者互斥访问的效果,虽然说uCOS 的信号量是允许一直释放的,但是,信号量的范围还需我们用户自己根据需求进行决定,当用作二值信号量的时候,必须确保其可用值在0~1 范围内;而用作计数信号量的话,其范围是由用户根据实际情况来决定的
函数入口:
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 信号量计数值已经达到最大范围了,这次提交会引起信号量计数值溢出。
返回值:
信号量计数值
使用实例:
OS_SEM SemOfKey; //标志KEY1 是否被按下的信号量
OSSemPost((OS_SEM *)&SemOfKey, //发布SemOfKey
(OS_OPT )OS_OPT_POST_ALL, //发布给所有等待任务
(OS_ERR *)&err); //返回错误类型
2.4 信号量获取函数OSSemPend()
当任务获取了某个信号量的时候,该信号量的可用个数就减一,当它减到0 的时候,任务就无法再获取了,并且获取的任务会进入阻塞态(假如用户指定了阻塞超时时间的话)。如果某个信号量中当前拥有1 个可用的信号量的话,被获取一次就变得无效了,那么此时另外一个任务获取该信号量的时候,就会无法获取成功,该任务便会进入阻塞态,阻塞时间由用户指定。uCOS 支持系统中多个任务获取同一个信号量,假如信号量中已有多个任务在等待,那么这些任务会按照优先级顺序进行排列,如果信号量在释放的时候选择只释放给一个任务,那么在所有等待任务中最高优先级的任务优先获得信号量,而如果信号量在释放的时候选择释放给所有任务,则所有等待的任务都会获取到信号量
函数入口:
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 | 成功获取 |
返回值:
信号量计数值
使用实例:
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
任务L最开始运行的时候获取信号量,之后任务H开始执行,抢占了任务L,在任务H运行的时候,刚好需要获取信号量,但此时信号量还在任务L的手里,于是任务H进入挂起队列,任务L继续运行,在任务L没有释放信号量的时候,任务M过来抢占L运行,在任务L释放信号量时,任务H才继续执行,若任务M 需要执行很长时间,则任务H 会被延迟很长时间才执行,这叫做优先级反转。
解决方法是临时提高任务L的优先级,这一内容我们下一节互斥量再分析
四、使用实例
功能:信号量管理停车位资源,按键2按下释放信号量,停车位+1,串口打印数目,按键1按下获取信号量,停车位-1,串口显示是否获取成功
启动任务创建信号量
/* 创建多值信号量 SemOfKey */
OSSemCreate((OS_SEM *)&SemOfKey, //指向信号量变量的指针
(CPU_CHAR *)"SemOfKey", //信号量的名字
(OS_SEM_CTR )5, //表示现有资源数目
(OS_ERR *)&err); //错误类型
再创建两个任务
任务一主体:
/*
*********************************************************************************************************
* 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扫描一次
}
}
任务二主体:
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扫描一次
}
}
串口现象:
- 点赞
- 收藏
- 关注作者
评论(0)