Zephyr内核对象--同步之事件

Creative Commons
本作品采用知识共享署名

本文简要说明Zephyr事件内核对象的使用和实现。

概念

事件对象是一种线程同步对象,允许一个或者多个线程等待同一个事件对象,当事件被传递到事件对象时,满足条件的线程都变为就绪。通常使用事件来通知一系列条件已满足。在Zephyr中每个事件对象使用一个32bit数来跟踪传递的事件集,每个bit表示一个事件。事件可以由ISR或者线程发送,发送事件有两种方法:

  • 设置:覆盖现有的事件集,重写这个32bit数
  • 发布:以bit方式添加事件到事件基,注意这里只能添加,不能删除

线程可以等待一个或多个事件。在等待多个事件时可以指定等待其中部分或者全部事件。线程在提出等待事件对象时,可以选择在等待前清空等待事件对象上的所有事件。
zephyr的事件内核对象是2021年10月引入https://github.com/zephyrproject-rtos/zephyr/commit/ae394bff7c54202adddbd20eee8a9dcba75d8da5。

API

宏定义

K_EVENT_DEFINE(name)
静态的定义和初始化事件对象。

参数

  • name – 事件对象名

函数

void k_event_init(struct k_event *event)
初始化事件对象,在使用事件对象前首先使用该函数对其进行初始化。

参数

  • event – 事件对象的地址

void k_event_post(struct k_event *event, uint32_t events)
发布一个或多个事件到事件对象。如果等待event的线程因为发布的事件满足等待条件,该线程被转为就绪,与设置事件不同,发布的事件是增加到事件对象中。
参数

  • event – 事件对象的地址
  • events – 发布的事件集合

void k_event_set(struct k_event *event, uint32_t events)
设置事件集合到事件对象,将事件对象中的事件设置为events。如果等待event的线程因为设置的事件满足等待条件,该线程被转为就绪。与发布事件不同,设置事件会取代事件对象中的事件集。
参数

  • event – 事件对象的地址
  • events – 设置的事件集合

uint32_t k_event_wait(struct k_event *event, uint32_t events, bool reset, k_timeout_t timeout)
等待任意指定事件,在event上等待,有任意指定的事件被发送,或者是等待时间超过timeout。一个线程最多可以等待32个事件,这些事件由events中的bit位置标识。
注意 reset = true将在等待事件前,将event中已发生的事件,使用的时候请特别注意。

参数

  • event – 事件对象的地址
  • events – 等待的事件集
  • reset – 如果为true,等待前清空event中已发生的事件,如果为false则不清空
  • timeout – 指定等待事件的最长事件,可以指定为K_NO_WAITK_FOREVER

返回值

  • 事件集 – 等待成功后返回收到匹配的事件集
  • 0 – 指定时间内未收到匹配事件

uint32_t k_event_wait_all(struct k_event *event, uint32_t events, bool reset, k_timeout_t timeout)
等待所有指定事件,在event上等待,指定的所有事件被发送,或者是等待时间超过timeout。一个线程最多可以等待32个事件,这些事件由events中的bit位置标识。

注意 reset = true将在等待事件前,将event中已发生的事件,使用的时候请特别注意。
参数

  • event – 事件对象的地址
  • events – 等待的事件集
  • reset – 如果为true,等待前清空event中已发生的事件,如果为false则不清空
  • timeout – 指定等待事件的最长事件,可以指定为K_NO_WAITK_FOREVER

返回值

  • 事件集 – 等待成功后返回收到匹配的事件集
  • 0 – 指定时间内未收到匹配事件

使用

配置

事件对象的配置选项是CONFIG_EVENTS,zephyr内核没有给事件对象配置选项设置默认值,编译时将被识别为未配置,因此要使用事件对象需要增加配置CONFIG_EVENTS=y

示例

初始化事件对象

使用函数

1
2
struct k_event my_event;
k_event_init(&my_event);

等效于

1
K_EVENT_DEFINE(my_event);

设置事件

下列示例在中断服务程序中设置事件为0x001

