RT-Thread版本:4.0.5
MCU型号:STM32F103RCT6(ARM Cortex-M3 内核)
1 事件集概念
事件是一种实现线程间通信的机制,主要用于实现线程间的同步,但事件通信只能是事件类型的通信,无数据传输。一个事件发生即是一个同步,事件集可以实现一对多(一个线程等待多个事件触发)、多对多(多个线程等待多个事件触发)的同步。
其中,一个线程与多个事件的关系可以设为:
- 特定事件触发唤醒线程
- 其中任意一个事件触发唤醒线程
- 几个事件都触发后才唤醒线程
1.1 事件集与信号量的区别
- 对于事件集,接收线程可以等待多个事件同步,即多个事件对应一个或多个线程;而信号量只能识别处理单一释放动作,而不能同时等待多种类型的释放。
- 事件的发送(相当于信号量的释放)在事件未清除前,是不可累加的,而信号量的释放动作是可以累加的(因此可以用于资源计数)。
1.2 事件集工作机制
事件的集合可以用一个 32 位无符号整型变量来表示,变量的每一位代表一个事件,线程通过 逻辑与
或逻辑或
将一个或多个事件关联起来,形成事件组合。
逻辑或
:也称为独立型同步,指线程与任何事件之一发生同步;逻辑与
:也称为关联型同步,指的是线程与若干事件都发生同步。
逻辑或
和逻辑与
只能二选一,不可一起使用
RT-Thread 定义的事件集有以下特点:
- 事件只与线程相关,事件相互独立,一个事件集中包含32个事件,采用一个 32 bit 无符号整型数进行记录,每一个 bit 代表一个事件(0表示该事件类型未发生,1表示该事件类型已发生);
- 事件仅用于同步,不提供数据传输功能;
- 事件无排队性,即多次向线程发生同一事件(如果线程没有及时读走),等效于只发送一次;
- 允许多个线程对同一事件进行读写操作。
每个线程都拥有一个事件信息标记,它有三个属性,分别是 RT_EVENT_FLAG_AND
(逻辑与),RT_EVENT_FLAG_OR
(逻辑或)以及 RT_EVENT_FLAG_CLEAR
(清除标记)。当线程等待事件同步时,可以通过32个事件标志和这个事件信息标记来判断当前接收的事件是否满足同步条件。
下面举例说明:
如上图所示,线程 #1 的事件标志中第 1 位和第 30 位被置位,此时,若事件信息标记位设为:
RT_EVENT_FLAG_AND
逻辑与:线程 #1 只有在事件 1 和事件 30 都发生以后才会被触发唤醒RT_EVENT_FLAG_OR
逻辑或:事件 1 或事件 30 中的任意一个发生都会触发唤醒线程 #1
如果信息标记同时设置了清除标记位RT_EVENT_FLAG_CLEAR
,则当线程 #1 唤醒后将主动把事件1 和事件 30 清为零,否则事件标志将依然存在(即该位依然为 1)。
问:为什么不用全局变量做标志呢?
答:裸机编程使用全局变量是没有问题的,但是在操作系统中,存在以下问题:
- 多线程同时访问全局变量时,如保护该临界资源?
- 使用全局变量,需要在线程中轮询查看事件是否发送,浪费CPU资源。
- 事件集具有等待超时机制,而全局变量只能由用户自己实现。
因此,在操作系统中,完全可以用事件集替代全局变量,做标志判断。
2 事件集控制块
事件集控制块比较简单:
struct rt_event
{
/* 继承自 ipc_object 类(含内核对象基本控制块rt_object 和 线程挂起等待链表) */
struct rt_ipc_object parent;
/* 事件集合,每一 bit 表示 1 个事件,bit 位的值可以标记某事件是否发生 */
rt_uint32_t set;
};
/* rt_event_t 是指向事件结构体的指针类型 */
typedef struct rt_event* rt_event_t;
3 事件集函数接口
3.1 创建/删除
- 创建事件集
rt_event_t rt_event_create(const char *name, rt_uint8_t flag)
{
rt_event_t event;
RT_DEBUG_NOT_IN_INTERRUPT;
/* 分配对象 */
event = (rt_event_t)rt_object_allocate(RT_Object_Class_Event, name);
if (event == RT_NULL)
return event;
/* 设置阻塞唤醒模式 */
event->parent.parent.flag = flag;
/* 初始化事件内核对象, 即初始化一个链表用于因记录访问此事件而被阻塞挂起的线程 */
_ipc_object_init(&(event->parent));
/* 事件集合清零 */
event->set = 0;
return event;
}
name
:事件集的名称flag
:事件集的标志:阻塞线程按优先级等待RT_IPC_FLAG_PRIO
或 按FIFO方式等待RT_IPC_FLAG_FIFO
,与信号量使用方法一致。(互斥量该标志已作废,内核均按RT_IPC_FLAG_PRIO
处理)- 删除事件集
rt_err_t rt_event_delete(rt_event_t event)
{
/* 事件句柄检查 */
RT_ASSERT(event != RT_NULL);
RT_ASSERT(rt_object_get_type(&event->parent.parent) == RT_Object_Class_Event);
RT_ASSERT(rt_object_is_systemobject(&event->parent.parent) == RT_FALSE);
RT_DEBUG_NOT_IN_INTERRUPT;
/* 恢复所有阻塞在此事件的线程 */
_ipc_list_resume_all(&(event->parent.suspend_thread));
/* 删除事件对象 */
rt_object_delete(&(event->parent.parent));
return RT_EOK;
}
当删除事件集时,所有等待此事件集的线程都将被唤醒(线程的返回值是 - RT_ERROR),然后系统再将该事件集从内核对象管理器链表中删除并释放内存。(删除一个事件集对象时,应确保该事件集不再被使用)
3.2 初始化/脱离
静态初始化与脱离(静态对象不能被删除)事件集对象,两函数成对使用,功能与上述函数一致,不再赘述。
3.3 发送事件
/**
* @brief 这个函数将向事件对象发送一个事件。如果事件中有一个线程挂起,该线程将被恢复。
*
* @note 当使用这个函数时,你需要使用参数(set)来指定事件对象的事件标志,然后函数将遍历挂起的等待事件对象的线程列表。
* 如果有线程挂起事件,并且线程的事件信息和当前事件对象的事件标志相匹配,线程将被恢复。
*
* @param event 事件集对象的结构体指针
*
* @param set 该事件设置的标志(可以设置一个事件标志,也可以通过逻辑或|操作设置多个标志)
*
* @return 当返回值为RT EOK时,表示操作成功。如果返回值是任何其他值,则意味着事件发生失败。
*/
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set)
{
struct rt_list_node *n;
struct rt_thread *thread;
register rt_ubase_t level;
register rt_base_t status;
rt_bool_t need_schedule;
/* 事件对象检查 */
RT_ASSERT(event != RT_NULL);
RT_ASSERT(rt_object_get_type(&event->parent.parent) == RT_Object_Class_Event);
if (set == 0)
return -RT_ERROR;
need_schedule = RT_FALSE;
/* 关中断 */
level = rt_hw_interrupt_disable();
/* 设置事件 */
event->set |= set;
RT_OBJECT_HOOK_CALL(rt_object_put_hook, (&(event->parent.parent)));
// 如果当前有线程因为等待某个事件进入阻塞态,则在阻塞列表中搜索线程
if (!rt_list_isempty(&event->parent.suspend_thread))
{
/* 搜索线程列表以恢复线程 */
n = event->parent.suspend_thread.next; // 从等待的线程中获取对应的线程控制块
while (n != &(event->parent.suspend_thread))
{
/* 找到要恢复的线程 */
thread = rt_list_entry(n, struct rt_thread, tlist);
status = -RT_ERROR;
// 如果线程等待事件的模式是 RT_EVENT_FLAG_AND(逻辑与),那么需要等待的事件都发生时才动作
if (thread->event_info & RT_EVENT_FLAG_AND)
{
if ((thread->event_set & event->set) == thread->event_set)
{
/* 收到了一个 AND 事件 */
status = RT_EOK;
}
}
// 如果线程等待事件的模式是 RT_EVENT_FLAG_OR(逻辑或),那么需线程等待的所有事件标记只要有1个及以上发生,即可唤醒该线程
else if (thread->event_info & RT_EVENT_FLAG_OR)
{
if (thread->event_set & event->set)
{
/* 保存收到的事件集(用于记录是哪个事件发生了) */
thread->event_set = thread->event_set & event->set;
/* 收到了一个 OR 事件 */
status = RT_EOK;
}
}
else // 其他数值值接返回无效参数
{
/* 开中断 */
rt_hw_interrupt_enable(level);
return -RT_EINVAL;
}
/* 将节点移动到下一个节点 */
n = n->next;
/* 条件满足,恢复线程 */
if (status == RT_EOK)
{
/* 清除事件标志位 */
if (thread->event_info & RT_EVENT_FLAG_CLEAR) // 如果在接收中设置了RT_EVENT_FLAG_CLEAR,线程唤醒时会将事件标志位清除, 防止一直响应时间
event->set &= ~thread->event_set;
/* 恢复阻塞的线程 */
rt_thread_resume(thread);
/* 需要进行线程调度 */
need_schedule = RT_TRUE;
}
}
}
/* 开中断 */
rt_hw_interrupt_enable(level);
/* 发起一次线程调度 */
if (need_schedule == RT_TRUE)
rt_schedule();
return RT_EOK;
}
可以理解为信号量/互斥量的释放操作
thread = rt_list_entry(n, struct rt_thread, tlist)这条语句作用是根据已知成员的地址,算出其结构体的首地址。具体原理可参考求结构体首地址rt_list_entry函数
线程对象有两个成员是专门用于处理事件集的:
thread->event_set
:若事件接收成功,则记录当前已接收到的所有感兴趣事件标志;若接收失败而挂起进行超时等待,则记录该线程所有感兴趣的事件标志(以便于后面成功唤醒线程后,可以利用该值顺利执行后续操作)。thread->event_info
:记录事件集关联与清除标志。
通过参数set指定的事件标志来设定event事件集对象的事件标志值,然后遍历等待在event事件集对象上的等待线程链表,判断是否有线程的事件激活要求与当前event对象事件标志值匹配,如果有则唤醒该线程(若设置了清除标志,则会先将相应的标志位清除event->set &= ~thread->event_set
)。
3.4 接收事件
/**
* @brief 这个函数将从事件对象接收一个事件。如果事件不可用,线程将等待该事件达到指定的时间。
*
* @param 事件发送操作的事件句柄(由用户自己定义,并且需要在创建事件后使用)
*
* @param set 接收线程感兴趣的事件(可以设置一个事件标志,也可以通过逻辑或|操作设置多个标志)
*
* @param Option 接收事件的选项:表示如何操作该接收事件。该选项可以为以下多个值中的一个或多个。
* 选择多个值时,请使用“逻辑或”进行操作。(注:RT EVENT FLAG OR和RT EVENT FLAG and只能选择一个):
*
* RT_EVENT_FLAG_OR 线程选择使用逻辑或来接收事件。
*
* RT_EVENT_FLAG_AND 线程选择使用逻辑与来接收事件。
*
* RT_EVENT_FLAG_CLEAR 当线程收到相应的事件时,函数决定是否清除事件标志。
*
* @param timeout 设置等待的超时时间(单位: 1 OS tick).
*
* @param recved 指向已接收事件的指针(若不关心可用RT_NULL进行设置):用于保存接收到的事件标志结果,用户通过它的值判断是否成功接收到事件
*
* @return 返回操作状态。当返回值为RT EOK时,表示操作成功。如果返回值为其他值,则表示事件接收失败。
*/
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)
{
struct rt_thread *thread;
register rt_ubase_t level;
register rt_base_t status;
RT_DEBUG_IN_THREAD_CONTEXT;
/* 检查事件句柄 */
RT_ASSERT(event != RT_NULL);
RT_ASSERT(rt_object_get_type(&event->parent.parent) == RT_Object_Class_Event);
if (set == 0)
return -RT_ERROR;
/* 初始化状态 */
status = -RT_ERROR;
/* 获取当前线程信息, 即获取调用接收事件的线程 */
thread = rt_thread_self();
/* 重置线程错误码 */
thread->error = RT_EOK;
RT_OBJECT_HOOK_CALL(rt_object_trytake_hook, (&(event->parent.parent)));
/* 关中断 */
level = rt_hw_interrupt_disable();
/* 检查事件接收选项 */
if (option & RT_EVENT_FLAG_AND) // 逻辑与:事件集合里的信息与线程感兴趣的信息全部吻合才标记接收成功
{
if ((event->set & set) == set)
status = RT_EOK;
}
else if (option & RT_EVENT_FLAG_OR) // 逻辑或:事件集合里的信息与线程感兴趣的信息只要有1个吻合就标记接收成功
{
if (event->set & set)
status = RT_EOK;
}
else
{
/* 应设置 RT_EVENT_FLAG_AND 或 RT_EVENT_FLAG_OR */
RT_ASSERT(0);
}
if (status == RT_EOK)
{
/* 返回接收的事件 */
if (recved)
*recved = (event->set & set);
/* 设置线程事件信息 */
thread->event_set = (event->set & set);
thread->event_info = option;
/* 接收事件清除 */
if (option & RT_EVENT_FLAG_CLEAR) //如果指定的option接收选项选择了RT_EVENT_FLAG_CLEAR,在接收完成的时候会清除对应的事件集合的标志位
event->set &= ~set;
}
else if (timeout == 0)
{
/* 不等待 */
thread->error = -RT_ETIMEOUT;
/* 关中断 */
rt_hw_interrupt_enable(level);
return -RT_ETIMEOUT;
}
else
{
/* 设置线程事件信息 */
thread->event_set = set;
thread->event_info = option;
/* 将线程添加到阻塞列表中 */
_ipc_list_suspend(&(event->parent.suspend_thread),
thread,
event->parent.parent.flag);
/* 如果有等待超时,则启动线程计时器 */
if (timeout > 0)
{
/* 重置线程超时时间并且启动定时器 */
rt_timer_control(&(thread->thread_timer),
RT_TIMER_CTRL_SET_TIME,
&timeout);
rt_timer_start(&(thread->thread_timer));
}
/* 开中断 */
rt_hw_interrupt_enable(level);
/* 发起一次线程调度 */
rt_schedule();
if (thread->error != RT_EOK)
{
/* 返回错误代码 */
return thread->error;
}
/* 接收一个事件,失能中断 */
level = rt_hw_interrupt_disable();
/* 返回接收到的事件 */
if (recved)
*recved = thread->event_set;
}
/* 开中断 */
rt_hw_interrupt_enable(level);
RT_OBJECT_HOOK_CALL(rt_object_take_hook, (&(event->parent.parent)));
return thread->error;
}
可以理解为信号量/互斥量的获取操作
调用该函数时,系统会根据set感兴趣事件参数和接收选项option(RT_EVENT_FLAG_OR
和RT_EVENT_FLAG_AND
)来判断它要接收的事件是否发生:
- 若已发生:根据参数option上是否设置
RT_EVENT_FLAG_CLEAR
决定是否清除事件相应标志位,然后返回(其中recved参数用于保存收到的事件) - 若没有发生:则把线程感兴趣的事件set和接收选项option填写到线程控制块中,然后把线程挂起在此事件对象的阻塞列表上,直到事件发生或等待时间超时(返回
- RT_ETIMEOUT
)
注:如果线程将option设置为需要清除标志,仅清除当前线程感兴趣且已发生事件的标志位,其它不感兴趣或感兴趣但未发生的事件标志位不受影响。所谓清除就是将事件对应的标志位清0,对于感兴趣但未发生的事件标志位本来就是0,0进行逻辑与操作当然还是0(event->set &= ~set
),对于不感兴趣的事件标志位清除时会和1进行逻辑与操作,保持原状态不变跳转到的地方。
在中断服务例程中可发送事件给事件集对象
感觉完全可以用
thread->event_set
替代*recved
所起到的作用,因为不管是接收成功还是超时等待它们的值都是相等的。如果接收成功,则表示当前已接收到的所有感兴趣事件标志值;如果超时等待,则表示该线程所有感兴趣的事件标志值(不管有没有收到该事件)。
4 事件集应用示例
本应用例程基于上文事件集工作机制举例,代码如下:
/*
* Date Author
* 2022-02-08 issac wan
*/
#include <rtthread.h>
#define my_printf(fmt, ...) rt_kprintf("[%u]"fmt"\n", rt_tick_get(), ##__VA_ARGS__)
#define THREAD_STACK_SIZE 512
#define THREAD_PRIORITY 20
#define THREAD_TIMESLICE 5
#define EVENT_FLAG1 (1 << 1)
#define EVENT_FLAG30 (1 << 30)
static struct rt_event event; // 静态事件集对象
static rt_thread_t thread1 = RT_NULL;
static rt_thread_t thread2 = RT_NULL;
/* 线程1入口函数——接收事件 */
static void thread_entry1(void* param){ // 顺序执行模式, 执行完毕后, 线程将被系统自动删除
rt_uint32_t event_recved;
/* 第一次接收事件,事件1或事件30任意一个可以触发线程1,接收完后清除事件标志 */
rt_event_recv(&event,
EVENT_FLAG1 | EVENT_FLAG30,
RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER,
&event_recved);
if (event_recved & (EVENT_FLAG1 | EVENT_FLAG30)){
my_printf("thread1: OR recv event 0x%08x", event_recved);
}
my_printf("thread1: delay 1s to prepare the second event");
rt_thread_mdelay(1000);
/* 第二次接收事件,事件1和事件30均发生时才可以触发线程1,接收完后清除事件标志 */
if (rt_event_recv(&event, // 或写成 if (event_recved == (EVENT_FLAG1 | EVENT_FLAG30))
EVENT_FLAG1 | EVENT_FLAG30,
RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR,
RT_WAITING_FOREVER,
&event_recved) == RT_EOK){
my_printf("thread1: AND recv event 0x%08x", event_recved);
}
my_printf("thread1 leave.");
}
/* 线程2入口函数——发送事件 */
static void thread_entry2(void* param){
my_printf("thread2: send event1");
rt_event_send(&event, EVENT_FLAG1);
rt_thread_mdelay(200);
my_printf("thread2: send event30");
rt_event_send(&event, EVENT_FLAG30);
rt_thread_mdelay(200);
my_printf("thread2: send event1");
rt_event_send(&event, EVENT_FLAG1);
my_printf("thread2 leave.");
rt_event_detach(&event); // 将静态事件集对象从内核对象链表中脱离
}
int event_sample(void){
rt_err_t result;
// 初始化静态事件集对象
result = rt_event_init(&event, "event", RT_IPC_FLAG_PRIO);
if (result != RT_EOK)
return -RT_ERROR;
thread1 = rt_thread_create("thread1",
thread_entry1,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY - 1,
THREAD_TIMESLICE);
if (thread1 == RT_NULL)
return -RT_ERROR;
else
rt_thread_startup(thread1);
thread2 = rt_thread_create("thread2",
thread_entry2,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY,
THREAD_TIMESLICE);
if (thread2 == RT_NULL)
return -RT_ERROR;
else
rt_thread_startup(thread2);
return RT_EOK;
}
INIT_APP_EXPORT(event_sample);
串口打印信息如下:
- 静态创建一个事件集对象,线程1前后两次接收事件(事件1和事件30),分别使用了 “逻辑或” 与“逻辑与”的方法。
- 线程2线程发生事件1,线程1采用 “逻辑或”方法接收后打印事件标志位信息,然后延时1s等待。然后线程2再发送事件30和事件1,线程1延时结束后采用“逻辑与”的方法接收事件,然后打印事件标志位信息,最后将该静态事件集对象从内核对象链表中脱离。
5 总结
- 事件集包含32个事件,用
uint32_t
变量表示,每一位代表一个事件,主要用于同步,可以实现一对多、多对多的同步,但不能传输数据。 - 每个事件相互独立,线程通过
逻辑或
与逻辑与
将事件关联起来(只能二选一),线程接收事件时可选择是否清除相应的事件标志位,但事件的发送在事件未清除之前,是不可以累加的。
事件标志位是在发送与接收事件函数中清除的:
- 发送事件函数:如果当前有线程因为等待某个事件进入阻塞态,当该事件发生后会唤醒该线程,若设置了清除标志位,则会在唤醒前执行
event->set &= ~thread->event_set
清除相应标志位,然后进行线程调度。- 接收事件函数:当调用该函数时,事件集合里面的事件与线程感谢的信息吻合时,若设置了清除标志位,则会执行
event->set &= ~set
清除相应标志位,然后返回RT_EOK
。
- 事件集与信号量区别是,信号量只能识别单一释放动作,相当于只有事件集的一个事件标志位,但是信号量释放动作支持累加,因此可以用于资源计数。
- 可以在中断服务例程里发送事件给事件集对象,但不能在中断里接收事件。
END