Zephyr内核对象--同步之条件变量

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

本文简要说明Zephyr条件变量的使用和实现。

去年在分析Zephyr内核对象的时候,同步对象还只Zephyr内核对象–同步之信号量Zephyr内核对象–同步之互斥量Zephyr内核对象–同步之轮询三种,最近再看的时候发现今年1月(2021.1)已经加入了条件变量的支持,对于一个RTOS的同步对象基本已经实现完整了。条件变量通常用于控制共享资源的访问,它允许一个线程等待其它线程创建共享资源需要的条件。

使用

API

#define K_CONDVAR_DEFINE(name)
作用:定义一个k_condvar,并初始化
name: k_condvar name

int k_condvar_init(struct k_condvar *condvar)
作用: 初始化条件变量
condvar: 要初始化的condvar
返回值: 0标示初始化成功

int k_condvar_signal(struct k_condvar *condvar)
作用:通知条件变量有效,最先加入条件变量列队的thread将优先获得该有效条件
condvar: 有效的条件变量
返回值: 0表示成功

int k_condvar_broadcast(struct k_condvar *condvar)
作用:广播条件变量有效,所有条件变量列队的thread将获得该有效条件
condvar: 有效的条件变量
返回值: 0表示成功

int k_condvar_wait(struct k_condvar *condvar, struct k_mutex *mutex, k_timeout_t timeout)
作用:等待条件变量有效
condvar:等待的条件变量
mutex:资源锁,条件变量和mutex搭配使用,在等待条件变量期间该mutex会被unlock
timeout: 等待时间,单位ms。K_NO_WAIT不等待, K_FOREVER一直等
返回值: 0表示等待成功,-EAGAIN表示等待超时

使用说明

初始化

下面两种方法都可以定义&初始化一个条件变量
编译期初始化

1
K_CONDVAR_DEFINE(my_condvar);

运行期初始化

1
2
struct k_condvar my_condvar;
k_condvar_init(&my_condvar);

等待条件有效

1
2
3
4
5
6
7
8
9
10
11
void thread_get(void)
{
k_mutex_lock(&mutex, K_FOREVER);

//等待使用资源的条件有效,等待器件会解锁mutex,当等到后会重新上锁Mutex
k_condvar_wait(&condvar, &mutex, K_FOREVER);

//条件变量有效,使用共享资源
...
k_mutex_unlock(&mutex);
}

通知条件变量有效

通知条件变量有效有两种方法:
通知一次,只有一个thread可以获得条件有效

1
2
3
4
5
6
7
void worker_thread(void)
{
k_mutex_lock(&mutex, K_FOREVER);
//通知条件有效
k_condvar_signal(&condvar);
k_mutex_unlock(&mutex);
}

广播,所有等待该条件的thread都会获得条件有效

1
2
3
4
5
6
7
void worker_thread(void)
{
k_mutex_lock(&mutex, K_FOREVER);
//广播条件有效
k_condvar_broadcast(&condvar);
k_mutex_unlock(&mutex);
}

使用条件变量需要搭配mutex来控制对共享资源的互斥访问。条件变量只是用来通知条件满足,条件变量不包含条件本身。

实现

struct k_condvar的结构体如下:

1
2
3
struct k_condvar {
_wait_q_t wait_q;
};

可以看到条件变量就是由内核的wait_q实现的,理解了wait_q工作原理可以很轻松的分析条件变量的实现原理,wait_q可以参考文章Zephyr线程阻塞和超时机制分析
条件变量的实现代码在kernel/condvar.c文件中,Zephyr中对内核对象的访问需要通过系统调用,Zephyr会将kernel.h中的API最后转化为调用系统调用的API例如:
k_condvar_init->z_impl_k_condvar_init
k_condvar_signal->z_impl_k_condvar_signal
k_condvar_broadcast->z_impl_k_condvar_broadcast
k_condvar_wait->z_impl_k_condvar_wait
详细可以参考Zephyr用户模式-系统调用

初始化

代码非常简单,无论是使用那种方式初始化都是初始化一个wait_q

1
2
3
4
5
6
7
8
9
10
11
12
int z_impl_k_condvar_init(struct k_condvar *condvar)
{
//初始化wait_q
z_waitq_init(&condvar->wait_q);

//初始化object,给userspace用,非userspace无作用
z_object_init(condvar);

SYS_PORT_TRACING_OBJ_INIT(k_condvar, condvar, 0);

return 0;
}

1
2
3
4
#define Z_CONDVAR_INITIALIZER(obj)                                             \
{ \
.wait_q = Z_WAIT_Q_INIT(&obj.wait_q), \
}

等待条件满足

等待条件满足,就是将thread加入到条件变量的wait_q内等待,等待过程中会将资源锁放掉,当条件满足后又重新拿资源锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int z_impl_k_condvar_wait(struct k_condvar *condvar, struct k_mutex *mutex,
k_timeout_t timeout)
{
k_spinlock_key_t key;
int ret;

//锁调度,避免后面放资源锁的时候引发调度
key = k_spin_lock(&lock);

//释放资源锁
k_mutex_unlock(mutex);

//将当前线程加入到wait_q中,挂起当前线程,切换上下文
ret = z_pend_curr(&lock, key, &condvar->wait_q, timeout);

//条件变量满足后,会从wait_q取出挂起的线程从这里恢复执行

//重新拿到资源锁
k_mutex_lock(mutex, K_FOREVER);


return ret;
}

通知条件满足

通知条件满足会从条件变量的wait_q中取出一个等待的thread恢复执行,实现分析如下

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
int z_impl_k_condvar_signal(struct k_condvar *condvar)
{
//锁调度
k_spinlock_key_t key = k_spin_lock(&lock);

//从条件变量的wait_q中选出最合适的thread
struct k_thread *thread = z_unpend_first_thread(&condvar->wait_q);

if (thread != NULL) {
//如果存在等待条件变量的thread

//设置返回为0
arch_thread_return_value_set(thread, 0);

//将其转为就绪状态
z_ready_thread(thread);

//引发重新调度
z_reschedule(&lock, key);
} else {
//如果不存在等待条件变量的thread,解锁调度,退出
k_spin_unlock(&lock, key);
}
return 0;
}

广播条件满足

广播条件满足会从条件变量的wait_q中取出所有的thread恢复执行,实现分析如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int z_impl_k_condvar_broadcast(struct k_condvar *condvar)
{
struct k_thread *pending_thread;
k_spinlock_key_t key;
int woken = 0;

//锁调度
key = k_spin_lock(&lock);


//从wait_q中取出所有thread进行恢复
while ((pending_thread = z_unpend_first_thread(&condvar->wait_q)) !=
NULL) {
//设置返回为0
arch_thread_return_value_set(pending_thread, 0);
//将其转为就绪状态
z_ready_thread(pending_thread);
}

//引发调度
z_reschedule(&lock, key);

return woken;
}

和信号量的区别

初看条件变量总有一点信号量的影子,Zephyr中二者主要有如下区别:

  1. 信号量无法进行广播。
  2. 多值信号量可以积压,没有消费者发送的信号依然被保存,而条件变量发送后没有消费者接受条件就过期了。
  3. 条件变量必须搭配互斥锁使用。
  4. Zephyr中信号量可以用于Poll,条件变量则不行。

参考

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