Zephyr RTOS -- Scheduling

Posted 搬砖-工人

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Zephyr RTOS -- Scheduling相关的知识,希望对你有一定的参考价值。

本笔记基于 Zephyr 版本 2.6.0-rc2

 

前言

本人正在学习 Zephyr,一个可移植性较强,可以兼容多种开发板及物联网设备的操作系统,如果你感兴趣,可以点此查看我的 学习笔记总述 进行了解!

 

Scheduling - (调度)

内核基于优先级的调度程序允许应用程序的线程共享 CPU。

 

1. Concepts - (概念)

调度程序确定在任何时间点允许执行哪个线程,该线程称为当前线程(current thread)。

在不同的时间点,可以让调度程序有机会更改当前线程的身份。这些点称为重新计划点(reschedule points)。一些潜在的重新计划点是:

当线程自动发起操作将自身转换为挂起或者等待状态时,该线程会进入睡眠状态(sleep)。

每当调度程序更改当前线程的标识时,或者当 ISR 替换当前线程的执行时,内核都会首先保存当前线程的 CPU 寄存器值。当线程之后恢复执行时,将恢复这些寄存器的值。

 

2. Scheduling Algorithm - (调度算法)

内核的调度程序选择优先级最高的就绪(ready)线程作为当前线程。当存在多个具有相同优先级的就绪(ready)线程时,调度程序将选择等待时间最长的线程。

Note: ISR 的执行优先于线程执行,因此,除非屏蔽了中断,否则随时可以用 ISR 替换当前线程的执行。这适用于协作线程和抢占线程。

可以使用就绪(ready)队列实现的几种选择之一来构建内核,当添加多个线程时,可以在代码大小,恒定因子运行时开销和性能扩展之间提供不同的选择:

  • Simple linked-list ready queue (CONFIG_SCHED_DUMB)
    调度程序就绪队列将实现为简单的无序列表,对单线程来说,具有非常快的恒定时间性能和非常小的代码大小。应该在代码大小受限的系统上选择此实现,该系统在任何给定时间都不会在队列中看到超过少数(可能是 3 个) 可运行线程。在大多数平台(未使用 red/black tree) 上,这节省了约 2k 的代码大小。

  • Red/black tree ready queue (CONFIG_SCHED_SCALABLE)
    调度程序就绪队列将实现为 red/black tree。这具有相当慢的固定时间插入和删除开销,并且在大多数平台(未在某处使用 red/black tree) 上,需要额外的约 2kb 代码。所产生的行为将干净而迅速地扩展到成千上万的线程中。

    将其用于需要许多并发可运行线程(> 20左右) 的应用程序。 大多数应用程序都不需要这种 ready queue 实现。

  • Traditional multi-queue ready queue (CONFIG_SCHED_MULTIQ)
    选中该选项后,调度程序就绪(ready)队列将实现为经典/教科书列表数组,每个优先级一个 (最多 32 个优先级)。

    dumb 调度程序相比,它仅产生很小的代码开销,并且在几乎所有情况下都以非常低的常数系数在 O(1) 时间中运行。 但是它需要相当大的 RAM 预算来存储这些列表头,并且有限的功能使其与诸如需要更精细地对线程进行排序的截止日期调度以及与 SMP 相似需要遍历线程列表的功能不兼容。

    具有少量可运行线程的典型应用程序可能需要 DUMB 调度程序。

    wait_q 抽象的在 IPC 原语中,用于等待线程挂起以便稍后唤醒与调度程序共享相同的后端数据结构选择,并且可以使用相同的选项。

  • Scalable wait_q implementation (CONFIG_WAITQ_SCALABLE)
    选中后,wait_q 将使用平衡树来实现。如果您希望有许多线程在等待各个原语(individual primitives),请选择此项。如果未在应用程序中的其他地方使用 red/black tree,则与 CONFIG_WAITQ_DUMB 相比,代码大小增加了约 2kb(可能与 CONFIG_SCHED_SCALABLE 共享),并且“小型”队列上的挂起/未挂起操作会稍微慢一些(尽管通常情况下性能并不如此)。

  • Simple linked-list wait_q (CONFIG_WAITQ_DUMB)
    选中后,wait_q 将使用双向链接列表来实现。如果您希望在任何单个 IPC 原语上仅阻塞几个线程,请选择此选项。

 

3. Cooperative Time Slicing - (协作时间分片)

一旦协作线程成为当前线程,它将一直保留在当前线程中,直到它执行使它变为未就绪态(unready)的操作为止。因此,如果协作线程执行冗长的计算,则可能导致其他线程的调度出现不可接受的延迟,包括优先级较高和优先级相同的线程。

协作线程时间片

为了克服这些问题,协作线程可以不时自愿放弃 CPU,以允许其他线程执行。线程可以通过两种方式放弃 CPU:

  • 调用 k_yield() 将线程放在调度程序的就绪(ready)线程优先列表的后面,然后调用调度程序。然后,在重新安排该让位线程之前,允许执行优先级高于或等于该让位线程的所有就绪线程。如果不存在这样的就绪线程,则调度程序将立即重新调度该让位线程,而无需切换。
  • 调用 k_sleep() 使线程在指定的时间段内处于未就绪态(unready),然后允许执行所有优先级的就绪线程。但是,不能保证优先级低于睡眠线程的线程实际上会在睡眠线程再次准备就绪之前进行调度。

 

4. Preemptive Time Slicing - (抢占时间分片)

