Zephyr内核调度之代码分析3--线程睡眠和挂起

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

本文分析调度时机中的线程睡眠和挂起。

前文参考
[1] Zephyr内核调度之调度方式与时机
[2] Zephyr线程阻塞和超时机制分析
[3] Zephyr用户模式-系统调用
[4] Zephyr内核调度之代码分析2-调度关键函数

在[1]中提到了17种调度时机,本文分析其中6种:涉及线程睡眠和挂起引起的调度

  • 线程挂起
    • k_thread_suspend
  • 线程挂起恢复
    • k_thread_resume
  • 线程睡眠
    • k_sleep
    • k_msleep
    • k_usleep
  • 线程睡眠时间到
    • z_thread_timeout
  • 线程唤醒
    • k_wakeup
  • 线程主动释放CPU控制权
    • k_yield

由于系统调用的关系可被应用调用的内核函数实际实现对应到sched.c中(参考[3]):
k_thread_suspend->z_impl_k_thread_suspend
k_thread_resume->z_impl_k_thread_resume
k_sleep->z_impl_k_sleep
k_usleep->z_impl_k_usleep
k_yield->z_impl_k_yield
k_wakeup->z_impl_k_wakeup

API实现分析

线程挂起

只有在挂起当前正在执行的线程时才会进行上下文切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void z_impl_k_thread_suspend(struct k_thread *thread)
{
(void)z_abort_thread_timeout(thread);

LOCKED(&sched_spinlock) {
//将要挂起的线程从ready_q中移除
if (z_is_thread_queued(thread)) {
dequeue_thread(&_kernel.ready_q.runq, thread);
}
z_mark_thread_as_suspended(thread);

//重新最合适的线程
update_cache(thread == _current);
}

//如果挂起的线程是当前线程,执行上下文切换
if (thread == _current) {
z_reschedule_unlocked();
}
}

作为调度的关键函数dequeue_thread,update_cache已经在[4]中分析过。z_reschedule_unlocked后文分析

线程挂起恢复

被挂起的线程恢复时,只有被恢复的线程是最高优先级时才会进行上下文切换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void z_impl_k_thread_resume(struct k_thread *thread)
{

k_spinlock_key_t key = k_spin_lock(&sched_spinlock);

//不是被挂起的线程,不需要做恢复
if (!z_is_thread_suspended(thread)) {
k_spin_unlock(&sched_spinlock, key);
return;
}

//将线程重新加入到就绪列队中,并选出最优的线程
z_mark_thread_as_not_suspended(thread);
ready_thread(thread);

//检查是否需要上下文切换(被重新加入到就绪列队的线程处于最高优先级),如果需要就执行切换
z_reschedule(&sched_spinlock, key);

}

ready_thread和z_reschedule后文分析

线程睡眠

线程睡眠,是线程自己主动调用内核API让出CPU.
k_msleep直接通过k_sleep实现

1
2
3
4
static inline int32_t k_msleep(int32_t ms)
{
return k_sleep(Z_TIMEOUT_MS(ms));
}

sched.c中提供2个睡眠API:

  • z_impl_k_sleep
  • z_impl_k_usleep
    z_impl_k_usleep会将时间单位转换为tick数,通过z_tick_sleep执行sleep
    1
    2
    3
    4
    5
    6
    7
    8
    9
    int32_t z_impl_k_usleep(int us)
    {
    int32_t ticks;

    ticks = k_us_to_ticks_ceil64(us);
    ticks = z_tick_sleep(ticks);

    return k_ticks_to_us_floor64(ticks);
    }

z_impl_k_sleep会先判断是不是永久sleep,永久的sleep会当做挂起处理,之后再通过z_tick_sleep执行sleep

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int32_t z_impl_k_sleep(k_timeout_t timeout)
{
k_ticks_t ticks;

__ASSERT(!arch_is_in_isr(), "");

if (K_TIMEOUT_EQ(timeout, K_FOREVER)) {
k_thread_suspend(_current);

SYS_PORT_TRACING_FUNC_EXIT(k_thread, sleep, timeout, (int32_t) K_TICKS_FOREVER);

return (int32_t) K_TICKS_FOREVER;
}

ticks = timeout.ticks;

ticks = z_tick_sleep(ticks);

int32_t ret = k_ticks_to_ms_floor64(ticks);

return ret;
}

