zephyr笔记 2.1.2 线程的调度

2020-04-17 11:29:12 浏览数 (1)

我正在学习 Zephyr,一个很可能会用到很多物联网设备上的操作系统,如果你也感兴趣,可点此查看帖子zephyr学习笔记汇总。

1 前言

Zephyr 使用基于优先级的调度器来让应用线程们共享CPU。

http://docs.zephyrproject.org/kernel/threads/scheduling.html

2 概念

2.1 线程状态

线程未就绪可能受如下影响:

  • 线程尚未启动。
  • 线程正在等待内核对象完成操作。(例如,线程正在使用不可用的信号量。)
  • 线程正在等待发生超时。
  • 该线程已被暂停。
  • 线程已终止或中止。

2.2 线程优先级

优先级是个整形值,可以是负的或者非负。数值越小,优先级越高。例如,优先级为4的线程A会比优先级为7的线程B拥有更高优先级,优先级为-2的线程C拥有比线程A和线程B更高的优先级。

  • 协作式线程使用负数优先级数值。一旦变为当前线程,协作线程将会持续保留,直到它执行动作进入未就绪状态。
  • 抢占式线程使用非负数优先级数值。一旦它变为当前线程,如果有协作式线程,或者更高或相等的抢占式线程进入就绪状态,当前抢占式线程将会被取代。

配置选项 CONFIG_NUM_COOP_PRIORITIES 以及 CONFIG_NUM_PREEMPT_PRIORITIES 指定不同类型线程的优先级等级数,限定如下的优先级范围:

  • 协作式线程: (-CONFIG_NUM_COOP_PRIORITIES) to -1
  • 抢占式线程: 0 to (CONFIG_NUM_PREEMPT_PRIORITIES - 1)

例如,把5个协作式线程的优先级设置为 -5 到 -1,以及抢占式线程的优先级设置为 0 到 9。

2.3 调度算法

内核调度器选择最高优先级的就绪线程作为当前线程。当有多个相同优先级的线程存在,调度器将会选择等最久的那个。

代码语言:javascript复制
注意:ISR 将会优先于线程执行,因此当前线程可随时被ISR所取代。这适用于协作式线程和抢占式线程。

2.4 协作式线程的时间切片

如果协作式线程执行冗长的计算,将会导致其他线程出现不可接受的延迟,包括那些更高或相等的优先级。

为了解决这个问题,协作式线程可以不时地放弃CPU,以允许其他线程执行。线程放弃CPU有两种方式:

http://docs.zephyrproject.org/kernel/threads/scheduling.html#cooperative-time-slicing

Calling puts the thread at the back of the scheduler’s prioritized list of ready threads, and then invokes the scheduler.

  • 调用 k_yield() 可以把放到就绪线程列表的最后,然后执行调度器。所有更高优先级或者相等优先级的就绪线程将会先被执行。如果没有这样的就绪线程,那么调度器则会继续执行当前线程,无需做任务切换。
  • 调用 k_sleep() 使得线程在一段时间内为未就绪状态。

2.5 抢占式线程的时间切片

一旦抢占式线程成为当前线程,它会一直维持为当前线程,直到更高优先级的线程就绪,或者这个线程操作自己变成非就绪状态。因此,如果抢占式线程执行复杂运算,就可能导致其他线程(包括那些相同优先级的线程)的调度出现不可接受的延时。

为了解决这样的问题,抢占式线程可以执行协作式的时间切片(同上描述),或者调度器的时间切片能力可以允许相同优先级的线程执行。

调度器将时间分成一系列时间切片,切片是以系统时钟tick为单位。切片的长度是可配的。

在每个时间切片的末尾,调度器会检查当前线程是否是抢占式的,如果是的话,就会执行 k_yield()。这样给与其他相同优先级的其他就绪线程寄回。如果没有就绪的相等优先级线程,当前线程会继续维持为当前线程。

超过限定优先级的线程将会被排除在抢占式时间切片之外,以及不能被相同优先级的线程所抢占。这使得只在处理更低优先级且对时间不敏感的线程时,应用程序才能使用抢占式的时间切片。

代码语言:javascript复制
内核的时间切片算法不能保证一系列优先级相等的线程可以获得相等的CPU时间,因为不能精确测量线程获得执行的时间。例如,一个线程可能在1个时间切片的末尾刚成为当前线程,接着直接被CPU放弃。算法可以保证,在没有被要求放弃的时候,线程绝不会执行超过1个时间切片。

2.6 调度锁定

在执行关键操作时不希望被抢占的可抢占线程可以通过调用k_sched_lock() 来指示调度器暂时将其视为协作线程。 这可以防止其他线程在关键操作执行时发生干扰。

一旦关键操作完成,可抢占线程就必须调用k_sched_unlock() 来恢复其正常的可抢占状态。

如果一个线程调用k_sched_lock() 并随后执行一个使其不准备的动作,调度器将切换锁定线程并允许其他线程执行。 当锁定线程再次成为当前线程时,将保持其不可抢占状态。

代码语言:javascript复制
锁定调度程序对于可抢占线程来说是一种更有效的方法来禁止抢占,而不是将其优先级改为负值。

2.7 线程休眠

一个线程可以调用k_sleep() 来延迟一段指定的时间再处理。 在线程休眠期间,放弃CPU以允许其他就绪线程执行。 一旦经过指定的延迟时间,线程就会就绪,并可以被再次调度。

休眠线程可以使用k_wakeup()由另一个线程提前唤醒。 这种技术有时可用于让辅助线程发信号通知睡眠线程发生了某些事情,而不需要线程定义内核同步对象,例如信号量。 唤醒未睡眠的线程是允许的,但不起作用。

2.8 忙等待

一个线程可以调用k_busy_wait() 来执行一个忙等待,将其延迟一段指定的时间再处理,而不会放弃CPU到另一个就绪线程。 当所需延迟太短而不能保证调度程序上下文从当前线程切换到另一个线程,然后再返回时,通常使用忙等待来代替线程休眠。

3 建议用法

使用协作线程进行设备驱动程序和其他性能关键型工作。

使用协作线程来实现互斥,而不需要内核对象,例如互斥锁。

使用抢占式线程优先处理时间敏感的事务,而非时间不敏感的事务。

4 配置选项

  • CONFIG_NUM_COOP_PRIORITIES
  • CONFIG_NUM_PREEMPT_PRIORITIES
  • CONFIG_TIMESLICING
  • CONFIG_TIMESLICE_SIZE
  • CONFIG_TIMESLICE_PRIORITY

5 API

下列线程API,都在 kernel.h 中提供了:

k_current_get() k_sched_lock() k_sched_unlock() k_yield() k_sleep() k_wakeup() k_busy_wait() k_sched_time_slice_set()

6 总结

抢占式线程要处理对时间敏感的时间,如果是关键任务,记得加锁。

如果线程处理中需要短暂延时,而不需要切换任务,那可以用忙等待接口。

End

0 人点赞