LK 博客
RTOS内核开发实战(3):任务创建、时间片轮转与状态提交,让调度器真正运转
嵌入式
约 1 分钟阅读 0 赞 0 条评论 鸿蒙黑体

RTOS内核开发实战(3):任务创建、时间片轮转与状态提交,让调度器真正运转

Yukikaze
Yukikaze @Yukikaze
累计点赞 0 登录后每个账号只能点一次
内容长度 0 正文词元数
正文
目录会跟随阅读位置移动。
阅读进度

有了 tcb_tready_queue_t,调度器就已经“能存东西”了,但还不等于“能工作”。

真正让内核运转起来的,是三个关键动作:

  1. 创建任务时把它合法地放进 runnable 集合。
  2. 调度器只负责决定“下一个该跑谁”。
  3. port 层在合适的时机真正提交这次切换。

这版实现里,这三个动作被刻意拆开了。

先看这一层到底在解决什么问题

一、任务创建不是简单 memset,而是一条完整的准入链路

task_create() 的职责不是“把结构体填满”这么简单,它要保证一个新任务在进入调度器之前已经满足所有前置条件:

os_status_t task_create(tcb_t *task, const task_init_config_t *config)
{
    os_status_t status = OS_STATUS_OK;
    uint8_t     scheduler_running = 0U;

    if (task == NULL)
    {
        return OS_STATUS_INVALID_PARAM;
    }

    if (g_task_system_initialized == 0U)
    {
        (void)task_system_init();
    }

    if (task_is_known_to_scheduler(task) != 0U)
    {
        return OS_STATUS_ALREADY_INITIALIZED;
    }

    status = task_init(task, config);
    if (status != OS_STATUS_OK)
    {
        return status;
    }

    ready_queue_insert_tail(&g_task_ready_queue, task);

    if (task->sched_node.owner != &g_task_ready_queue.ready_lists[task->priority])
    {
        task->state = TASK_DELETED;
        return OS_STATUS_INSERT_FAILED;
    }

    scheduler_running = (uint8_t)(g_current_task != NULL);
    status = task_schedule();

    if (status == OS_STATUS_NO_CHANGE)
    {
        return OS_STATUS_OK;
    }

    if ((status == OS_STATUS_SWITCH_REQUIRED) && (scheduler_running == 0U))
    {
        return OS_STATUS_OK;
    }

    return status;
}

这里有两个设计非常关键。

第一,task_create() 会在首次创建任务时顺手初始化调度器全局状态。这意味着系统可以不依赖一个额外的“必须先手工 init”的固定启动顺序,使用体验会更接近一个自洽的内核接口。

第二,创建接口会区分“首任务尚未启动”和“系统已经在跑”两种场景。对于首任务阶段,task_schedule() 返回 OS_STATUS_SWITCH_REQUIRED 是正常现象,但不能误报成“运行期抢占”。所以代码用 scheduler_running 做了一次语义消歧。

二、调度器只做选择,不直接切换

当前实现把“选出 next task”和“把 next task 真正切成 current task”分成了两步。task_schedule() 只负责前者:

os_status_t task_schedule(void)
{
    os_status_t status = OS_STATUS_OK;

    status = task_select_next();
    if (status != OS_STATUS_OK)
    {
        return status;
    }

    if (g_current_task == NULL)
    {
        return OS_STATUS_SWITCH_REQUIRED;
    }

    if (g_next_task != g_current_task)
    {
        return OS_STATUS_SWITCH_REQUIRED;
    }

    return OS_STATUS_NO_CHANGE;
}

为什么要这么拆?

  1. 调度器运行在 C 层,负责根据 ready queue 给出一个确定结果。
  2. 真正的上下文切换运行在 port 层,通常要配合 PendSV、栈切换和汇编恢复现场。
  3. 只要把 g_next_task 维护好,调度器和硬件相关代码就能通过一个清晰边界协作,而不是互相掺在一起。

这种拆分还带来一个副产品:调试非常直接。

你可以先验证“调度决策对不对”,再验证“切换动作对不对”,两个问题不会缠在一起。

三、状态提交集中在 task_set_current()

既然 task_schedule() 不直接切任务,那么谁来真正修改 g_current_task 和任务状态?答案是 task_set_current()

os_status_t task_set_current(tcb_t *task)
{
    if (task == NULL)
    {
        return OS_STATUS_INVALID_PARAM;
    }

    if (task_is_valid(task) == 0U)
    {
        return OS_STATUS_INVALID_STATE;
    }

    if (task != g_next_task)
    {
        return OS_STATUS_INVALID_STATE;
    }

    if (task_is_in_runnable_queue(task) == 0U)
    {
        return OS_STATUS_INVALID_STATE;
    }

    if ((task != g_current_task) && (task->state != TASK_READY))
    {
        return OS_STATUS_INVALID_STATE;
    }

    if ((g_current_task != NULL) && (g_current_task != task) && (g_current_task->state == TASK_RUNNING))
    {
        g_current_task->state = TASK_READY;
    }

    g_current_task = task;
    g_current_task->state = TASK_RUNNING;
    return OS_STATUS_OK;
}