抢占式线程一旦成为当前线程,它将一直保留在当前线程中,直到更高优先级的线程准备就绪(ready),或者直到该线程执行了使其转换为未就绪(unready)的操作为止。因此,如果抢占式线程执行冗长的计算,则可能导致其他线程(包括优先级相同的线程)的调度出现不可接受的延迟。

抢占线程时间片

为了克服这些问题,抢占线程可以执行协作时间分片(如上所述),或者可以使用调度程序的时间分片功能来允许具有相同优先级的其他线程执行。
调度时间片

调度程序将时间分为一系列时间片,其中时间片以系统时钟滴答为单位进行测量。时间片大小是可配置的,但是可以在应用程序运行时更改此大小。

在每个时间片的末尾,调度程序检查当前线程是否可抢占,如果是,则k_yield() 代表该线程隐式调用。这使其他具有相同优先级的就绪线程有机会在再次调度当前线程之前执行。如果没有同等优先级的线程准备就绪,则当前线程仍为当前线程。

优先级高于指定限制的线程免于抢占时间切片,并且永远不会被优先级相同的线程抢占。这允许应用程序仅在处理对时间敏感度较低的优先级较低的线程时才使用抢占式时间切片。

Note: 内核的时间分片算法不能确保一组相等优先级的线程获得相等数量的 CPU 时间,因为它无法衡量线程实际执行的时间。但是,该算法确实确保了一个线程在不被让位的情况下执行的时间永远不会比单个时间片长。

 

5. Scheduler Locking - (调度锁定)

当一个抢占线程在执行关键操作不希望被其他抢占线程抢占时,可以通过指示调度程序执行 k_sched_lock() 将其视为协作线程。这样可以防止其他线程在执行关键操作时发生干扰。关键操作完成之后,必须执行 k_sched_unlock() 恢复其正常的,可抢占状态。

如果某个线程调用 k_sched_lock() 并随后执行了使其转换为未就绪态(unready)的操作,则调度程序将切换出锁定线程并允许其他线程执行。当锁定线程再次成为当前线程时,将保持其不可抢占状态。

Note: 对于可抢占的线程而言,锁定调度程序是一种比将其优先级级别更改为负值更有效的方式来防止抢占。

 

6. Meta-IRQ Priorities - (元优先级)

启用后(请参阅 CONFIG_NUM_METAIRQ_PRIORITIES 参考资料),在优先级空间的最高(数字最低)端有一个特殊的协作优先级子类:meta-IRQ 线程。这些线程是根据其正常优先级进行调度的,但也具有以较低优先级抢占所有其他线程(和其他 meta-irq 线程)的特殊能力,即使这些线程是协作的和/或已使用调度程序锁定。

这种行为使低优先级线程完成了将 meta-IRQ 线程解锁 (通过任何方式,例如,创建它,调用 k_sem_give() 等) 为等效于同步系统调用的动作,或者从真正的中断上下文完成时将其变为 ARM 式的 “pended IRQ”,目的是在驱动程序子系统中,使用此功能实现中断 “bottom half” 处理和/或 “tasklet” 功能, 一旦线程唤醒,将保证该线程在当前 CPU 返回应用程序代码之前运行。

与其他 OS 中的类似功能不同,meta-IRQ 线程是真正的线程,它们在自己的堆栈(必须正常分配)上运行,而不是在每个CPU的中断堆栈上运行。正在进行有关在支持的体系结构上使用IRQ堆栈的设计工作

Note: 请注意,因为这违反了 Zephyr API 对协作线程的承诺(即 OS 不会在当前线程被故意阻塞之前才调度其他线程),因此应在应用程序代码中格外小心地使用它。这些不仅是非常高优先级的线程,不应这样使用。

 

7. Thread Sleeping - (线程休眠)

线程可以调用 k_sleep() 以将其处理延迟指定的时间段。在线程休眠期间,放弃了 CPU,以允许其他就绪 (ready) 线程执行。一旦经过指定的延迟,线程准备就绪 (ready) ,并有资格再次进行调度。

睡眠线程可以由另一个线程过早地唤醒 k_wakeup()。有时可以使用此技术来允许辅助线程向睡眠线程发信号通知发生了某些事情,而无需线程定义内核同步对象(例如信号量)。允许唤醒不休眠的线程,但没有效果

 

8. Busy Waiting - (忙碌等待)

线程可以调用 k_busy_wait() 来执行 busy wait,这会将它的处理延迟指定的时间段,而无需将 CPU 释放给另一个就绪的线程。

当所需的延迟太短而无法保证调度程序从当前线程切换到另一个线程然后再次返回时,通常使用 busy wait 而不是Thread Sleeping

 

9. 调度触发的时机

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 的是抢占式线程,将发生上下文切换。

 

10. Suggested Uses - (建议用途)

  • 将协作线程用于设备驱动程序和其他对性能至关重要的工作。

  • 使用协作线程可以实现互斥,而无需内核对象 (例如互斥体)。

  • 使用抢占式线程将时间敏感的处理优先于时间不敏感的处理。

 

参考链接

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

以上是关于Zephyr RTOS -- Scheduling的主要内容,如果未能解决你的问题,请参考以下文章

Zephyr RTOS -- Stacks

Zephyr RTOS -- Stacks

Zephyr RTOS -- Stacks

Zephyr RTOS -- Polling API

Zephyr RTOS -- Polling API

Zephyr RTOS -- Message Queues