本文参考自[野火EmbedFire]《RT-Thread内核实现与应用开发实战——基于STM32》,仅作为个人学习笔记。更详细的内容和步骤请查看原文(可到野火资料下载中心下载)
文章目录
信号量的基本概念
信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。信号量就像一把钥匙,把一段临界区给锁住,只允许有钥匙的线程进行访问:线程拿到了钥匙,才允许它进入临界区;而离开后把钥匙传递给排队在后面的等待线程,让后续线程依次进入临界区。
信号量工作示意图如图 信号量工作示意图 所示,每个信号量对象都有一个信号量值和一个线程等待队列,信号量的值对应了信号量对象的实例数目、资源数目,假如信号量值为5,则表示共有5个信号量实例(资源)可以被使用,当信号量实例数目为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量实例(资源)。——RT-Thread官方中文手册
信号量计数值对应有效资源数,其含义包含下面两种情况:
- 0:表示不能再释放信号量,且可能有线程在此信号量上被阻塞。
- 正值:表示可以释放一个或多个信号量。
二值信号量的应用场景
二值信号量是线程间、线程与中断间同步的重要手段。之所以叫二值信号量,是因为该信号量的值最大为1,即信号量值只有0和1两种情况。
线程间同步:假如线程1需要等待线程2执行完某项操作后才开始工作,一般在这种情况下,我们会在线程1中轮询判断一个全局标志,这样太消耗CPU资源。如果改用二值信号量,线程2如果获取了信号量而不释放,线程1就会进入阻塞状态,直到线程2释放信号量,线程1才继续执行,这样既能实现两个线程的同步,也不会像轮询那样空占CPU资源。
线程与中断同步:与线程间同步类似,只不过此时的“标志”改变发生在中断函数中。
二值信号量的运作机制
创建二值信号量,为创建的信号量对象分配内存,并把可用信号量初始化为用户自定义的个数,二值信号量的最大可用信号量个数为 1。
信号量获取,从创建的信号量资源中获取一个信号量,获取成功返回正确。否则线程会等待其它线程释放该信号量,超时时间由用户设定。当线程获取信号量失败时,线程将进入阻塞态,系统将线程挂到该信号量的阻塞列表中。——原文
计数型信号量的运作机制
当创建信号量时设置的信号量初始值大于1,此时信号量就是计数型信号量。计数型信号量支持多个线程获取信号量,当获取信号量的线程数达到信号量初始值时,后面再获取信号量的线程就会被阻塞,直到持有信号量的线程将信号量释放。
下图是一个信号量初始值为3的计数型信号量的运作示意图。
信号量控制块
信号量控制块的结构体成员非常简单,主要成员只有信号量值。
struct rt_semaphore
{
struct rt_ipc_object parent; /**< 继承自 ipc_object 类 */
rt_uint16_t value; /**< 信号量的值,最大为 65535 */
rt_uint16_t reserved; /**< 保留 */
};
rt_semaphore对象从 rt_ipc_object中派生,由 IPC容器所管理。信号量的最大值是65535。
常用信号量接口
只介绍几个常用的接口
创建信号量 rt_sem_create()
rt_sem_t
rt_sem_create
(const char* name, rt_uint32_t value, rt_uint8_t flag);
当调用这个函数时,系统将先分配一个semaphore对象,并初始化这个对象,然后初始化IPC对象以及与semaphore相关的部分。flag决定在信号量不可用时,线程等待的方式(FIFO方式或PRIO方式)。
参数 | 描述 |
---|---|
name | 信号量的名称 |
value | 信号量初始值 |
flag | 信号量等待方式 |
删除信号量 rt_sem_delete()
rt_err_t
rt_sem_delete
(rt_sem_t sem);
调用这个函数时,系统将删除这个信号量。如果删除该信号量时,有线程正在等待该信号量,那么删除操作会先唤醒等待在该信号量上的线程(等待线程的返回值是-RT_ERROR),然后再释放信号量的内存资源。
参数 | 描述 |
---|---|
sem | rt_sem_create创建处理的信号量对象 |
初始化信号量 rt_sem_init()
rt_err_t
rt_sem_init
(rt_sem_t sem, const char* name, rt_uint32_t value, rt_uint8_t flag);
当调用这个函数时,系统将先分配一个semaphore对象,并初始化这个对象,然后初始化IPC对象以及与semaphore相关的部分。flag决定在信号量不可用时,线程等待的方式(FIFO方式或PRIO方式)。
参数 | 描述 |
---|---|
sem | 信号量对象的句柄 |
name | 信号量的名称 |
value | 信号量初始值 |
flag | 信号量等待方式 |
信号量释放 rt_sem_release()
rt_err_t
rt_sem_release
(rt_sem_t sem);
当信号量的值等于零时,并且有线程等待这个信号量时,将唤醒等待在该信号量线程队列中的第一个线程,由它获取信号量。否则将把信号量的值加一。
参数 | 描述 |
---|---|
sem | 信号量对象的句柄 |
信号量获取 rt_sem_take()
rt_err_t
rt_sem_take
(rt_sem_t sem, rt_int32_t time);
在调用这个函数时,如果信号量的值等于零,那么说明当前信号量资源实例不可用,申请该信号量的线程将根据time参数的情况选择直接返回、或挂起等待一段时间、或永久等待,直到其他线程或中断释放该信号量。如果在参数time指定的时间内依然得不到信号量,线程将超时返回,返回值是-RT_ETIMEOUT。
参数 | 描述 |
---|---|
sem | 信号量对象的句柄 |
time | 指定的等待时间,单位是操作系统时钟节拍(OS Tick) |
信号量实验
代码参考《RT-Thread内核实现与应用开发实战——基于STM32》
二值信号量同步实验
本实验创建了两个线程,分别是接收线程和发送线程,实验功能:保证接收线程不打断发送线程内的数据更新。
#include "board.h"
#include "rtthread.h"
// 定义线程控制块指针
static rt_thread_t recv_thread = RT_NULL;
static rt_thread_t send_thread = RT_NULL;
// 定义信号量控制块
static rt_sem_t test_sem = RT_NULL;
// 应用程序要用到的全局变量
uint8_t ucValue[2] = {
0x00, 0x00};
/******************************************************************************
* @ 函数名 : recv_thread_entry
* @ 功 能 : 接收线程入口函数
* @ 参 数 : parameter 外部传入的参数
* @ 返回值 : 无
******************************************************************************/
static void recv_thread_entry(void *parameter)
{
while(1)
{
// 获取信号量,一直阻塞等待
rt_sem_take(test_sem, RT_WAITING_FOREVER);
if(ucValue[0] == ucValue[1]) //数据更新是否完成
{
rt_kprintf("sucess\n");
}
else
{
rt_kprintf("fail\n");
}
rt_sem_release(test_sem); // 释放信号量
rt_thread_delay(1000);
}
}
/******************************************************************************
* @ 函数名 : send_thread_entry
* @ 功 能 : 发送线程入口函数
* @ 参 数 : parameter 外部传入的参数
* @ 返回值 : 无
******************************************************************************/
static void send_thread_entry(void *parameter)
{
while(1)
{
// 获取信号量,一直阻塞等待
rt_sem_take(test_sem, RT_WAITING_FOREVER);
//数据更新
ucValue[0]++;
rt_thread_delay(100);
ucValue[1]++;
rt_sem_release(test_sem); // 释放信号量
rt_thread_yield(); // 放弃剩下的时间片,进行线程切换
}
}
int main(void)
{
// 硬件初始化和RTT的初始化已经在component.c中的rtthread_startup()完成
// 创建一个信号量
test_sem = // 信号量控制块指针
rt_sem_create("test_sem", // 信号量名字
1, // 信号量初始值
RT_IPC_FLAG_FIFO); // FIFO队列模式(先进先出)
if(test_sem != RT_NULL)
rt_kprintf("信号量创建成功!\n");
// 创建一个动态线程
recv_thread = // 线程控制块指针
rt_thread_create("recv", // 线程名字
recv_thread_entry, // 线程入口函数
RT_NULL, // 入口函数参数
255, // 线程栈大小
5, // 线程优先级
10); // 线程时间片
// 开启线程调度
if(recv_thread != RT_NULL)
rt_thread_startup(recv_thread);
else
return -1;
// 创建一个动态线程
send_thread = // 线程控制块指针
rt_thread_create("send", // 线程名字
send_thread_entry, // 线程入口函数
RT_NULL, // 入口函数参数
255, // 线程栈大小
5, // 线程优先级
10); // 线程时间片
// 开启线程调度
if(send_thread != RT_NULL)
rt_thread_startup(send_thread);
else
return -1;
}
实验现象
发送线程负责对两个全局变量进行更新,但是中间有100ms挂起状态,接收线程在发送线程处理完两个全局变量的更新后才能获取到信号量从而继续运行,同时打印“success”。
计数信号量实验
此实验模拟停车场,信号量初始信号量值为5,即有5个停车位,创建两个线程,分别进行信号量的接收(获取)和发送(释放)(获取相当于进入停车场停车,释放相当于离开停车场)。
下文不包括硬件初始化代码
#include "board.h"
#include "rtthread.h"
// 定义线程控制块指针
static rt_thread_t recv_thread = RT_NULL;
static rt_thread_t send_thread = RT_NULL;
// 定义信号量控制块
static rt_sem_t test_sem = RT_NULL;
/******************************************************************************
* @ 函数名 : recv_thread_entry
* @ 功 能 : 接收线程入口函数
* @ 参 数 : parameter 外部传入的参数
* @ 返回值 : 无
******************************************************************************/
static void recv_thread_entry(void *parameter)
{
rt_err_t uwRet = RT_EOK;
while(1)
{
// KEY0 被按下
if(Key_Scan(KEY0_GPIO_PORT, KEY0_GPIO_PIN) == KEY_ON)
{
// 获取一个信号量,不等待
uwRet = rt_sem_take(test_sem, 0);
if(uwRet == RT_EOK)
{
rt_kprintf("成功获取车位!\n");
}
else
{
rt_kprintf("停车位已满!\n");
}
}
rt_thread_delay(20);
}
}
/******************************************************************************
* @ 函数名 : send_thread_entry
* @ 功 能 : 发送线程入口函数
* @ 参 数 : parameter 外部传入的参数
* @ 返回值 : 无
******************************************************************************/
static void send_thread_entry(void *parameter)
{
rt_err_t uwRet = RT_EOK;
while(1)
{
// WK_UP 被按下
if(Key_Scan(WK_UP_GPIO_PORT, WK_UP_GPIO_PIN) == KEY_ON)
{
// 释放一个信号量
uwRet = rt_sem_release(test_sem);
if(uwRet == RT_EOK)
{
rt_kprintf("停车位+1!\n");
}
else
{
rt_kprintf("停车场已无车!\n");
}
}
rt_thread_delay(20);
}
}
int main(void)
{
// 硬件初始化和RTT的初始化已经在component.c中的rtthread_startup()完成
// 创建一个信号量
test_sem = // 信号量控制块指针
rt_sem_create("test_sem", // 信号量名字
5, // 信号量初始值
RT_IPC_FLAG_FIFO); // FIFO队列模式(先进先出)
if(test_sem != RT_NULL)
rt_kprintf("信号量创建成功!\n");
// 创建一个动态线程
recv_thread = // 线程控制块指针
rt_thread_create("recv", // 线程名字
recv_thread_entry, // 线程入口函数
RT_NULL, // 入口函数参数
255, // 线程栈大小
5, // 线程优先级
10); // 线程时间片
// 开启线程调度
if(recv_thread != RT_NULL)
rt_thread_startup(recv_thread);
else
return -1;
// 创建一个动态线程
send_thread = // 线程控制块指针
rt_thread_create("send", // 线程名字
send_thread_entry, // 线程入口函数
RT_NULL, // 入口函数参数
255, // 线程栈大小
5, // 线程优先级
10); // 线程时间片
// 开启线程调度
if(send_thread != RT_NULL)
rt_thread_startup(send_thread);
else
return -1;
}
实验现象
按下KEY0,获取车位,最多获取5个车位,如果超过5个,会打印车位已满;按下WK_UP按键,释放信号量,停车位+1。(但是信号量值为5时继续释放信号量是不会返回错误的)