上文说到 RT-Thread 对临界区的处理方式有多种,其中已经分析了关闭调度器和屏蔽中断的方式,
本文就来学学另外的线程同步方式。
目录
前言
一、IPC机制
二、信号量
2.1 信号量控制块
2.2 信号量操作
2.2.1 创建和删除
2.2.2 初始化和脱离
2.2.3 获取信号量
2.2.4 释放信号量
2.2.5 信号量控制
2.3 示例(典型停车场模型)
三、互斥量
3.1 优先级翻转
3.2 优先级继承
3.3 互斥量控制块
3.4 互斥量操作
3.2.1 创建和删除
3.2.2 初始化和脱离
3.2.3 获取互斥量
3.2.4 释放互斥量
3.5 示例(优先级继承)
四、事件集
4.1 事件集控制块
4.2 事件集操作
4.2.1 创建和删除
4.2.2 初始化和脱离
4.2.3 发送事件
4.2.4 接收事件
4.3 示例(逻辑与和逻辑或)
结语
在我们专栏前面的文章中,已经学习过 RT-Thread 线程操作函数、软件定时器、临界区的保护,我们都进行了一些底层的分析,能让我们更加理解 RT-Thread 的内核,但是也不要忽略了上层的函数使用 要理解 RT-Thread 面向对象的思想,对所有的这些线程啊,定时器,包括要介绍的信号量,邮箱这些,都是以 对象 来操作,直白的说来就是 对于所有这些对象,都是以结构体的形式来表示,然后通过对这个对象结构体的操作来进行的。
本文所要介绍的内容属于 IPC机制,这些内容相对来说比较简单,我们重点在于学会如何使用以及了解他们的使用场合。
本 RT-Thread 专栏记录的开发环境:
RT-Thread记录(一、版本开发环境及配合CubeMX) + https://www.elecfans.com/d/1850333.html
RT-Thread记录(二、RT-Thread内核启动流程)+ https://www.elecfans.com/d/1850347.html
RT-Thread 内核篇系列博文链接:
RT-Thread记录(三、RT-Thread线程操作函数)+ https://www.elecfans.com/d/1850351.html
RT-Thread记录(四、RTT时钟节拍和软件定时器)+ https://www.elecfans.com/d/1850554.html
RT-Thread记录(五、RT-Thread 临界区保护) + https://www.elecfans.com/d/1850712.html
在嵌入式操作系统中,运行代码主要包括线程 和 ISR,在他们的运行过程中,因为应用或者多线程模型带来的需求,有时候需要同步,有时候需要互斥,有时候也需要彼此交换数据。操作系统必须提供相应的机制来完成这些功能,这些机制统称为 线程间通信(IPC机制)。
本文所要介绍的就是关于线程同步的信号量、互斥量、事件 也属于 IPC机制。
RT-Thread 中的 IPC机制包括信号量、互斥量、事件、邮箱、消息队列。对于学习 RT-Thread ,这些IPC机制我们必须要学会灵活的使用。
为什么要说一下这个IPC机制?
我们前面说到过,RT-Thread 面向对象的思想,所有的这些 IPC 机制都被当成一个对象,都有一个结构体控制块,我们用信号量结构体来看一看:
Kernel object
有哪些,我们可以从基础内核对象结构体定义下面的代码找到:
本节说明了 RT-Thread 的 IPC 机制,同时通过 信号量的结构体控制块再一次的认识了 RT-Thread 面向对象的设计思想。
在我的 FreeRTOS 专栏中,对于FreeRTOS 的信号量,互斥量,事件集做过说明和测试。在这个部分,实际上 RT-Thread 与 FreeRTOS 是类似的,都是一样的思想。所以如果属熟悉FreeRTOS的话,这部分是简单的,我们要做的就是记录一下 对象的控制块,和操作函数,加以简单的示例测试。
信号量官方的说明是:信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。
信号量非常灵活,可以使用的场合也很多:
在 FreeRTOS 中存在二值信号量,但是 RT-Thread 中已经没有了,官方有说明:
信号量记住一句话基本就可以,释放一次信号量就+1,获取一次就-1,如果信号量数据为0,那么尝试获取的线程就会挂机,直到有线程释放信号量使得信号量大于0。
老规矩用源码,解释看注释(使用起来也方便复制 ~ ~!):
#ifdef RT_USING_SEMAPHORE
/**
* Semaphore structure
* value 信号量的值,直接表明目前信号量的数量
*/
struct rt_semaphore
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint16_t value; /**< value of semaphore. */
rt_uint16_t reserved; /**< reserved field */
};
/*
rt_sem_t 是指向 semaphore 结构体的指针类型
*/
typedef struct rt_semaphore *rt_sem_t;
#endif
同以前的线程那些一样,动态的方式,先定义一个信号量结构体的指针变量,接收创建好的句柄。
创建信号量:
/*
参数的含义:
1、name 信号量名称
2、value 信号量初始值
3、flag 信号量标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回值:
信号量创建成功,返回信号量的控制块指针
信号量创建失败,返回RT_BULL
*/
rt_sem_t rt_sem_create(const char *name, rt_uint32_t value, rt_uint8_t flag)
对于最后的参数 flag,决定了当信号量不可用时(就是当信号量为0的时候),多个线程等待的排队方式。只有RT_IPC_FLAG_FIFO
(先进先出)或 RT_IPC_FLAG_PRIO
(优先级等待)两种 flag。
关于用哪一个,要看具体的情况,官方有特意说明:
删除信号量:
/*
参数:
sem rt_sem_create() 创建的信号量对象,信号量句柄
返回值:
RT_EOK 删除成功
*/
rt_err_t rt_sem_delete(rt_sem_t sem)
静态的方式,先定义一个信号量结构体,然后对他进行初始化。
初始化信号量:
/**
参数的含义:
1、sem 信号量对象的句柄,就是开始定义的信号量结构体变量
2、name 信号量名称
3、value 信号量初始值
4、flag 信号量标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回值:
RT_EOK 初始化成功
*/
rt_err_t rt_sem_init(rt_sem_t sem,
const char *name,
rt_uint32_t value,
rt_uint8_t flag)
脱离信号量:
/*
参数:
sem 信号量对象的句柄
返回值:
RT_EOK 脱离成功
*/
rt_err_t rt_sem_detach(rt_sem_t sem);
当信号量值大于零时,线程将获得信号量,并且相应的信号量值会减 1。
/**
参数:
1、sem 信号量对象的句柄
2、time 指定的等待时间,单位是操作系统时钟节拍(OS Tick)
返回值:
RT_EOK 成功获得信号量
-RT_ETIMEOUT 超时依然未获得信号量
-RT_ERROR 其他错误
*/
rt_err_t rt_sem_take(rt_sem_t sem, rt_int32_t time)
注意!要等待的时间是系统时钟节拍(OS Tick)。
无等待获取信号量:
//就是上面获取的等待时间为0的方式
rt_err_t rt_sem_trytake(rt_sem_t sem)
{
return rt_sem_take(sem, 0);
}
当线程申请的信号量资源实例为0时,直接返回 - RT_ETIMEOUT。
释放信号量可以使得该信号量+1,如果有线程在等待这个信号量,可以唤醒这个线程。
/**
参数:
sem 信号量对象的句柄
返回值:
RT_EOK 成功释放信号量
*/
rt_err_t rt_sem_release(rt_sem_t sem)
信号量控制函数,用来重置信号量,使得信号量恢复为设定的值:
/**
* This function can get or set some extra attributions of a semaphore object.
参数:
sem 信号量对象的句柄
cmd 信号量控制命令 ,支持命令:RT_IPC_CMD_RESET
arg 暂时不知道
返回值:
RT_EOK 成功释放信号量
*/
rt_err_t rt_sem_control(rt_sem_t sem, int cmd, void *arg)
{
rt_ubase_t level;
/* parameter check */
RT_ASSERT(sem != RT_NULL);
RT_ASSERT(rt_object_get_type(&sem->parent.parent) == RT_Object_Class_Semaphore);
if (cmd == RT_IPC_CMD_RESET)
{
rt_ubase_t value;
/* get value */
value = (rt_ubase_t)arg;
/* disable interrupt */
level = rt_hw_interrupt_disable();
/* resume all waiting thread */
rt_ipc_list_resume_all(&sem->parent.suspend_thread);
/* set new value */
sem->value = (rt_uint16_t)value;
/* enable interrupt */
rt_hw_interrupt_enable(level);
rt_schedule();
return RT_EOK;
}
return -RT_ERROR;
}
使用示例:
rt_err_t result;
rt_uint32_t value;
value = 10; /* 重置的值,即重置为10 */
result = rt_sem_control(sem, RT_IPC_CMD_RESET, (void*)value)
/* 重置为0 */
rt_sem_control(sem, RT_IPC_CMD_RESET, RT_NULL)
对sem重置后,会先把sem上挂起的所有任务进行唤醒(任务的error是-RT_ERROR),然后把sem的值会重新初始化成设定的值。
在官方论坛有如下说明:
在rt_sem_release后使用rt_sem_control的目的是因为在某些应用中必须rt_sem_take和rt_sem_release依次出现,而不允许rt_sem_release被连续多次调用,一旦出现这种情况会被认为是出现了异常,通过调用rt_sem_control接口来重新初始化 sem_ack恢复异常。
前面说到过,信号量非常灵活,可以使用的场合也很多,官方也有很多例子,我们这里做个典型的示例
— 停车场模型(前面用截图做解释,后面会附带源码)。
示例中,我们使用两个不同的按键来模拟车辆的进出,但是考虑到我们还没有学设备和驱动,没有添加按键驱动,所以我们用古老的方式来实现按键操作:
按键key3,代表车辆离开:
按键key2,代表车辆进入:
信号量的创建,初始10个车位:
当然不要忘了,车辆进入和车辆离开(两个按键)是需要两个线程的。
我们来看看测试效果,说明如图:
注意上图测试最后的细节,虽然 one car get out!
但是打印出来的停车位还是0,可以这么理解,key3_thread_entry
线程释放了信号量以后还没来得及打印,等待信号量的线程key2_thread_entry
就获取到了信号量。
具体的分析需要看rt_sem_release
函数源码,里面会判断是否需要值+1,以及是否需要调度:
附上上面测试代码:
/*
* Copyright (c) 2006-2022, RT-Thread Development Team
*
* SPDX-License-Identifier: Apache-2.0
*
* Change Logs:
* Date Author Notes
* 2022-02-16 RT-Thread first version
*/
#include
#include "main.h"
#include "usart.h"
#include "gpio.h"
#define DBG_TAG "main"
#define DBG_LVL DBG_LOG
#include
static struct rt_thread led1_thread; //led1线程
static char led1_thread_stack[256];
static rt_thread_t led2_thread = RT_NULL; //led2线程
static rt_thread_t key2_thread = RT_NULL; //
static rt_thread_t key3_thread = RT_NULL; //
rt_sem_t mysem;
static void led1_thread_entry(void *par){
while(1){
LED1_ON;
rt_thread_mdelay(1000);
LED1_OFF;
rt_thread_mdelay(1000);
}
}
static void led2_thread_entry(void *par){
while(1){
LED2_ON;
rt_thread_mdelay(500);
LED2_OFF;
rt_thread_mdelay(500);
}
}
static void key2_thread_entry(void *par){
static rt_err_t result;
while(1){
if(key2_read == 0){
rt_thread_mdelay(10); //去抖动
if(key2_read == 0){
result = rt_sem_take(mysem, 1000);
if (result != RT_EOK)
{
rt_kprintf("the is no parking spaces now...\r\n");
}
else
{
rt_kprintf("one car get in!,we have %d parking spaces now...\r\n",mysem->value);
}
while(key2_read == 0){rt_thread_mdelay(10);}
}
}
rt_thread_mdelay(1);
}
}
static void key3_thread_entry(void *par){
while(1){
if(key3_read == 0){
rt_thread_mdelay(10); //去抖动
if(key3_read == 0){
if(mysem->value < 10){
rt_sem_release(mysem);
rt_kprintf("one car get out!,we have %d parking spaces now...\r\n",mysem->value);
}
while(key3_read == 0){rt_thread_mdelay(10);} //去抖动
}
}
rt_thread_mdelay(1);
}
}
int main(void)
{
MX_GPIO_Init();
MX_USART1_UART_Init();
rt_err_t rst2;
rst2 = rt_thread_init(&led1_thread,
"led1_blink ",
led1_thread_entry,
RT_NULL,
&led1_thread_stack[0],
sizeof(led1_thread_stack),
RT_THREAD_PRIORITY_MAX -1,
50);
if(rst2 == RT_EOK){
rt_thread_startup(&led1_thread);
}
mysem = rt_sem_create("my_sem1", 10, RT_IPC_FLAG_FIFO);
if(RT_NULL == mysem){
LOG_E("create sem failed!...\n");
}
else LOG_D("we have 10 parking spaces now...\n");
key2_thread = rt_thread_create("key2_control",
key2_thread_entry,
RT_NULL,
512,
RT_THREAD_PRIORITY_MAX -2,
50);
/* 如果获得线程控制块,启动这个线程 */
if (key2_thread != RT_NULL)
rt_thread_startup(key2_thread);
key3_thread = rt_thread_create("key3_control",
key3_thread_entry,
RT_NULL,
512,
RT_THREAD_PRIORITY_MAX -2,
50);
/* 如果获得线程控制块,启动这个线程 */
if (key3_thread != RT_NULL)
rt_thread_startup(key3_thread);
return RT_EOK;
}
void led2_Blink(){
led2_thread = rt_thread_create("led2_blink",
led2_thread_entry,
RT_NULL,
256,
RT_THREAD_PRIORITY_MAX -1,
50);
/* 如果获得线程控制块,启动这个线程 */
if (led2_thread != RT_NULL)
rt_thread_startup(led2_thread);
}
MSH_CMD_EXPORT(led2_Blink, Led2 sample);
互斥量是一种特殊的二值信号量。互斥量的状态只有两种,开锁或闭锁(两种状态值)。
互斥量支持递归,持有该互斥量的线程也能够再次获得这个锁而不被挂起。自己能够再次获得互斥量。
互斥量可以解决优先级翻转问题,它能够实现优先级继承。
互斥量互斥量不能在中断服务例程中使用。
优先级翻转,我以前写过:
再用官方的图加深理解:
优先级继承,以前也写过:
再用官方的图加深理解:
需要切记的是互斥量不能在中断服务例程中使用。
#ifdef RT_USING_MUTEX
/**
* Mutual exclusion (mutex) structure
* parent 继承ipc类
* value 互斥量的值
* original_priority 持有线程的原始优先级
* hold 持有线程的持有次数,可以多次获得
* *owner 当前拥有互斥量的线程
*/
struct rt_mutex
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint16_t value; /**< value of mutex */
rt_uint8_t original_priority; /**< priority of last thread hold the mutex */
rt_uint8_t hold; /**< numbers of thread hold the mutex */
struct rt_thread *owner; /**< current owner of mutex */
};
/* rt_mutext_t 为指向互斥量结构体的指针类型 */
typedef struct rt_mutex *rt_mutex_t;
#endif
先定义一个指向互斥量结构体的指针变量,接收创建好的句柄。
创建互斥量:
/**
参数的含义:
1、name 互斥量名称
2、flag 该标志已经作废,无论用户选择 RT_IPC_FLAG_PRIO 还是 RT_IPC_FLAG_FIFO,
内核均按照 RT_IPC_FLAG_PRIO 处理
返回值:
互斥量创建成功,返回互斥量的控制块指针
互斥量创建失败,返回RT_BULL
*/
rt_mutex_t rt_mutex_create(const char *name, rt_uint8_t flag)
删除互斥量:
/**
参数:
mutex 互斥量对象的句柄
返回值:
RT_EOK 删除成
*/
rt_err_t rt_mutex_delete(rt_mutex_t mutex)
静态的方式,先定义一个互斥量结构体,然后对他进行初始化。
初始化互斥量:
/**
参数的含义:
1、mutex 互斥量对象的句柄,指向互斥量对象的内存块,开始定义的结构体
2、name 互斥量名称
3、flag 该标志已经作废,按照 RT_IPC_FLAG_PRIO (优先级)处理
返回值:
RT_EOK 初始化成功
*/
rt_err_t rt_mutex_init(rt_mutex_t mutex, const char *name, rt_uint8_t flag)
脱离互斥量:
/**
参数:
mutex 互斥量对象的句柄
返回值:
RT_EOK 成功
*/
rt_err_t rt_mutex_detach(rt_mutex_t mutex)
一个时刻一个互斥量只能被一个线程持有。
如果互斥量没有被其他线程控制,那么申请该互斥量的线程将成功获得该互斥量。如果互斥量已经被当前线程线程控制,则该互斥量的持有计数加 1,当前线程也不会挂起等待。
/**
参数:
1、mutex 互斥量对象的句柄
2、time 指定的等待时间,单位是操作系统时钟节拍(OS Tick)
返回值:
RT_EOK 成功获得互斥量
-RT_ETIMEOUT 超时依然未获得互斥量
-RT_ERROR 获取失败
*/
rt_err_t rt_mutex_take(rt_mutex_t mutex, rt_int32_t time)
在获得互斥量后,应该尽可能的快释放互斥量。
/**
参数:
mutex 互斥量对象的句
返回值:
RT_EOK 成功
*/
rt_err_t rt_mutex_release(rt_mutex_t mutex)
互斥量做一个简单的示例,但是即便简单,也能体现出优先级继承这个机制。
示例中,我们使用两个按键,key2按键,按一次获取互斥量,再按一次释放互斥量,打印自己初始优先级,当前优先级,互斥量占有线程优先级这几个量。key3按键,按一次,获取互斥量,立马就释放,也打印几个优先级。
互斥量的创建,和两个线程的优先级:
key2操作:
key3操作:
测试结果说明图:
示例中为了更好的演示并没有快进快出,实际使用还是需要快进快出,除非你自己就是有这种特出需求。
还有一个细节,就是 RT-Thread 中对象的 名字,只能显示8个字符长度,长了会截断,并不影响使用。
事件集这部分与 FreeRTOS 基本一样。
事件集主要用于线程间的同步,它的特点是可以实现一对多,多对多的同步。即一个线程与多个事件的关系可设置为:其中任意一个事件唤醒线程,或几个事件都到达后才唤醒线程进行后续的处理;同样,事件也可以是多个线程同步多个事件。
RT-Thread 定义的事件集有以下特点:
#ifdef RT_USING_EVENT
/**
* flag defintions in event
* 逻辑与
* 逻辑或
* 清除标志位
*/
#define RT_EVENT_FLAG_AND 0x01 /**< logic and */
#define RT_EVENT_FLAG_OR 0x02 /**< logic or */
#define RT_EVENT_FLAG_CLEAR 0x04 /**< clear flag */
/*
* event structure
* set:事件集合,每一 bit 表示 1 个事件,bit 位的值可以标记某事件是否发生
*/
struct rt_event
{
struct rt_ipc_object parent; /**< inherit from ipc_object */
rt_uint32_t set; /**< event set */
};
/* rt_event_t 是指向事件结构体的指针类型 */
typedef struct rt_event *rt_event_t;
#endif
先定义一个指向事件集结构体的指针变量,接收创建好的句柄。
创建事件集:
/**
参数的含义:
1、name 事件集的名称
2、flag 事件集的标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO理
返回值:
事件集创建成功,返回事件集的控制块指针
事件集创建失败,返回RT_BULL
*/
rt_event_t rt_event_create(const char *name, rt_uint8_t flag)
flag 使用哪一个,解释和信号量一样,可参考信号量创建部分说明。
删除事件集:
/**
参数:
event 事件集对象的句柄
返回值:
RT_EOK 成功
*/
rt_err_t rt_event_delete(rt_event_t event)
静态的方式,先定义一个事件集结构体,然后对他进行初始化。
初始化事件集:
/**
参数的含义:
1、event 事件集对象的句柄
2、name 事件集的名称
3、flag 事件集的标志,它可以取如下数值: RT_IPC_FLAG_FIFO 或 RT_IPC_FLAG_PRIO
返回值:
RT_EOK 初始化成功
*/
rt_err_t rt_event_init(rt_event_t event, const char *name, rt_uint8_t flag)
脱离事件集:
/**
参数:
event 事件集对象的句柄
返回值:
RT_EOK 成功
*/
rt_err_t rt_event_detach(rt_event_t event)
发送事件函数可以发送事件集中的一个或多个事件。
/**
参数的含义:
1、event 事件集对象的句柄
2、set 发送的一个或多个事件的标志值
返回值:
RT_EOK 成功
*/
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set)
内核使用 32 位的无符号整数来标识事件集,它的每一位代表一个事件,因此一个事件集对象可同时等待接收 32 个事件,内核可以通过指定选择参数 “逻辑与” 或“逻辑或”来选择如何激活线程。
/**
参数的含义:
1、event 事件集对象的句柄
2、set 接收线程感的事件
3、option 接收选项,可取的值为
#define RT_EVENT_FLAG_AND 0x01 逻辑与
#define RT_EVENT_FLAG_OR 0x02 逻辑或
#define RT_EVENT_FLAG_CLEAR 0x04 选择清除重置事件标志位
4、timeout 指定超时时间
5、recved 指向接收到的事件,如果不在意,可以使用 NULL
返回值:
RT_EOK 成功
-RT_ETIMEOUT 超时
-RT_ERROR 错误
*/
rt_err_t rt_event_recv(rt_event_t event,
rt_uint32_t set,
rt_uint8_t option,
rt_int32_t timeout,
rt_uint32_t *recved)
事件集通过示例可以很好的理解怎么使用,我们示例中,用按钮发送事件,其他线程接收事件,进行对应的处理。
按键操作:
线程逻辑或处理:
逻辑或测试结果:
线程逻辑与处理:
逻辑与测试结果:
本文虽然只是介绍了信号量、互斥量和事件集这几个比较简单的线程同步操作,但是最终完成了后发现内容还是很多的。
洋洋洒洒这么多字,最终看下来自己还是挺满意的,希望我把该表述的都表达清楚了,希望大家多多提意见,让博主能给大家带来更好的文章。
那么下一篇的 RT-Thread 记录,就要来说说与线程通讯 有关的 邮箱、消息队列和信号内容了。
谢谢!
全部0条评论
快来发表一下你的评论吧 !