1. 源码结构
调度器相关的源文件主要位于 sched/sched/
目录下,核心文件包括:
1.1 基础调度功能
sched_addreadytorun.c - 添加任务到就绪队列
sched_removereadytorun.c - 从就绪队列移除任务
sched_addblocked.c/sched_removeblocked.c - 阻塞任务管理
sched_setscheduler.c - 设置调度策略(FIFO/RR/SPORADIC)
sched_getscheduler.c - 获取调度策略
sched_roundrobin.c - 时间片轮转调度实现
1.2 任务优先级管理
sched_setparam.c - 设置任务调度参数
sched_getparam.c - 获取任务调度参数
sched_setpriority.c - 修改任务优先级
sched_reprioritizertr.c - 就绪任务优先级调整
sched_reprioritize.c - 优先级继承(CONFIG_PRIORITY_INHERITANCE)
1.3 临界区和同步
sched_lock.c/sched_unlock.c - 调度器锁定/解锁
sched_lockcount.c - 调度器锁计数管理
1.4 多核支持(SMP)
sched_smp.c - SMP调度实现
sched_getcpu.c - CPU亲和度管理
sched_getaffinity.c/sched_setaffinity.c - CPU亲和度设置/获取
2. 核心数据结构
2.1 任务控制块(TCB)
struct tcb_s {
/* 任务状态信息 */
uint8_t task_state; // 任务状态(就绪、运行、阻塞等)
uint8_t sched_priority; // 调度优先级
uint8_t init_priority; // 初始优先级
/* 调度相关 */
uint16_t flags; // 任务标志(调度策略等)
int16_t lockcount; // 调度器锁定计数
#if CONFIG_RR_INTERVAL > 0
int32_t timeslice; // 剩余时间片
#endif
#ifdef CONFIG_SMP
cpu_set_t affinity; // CPU亲和度掩码
uint8_t cpu; // 当前运行的CPU
#endif
/* 其他成员... */
};
2.2 任务状态
// 任务状态定义
#define TSTATE_TASK_INVALID 0 // 无效状态
#define TSTATE_TASK_PENDING 1 // 等待调度器解锁
#define TSTATE_TASK_READYTORUN 2 // 就绪状态
#define TSTATE_TASK_RUNNING 3 // 运行状态
#define TSTATE_TASK_INACTIVE 4 // 初始化未激活
#define TSTATE_WAIT_SEM 5 // 等待信号量
// ...其他状态
2.3 任务列表
NuttX使用多个双向链表来管理不同状态的任务:
/* 基本任务列表 */
extern dq_queue_t g_readytorun; // 就绪任务列表
extern dq_queue_t g_pendingtasks; // 挂起任务列表
extern dq_queue_t g_waitingforsignal; // 等待信号任务列表
/* 特殊功能任务列表 */
extern dq_queue_t g_waitingforfill; // 等待页面填充任务列表
extern dq_queue_t g_stoppedtasks; // 停止状态任务列表
extern dq_queue_t g_inactivetasks; // 未激活任务列表
/* SMP模式任务列表 */
#ifdef CONFIG_SMP
extern dq_queue_t g_assignedtasks[CONFIG_SMP_NCPUS]; // CPU任务分配列表
extern FAR struct tcb_s *g_delivertasks[CONFIG_SMP_NCPUS]; // 待传递任务列表
#endif
2.3.1 基本任务列表说明
g_readytorun 就绪列表:
单核模式下:
包含所有就绪态任务
按优先级排序
列表头是当前运行任务
列表尾总是空闲任务(最低优先级)
SMP模式下:
仅包含未分配CPU的就绪任务
不包含正在运行或已分配CPU的任务
用于全局任务调度
g_pendingtasks 挂起列表:
存放被抢占但因调度器锁定而无法立即执行的任务
条件满足时(如调度器解锁)会被合并到就绪列表
主要用于处理优先级抢占的延迟执行
g_waitingforsignal 等待信号任务列表:
存放因等待信号量而阻塞的任务
信号量被释放后,任务会被移到就绪列表
用于实现任务间的同步与通信
2.3.4 g_waitingforfill 页面填充等待列表
仅在开启 CONFIG_LEGACY_PAGING 时有效
存放等待页面从存储设备加载到内存的任务
用于支持虚拟内存管理
页面加载完成后任务会被移回就绪列表
2.3.5 g_stoppedtasks 停止任务列表
仅在开启 CONFIG_SIG_SIGSTOP_ACTION 时有效
存放因接收到 SIGSTOP/SIGTSTP 信号而停止的任务
任务可通过接收 SIGCONT 信号重新激活
主要用于任务的暂停/继续控制
2.3.6 g_inactivetasks 未激活任务列表
存放已初始化但尚未激活的任务
唯一一个不需要按优先级排序的列表
任务激活时会被移到相应的运行列表
用于任务创建过程中的临时存储
CPU任务分配列表
仅在 SMP 配置下有效
每个 CPU 核心维护一个独立的任务列表
列表特点:
包含分配给该 CPU 的所有任务
列表头是该 CPU 当前运行的任务
按优先级排序
列表尾是该 CPU 的空闲任务(IDLE)
任务分配方式:
通过 pthread_attr_setaffinity() 等接口显式指定
调度器根据负载均衡动态分配
与 g_readytorun 的区别:
g_readytorun 存放未分配 CPU 的就绪任务
存放已分配给特定 CPU 的任务
2.4 任务列表访问接口
/* 任务列表访问宏 */
#define list_readytorun() (&g_readytorun)
#define list_pendingtasks() (&g_pendingtasks)
#define list_waitingforfill() (&g_waitingforfill)
#define list_stoppedtasks() (&g_stoppedtasks)
#define list_inactivetasks() (&g_inactivetasks)
#define list_assignedtasks(cpu) (&g_assignedtasks[cpu])
这些宏提供了统一的任务列表访问接口:
封装了具体的列表实现细节
支持不同配置选项(如SMP)的条件编译
便于维护和修改列表实现
3. 调度策略实现
3.1 FIFO 调度
不支持时间片轮转
任务运行直到主动让出CPU
高优先级可以抢占低优先级
实现代码:
case SCHED_FIFO:
{
tcb->flags |= TCB_FLAG_SCHED_FIFO;
tcb->timeslice = 0; // FIFO不使用时间片
}
3.2 时间片轮转(RR)调度
支持时间片轮转
同优先级任务轮流执行
时间片用完后切换到同优先级队列中下一个任务
关键实现:
uint32_t nxsched_process_roundrobin(FAR struct tcb_s *tcb, uint32_t ticks)
{
// 减少时间片计数
tcb->timeslice -= ticks;
// 时间片用完且存在同优先级任务时切换
if (tcb->timeslice <= 0 && tcb->flink &&
tcb->flink->sched_priority >= tcb->sched_priority)
{
// 重置时间片
tcb->timeslice = MSEC2TICK(CONFIG_RR_INTERVAL);
// 切换到下一个任务
up_switch_context(tcb->flink, tcb);
}
}
3.3 Sporadic 调度
支持优先级动态调整
适用于需要精确控制CPU使用率的场景
通过补充预算方式限制CPU占用
4. 调度流程及源码实现
4.1 任务创建流程
完整的任务创建流程涉及多个函数调用:
创建任务入口 - task_create():
int task_create(FAR const char *name, int priority,
int stack_size, main_t entry, FAR char * const argv[ ])
{
pid_t pid;
int ret;
// 1. 创建任务相关的TCB等结构
ret = nxtask_spawn_exec((FAR const char *)name, (uint8_t)priority,
(FAR uint32_t *)stack_size, entry,
(FAR char * const *)argv, &pid);
return ret == OK ? pid : ERROR;
}
初始化TCB - nxtask_init():
int nxtask_init(FAR struct task_tcb_s *tcb, const char *name,
int priority, FAR uint32_t *stack, uint32_t stack_size,
main_t entry, FAR char * const argv[ ])
{
// 1. 设置任务基本信息
tcb->cmn.task_state = TSTATE_TASK_INVALID; // 初始状态为无效
tcb->cmn.sched_priority = priority; // 设置优先级
tcb->cmn.init_priority = priority; // 保存初始优先级
tcb->cmn.flags = ttype; // 设置任务类型标志
// 2. 初始化任务栈
up_initial_state(&tcb->cmn);
// 3. 保存任务入口和参数
tcb->cmn.entry.main = entry;
// 4. 继承调度器属性(如CPU亲和性)
nxtask_inherit_scheduler(tcb);
return OK;
}
激活任务 - nxtask_activate():
void nxtask_activate(FAR struct tcb_s *tcb)
{
irqstate_t flags = enter_critical_section();
// 1. 通知监控器任务开始运行
sched_note_start(tcb);
// 2. 初始化运行时统计信息
tcb->start_time = clock_systime_ticks();
// 3. 添加到就绪队列
nxsched_add_readytorun(tcb);
// 4. 如果优先级更高则触发调度
if (tcb->sched_priority > this_task()->sched_priority)
{
// 进行任务切换
up_switch_context(tcb, this_task());
}
leave_critical_section(flags);
}
4.2 任务切换流程
上层切换接口 - up_switch_context():
void up_switch_context(FAR struct tcb_s *rtcb, FAR struct tcb_s *dtcb)
{
// 1. 保存当前任务上下文
up_savestate(rtcb->xcp.regs);
// 2. 更新运行任务指针
g_running_tasks[this_cpu()] = dtcb;
// 3. 切换地址空间(如果需要)
#ifdef CONFIG_ARCH_ADDRENV
addrenv_switch(dtcb);
#endif
// 4. 恢复新任务上下文
up_restorestate(dtcb->xcp.regs);
}
底层上下文切换 - up_fullcontextrestore():
// arch相关实现,以ARM为例
void up_fullcontextrestore(uint32_t *restoreregs)
{
// 1. 禁用中断
__asm__ __volatile__("cpsid i");
// 2. 恢复通用寄存器
__asm__ __volatile__("ldmia %0!, {r4-r11}" : : "r"(restoreregs));
// 3. 恢复程序计数器和状态寄存器
__asm__ __volatile__("ldmia %0!, {r0-r3,ip,lr,pc}^" : : "r"(restoreregs));
}
4.3 优先级抢占流程
优先级抢占主要在添加就绪任务时处理:
bool nxsched_add_readytorun(FAR struct tcb_s *btcb)
{
FAR struct tcb_s *rtcb = this_task();
bool ret;
// 1. 检查是否需要抢占
if (rtcb->lockcount > 0 &&
rtcb->sched_priority < btcb->sched_priority)
{
// 当前任务锁定时,新任务进入pending队列
btcb->task_state = TSTATE_TASK_PENDING;
nxsched_add_prioritized(btcb, list_pendingtasks());
ret = false;
}
else
{
// 2. 添加到就绪队列
if (nxsched_add_prioritized(btcb, list_readytorun()))
{
// 新任务成为最高优先级任务
btcb->task_state = TSTATE_TASK_RUNNING;
btcb->flink->task_state = TSTATE_TASK_READYTORUN;
ret = true;
}
else
{
// 新任务不是最高优先级
btcb->task_state = TSTATE_TASK_READYTORUN;
ret = false;
}
}
return ret;
}
4.4 时间片轮转流程
时间片轮转由系统定时器触发:
定时器中断处理:
void up_timerisr(void)
{
// 1. 更新系统时钟
nxsched_process_timer();
// 2. 处理时间片
nxsched_process_scheduler();
}
时间片处理:
void nxsched_process_scheduler(void)
{
FAR struct tcb_s *rtcb = this_task();
// 1. 检查是否为时间片调度任务
if ((rtcb->flags & TCB_FLAG_POLICY_MASK) == TCB_FLAG_SCHED_RR)
{
// 2. 处理时间片轮转
if (nxsched_process_roundrobin(rtcb, 1))
{
// 需要切换任务
up_switch_context(rtcb->flink, rtcb);
}
}
}
时间片轮转处理:
bool nxsched_process_roundrobin(FAR struct tcb_s *tcb, uint32_t ticks)
{
// 1. 减少时间片计数
tcb->timeslice -= ticks;
// 2. 检查时间片是否用完
if (tcb->timeslice <= 0)
{
// 3. 检查是否存在同优先级任务
if (tcb->flink &&
tcb->flink->sched_priority >= tcb->sched_priority)
{
// 4. 重置时间片
tcb->timeslice = MSEC2TICK(CONFIG_RR_INTERVAL);
// 5. 将当前任务放到队列尾部
if (nxsched_reprioritize_rtr(tcb, tcb->sched_priority))
{
// 需要切换到下一个任务
return true;
}
}
}
return false;
}
关键实现细节:
任务创建流程保证了:
TCB结构的正确初始化
任务栈和入口点的设置
继承调度器属性
添加到合适的任务队列
任务切换流程确保了:
当前任务上下文的保存
新任务上下文的恢复
地址空间的正确切换
运行任务指针的更新
优先级抢占实现了:
高优先级任务的及时响应
调度锁机制的正确处理
任务状态的正确迁移
时间片轮转保证了:
同优先级任务的公平调度
时间片到期的及时处理
任务队列的动态调整
5. 同步机制
5.1 调度器锁
sched_lock() 增加锁计数
sched_unlock() 减少锁计数
锁计数不为0时禁止任务切换
5.2 临界区保护
irqstate_t flags;
flags = enter_critical_section(); // 进入临界区
// 临界区代码
leave_critical_section(flags); // 退出临界区
6. 多核调度(SMP)
6.1 CPU亲和度
通过掩码控制任务可运行的CPU
支持负载均衡和绑核运行
6.2 任务迁移
支持跨核任务切换
需要考虑缓存一致性问题
7. 调试功能
7.1 CPU负载统计
周期性采样统计CPU使用率
支持实时监控系统负载
7.2 临界区监控
监控临界区执行时间
检测长时间禁用抢占的情况
8. 实战经验
优先级设计建议:
关键任务使用较高优先级(>100)
普通任务使用中等优先级(50-100)
空闲任务使用最低优先级(0)
调度策略选择:
实时性要求高的用FIFO
需要公平调度的用RR
需要精确控制CPU占用率的用Sporadic
性能优化:
合理使用临界区保护
避免长时间禁用抢占
注意负载均衡(SMP)
调试技巧:
使用CPU负载监控
观察任务切换行为
分析临界区时间
9. 参考资料
NuttX官方文档:
调度器设计文档
API参考手册
源码阅读:
sched/sched/目录下源文件
include/nuttx/sched.h 头文件