NuttX 调度器源码学习

发布于:2025-06-15 ⋅ 阅读:(13) ⋅ 点赞:(0)

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])

这些宏提供了统一的任务列表访问接口:

  1. 封装了具体的列表实现细节

  2. 支持不同配置选项(如SMP)的条件编译

  3. 便于维护和修改列表实现

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 头文件


网站公告

今日签到

点亮在社区的每一天
去签到