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

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

前文参考 [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实现分析 链接到标题

线程挂起 链接到标题

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

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后文分析

线程挂起恢复 链接到标题

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

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实现

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
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

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,必然会立即引发调度

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

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退出时会判断是否要进行线程的上下文切换。

线程唤醒 链接到标题

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

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控制权,如果有更高或者同优先级的线程在就绪列队中必然引起调度

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

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