操作系统任务调度器的实现与优化
1. 简单调度器的运行与问题
最初实现的小操作系统已能正常工作,内核会按顺序调度两个任务。在每个循环开始时,ID 为 0 的任务会恢复执行,但在这个简单示例中,内核任务只是循环调用调度函数,然后立即恢复 ID 为 1 的任务。不过,这种设计下系统的响应性完全取决于任务的实现,因为每个任务都可能无限占用 CPU,阻止其他任务运行。这种协作模型仅适用于特定场景,每个任务需自行调节 CPU 周期并与其他线程协作,这可能影响整个系统的响应性和公平性。
此外,当前实现未考虑
jiffies
变量的溢出问题。若
jiffies
每毫秒递增一次,大约 42 天后就会溢出其最大值。而实际操作系统需要实现合适的机制来比较时间变量,以检测溢出并计算时间差。
2. 并发与时间片调度
为解决上述问题,可采用为每个任务分配短时间的 CPU 时间,并以极短间隔连续切换进程的方法。抢占式调度器能自主中断正在运行的任务,恢复下一个任务,无需任务自身明确请求。它还能制定选择下一个运行任务的策略以及每个任务的时间片。
从任务角度看,执行过程可连续且完全独立于调度器,调度器在后台不断中断和恢复每个任务,让人感觉所有任务在同时运行。以下是重新定义的两个线程,用于以不同间隔闪烁 LED:
void task_test0(void *arg)
{
uint32_t now = jiffies;
blue_led_on();
while(1) {
if ((jiffies - now) > 500) {
blue_led_toggle();
now = jiffies;
}
}
}
void task_test1(void *arg)
{
uint32_t now = jiffies;
red_led_on();
while(1) {
if ((jiffies - now) > 125) {
red_led_toggle();
now = jiffies;
}
}
}
为实现任务的轮询交替,可在
SysTick
处理程序中触发
PendSV
的执行,从而实现定期的任务切换。新的
SysTick
处理程序每
TIMESLICE
毫秒触发一次上下文切换:
#define TIMESLICE (20)
void isr_systick(void)
{
if ((++jiffies % TIMESLICE) == 0)
schedule();
}
在这种新配置下,系统有了更完整的模型,允许多个任务独立运行,且调度完全由内核监督。
3. 阻塞任务的引入
之前实现的简单调度器仅为任务提供了
TASK_READY
和
TASK_RUNNING
两种状态。可引入第三种状态
TASK_WAITING
来定义因阻塞而等待事件或超时的任务。任务可能等待的系统事件包括:
- 任务使用的输入/输出(I/O)设备的中断事件
- 来自其他任务的通信,如 TCP/IP 栈
- 同步机制,如互斥锁或信号量,以访问系统中当前不可用的共享资源
- 超时事件
为管理这些不同状态,调度器可实现两个或更多列表,将当前运行或准备运行的任务与等待事件的任务分开。调度器会从
TASK_READY
状态的任务中选择下一个任务,忽略阻塞任务列表中的任务。
新的调度器使用全局指针跟踪当前运行的任务,并将任务组织成两个列表:
-
tasklist_active
:包含正在运行的任务块和所有处于
TASK_READY
状态、等待调度的任务
-
tasklist_waiting
:包含当前被阻塞的任务块
为实现这一机制,可引入
sleep_ms
函数,让任务暂时切换到等待状态,并设置未来的恢复点以便再次调度。以下是使用
sleep_ms
函数的任务示例:
void task_test0(void *arg){
blue_led_on();
while(1) {
sleep_ms(500);
blue_led_toggle();
}
}
void task_test1(void *arg)
{
red_led_on();
while(1) {
sleep_ms(125);
red_led_toggle();
}
}
为将任务块组织成列表,需在结构中添加指向下一个元素的指针,并添加一个新字段来跟踪任务应被放入活动列表恢复执行的系统时间:
struct task_block {
char name[TASK_NAME_MAXLEN];
int id;
int state;
void (*start)(void *arg);
void *arg;
uint8_t *sp;
uint32_t wakeup_time;
struct task_block *next;
};
同时,实现两个简单函数来插入和删除列表元素:
struct task_block *tasklist_active = NULL;
struct task_block *tasklist_waiting = NULL;
static void tasklist_add(struct task_block **list,struct task_block *el)
{
el->next = *list;
*list = el;
}
static int tasklist_del(struct task_block **list, struct task_block *delme)
{
struct task_block *t = *list;
struct task_block *p = NULL;
while (t) {
if (t == delme) {
if (p == NULL)
*list = t->next;
else
p->next = t->next;
return 0;
}
p = t;
t = t->next;
}
return -1;
}
另外,添加两个函数用于在活动列表和等待列表之间移动任务,并改变任务的状态:
static void task_waiting(struct task_block *t)
{
if (tasklist_del(&tasklist_active, t) == 0) {
tasklist_add(&tasklist_waiting, t);
t->state = TASK_WAITING;
}
}
static void task_ready(struct task_block *t)
{
if (tasklist_del(&tasklist_waiting, t) == 0) {
tasklist_add(&tasklist_active, t);
t->state = TASK_READY;
}
}
sleep_ms
函数设置恢复时间,将任务移动到等待状态,然后激活调度器以抢占该任务:
void sleep_ms(int ms)
{
if (ms < TASK_TIMESLICE)
return;
t_cur->wakeup_time = jiffies + ms;
task_waiting(t_cur);
schedule();
}
新的
PendSV
处理程序从活动列表中选择下一个要运行的任务,通过
tasklist_next_ready
函数确保即使当前任务已从活动列表移除或为最后一个任务,也能选择活动列表的头部作为下一个时间片的任务:
static inline struct task_block *tasklist_next_ready(struct task_block *t)
{
if ((t->next == NULL) || (t->next->state != TASK_READY))
return tasklist_active;
return t->next;
}
PendSV
处理程序的代码如下:
void __attribute__((naked)) isr_pendsv(void)
{
store_context();
asm volatile("mrs %0, msp" : "=r"(t_cur->sp));
if (t_cur->state == TASK_RUNNING) {
t_cur->state = TASK_READY;
}
t_cur = tasklist_next_ready(t_cur);
t_cur->state = TASK_RUNNING;
asm volatile("msr msp, %0" ::"r"(t_cur->sp));
restore_context();
asm volatile("mov lr, %0" ::"r"(0xFFFFFFF9));
asm volatile("bx lr");
}
最后,内核会遍历等待任务列表,检查每个睡眠任务的唤醒时间,当唤醒时间过去时将任务块移回活动列表。内核初始化时也增加了一些步骤,确保内核任务在启动时被放入运行任务列表:
void main(void) {
clock_pll_on(0);
led_setup();
button_setup();
systick_enable();
kernel.name[0] = 0;
kernel.id = 0;
kernel.state = TASK_RUNNING;
kernel.wakeup_time = 0;
tasklist_add(&tasklist_active, &kernel);
task_create("test0",task_test0, NULL);
task_create("test1",task_test1, NULL);
task_create("test2",task_test2, NULL);
while(1) {
struct task_block *t = tasklist_waiting;
while (t) {
if (t->wakeup_time && (t->wakeup_time < jiffies)) {
t->wakeup_time = 0;
task_ready(t);
}
t = t->next;
}
WFI();
}
}
4. 资源等待与任务阻塞
任务暂时从活动列表中排除的方式不止定时阻塞一种。内核可实现其他事件和中断处理程序,使任务在
TASK_WAITING
状态下等待特定资源的 I/O 事件时被阻塞,然后重新进入调度循环。
在示例代码中,实现了一个
read
函数来从任务中获取按钮的状态,该函数会阻塞,直到按钮被按下才返回。在此之前,调用任务会留在等待列表中,不会被调度。以下是一个每次按钮按下时切换绿色 LED 的任务:
#define BUTTON_DEBOUNCE_TIME 120
void task_test2(void *arg)
{
uint32_t toggle_time = 0;
green_led_off();
while(1) {
if (button_read()) {
if((jiffies - toggle_time) > BUTTON_DEBOUNCE_TIME)
{
green_led_toggle();
toggle_time = jiffies;
}
}
}
}
button_read
函数会跟踪调用任务,使用
button_task
指针在按钮按下时唤醒任务。任务会被移动到等待列表,驱动程序会启动读取操作,然后任务被抢占:
struct task_block *button_task = NULL;
int button_read(void)
{
if (button_task)
return 0;
button_task = t_cur;
task_waiting(t_cur);
button_start_read();
schedule();
return 1;
}
为在按钮按下时通知调度器,驱动程序使用内核初始化时指定的回调函数,并将其作为参数传递给
button_setup
:
static void (*button_callback)(void) = NULL;
void button_setup(void (*callback)(void))
{
AHB1_CLOCK_ER |= GPIOA_AHB1_CLOCK_ER;
GPIOA_MODE &= ~ (0x03 << (BUTTON_PIN * 2));
EXTI_CR0 &= ~EXTI_CR_EXTI0_MASK;
button_callback = callback;
}
内核将
button_wakeup
函数与驱动程序回调关联,当事件发生时,若有任务等待按钮按下通知,会将其移回活动任务列表,待调度器选择后恢复执行:
void button_wakeup(void)
{
if (button_task) {
task_ready(button_task);
button_task = NULL;
schedule();
}
}
在按钮驱动中,为启动阻塞操作,会启用中断并将其与信号的上升沿关联,对应按钮按下事件:
void button_start_read(void)
{
EXTI_IMR |= (1 << BUTTON_PIN);
EXTI_EMR |= (1 << BUTTON_PIN);
EXTI_RTSR |= (1 << BUTTON_PIN);
nvic_irq_enable(NVIC_EXTI0_IRQN);
}
当检测到事件时,回调函数会在中断上下文中执行,中断会被禁用,直到下一次调用
button_start_read
:
void isr_exti0(void)
{
nvic_irq_disable(NVIC_EXTI0_IRQN);
EXTI_PR |= (1 << BUTTON_PIN);
if (button_callback)
button_callback();
}
任何依赖中断处理来解锁关联任务的设备驱动或系统模块都可使用回调机制与调度器交互。类似的阻塞策略可用于实现读写操作,使调用任务留在等待列表中,直到检测到并处理所需事件。
其他为裸机嵌入式应用设计的系统组件和库可能需要额外的层来集成到支持阻塞调用的操作系统中。例如,嵌入式 TCP/IP 栈实现(如 lwIP 和 picoTCP)提供了可移植的 RTOS 集成层,包括阻塞套接字调用,通过在专用任务中运行循环函数来管理与其他任务中使用的套接字 API 的通信。锁机制(如互斥锁和信号量)预计会实现阻塞调用,当请求的资源不可用时暂停任务。
目前实现的调度策略具有很强的响应性,任务间交互良好,但未考虑优先级,而这在设计实时系统时是必要的。
5. 实时调度的需求与实现
实时操作系统的关键要求之一是能够在短且可预测的时间内对选定的事件做出反应,执行相关代码。为实现具有严格时间要求的功能,操作系统需专注于快速的中断处理和调度,而非吞吐量或公平性等指标。每个任务可能有特定要求,如截止时间,表明执行必须开始或停止的准确时间,或与共享资源相关,可能会引入与系统中其他任务的依赖关系。能满足确定性时间要求执行任务的系统必须能在可测量的固定时间内完成截止时间。
实时调度是一个复杂的问题,已有权威文献对此进行研究,这里不做详细解释。研究表明,基于为每个任务分配优先级,并结合在运行时切换任务的适当策略的几种方法,可为实时需求提供足够的近似解决方案。
为支持具有确定性截止时间的硬实时任务,操作系统应考虑实现以下特性:
- 调度器中实现快速的上下文切换过程
- 可测量的系统在禁用中断情况下运行的间隔
- 短的中断处理程序
- 支持中断优先级
- 支持任务优先级,以最小化硬实时任务的延迟
从任务调度的角度看,实时任务的延迟主要与系统在外部事件发生时恢复任务的能力有关。为保证选定任务组的确定性延迟,实时操作系统(RTOS)通常实现固定优先级级别,在任务创建时分配给任务,并决定在每次调度器的管理调用执行时选择下一个任务的顺序。
时间关键操作应在优先级较高的任务中实现。为优化实时任务的反应时间,同时保持系统响应性并避免低优先级任务可能的饥饿问题,人们研究了许多调度策略。为特定场景找到最优调度策略可能非常困难,关于实时系统延迟和抖动的确定性计算细节超出了本文范围。
一种在实时操作系统中非常流行的方法是在每次调用调度器管理调用时,从准备执行的任务中选择优先级最高的任务,立即进行上下文切换。这种调度策略称为静态优先级驱动的抢占式调度,并非在所有情况下都是最优的,因为任务的延迟取决于相同优先级级别的任务数量,且没有机制防止在系统负载较高时低优先级任务的潜在饥饿问题。然而,该机制足够简单,可轻松实现以展示优先级机制对实时任务延迟的影响。
另一种方法是根据任务的特性在运行时动态重新分配优先级。实时调度器可受益于一种机制,确保首先选择截止时间最接近的任务。这种方法称为最早截止时间优先调度(EDF),在负载较重的系统中更能有效满足实时截止时间。从 Linux 3.14 版本开始包含的
SCHED_DEADLINE
调度器就是这种机制的实现,尽管相对容易实现,但在嵌入式操作系统中不太流行。
以下是一个简单的静态优先级驱动调度器的实现示例。使用四个独立列表来存储活动任务,每个列表对应系统支持的一个优先级级别。任务创建时会分配一个优先级级别,内核任务优先级为 0,其主要任务是在所有其他任务休眠时运行,检查休眠任务的定时器。任务准备好时可插入相应优先级的活动任务列表,阻塞时移动到等待列表。为跟踪任务的静态优先级,在任务块中添加了
priority
字段:
struct task_block {
char name[TASK_NAME_MAXLEN];
int id;
int state;
void (*start)(void *arg);
void *arg;
uint8_t *sp;
uint32_t wakeup_time;
uint8_t priority;
struct task_block *next;
};
定义两个快捷函数,用于快速将任务块添加或从相同优先级的任务列表中移除:
static void tasklist_add_active(struct task_block *el)
{
tasklist_add(&tasklist_active[el->priority], el);
}
static int tasklist_del_active(struct task_block *el)
{
return tasklist_del(&tasklist_active[el->priority], el);
}
在新的
task_waiting
和
task_ready
函数中使用这些快捷函数,当任务从相应优先级的活动任务列表中移除或插入时改变任务状态:
static void task_waiting(struct task_block *t)
{
if (tasklist_del_active(t) == 0) {
tasklist_add(&tasklist_waiting, t);
t->state = TASK_WAITING;
}
}
static void task_ready(struct task_block *t)
{
if (tasklist_del(&tasklist_waiting, t) == 0) {
tasklist_add_active(t);
t->state = TASK_READY;
}
}
系统中创建了三个任务,其中在按钮按下事件时会阻塞的任务被赋予较高的优先级:
void main(void) {
clock_pll_on(0);
led_setup();
button_setup(button_wakeup);
systick_enable();
kernel.name[0] = 0;
kernel.id = 0;
kernel.state = TASK_RUNNING;
kernel.wakeup_time = 0;
kernel.priority = 0;
tasklist_add_active(&kernel);
task_create("test0",task_test0, NULL, 1);
task_create("test1",task_test1, NULL, 1);
task_create("test2",task_test2, NULL, 3);
while(1) {
struct task_block *t = tasklist_waiting;
while (t) {
if (t->wakeup_time && (t->wakeup_time < jiffies)) {
t->wakeup_time = 0;
task_ready(t);
}
t = t->next;
}
WFI();
}
}
选择下一个任务的函数经过重新设计,从最高到最低遍历优先级列表,找到准备运行的优先级最高的任务。如果最高优先级列表与当前任务所在列表相同,且当前任务的下一个任务状态为
TASK_READY
,则选择下一个任务,以保证在相同优先级级别内竞争 CPU 的任务采用轮询机制。否则,选择最高优先级列表中的第一个任务:
static int idx;
static inline struct task_block *
tasklist_next_ready(struct task_block *t)
{
for (idx = MAX_PRIO - 1; idx >= 0; idx--) {
if ((idx == t->priority) && (t->next != NULL) &&
(t->next->state == TASK_READY))
return t->next;
if (tasklist_active[idx])
return tasklist_active[idx];
}
return t;
}
这种调度器与单优先级调度器在处理 ID 为 2 的任务对按钮按下事件的反应上的主要区别在于,按钮按下事件与任务自身反应之间的时间间隔不同。两种调度器都通过在按钮事件的中断处理程序中将任务立即恢复到就绪状态来实现抢占。
操作系统任务调度器的实现与优化(续)
6. 不同调度策略对比
为了更清晰地理解不同调度策略的特点,下面对前面提到的几种调度策略进行对比分析。
| 调度策略 | 特点 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 协作式调度 | 任务自行调节 CPU 周期并与其他线程协作,可无限占用 CPU | 实现简单 | 系统响应性和公平性差,可能导致部分任务无法运行 | 特定场景,任务能自觉合理分配 CPU 时间 |
| 时间片轮转调度 | 为每个任务分配固定时间片,按顺序轮流执行 | 公平性好,多个任务可独立运行 | 不考虑任务优先级,对实时任务支持不足 | 对实时性要求不高的多任务场景 |
| 静态优先级驱动的抢占式调度 | 任务创建时分配固定优先级,高优先级任务可抢占低优先级任务 | 能快速响应高优先级任务,适合实时任务 | 低优先级任务可能饥饿,任务延迟受同优先级任务数量影响 | 实时性要求较高,有明显优先级区分的场景 |
| 最早截止时间优先调度(EDF) | 根据任务截止时间动态分配优先级,优先执行截止时间近的任务 | 能有效满足实时任务的截止时间要求 | 实现相对复杂,对系统资源要求较高 | 负载较重且对任务截止时间要求严格的实时场景 |
7. 调度器优化建议
根据前面的分析,为了进一步优化调度器的性能,可考虑以下建议:
- 优化上下文切换 :上下文切换是调度器的关键操作,减少上下文切换的时间开销能提高系统的整体性能。可以通过优化寄存器保存和恢复的代码,减少不必要的操作,或者采用硬件辅助的上下文切换机制。
- 合理分配优先级 :在设计系统时,要根据任务的重要性和实时性要求合理分配优先级。避免高优先级任务过多导致低优先级任务饥饿,同时确保关键任务能及时得到执行。
- 优化中断处理 :缩短中断处理程序的执行时间,减少中断对任务执行的影响。可以采用中断嵌套和中断优先级机制,确保高优先级中断能及时得到处理。
- 动态调整调度策略 :根据系统的负载情况和任务的实时性要求,动态调整调度策略。例如,在系统负载较轻时采用时间片轮转调度,在负载较重时采用优先级调度。
8. 总结
本文详细介绍了操作系统任务调度器的实现与优化,从简单的协作式调度开始,逐步引入了时间片轮转调度、阻塞任务管理和实时调度等概念。通过对不同调度策略的分析和对比,我们了解了它们的优缺点和适用场景。同时,为了提高调度器的性能,提出了一些优化建议。
在实际应用中,要根据系统的需求和特点选择合适的调度策略,并进行适当的优化。实时系统对任务的响应时间和截止时间有严格要求,需要采用支持优先级和快速上下文切换的调度策略;而对于一般的多任务系统,时间片轮转调度可能是一个不错的选择。通过合理设计和优化调度器,能够提高系统的性能和可靠性,满足不同应用场景的需求。
9. 代码示例总结
以下是本文中涉及的主要代码示例总结:
// 时间片轮转调度相关代码
#define TIMESLICE (20)
void isr_systick(void)
{
if ((++jiffies % TIMESLICE) == 0)
schedule();
}
// 阻塞任务相关代码
struct task_block {
char name[TASK_NAME_MAXLEN];
int id;
int state;
void (*start)(void *arg);
void *arg;
uint8_t *sp;
uint32_t wakeup_time;
struct task_block *next;
};
struct task_block *tasklist_active = NULL;
struct task_block *tasklist_waiting = NULL;
static void tasklist_add(struct task_block **list,struct task_block *el)
{
el->next = *list;
*list = el;
}
static int tasklist_del(struct task_block **list, struct task_block *delme)
{
struct task_block *t = *list;
struct task_block *p = NULL;
while (t) {
if (t == delme) {
if (p == NULL)
*list = t->next;
else
p->next = t->next;
return 0;
}
p = t;
t = t->next;
}
return -1;
}
static void task_waiting(struct task_block *t)
{
if (tasklist_del(&tasklist_active, t) == 0) {
tasklist_add(&tasklist_waiting, t);
t->state = TASK_WAITING;
}
}
static void task_ready(struct task_block *t)
{
if (tasklist_del(&tasklist_waiting, t) == 0) {
tasklist_add(&tasklist_active, t);
t->state = TASK_READY;
}
}
void sleep_ms(int ms)
{
if (ms < TASK_TIMESLICE)
return;
t_cur->wakeup_time = jiffies + ms;
task_waiting(t_cur);
schedule();
}
// 静态优先级驱动的抢占式调度相关代码
struct task_block {
char name[TASK_NAME_MAXLEN];
int id;
int state;
void (*start)(void *arg);
void *arg;
uint8_t *sp;
uint32_t wakeup_time;
uint8_t priority;
struct task_block *next;
};
static void tasklist_add_active(struct task_block *el)
{
tasklist_add(&tasklist_active[el->priority], el);
}
static int tasklist_del_active(struct task_block *el)
{
return tasklist_del(&tasklist_active[el->priority], el);
}
static void task_waiting(struct task_block *t)
{
if (tasklist_del_active(t) == 0) {
tasklist_add(&tasklist_waiting, t);
t->state = TASK_WAITING;
}
}
static void task_ready(struct task_block *t)
{
if (tasklist_del(&tasklist_waiting, t) == 0) {
tasklist_add_active(t);
t->state = TASK_READY;
}
}
static int idx;
static inline struct task_block *
tasklist_next_ready(struct task_block *t)
{
for (idx = MAX_PRIO - 1; idx >= 0; idx--) {
if ((idx == t->priority) && (t->next != NULL) &&
(t->next->state == TASK_READY))
return t->next;
if (tasklist_active[idx])
return tasklist_active[idx];
}
return t;
}
通过这些代码示例,我们可以更直观地了解不同调度策略的实现方式,为实际开发提供参考。
9. 未来展望
随着计算机技术的不断发展,对操作系统调度器的性能和功能要求也越来越高。未来,调度器可能会朝着以下方向发展:
- 智能化调度 :利用人工智能和机器学习技术,根据任务的历史执行情况和系统的当前状态,动态调整调度策略,实现更高效的任务调度。
- 多核调度优化 :随着多核处理器的广泛应用,如何充分利用多核资源,提高系统的并行处理能力是调度器面临的重要挑战。未来的调度器可能会采用更复杂的多核调度算法,实现任务在多核之间的合理分配。
- 实时性增强 :对于实时系统,如工业控制、自动驾驶等领域,对任务的实时性要求越来越高。调度器需要进一步优化,确保任务能在严格的时间约束内完成。
- 绿色节能调度 :在移动设备和数据中心等场景中,节能是一个重要的考虑因素。调度器可以通过合理分配任务和调整系统资源,降低系统的能耗。
总之,操作系统任务调度器的发展将不断适应新的应用场景和技术需求,为计算机系统的高效运行提供有力支持。
超级会员免费看

被折叠的 条评论
为什么被折叠?



