【TencentOS tiny】深度源码分析(5)——信号量
信号量
信号量(sem
)在操作系统中是一种实现系统中任务与任务、任务与中断间同步或者临界资源互斥保护的机制。在多任务系统中,各任务之间常需要同步或互斥,信号量就可以为用户提供这方面的支持。
抽象来说,信号量是一个非负整数,每当信号量被获取(pend
)时,该整数会减一,当该整数的值为 0
时,表示信号量处于无效状态,将无法被再次获取,所有试图获取它的任务将进入阻塞态。通常一个信号量是有计数值的,它的计数值可以用于系统资源计数(统计)。
一般来说信号量的值有两种:
- 0:表示没有积累下来的
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
。
实际上就是调用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开发”公众号!
【TencentOS tiny】深度源码分析(5)——信号量的更多相关文章
- 【TencentOS tiny】深度源码分析(4)——消息队列
消息队列 在前一篇文章中[TencentOS tiny学习]源码分析(3)--队列 我们描述了TencentOS tiny的队列实现,同时也点出了TencentOS tiny的队列是依赖于消息队列的, ...
- Spring5深度源码分析(三)之AnnotationConfigApplicationContext启动原理分析
代码地址:https://github.com/showkawa/spring-annotation/tree/master/src/main/java/com/brian AnnotationCon ...
- 【TencentOS tiny】深度源码分析(2)——调度器
温馨提示:本文不描述与浮点相关的寄存器的内容,如需了解自行查阅(毕竟我自己也不懂) 调度器的基本概念 TencentOS tiny中提供的任务调度器是基于优先级的全抢占式调度,在系统运行过程中,当有比 ...
- 【TencentOS tiny】深度源码分析(1)——task
任务的基本概念 从系统的角度看,任务是竞争系统资源的最小运行单元.TencentOS tiny是一个支持多任务的操作系统,任务可以使用或等待CPU.使用内存空间等系统资源,并独立于其它任务运行,理论上 ...
- 【TencentOS tiny】深度源码分析(6)——互斥锁
互斥锁 互斥锁又称互斥互斥锁,是一种特殊的信号量,它和信号量不同的是,它具有互斥锁所有权.递归访问以及优先级继承等特性,在操作系统中常用于对临界资源的独占式处理.在任意时刻互斥锁的状态只有两种,开锁或 ...
- 【TencentOS tiny】深度源码分析(7)——事件
引言 大家在裸机编程中很可能经常用到flag这种变量,用来标志一下某个事件的发生,然后在循环中判断这些标志是否发生,如果是等待多个事件的话,还可能会if((xxx_flag)&&(xx ...
- 【TencentOS tiny】深度源码分析(3)——队列
队列基本概念 队列是一种常用于任务间通信的数据结构,队列可以在任务与任务间.中断和任务间传递消息,实现了任务接收来自其他任务或中断的不固定长度的消息,任务能够从队列里面读取消息,当队列中的消息是空时, ...
- 【TencentOS tiny】深度源码分析(8)——软件定时器
软件定时器的基本概念 TencentOS tiny 的软件定时器是由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受硬件定时器资源限制的定时器服务,本质上软件定时器的使用相当 ...
- 转:[gevent源码分析] 深度分析gevent运行流程
[gevent源码分析] 深度分析gevent运行流程 http://blog.csdn.net/yueguanghaidao/article/details/24281751 一直对gevent运行 ...
随机推荐
- Django基础day01
后端(******) 软件开发结构c/s http协议的由来 sql语句的由来 统一接口统一规范 HTTP协议 1.四大特性 1.基于TCP/IP作用于应用层之上的协议 2.基于请求响应 3.无状态 ...
- springmvc源码学习
1.springmvc运行流程 流程图是直接在百度图片中找的一张 >.前台发送请求,请求会首先通过DispatcherServlet进行url的匹配;如果匹配不到,看是否配置<mvc: ...
- 使用Future
/** * Runnable接口有个问题,它的方法没有返回值.如果任务需要一个返回结果,那么只能保存到变量,还要提供额外的方法读取,非常不便. * Java标准库还提供了一个Callable接口,和R ...
- Homebrew的安装
Homebrew是一款Mac OS平台下的软件包管理工具. 安装方法:命令行输入 /usr/bin/ruby -e "$(curl -fsSL https://raw.githubuserc ...
- 小白的springboot之路(十三)、过滤器、监听器、拦截器
0.前言 过滤器.监听器.拦截器在实际开发中经常需要用到,下面我们来介绍一下spring boot中如何使用: 一.------ 过滤器 ----- 1.作用: 过滤器是客户端与服务器资源文件之间的一 ...
- Java工作流引擎表单引擎之JS表单字段输入脚本验证
关键字: 表单设计器, 字段验证. workflow,ccform, ccBPM. 工作流快速开发平台 工作流流设计 业务流程管理 asp.net 开源工作流bpm工作流系统 java工作流 ...
- Linux电源管理(7)_Wakeup events framework
1. 前言 本文继续"Linux电源管理(6)_Generic PM之Suspend功能"中有关suspend同步以及PM wakeup的话题.这个话题,是近几年Linux ker ...
- Linux 使用vi命令的教程
一.首先用vi命令打卡要编辑的文件: 注意:vi命令的使用如下: 打开或新建文件,并将光标至于第一行首:[root@centos6 /]# vi /etc/my.cnf 打开文件,并将光标移至最后一行 ...
- 2019年Java面试题基础系列228道(2)
21.描述一下 JVM 加载 class 文件的原理机制? JVM 中类的装载是由类加载器(ClassLoader)和它的子类来实现的,Java 中的类加载器是一个重要的 Java 运行时系统组件,它 ...
- javax.servlet.ServletException: Could not resolve view with name 'order/list' in servlet with name 'dispatcherServlet'
javax.servlet.ServletException: Could not resolve view with name 'order/list' in servlet with name ' ...