这段代码把“状态迁移”的约束卡得很紧。

它不允许调用方绕过调度器,直接把任意一个 READY 任务强行设成 current;它只接受“刚刚被选中的 g_next_task”。

这就是一个很典型的内核设计原则:所有状态提交都尽量经过单一入口,避免多个路径同时拥有写状态的权力。

四、时间片轮转的前提不是 tick,而是 runnable 集合语义要先统一

很多人写 round-robin 时一上来就看 SysTick,但当前实现先把“同优先级任务如何在 ready list 中轮转”定义清楚了。

主动让出 CPU 的逻辑很直接:

os_status_t task_yield(void)
{
    os_status_t status = OS_STATUS_OK;
    list_t     *ready_list = NULL;

    status = task_validate_running_task(g_current_task);
    if (status != OS_STATUS_OK)
    {
        return status;
    }

    ready_list = task_get_ready_list_by_priority(g_current_task->priority);
    if (ready_list == NULL)
    {
        return OS_STATUS_INVALID_STATE;
    }

    g_current_task->time_slice = g_current_task->time_slice_reload;

    if ((ready_list->item_count > 1U) && (ready_list->head == &g_current_task->sched_node))
    {
        ready_queue_rotate(&g_task_ready_queue, g_current_task->priority);
    }

    return task_schedule();
}

这里的关键点不在于“恢复时间片”,而在于只在当前任务确实位于本优先级队头,且同优先级还有其他任务时,才执行头尾轮转。

也就是说,轮转动作本身就是对 ready queue 语义的消费。

五、为什么 tick 里先调度,再看时间片

时间片 tick 逻辑也采用相同原则:

os_status_t task_time_slice_tick(void)
{
    os_status_t status = OS_STATUS_OK;
    list_t     *ready_list = NULL;
    tcb_t      *current_task = g_current_task;

    if (g_task_system_initialized == 0U)
    {
        return OS_STATUS_NOT_INITIALIZED;
    }

    if (current_task == NULL)
    {
        return OS_STATUS_NO_CHANGE;
    }

    status = task_validate_running_task(current_task);
    if (status != OS_STATUS_OK)
    {
        return status;
    }

    ready_list = task_get_ready_list_by_priority(current_task->priority);
    if (ready_list == NULL)
    {
        return OS_STATUS_INVALID_STATE;
    }

    status = task_schedule();
    if (status == OS_STATUS_SWITCH_REQUIRED)
    {
        return status;
    }

    if (status != OS_STATUS_NO_CHANGE)
    {
        return status;
    }

    if (ready_list->item_count <= 1U)
    {
        current_task->time_slice = current_task->time_slice_reload;
        return OS_STATUS_NO_CHANGE;
    }

    if (current_task->time_slice > 0U)
    {
        current_task->time_slice--;
    }

    if (current_task->time_slice > 0U)
    {
        return OS_STATUS_NO_CHANGE;
    }

    current_task->time_slice = current_task->time_slice_reload;

    if (ready_list->head == &current_task->sched_node)
    {
        ready_queue_rotate(&g_task_ready_queue, current_task->priority);
    }

    return task_schedule();
}

为什么 tick 开头先调用一次 task_schedule()?因为这能优先发现“有更高优先级任务已经就绪”的情况,避免时间片逻辑只盯着同优先级任务,反而漏掉真正应该立即抢占的对象。

所以当前实现的思路不是“tick 触发了调度”,而是“tick 只是可能触发重新决策的一个事件源”。

这个角度非常重要,因为以后加上阻塞唤醒、中断投递消息、超时到期,本质上都会变成类似的调度触发点。

小结

从当前代码看,调度器已经把以下几件事理顺了:

  1. 任务创建与调度器准入分开处理。
  2. 调度决策与状态提交分开处理。
  3. 时间片轮转建立在 ready list 头尾操作之上,而不是写一堆额外标志位。

下一篇就进入硬件相关部分:既然 g_next_task 已经能稳定选出来,Cortex-M3 端口层是怎样通过初始栈帧和 PendSV 把这个“决策”变成一次真实上下文切换的。

作者名片

Yukikaze
Yukikaze
@Yukikaze

私にできることなら何でもするから

评论区
文章作者和管理员都可以管理这里的评论。
0 条评论
登录后即可参与评论。 去登录
还没有评论,欢迎留下第一条交流内容。