RT-Thread 中的事件集,也就是其他 RTOS 中的事件标志组。事件集也是线程(任务)间同步的一种机制。
前面介绍的两种线程间同步的方式(信号量和互斥量)都是一对一;而事件集可以实现一对多、多对多的线程同步。
第一:事件集的工作机制
1. 理解事件集
多个事件的集合用一个 32 位无符号整型变量来表示,变量的每一位代表一个事件,线程通过 “逻辑与” 或 “逻辑或” 将一个或多个事件关联起来,形成事件组合。
RT-Thread 中的事件集有以下特点:
- 事件只与线程相关,事件间相互独立。
- 事件仅用于同步,不提供数据传输功能
- 事件无排队性,即多次发送同一个事件(且线程还未读走),其效果等同于发送了一次。
当线程等待事件同步时,可以通过 32 个事件标志和这个事件信息标记来判断当前接收的事件是否满足同步条件。
举例理解事件集,如下图所示,线程 #1 的事件标志中第 1 位和第 30 位被置位,等待事件发生。
- 逻辑与,则表示线程 #1 只有在事件 1 和事件 30 都发生以后才会被触发唤醒。
- 逻辑或,则事件1 或事件 30 中的任意一个发生都会触发唤醒线程 #1。
- 同时设置了清除标记位,则当线程 #1 唤醒后将主动把事件 1 和事件 30 清为零,否则事件标志将依然存在(即置 1)。
2. 事件集控制块
在 RT-Thread 中,操作系统管理事件的数据结构称为事件集控制块,由结构体 struct rt_event
表示。另外,rt_event_t
表示的是事件集的句柄,即指向事件集控制块的指针。
事件集控制块结构体定义如下:
struct rt_event { /* 继承自 ipc_object 类 */ struct rt_ipc_object parent; /* 事件集合,每一 bit 表示 1 个事件,bit 位的值可以标记某事件是否发生 */ rt_uint32_t set; }; /* rt_event_t 是指向事件结构体的指针类型 */ typedef struct rt_event* rt_event_t;
rt_event
对象从 rt_ipc_object
中派生,由 IPC容器管理。结构体 rt_ipc_object
定义如下:
struct rt_object { char name[RT_NAME_MAX]; /* 内核对象名称 */ rt_uint8_t type; /* 内核对象类型 */ rt_uint8_t flag; /* 内核对象的参数 */ #ifdef RT_USING_MODULE void *module_id; /* 应用程序模块 ID */ #endif rt_list_t list; /* 内核对象管理链表 */ }; struct rt_ipc_object { struct rt_object parent; /* 继承自 rt_object */ rt_list_t suspend_thread; /* 挂起的线程链表 */ };
结构体定义中,继承关系一目了然,不再赘述。下边看看如何对一个事件集进行操作。
第二:管理事件集
事件集相关的操作函数如下图所示,主要包含:创建/初始化事件集、发送事件、接收事件、删除/脱离事件集。
本文只介绍常用的几种系统函数。
1. 创建事件集
同信号量类似。RT-Thread 事件集创建也有两种方式:动态创建、静态初始化。
动态创建一个事件集的函数接口如下,调用这个函数创建一个事件集时,内核首先创建一个事件集控制块,然后对其进行基本的初始化。
rt_event_t rt_event_create(const char* name, rt_uint8_t flag);
参数 name
为事件集的名称;flag
为事件集的标志,取值为 RT_IPC_FLAG_FIFO
或 RT_IPC_FLAG_PRIO
,这两个标志值之前介绍过。
创建成功,返回事件控制块的句柄。创建失败,则返回 RT_NULL。
调用这个函数接口时,系统会从对象管理器中分配一个事件集对象,并对其初始化;然后初始化父类 IPC
对象。
静态创建事件集有两步:(1)定义一个事件控制块结构体变量(2)调用函数对结构体变量初始化。
对事件集控制块变量初始化的函数为:
rt_err_t rt_event_init(rt_event_t event, const char* name, rt_uint8_t flag)
该函数对 event
指向的事件集控制块初始化,然后将其加入到系统对象容器中进行管理。
创建事件集的标志变量取值有两种:
RT_IPC_FLAG_FIFO
,等待事件集的线程按照先进先出的方式进行排列。RT_IPC_FLAG_PRIO
,等待事件集的线程按照优先级的方式进行排列。
2. 发送事件
RT-Thread 提供的发送事件函数,可以一次性发送事件集中的一个或多个事件,函数原型如下:
rt_err_t rt_event_send(rt_event_t event, rt_uint32_t set);
使用该函数接口时,通过参数 set
指定的事件标志来设定 event
事件集对象的事件标志值。然后遍历等待在 event
事件集对象上的等待线程链表,判断是否有线程的事件激活要求与当前 event
对象事件标志值匹配,如果有,则唤醒该线程。
3. 接收事件
一个事件集对象可以同时等待接收多个事件,内核有两种方式唤醒等待事件集的线程:
- 逻辑与,表示只有当所有等待的事件都发生时才激活线程
- 逻辑或,只要有一个等待的事件发生就激活线程。
接收事件的函数原型如下:
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)
调用这个函数时,系统首先根据 set
参数和接收选项 option
来判断它要接收的事件是否发生了。
函数的各个参数解释如下:
参数 | 描述 |
event | 事件集控制块指针 |
set | 接收线程等待的事件 |
option | 接收选项 |
timeout | 等待事件的超时时间,单位 系统时钟节拍 |
recved | 指向收到的事件 |
接收成功,函数返回 RT_EOK
;超时,返回 -RT_ETIMEOUT
;出错,则返回 -RT_ERROR
。
参数 option
的取值如下:
/* 选择逻辑与或逻辑或的方式接收事件 */ #define RT_EVENT_FLAG_AND 0x01 /* 逻辑与 */ #define RT_EVENT_FLAG_OR 0x02 /* 逻辑或 */ /* 选择清除重置事件标志位 */ #define RT_EVENT_FLAG_CLEAR 0x04 /* 清除事件标志位 */
当调用 rt_event_recv()
函数时,系统首先根据 set
参数和接收选项 option
来判断它要接收的事件是否发生:
- 事件已经发生,则根据参数
option
上是否设置有RT_EVENT_FLAG_CLEAR
来决定是否重置事件的相对应的标志位。将发送的事件标志位填充到recved
指向的标志变量中。 - 事件没有发生,把等待的
set
和option
参数填入线程控制块结构中,然后线程进入到挂起状态。直到等待的事件满足条件或等待时间超过指定的时间。
若超时时间设置为零,则表示线程在调用这个函数接收事件,若不满足要求时,不再等待,直接返回 -RT_ETIMEOUT
。
第三:实战演练
举例来说明事件集操作函数的用法,代码如下。创建两个线程,一个线程等待事件,一个线程发送事件。
#include <rtthread.h> #define THREAD_PRIORITY 8 #define THREAD_TIMESLICE 5 #define EVENT_FLAG3 (1 << 3) #define EVENT_FLAG5 (1 << 5) /* 事 件 控 制 块 */ static struct rt_event event; static void rt_thread1_entry(void *parameter) { rt_uint32_t e; /* 第一次接收事件, 事件3或事件5任意一个可以触发线程1,接收完后清除事件标志*/ if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5), RT_EVENT_FLAG_OR | RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER, &e) == RT_EOK) { rt_kprintf("thread1: OR recv event 0x%x\n", e); } rt_kprintf("thread1: delay 1s to prepare the second event\n"); rt_thread_mdelay(1000); /* 第二次接收事件,事件3和事件5均发生时才可以触发线程1,接收完后清除事件标志 */ if (rt_event_recv(&event, (EVENT_FLAG3 | EVENT_FLAG5), RT_EVENT_FLAG_AND | RT_EVENT_FLAG_CLEAR, RT_WAITING_FOREVER, &e) == RT_EOK) { rt_kprintf("thread1: AND recv event 0x%x\n", e); } rt_kprintf("thread1 leave.\n"); } static void rt_thread2_entry(void *parameter) { rt_kprintf("thread2: send event3\n"); rt_event_send(&event, EVENT_FLAG3); rt_thread_mdelay(200); rt_kprintf("thread2: send event5\n"); rt_event_send(&event, EVENT_FLAG5); rt_thread_mdelay(200); rt_kprintf("thread2: send event3\n"); rt_event_send(&event, EVENT_FLAG3); rt_kprintf("thread2 leave.\n"); } int main() { /* 线程控制块指针 */ rt_thread_t thread1 = RT_NULL; rt_thread_t thread2 = RT_NULL; rt_err_t result; /* 初始化事件对象 */ result = rt_event_init(&event, "event", RT_IPC_FLAG_FIFO); if (result != RT_EOK) { rt_kprintf("init event failed.\n"); return -1; } /* 动态创建线程1 */ thread1 = rt_thread_create("thread1", rt_thread1_entry, RT_NULL, 1024, THREAD_PRIORITY - 1, THREAD_TIMESLICE); if(thread1 != RT_NULL) { /* 启动线程 */ rt_thread_startup(thread1); } /* 动态创建线程2 */ thread2 = rt_thread_create("thread2", rt_thread2_entry, RT_NULL, 1024, THREAD_PRIORITY, THREAD_TIMESLICE); if(thread2 != RT_NULL) { /* 启动线程 */ rt_thread_startup(thread2); } }
编译,运行结果如下:
第四:其他操作函数
对于 RT-Thread 事件集操作来说,还有删除事件集函数没有介绍。可以简单了解一下。
1. 删除动态创建的事件集
删除由 rt_event_create()
函数创建的事件集,可以调用如下函数:
rt_err_t rt_event_delete(rt_event_t event);
调用此函数,可以释放事件集控制块占用的内存资源。在删除一个事件集对象时,应该确保该事件集不再被使用。
在删除前会唤醒所有挂起在该事件集上的线程,然后释放事件集对象占用的内存块。
2. 脱离静态创建的事件集脱离
删除 rt_event_init()
初始化的事件集,可以用如下函数:
rt_err_t rt_event_detach(rt_event_t event)
调用此函数时,首先会唤醒所有挂起在该事件集等待队列上的线程,然后将该事件集从内核对象管理器中脱离。
第五:小结
至此,RT-Thread 中用于线程间同步的三种方式,全部完毕。
- 信号量(包含计数信号量、二值信号量)
- 互斥量(互斥信号量)
- 事件集(事件标志组)
这三种同步机制相关的知识,与其他 RTOS 是相通的。学会了 RT-Thread 中的这三方面内容,再去学习其他款 RTOS 对应的内容时,很轻松就能掌握。
因此,学习一项新知识点,尽量掌握它的本质原理,争取做到触类旁通。