sleep单只对线程有效,在isr中执行会直接assert. 只要线程执行了sleep,就会让出CPU,必然会立即引发调度

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
static int32_t z_tick_sleep(k_ticks_t ticks)
{
uint32_t expected_wakeup_ticks;

//不允许在isr中sleep
__ASSERT(!arch_is_in_isr(), "");

//如果sleep 0个tick,当做yield处理让出CPU,后文分析k_yield
if (ticks == 0) {
k_yield();
return 0;
}

//计算timeout到期的tick
k_timeout_t timeout = Z_TIMEOUT_TICKS(ticks);
if (Z_TICK_ABS(ticks) <= 0) {
expected_wakeup_ticks = ticks + sys_clock_tick_get_32();
} else {
expected_wakeup_ticks = Z_TICK_ABS(ticks);
}

k_spinlock_key_t key = k_spin_lock(&sched_spinlock);


pending_current = _current;

//将当前thread从就绪列表中移除,并选出最优的线程
unready_thread(_current);

//将当前线程加入到timeout list中
z_add_thread_timeout(_current, timeout);
z_mark_thread_as_suspended(_current);

//进行上下文切换
(void)z_swap(&sched_spinlock, key);

__ASSERT(!z_is_thread_state_set(_current, _THREAD_SUSPENDED), "");

ticks = (k_ticks_t)expected_wakeup_ticks - sys_clock_tick_get_32();
if (ticks > 0) {
return ticks;
}

return 0;
}

作为调度的关键函数z_swap已经在[4]中分析过。z_add_thread_timeout在[2]中已经分析,unready_thread后文分析

线程睡眠时间到

从[2]的分析中我们知道,线程sleep时会通过z_add_thread_timeout加入到timer中,当sleep tick到的时候会呼叫z_thread_timeout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void z_thread_timeout(struct _timeout *timeout)
{
struct k_thread *thread = CONTAINER_OF(timeout,
struct k_thread, base.timeout);

LOCKED(&sched_spinlock) {
//检查线程在sleep期间是否被其它线程/ISR做了abort
bool killed = ((thread->base.thread_state & _THREAD_DEAD) ||
(thread->base.thread_state & _THREAD_ABORTING));

if (!killed) {
//检查线程是否因等内核对象超时,在之后内核对象引起的调度文章中分析
if (thread->base.pended_on != NULL) {
unpend_thread_no_timeout(thread);
}

z_mark_thread_as_started(thread);
z_mark_thread_as_not_suspended(thread);
//将线程重新加入到就绪列队中,并选出最优的线程
ready_thread(thread);
}
}
}

ready_thread在后文分析,由于z_thread_timeout是在tickISR中执行,在ISR退出时会判断是否要进行线程的上下文切换。

线程唤醒

处于睡眠状态的线程可以被提前唤醒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void z_impl_k_wakeup(k_tid_t thread)
{
//等待内核对象的线程不能被唤醒
if (z_is_thread_pending(thread)) {
return;
}

//将线程从timeout list中移除
if (z_abort_thread_timeout(thread) < 0) {
/* Might have just been sleeping forever */
if (thread->base.thread_state != _THREAD_SUSPENDED) {
return;
}
}

z_mark_thread_as_not_suspended(thread);
//将线程重新加入到就绪列队中,并选出最优的线程
z_ready_thread(thread);

//如果不是在IRS中唤醒线程,需要检查并执行上下文切换. 如果是在ISR中唤醒线程,在ISR退出时会判断是否要进行线程的上下文切换。
if (!arch_is_in_isr()) {
z_reschedule_unlocked();
}
}

z_ready_thread在后文分析

线程主动释放CPU控制权

k_yield主动释放CPU控制权,如果有更高或者同优先级的线程在就绪列队中必然引起调度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void z_impl_k_yield(void)
{
//ISR中不能进行yield
__ASSERT(!arch_is_in_isr(), "");

k_spinlock_key_t key = k_spin_lock(&sched_spinlock);

//用dequeue_thread将线程从就绪列队中取出,再用dequeue_thread重新加入,相当于会将当前线程排列在高优先级和同等优先级之后
if (!IS_ENABLED(CONFIG_SMP) ||
z_is_thread_queued(_current)) {
dequeue_thread(&_kernel.ready_q.runq,
_current);
}
queue_thread(&_kernel.ready_q.runq, _current);

//选出最优的线程
update_cache(1);

//执行上下文切换
z_swap(&sched_spinlock, key);
}

dequeue_thread,queue_thread,z_swap均是[4]中分析过的关键函数。

内部函数分析

上一节中有提到一些和调度相关的内部函数,这里做展开分析
z_ready_thread,ready_thread,unready_thread
z_ready_thread调用的就是ready_thread,因此只用分析ready_thread和unready_thread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void ready_thread(struct k_thread *thread)
{
//判断thread没有在read_q中才做添加
if (!z_is_thread_queued(thread) && z_is_thread_ready(thread)) {
//将thread添加到ready_q中
queue_thread(&_kernel.ready_q.runq, thread);
//并选出合适的thread
update_cache(0);

}
}

static void unready_thread(struct k_thread *thread)
{
//判断thread在read_q中才做移除
if (z_is_thread_queued(thread)) {
//将thread从ready_q中移除
dequeue_thread(&_kernel.ready_q.runq, thread);
}

//并选出合适的thread
update_cache(thread == _current);
}

z_reschedule,z_reschedule_unlocked和上下文切换的调用关系如下:
z_reschedule->z_swap
z_reschedule_unlocked->z_reschedule_irqlock->z_swap_irqlock