1
2
3
4
5
6
7
void input_available_interrupt_handler(void *arg)
{
/* 通知线程数据有效 */
k_event_set(&my_event, 0x001);

...
}

发布事件

下面示例在中断服务程序中发布事件0x120,如果前面的设置事件未被清除,此时my_event中的事件为0x121

1
2
3
4
5
6
7
8
9
10
void input_available_interrupt_handler(void *arg)
{
...

/* notify threads that more data is available */

k_event_post(&my_event, 0x120);

...
}

等待事件

下面示例等待事件50毫秒,在50毫秒内只要前面的设置和发布示例中任意一个发生都会等待成功

1
2
3
4
5
6
7
8
9
10
11
12
13
void consumer_thread(void)
{
uint32_t events;

events = k_event_wait(&my_event, 0xFFF, false, K_MSEC(50));
if (events == 0) {
printk("No input devices are available!");
} else {
/* 收到事件,根据events进行处理 */
...
}
...
}

下面示例等待事件50毫秒,在50毫秒没只有前面的设置和发布示例都发生了才会等待成功

1
2
3
4
5
6
7
8
9
10
11
12
13
void consumer_thread(void)
{
uint32_t events;

events = k_event_wait_all(&my_event, 0x121, false, K_MSEC(50));
if (events == 0) {
printk("At least one input device is not available!");
} else {
/* 事件全部收齐,进行处理 */
...
}
...
}

代码分析

事件对象的代码实现在kernel\events.c,事件对象是由struct k_event进行管理

1
2
3
4
5
struct k_event {
_wait_q_t wait_q;
uint32_t events;
struct k_spinlock lock;
};
  • wait_q 用于管理等待该事件对象的线程

  • events 用于保存当前事件对象收到的事件

  • lock 用于保护内核对事件对象的操作的原子性

事件对象的所有操作都是围绕着struct k_event进行的。

初始化

函数实现代码如下,就是对struct k_event定义事件的各个成员进行初始化

1
2
3
4
5
6
7
8
9
void z_impl_k_event_init(struct k_event *event)
{
event->events = 0;
event->lock = (struct k_spinlock) {};

z_waitq_init(&event->wait_q);

z_object_init(event);
}

用宏可以达到同时定义和初始化的目的,实现如下

1
2
3
4
5
6
7
8
9
#define Z_EVENT_INITIALIZER(obj) \
{ \
.wait_q = Z_WAIT_Q_INIT(&obj.wait_q), \
.events = 0 \
}

#define K_EVENT_DEFINE(name) \
STRUCT_SECTION_ITERABLE(k_event, name) = \
Z_EVENT_INITIALIZER(name);

等待事件

等待事件可以等待任意指定事件和全部事件,在events.c中都是由同一个内部函数k_event_wait_internal实现,只是指定的参数不一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
uint32_t z_impl_k_event_wait(struct k_event *event, uint32_t events,
bool reset, k_timeout_t timeout)
{
uint32_t options = reset ? K_EVENT_WAIT_RESET : 0;

return k_event_wait_internal(event, events, options, timeout);
}

uint32_t z_impl_k_event_wait_all(struct k_event *event, uint32_t events,
bool reset, k_timeout_t timeout)
{
/* 使用K_EVENT_WAIT_ALL选项,表示要等待所有的事件收齐 */
uint32_t options = reset ? (K_EVENT_WAIT_RESET | K_EVENT_WAIT_ALL)
: K_EVENT_WAIT_ALL;

return k_event_wait_internal(event, events, options, timeout);
}

内部函数k_event_wait_internal的第三个参数opetions用于指定等待的选项,选项定义在events.c

1
2
3
4
5
#define K_EVENT_WAIT_ANY      0x00   /* 有1个或者以上事件满足就可退出等待 */
#define K_EVENT_WAIT_ALL 0x01 /* 所有事件满足才可退出等待 */
#define K_EVENT_WAIT_RESET 0x02 /* 等待事件前先清空已有事件 */

