信号量(sem
)在操做系统中是一种实现系统中任务与任务、任务与中断间同步或者临界资源互斥保护的机制。在多任务系统中,各任务之间常须要同步或互斥,信号量就能够为用户提供这方面的支持。设计模式
抽象来讲,信号量是一个非负整数,每当信号量被获取(pend
)时,该整数会减一,当该整数的值为 0
时,表示信号量处于无效状态,将没法被再次获取,全部试图获取它的任务将进入阻塞态。一般一个信号量是有计数值的,它的计数值能够用于系统资源计数(统计)。api
通常来讲信号量的值有两种:数据结构
post
信号量操做,且可能有任务阻塞在此信号量上。post
信号量操做。通常来讲信号量多用于同步而非互斥,由于操做系统中会提供另外一种互斥机制(互斥锁),互斥量的互斥做用更完善:互斥锁有优先级继承机制,而信号量则没有这个机制,此外互斥量还拥有全部者属性,咱们会在后续讲解。函数
信号量也如队列同样,拥有阻塞机制
。任务须要等待某个中断发生后,再去执行对应的处理,那么任务能够处于阻塞态等待信号量,直到中断发生后释放信号量后,该任务才被唤醒去执行对应的处理。在释放(post
)信号量的时候能当即将等待的任务转变为就绪态,若是任务的优先级在就绪任务中是最高的,任务就能当即被运行,这就是操做系统中的“实时响应,实时处理
”。在操做系统中使用信号量能够提升处理的效率。post
TencentOS tiny
经过信号量控制块操做信号量,其数据类型为k_sem_t
,信号量控制块由多个元素组成,主要有 pend_obj_t
类型的pend_obj
以及k_sem_cnt_t
类型的count
。而pend_obj
有点相似于面向对象的继承,继承一些属性,里面有描述内核资源的类型(如信号量、队列、互斥量等,同时还有一个等待列表list
)。而count
则是一个简单的变量(它是16位的无符号整数),表示信号量的值。学习
typedef struct k_sem_st { pend_obj_t pend_obj; k_sem_cnt_t count; } k_sem_t;
在tos_config.h
中,使能信号量的宏定义是TOS_CFG_SEM_EN
操作系统
#define TOS_CFG_SEM_EN 1u
TencentOS tiny
中实现信号量很是简单,核心代码仅仅只有125
行,能够说是很是少了。设计
系统中每一个信号量都有对应的信号量控制块,信号量控制块中包含了信号量的全部信息,好比它的等待列表、它的资源类型,以及它的信号量值,那么能够想象一下,建立信号量的本质是否是就是对信号量控制块进行初始化呢?很显然就是这样子的。由于在后续对信号量的操做都是经过信号量控制块来操做的,若是控制块没有信息,那怎么能操做嘛~指针
建立信号量函数是tos_sem_create()
,传入两个参数,一个是信号量控制块的指针*sem
,另外一个是信号量的初始值init_count
,该值是非负整数便可,但主要不能超过65535
。code
实际上就是调用pend_object_init()
函数将信号量控制块中的sem->pend_obj
成员变量进行初始化,它的资源类型被标识为PEND_TYPE_SEM
。而后将sem->count
成员变量设置为传递进来的信号量的初始值init_count
。
__API__ k_err_t tos_sem_create(k_sem_t *sem, k_sem_cnt_t init_count) { TOS_PTR_SANITY_CHECK(sem); pend_object_init(&sem->pend_obj, PEND_TYPE_SEM); sem->count = init_count; return K_ERR_NONE; }
信号量销毁函数是根据信号量控制块直接销毁的,销毁以后信号量的全部信息都会被清除,并且不能再次使用这个信号量,当信号量被销毁时,其等待列表中存在任务,系统有必要将这些等待这些任务唤醒,并告知任务信号量已经被销毁了PEND_STATE_DESTROY
。而后产生一次任务调度以切换到最高优先级任务执行。
TencentOS tiny
对信号量销毁的处理流程以下:
pend_is_nopending()
函数判断一下是否有任务在等待信号量pend_wakeup_all()
函数将这些任务唤醒,而且告知等待任务信号量已经被销毁了(即设置任务控制块中的等待状态成员变量pend_state
为PEND_STATE_DESTROY
)。pend_object_deinit()
函数将信号量控制块中的内容清除,最主要的是将控制块中的资源类型设置为PEND_TYPE_NONE
,这样子就没法使用这个信号量了。knl_sched()
注意:若是信号量控制块的RAM是由编译器静态分配
的,因此即便是销毁了信号量,这个内存也是没办法释放的。固然你也可使用动态内存为信号量控制块分配内存,只不过在销毁后要将这个内存释放掉,避免内存泄漏。
__API__ k_err_t tos_sem_destroy(k_sem_t *sem) { TOS_CPU_CPSR_ALLOC(); TOS_PTR_SANITY_CHECK(sem); #if TOS_CFG_OBJECT_VERIFY_EN > 0u if (!pend_object_verify(&sem->pend_obj, PEND_TYPE_SEM)) { return K_ERR_OBJ_INVALID; } #endif TOS_CPU_INT_DISABLE(); if (!pend_is_nopending(&sem->pend_obj)) { pend_wakeup_all(&sem->pend_obj, PEND_STATE_DESTROY); } pend_object_deinit(&sem->pend_obj); TOS_CPU_INT_ENABLE(); knl_sched(); return K_ERR_NONE; }
tos_sem_pend()
函数用于获取信号量,当信号量有效的时候,任务才能获取信号量。任务获取了某个信号量时,该信号量的可用个数减一,当它为0的时候,获取信号量的任务会进入阻塞态,阻塞时间timeout
由用户指定,在指定时间还没法获取到信号量时,将发送超时,等待任务将自动恢复为就绪态。
获取信号量的过程以下:
count
成员变量是否大于0
,大于0表示存在可用信号量,将count
成员变量的值减1
,任务获取成功后返回K_ERR_NONE
。timeout
是否为不阻塞TOS_TIME_NOWAIT
,若是不阻塞则直接返回K_ERR_PEND_NOWAIT
错误代码。knl_is_sched_locked()
,则没法进行等待操做,返回错误代码K_ERR_PEND_SCHED_LOCKED
,毕竟须要切换任务,调度器被锁则没法切换任务。pend_task_block()
函数将任务阻塞,该函数实际上就是将任务从就绪列表中移除k_rdyq.task_list_head[task_prio]
,而且插入到等待列表中object->list
,若是等待的时间不是永久等待TOS_TIME_FOREVER
,还会将任务插入时间列表中k_tick_list
,阻塞时间为timeout
,而后进行一次任务调度knl_sched()
。pend_state2errno()
时,则表示任务等获取到信号量
,又或者等待发生了超时
,那么就调用pend_state2errno()
函数获取一下任务的等待状态,看一下是哪一种状况致使任务恢复运行,而且将结果返回给调用获取信号量的任务。注意:当获取信号量的任务能从阻塞中恢复运行,也不必定是获取到信号量,也多是发生了超时,所以在写程序的时候必需要判断一下获取的信号量状态,若是是K_ERR_NONE
则表示获取成功!
__API__ k_err_t tos_sem_pend(k_sem_t *sem, k_tick_t timeout) { TOS_CPU_CPSR_ALLOC(); TOS_PTR_SANITY_CHECK(sem); TOS_IN_IRQ_CHECK(); #if TOS_CFG_OBJECT_VERIFY_EN > 0u if (!pend_object_verify(&sem->pend_obj, PEND_TYPE_SEM)) { return K_ERR_OBJ_INVALID; } #endif TOS_CPU_INT_DISABLE(); if (sem->count > (k_sem_cnt_t)0u) { --sem->count; TOS_CPU_INT_ENABLE(); return K_ERR_NONE; } if (timeout == TOS_TIME_NOWAIT) { // no wait, return immediately TOS_CPU_INT_ENABLE(); return K_ERR_PEND_NOWAIT; } if (knl_is_sched_locked()) { TOS_CPU_INT_ENABLE(); return K_ERR_PEND_SCHED_LOCKED; } pend_task_block(k_curr_task, &sem->pend_obj, timeout); TOS_CPU_INT_ENABLE(); knl_sched(); return pend_state2errno(k_curr_task->pend_state); }
任务或者中断服务程序均可以释放信号量(post),释放信号量的本质就是将信号量控制块的count
成员变量的值加1
,表示信号量有效,不过若是有任务在等待这个信号量时,信号量控制块的count
成员变量的值是不会改变的,由于要唤醒等待任务,而唤醒等待任务的本质就是等待任务获取到信号量,信号量控制块的count
成员变量的值要减1
,这一来一回中,信号量控制块的count
成员变量的值是不会改变的。
TencentOS tiny
中能够只让等待中的一个任务获取到信号量,也可让全部等待任务都获取到信号量。分别对应的API是tos_sem_post()
与tos_sem_post_all()
。顺便提一点,tos_sem_post_all()
的设计模式实际上是观察者模式,当一个观察的对象改变后,那么全部的观察者都会知道它改变了,具体能够看看《大话设计模式》这本书。
TencentOS tiny
中设计的很好的地方就是简单与低耦合,这两个api接口本质上都是调用sem_do_post()
函数去释放信号量,只是经过opt
参数不一样选择不一样的处理方法。
在sem_do_post()
函数中的处理也是很是简单明了的,其执行思路以下:
count
成员变量的值加1
吧,所以必需要判断一下是否溢出,若是sem->count
的值为 (k_sem_cnt_t)-1
,则表示已经溢出,没法继续释放信号量,返回错误代码K_ERR_SEM_OVERFLOW。pend_is_nopending()
函数判断一下是否有任务在等待信号量,若是没有则将count
成员变量的值加1
,返回K_ERR_NONE
表示释放信号量成功,由于此时没有唤醒任务也就无需任务调度,直接返回便可。count
成员变量的值无需加1
,直接调用pend_wakeup
唤醒对应的任务便可,唤醒任务则是根据opt
参数进行唤醒,能够唤醒等待中的一个任务或者是全部任务。knl_sched()
。__API__ k_err_t tos_sem_post(k_sem_t *sem) { TOS_PTR_SANITY_CHECK(sem); return sem_do_post(sem, OPT_POST_ONE); } __API__ k_err_t tos_sem_post_all(k_sem_t *sem) { TOS_PTR_SANITY_CHECK(sem); return sem_do_post(sem, OPT_POST_ALL); } __STATIC__ k_err_t sem_do_post(k_sem_t *sem, opt_post_t opt) { TOS_CPU_CPSR_ALLOC(); #if TOS_CFG_OBJECT_VERIFY_EN > 0u if (!pend_object_verify(&sem->pend_obj, PEND_TYPE_SEM)) { return K_ERR_OBJ_INVALID; } #endif TOS_CPU_INT_DISABLE(); if (sem->count == (k_sem_cnt_t)-1) { TOS_CPU_INT_ENABLE(); return K_ERR_SEM_OVERFLOW; } if (pend_is_nopending(&sem->pend_obj)) { ++sem->count; TOS_CPU_INT_ENABLE(); return K_ERR_NONE; } pend_wakeup(&sem->pend_obj, PEND_STATE_POST, opt); TOS_CPU_INT_ENABLE(); knl_sched(); return K_ERR_NONE; }
关于为何判断sem->count
是(k_sem_cnt_t)-1
就表明溢出呢?我在C语言中举了个简单的例子:
#include <stdio.h> int main() { unsigned int a = ~0; if(a == (unsigned int)0XFFFFFFFF) { printf("OK\n"); } if(a == (unsigned int)-1) { printf("OK\n"); } printf("unsigned int a = %d \n",a); return 0; } 输出: OK OK unsigned int a = -1
代码精悍短小,思想清晰,很是建议深刻学习~
相关代码能够在公众号后台回复 “ 19 ” 获取。 更多资料欢迎关注“物联网IoT开发”公众号!