NuttX 调度器源码学习
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 > 0int32_t timeslice; // 剩余时间片
#endif#ifdef CONFIG_SMPcpu_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_ADDRENVaddrenv_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 头文件