Zephyr内核调度之调度方式与时机

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

本文说明Zephyr内核调度方式和调度时机。

调度方式

Zephyr中存在协作式和抢占式两种线程类型,不同类型的线程面对相同的触发条件有不同的反应.

抢占式线程

当抢占式线程占用CPU时,其它高优先级线程就绪后可以立即抢占CPU:

上图的(1)中ISR给高优先级线程T2等待资源,T2进入就绪状态,可以立即抢占T1。协作式线程的优先级高于抢占式线程,因此协作式线程和高优先级抢占式线程就绪时都可以抢占低优先级抢占式线程。

抢占式线程时间片

相同优先级的抢占式线程之间以时间片为单位轮流使用CPU:

在Zephyr中通过配置CONFIG_TIMESLICE_SIZE指定每个时间片占用的Tick数。为了仅允许应用程序在处理时间敏感性较低的较低优先级线程时使用抢占式时间切片,在Zephyr中通过配置CONFIG_TIMESLICE_PRIORITY指定低于该优先级的线程才使用时间片。 当一个线程执行的时间片用完后,这个将从就绪线列队中移除然后再加入到就绪列队中,这样保证其它相同优先级的线程走到就绪列队的前面而使用下一个CPU时间片。Zephyr的时间片计算是维护在内核数据结构struct _cpu中的slice_ticks,也就是说一个CPU只会记录一个slice_ticks,当有高优先级线程抢占CPU时slice_ticks会被复位,因此当一个线程在当前时间片没使用完时被抢占,并不会记录这个线程当前已经运行了多少时间片,当线程再次被调度时任然会执行一个完成的时间片。除了使用配置选项配置默认的时间片大小和时间片优先级外也可以使用void k_sched_time_slice_set(s32_t slice, int prio)在运行时进行设置。

协作式线程

当协作线程占用CPU时,如果不主动放弃CPU,无论已就绪线程的优先级高低都无法获得CPU:

上图的(1)中ISR给高优先级线程T2等待资源,T2进入就绪状态,虽然T2优先级比T1高,但也不能抢占T1。协作式线程不会被抢占也没有时间片,如果协作式线程长时间使用CPU会导致高优先级和同等优先级的线程调度被延迟,为了解决这种情况协作式线程需要主动让出CPU,让出CPU的方式有两种:

  • 调用k_yield,该方式是将正在执行的线程从就绪列队移除再立即加入到就绪列队,该线程再就绪列队内就排到同等优先级线程的最后,让高优先级和同等优先级的其它线程获取到CPU。如果就绪列队中没有高优先级和同等优先级的线程,那么该线程还是排到最前面,Zephyr将不会进行上下文切换,线程继续运行。
  • 调用k_sleep, k_sleep会让当前线程退出就绪状态,让低优先级的线程也有机会使用CPU。

调度触发时机

在Zephyr中有如下时机会触发调度

  1. 等待内核对象:当线程等待信号量,互斥量,消息队列,分配内存等内核对象时,线程会进入等待状态,需要重新从就绪列队中选出新的线程进行上下文切换。抢占式线程和协作式线程在等待内核对象时都会让出CPU,其它就绪线程将被调度占用CPU,此时必定发生上下文切换。
  2. 等待内核对象发生超时:当线程等待内核对象超时后,线程又会从等待状态变为就绪状态,线程被放回就绪列队中。如果被放回就绪列队的线程是列队中最高优先级线程,且当前占用CPU的是抢占式线程,将发生上下文切换。
  3. 发送内核对象:当线程发送信号量,互斥量,消息列队等内核对象时,会让其它等待这些内核对象的线程变为就绪,被重新加入就绪列队。如果被放回就绪列队的线程是列队中最高优先级线程,且当前占用CPU的是抢占式线程,将发生上下文切换。
  4. 放弃等待内核对象:ISR或者其它线程可以让等待FIFO的线程放弃等待,这些线程将从等待状态变为就绪,被重新加入就绪列队。如果被放回就绪列队的线程是列队中最高优先级线程,且当前占用CPU的是抢占式线程,那么将发生上下文切换。
  5. 清空消息列队:ISR或者其它线程可以清空消息列队,清空时会让所有等待消息的线程变为就绪,被重新加入就绪列队。如果被放回就绪列队的线程是列队中最高优先级线程,且当前占用CPU的是抢占式线程,那么将发生上下文切换。
  6. 线程睡眠:占用CPU的线程通过k_sleep进行睡眠让出CPU,线程会进入等待状态,需要重新从就绪列队中选出新的线程进行上下文切换。抢占式线程和协作式线程在睡眠时都会让出CPU,其它就绪线程将被调度占用CPU,此时必定发生上下文切换。
  7. 线程睡眠时间到:进行k_sleep的线程睡眠时间到,重新进入就绪状态。如果被放回就绪列队的线程是列队中最高优先级线程,且当前占用CPU的是抢占式线程,将发生上下文切换。
  8. 线程唤醒:通过k_wakeup提前唤醒正在随眠的线程,重新进入就绪状态。如果被放回就绪列队的线程是列队中最高优先级线程,且当前占用CPU的是抢占式线程,将发生上下文切换。
  9. 线程挂起:通过k_thread_suspend挂起正在运行的线程时,线程会进入挂起状态,需要从就绪列队中选出新的线程进行上下文切换。抢占式线程和协作式线程在挂起自己时让出CPU,其它就绪线程将被调度占用CPU,此时必定发生上下文切换。
  10. 线程挂起恢复:挂起的线程被其它线程通过k_thread_resume恢复时,重新进入就绪状态。如果被放回就绪列队的线程是列队中最高优先级线程,且当前占用CPU的是抢占式线程,将发生上下文切换。
  11. 线程主动释放CPU控制权:使用k_yield的线程释放CPU,会将当前线程取出并重新放入就绪列队,此时如果有高优先级或者相同优先级的线程在就绪列队中将会发生上下文切换。
  12. 解锁调度:使用k_sched_unlock解锁调度时,可能在k_sched_lock锁调度期间发送的内核对象让其它高优先级线程已经就绪,但由于调度被锁没有调度上线文切换,而延迟到解锁的时候进行重新调度。解锁时如果发现就绪列队中最高优先级线程不是当前线程,且当前占用CPU的是抢占式线程,将发生上下文切换。
  13. 时间片到:每个抢占式线程都有自己的时间片,相同优先级线程之间如果时间片用完了就在Tick中断退出时进行上下文切换
  14. 线程中止/终止:前线程终止/中止时,不再占用CPU,选出就绪列表中最高优先级线程进行调度,必定引发上下文切换。
  15. 设置线程优先级:修改就绪线程优先级后,就绪列队将重新排序。需要从就绪列队中选出新的最高优先级线程进行上下文切换。注意:如果协作式线程将其它就绪线程的优先级设置得比自己高,也会立即引发调度,进行上下文切换。
  16. 停止内核timer:调用k_timer_stop时,等待该timer的线程将进入就绪状态,如果被放回就绪列队的线程是列队中最高优先级线程,且当前占用CPU的是抢占式线程,将发生上下文切换。
  17. 开始调度新建的线程:新建立的线程开始调度时会被先放入到就绪列队。如果被放回就绪列队的线程是列队中最高优先级线程,且当前占用CPU的是抢占式线程,将发生上下文切换。

参考

https://docs.zephyrproject.org/latest/reference/kernel/scheduling/index.html