#define K_EVENT_WAIT_MASK 0x01 /* 用于获取等待类型 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static uint32_t k_event_wait_internal(struct k_event *event, uint32_t events,
unsigned int options, k_timeout_t timeout)
{
uint32_t rv = 0;
unsigned int wait_condition;
struct k_thread *thread;

/* isr中只能不做任何等待的等待事件 */
__ASSERT(((arch_is_in_isr() == false) ||
K_TIMEOUT_EQ(timeout, K_NO_WAIT)), "");

/* 不允许等待的事件集为0,相当于未等待任何事件 */
if (events == 0) {
return 0;
}

wait_condition = options & K_EVENT_WAIT_MASK;
thread = z_current_get();

k_spinlock_key_t key = k_spin_lock(&event->lock);

/* 检查是否需要清空已有事件 */
if (options & K_EVENT_WAIT_RESET) {
event->events = 0;
}

/* 检查事件对象已有的事件是否已经满足线程,如果满足则退出 */
if (are_wait_conditions_met(events, event->events, wait_condition)) {
rv = event->events;

k_spin_unlock(&event->lock, key);
goto out;
}

/* 如果等待的超时未立即退出,则不进行等待,此时rv=0 */
if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) {
k_spin_unlock(&event->lock, key);
goto out;
}

/* 将线程要等待的事件集和方式保存到线程中 */
thread->events = events;
thread->event_options = options;

/* 等待事件发生,如果等待超时rv仍然保持为0 */
if (z_pend_curr(&event->lock, key, &event->wait_q, timeout) == 0) {
/* 等待事件已发生,发送事件者将把满足的事件交换到线程内的events中,
rv中保存了等待到的事件
*/
rv = thread->events;
}

out:
/* 由于发生的事件可能会超出等待的事件,因此需要做位与返回 */
return rv & events;
}

发送事件

发送事件分为设置和发布,在events.c中都是由同一个内部函数k_event_post_internal实现,只是指定的参数不一样

1
2
3
4
5
6
7
8
9
void z_impl_k_event_post(struct k_event *event, uint32_t events)
{
k_event_post_internal(event, events, true);
}

void z_impl_k_event_set(struct k_event *event, uint32_t events)
{
k_event_post_internal(event, events, false);
}

内部函数k_event_post_internal的第三个参数accumulattrue时表示发送的events是添加到event内,为false时表示是覆盖event的已有事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
static void k_event_post_internal(struct k_event *event, uint32_t events,
bool accumulate)
{
k_spinlock_key_t key;
struct k_thread *thread;
unsigned int wait_condition;
struct k_thread *head = NULL;

/* 上锁,保证内核对事件对象操作的原子性 */
key = k_spin_lock(&event->lock);

/* 检查是附加事件,还是要重置事件 */
if (accumulate) {
/* 附加事件 */
events |= event->events;
}
/* 对发生的事件进行更新 */
event->events = events;

/* 遍历事件对象中wait_q内存放等待的thread,并将事件能满足的线程加入到单链表中 */
_WAIT_Q_FOR_EACH(&event->wait_q, thread) {
/* 获取等待类型K_EVENT_WAIT_MASK为0x01,只取最低位,
这里wait_condition是K_EVENT_WAIT_ANY或K_EVENT_WAIT_ALL
*/
wait_condition = thread->event_options & K_EVENT_WAIT_MASK;

/* 根据等待类型对线程进行事件匹配,匹配上的线程放入单链表head中 */
if (are_wait_conditions_met(thread->events, events,
wait_condition)) {
thread->next_event_link = head;
head = thread;
}
}

/* 对事件匹配上的线程单链表进行遍历,通知线程就绪 */
if (head != NULL) {
thread = head;
do {
z_unpend_thread(thread);
arch_thread_return_value_set(thread, 0);
/* 更新线程收到的事件 */
thread->events = events;
/* 线程恢复就绪 */
z_ready_thread(thread);
thread = thread->next_event_link;
} while (thread != NULL);
}
/* 发送完事件后,引发调度,让刚变为就绪线程有机会执行 */
z_reschedule(&event->lock, key);
}

参考

https://docs.zephyrproject.org/latest/reference/kernel/synchronization/events.html