4.2.2 任务创建与销毁
4.2.2 任务创建与销毁
📚 本节导读
学习时长: 约 20 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: 任务控制块(TCB)、任务状态机
🎯 学习目标
- 掌握
os_task_create()的完整参数和返回值 - 理解静态创建与动态创建的区别和使用场景
- 掌握
os_task_startup()和os_task_destroy()的用法 - 了解清理回调和用户数据的作用
一、os_task_create() —— 创建任务
1.1 函数签名
os_task_id os_task_create(os_task_dummy_t *task_cb,
void *stack_begin,
uint32_t stack_size,
const char *name,
void (*entry)(void *arg),
void *arg,
uint8_t priority);1.2 参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
task_cb | os_task_dummy_t * | 任务控制块指针。传入 NULL 表示由内核动态分配 |
stack_begin | void * | 任务栈起始地址。传入 NULL 表示由内核动态分配 |
stack_size | uint32_t | 任务栈大小(字节),必须大于 0 |
name | const char * | 任务名称,最大长度为 OS_NAME_MAX(默认 15 字符) |
entry | void (*)(void *arg) | 任务入口函数指针 |
arg | void * | 传递给入口函数的参数 |
priority | uint8_t | 任务优先级,范围 0 ~ OS_TASK_PRIORITY_MAX - 1(0 最高) |
1.3 返回值
| 返回值 | 说明 |
|---|---|
非 NULL | 任务创建成功,返回任务 ID(即 TCB 指针) |
NULL | 任务创建失败(如内存不足、TCB 已存在) |
1.4 内部工作流程
1.4.1 完整源码
来源: kernel/source/os_task.c
os_task_create() 的完整源码如下(与项目中完全一致):
os_task_id os_task_create(os_task_dummy_t *task_cb,
void *stack_begin,
uint32_t stack_size,
const char *name,
void (*entry)(void *arg),
void *arg,
uint8_t priority)
{
os_task_t *task;
uint8_t object_alloc_type;
os_task_t *iter_task;
os_list_node_t *pos;
os_bool_t exist;
OS_ASSERT(OS_NULL != entry);
OS_ASSERT(stack_size > 0);
OS_ASSERT(priority < OS_TASK_PRIORITY_MAX);
OS_ASSERT(OS_FALSE == os_is_irq_active());
#ifndef OS_USING_HEAP
OS_ASSERT(OS_NULL != stack_begin);
OS_ASSERT(OS_NULL != task_cb);
#endif
object_alloc_type = 0;
#ifdef OS_USING_HEAP
if (OS_NULL == task_cb)
{
task = (os_task_t *)OS_KERNEL_MALLOC(sizeof(os_task_t));
object_alloc_type |= OS_KOBJ_ALLOC_TYPE_DYNAMIC;
}
else
#endif
{
task = OS_TYPE_CONVERT(os_task_t *, task_cb);
}
#ifdef OS_USING_HEAP
if (OS_NULL == stack_begin)
{
stack_size = OS_ALIGN_UP(stack_size, OS_ARCH_STACK_ALIGN_SIZE);
stack_begin = OS_KERNEL_MALLOC_ALIGN(OS_ARCH_STACK_ALIGN_SIZE, stack_size);
object_alloc_type |= OS_KDATA_ALLOC_TYPE_DYNAMIC;
}
if ((OS_NULL == stack_begin) || (OS_NULL == task))
{
OS_KERN_LOG(KERN_ERROR, TASK_TAG, "Malloc failed, stack_begin(%p), task(%p)", stack_begin, task);
if (OS_KOBJ_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC))
{
if (OS_NULL != task)
{
OS_KERNEL_FREE(task);
}
}
/* If the space malloc fails, the return task must be NULL. */
task = OS_NULL;
if (OS_KDATA_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KDATA_ALLOC_TYPE_DYNAMIC))
{
if (OS_NULL != stack_begin)
{
OS_KERNEL_FREE(stack_begin);
stack_begin = OS_NULL;
}
}
}
else
#endif
{
exist = OS_FALSE;
os_spin_lock(&gs_os_task_resource_list_lock);
if (0 == (object_alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC))
{
os_list_for_each(pos, &gs_os_task_resource_list_head)
{
iter_task = os_list_entry(pos, os_task_t, resource_node);
if (iter_task == task)
{
task = OS_NULL;
OS_KERN_LOG(KERN_ERROR, TASK_TAG, "The task(addr: 0x%p, name: %s) has been exist",
iter_task,
iter_task->name);
exist = OS_TRUE;
break;
}
}
}
if (OS_FALSE == exist)
{
os_list_add_tail(&gs_os_task_resource_list_head, &task->resource_node);
os_spin_unlock(&gs_os_task_resource_list_lock);
_k_task_init(task, name, entry, arg, stack_begin, stack_size, priority, object_alloc_type);
task->state = OS_TASK_STATE_INIT;
task->object_inited = OS_KOBJ_INITED;
}
else
{
os_spin_unlock(&gs_os_task_resource_list_lock);
#ifdef OS_USING_HEAP
if (OS_KDATA_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KDATA_ALLOC_TYPE_DYNAMIC))
{
OS_KERNEL_FREE(stack_begin);
stack_begin = OS_NULL;
}
#endif
}
}
return OS_TYPE_CONVERT(os_task_id, task);
}os_task_create() 在初始化阶段调用了关键的辅助函数 _k_task_init()(同样位于 kernel/source/os_task.c),负责填充 TCB 中的各字段:
static void _k_task_init(os_task_t *task,
const char *name,
void (*entry)(void *arg),
void *arg,
void *stack_begin,
uint32_t stack_size,
uint8_t priority,
uint8_t object_alloc_type)
{
if (OS_NULL != name)
{
(void)strncpy(task->name, name, OS_NAME_MAX);
task->name[OS_NAME_MAX] = '\0';
}
else
{
task->name[0] = '\0';
}
task->object_alloc_type = object_alloc_type;
task->err_code = OS_SUCCESS;
task->switch_retval = OS_SUCCESS;
task->backup_priority = priority;
task->current_priority = priority;
task->time_slice = OS_SCHEDULE_TIME_SLICE;
task->remaining_time_slice = OS_SCHEDULE_TIME_SLICE;
task->user_data = OS_NULL;
task->block_list_head = OS_NULL;
task->stack_begin = stack_begin;
task->stack_end = (void *)((uint8_t *)stack_begin + stack_size);
task->stack_top = os_hw_stack_init(entry, arg, stack_begin, stack_size, k_task_exit);
os_list_init(&task->task_node);
os_list_init(&task->tick_node);
#ifdef OS_USING_MUTEX
os_list_init(&task->hold_mutex_list_head);
#endif
#ifdef OS_USING_EVENT
task->event_set = 0U;
task->event_option = 0U;
#endif
task->swap_data = 0U;
task->cleanup = OS_NULL;
task->user_data = OS_NULL;
#ifdef OS_USING_CPU_MONITOR
task->usage_info.start_time = 0;
task->usage_info.total_time = 0;
#endif
#ifdef OS_USING_SMP
os_list_init(&task->join_node);
task->cpu_index = -1;
task->cpu_affinity = -1;
#endif
return;
}1.4.2 分步详解
os_task_create() 的执行流程可以划分为 8 个步骤:参数检查、分配/获取 TCB、分配/获取任务栈、防重复创建检查、加入资源链表、TCB 字段初始化、设置初始状态、返回任务 ID。
第一步:参数合法性检查
OS_ASSERT(OS_NULL != entry);
OS_ASSERT(stack_size > 0);
OS_ASSERT(priority < OS_TASK_PRIORITY_MAX);
OS_ASSERT(OS_FALSE == os_is_irq_active());
#ifndef OS_USING_HEAP
OS_ASSERT(OS_NULL != stack_begin);
OS_ASSERT(OS_NULL != task_cb);
#endif详细解释:
| 断言 | 作用 |
|---|---|
OS_ASSERT(OS_NULL != entry) | 入口函数不能为空,否则任务无法运行 |
OS_ASSERT(stack_size > 0) | 栈大小必须大于 0 |
OS_ASSERT(priority < OS_TASK_PRIORITY_MAX) | 优先级必须在合法范围 0 ~ OS_TASK_PRIORITY_MAX - 1 内 |
OS_ASSERT(OS_FALSE == os_is_irq_active()) | 不能在中断上下文中创建任务 |
OS_ASSERT(OS_NULL != stack_begin) | 未启用堆时,必须由用户提供栈空间 |
OS_ASSERT(OS_NULL != task_cb) | 未启用堆时,必须由用户提供 TCB |
关键点:
OS_ASSERT是断言宏,条件不满足时会触发错误并停止执行,用于在开发阶段尽早暴露错误。- 最后两个断言只在未启用
OS_USING_HEAP时生效。因为未启用堆就无法动态分配,所以 TCB 和栈都必须由用户静态提供。
第二步:分配/获取 TCB 内存
object_alloc_type = 0;
#ifdef OS_USING_HEAP
if (OS_NULL == task_cb)
{
task = (os_task_t *)OS_KERNEL_MALLOC(sizeof(os_task_t));
object_alloc_type |= OS_KOBJ_ALLOC_TYPE_DYNAMIC;
}
else
#endif
{
task = OS_TYPE_CONVERT(os_task_t *, task_cb);
}详细解释:
| 分支 | 触发条件 | 行为 |
|---|---|---|
if (OS_NULL == task_cb) | 动态创建(传入 NULL) | 从堆中分配一个 sizeof(os_task_t) 大小的 TCB,并标记为动态分配 |
else | 静态创建(传入地址) | 直接把传入的 task_cb 指针转换为 os_task_t * 使用 |
关键点:
object_alloc_type是一个标志位变量,用位或|=记录内存是否动态分配,后续步骤会用它判断是否需要释放内存以及是否需要进行防重复检查。OS_KOBJ_ALLOC_TYPE_DYNAMIC表示 TCB 本身是动态分配的。
具体示例:object_alloc_type 的位定义(见 kernel/source/os_kernel_internal.h)
#define OS_ALLOC_TYPE_STATIC 0 /* 0x00:静态分配 */
#define OS_KOBJ_ALLOC_TYPE_DYNAMIC (1 << 0) /* 0x01:bit0 —— TCB 动态分配 */
#define OS_KDATA_ALLOC_TYPE_DYNAMIC (1 << 1) /* 0x02:bit1 —— 栈(数据区)动态分配 */其中两个动态标志各占一个独立 bit,可以同时置位。下面通过几种典型的创建方式来演示 object_alloc_type 的变化过程:
示例 1:完全动态创建(TCB 和栈都传 NULL)
初始值 object_alloc_type = 0x00 (0000 0000)
TCB 动态分配后(第二步) |= 0x01 → 0x01 (0000 0001)bit0=1
栈动态分配后(第三步) |= 0x02 → 0x03 (0000 0011)bit0、bit1 都为 1最终 object_alloc_type = 0x03,表示 TCB 和栈都是动态分配的。销毁时两部分都需要释放。
示例 2:混合创建(静态 TCB + 动态栈,见 3.2 节)
初始值 object_alloc_type = 0x00 (0000 0000)
TCB 是静态的(第二步走 else) 不置位 → 0x00 (0000 0000)bit0=0
栈动态分配后(第三步) |= 0x02 → 0x02 (0000 0010)bit1=1最终 object_alloc_type = 0x02,表示 只有栈是动态分配的。销毁时只需释放栈,TCB 是用户提供的静态内存,不能释放。
示例 3:完全静态创建(TCB 和栈都传地址)
初始值 object_alloc_type = 0x00 (0000 0000)
TCB 是静态的(第二步走 else) 不置位 → 0x00 (0000 0000)bit0=0
栈是静态的(第三步走 else) 不置位 → 0x00 (0000 0000)bit1=0最终 object_alloc_type = 0x00,表示 TCB 和栈都是静态的。销毁时什么都不用释放。
后续如何使用这个标志位:在 os_task_create() 内部及任务销毁流程中,都用按位与 & 来判断某个位是否被置位。例如判断"TCB 是否为动态分配":
if (OS_KOBJ_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC))
{
OS_KERNEL_FREE(task); /* 只有动态分配的 TCB 才需要释放 */
}以示例 1 的 object_alloc_type = 0x03 为例,计算过程:
object_alloc_type 0x03 → 0000 0011
& OS_KOBJ_ALLOC_TYPE_DYNAMIC 0x01 → 0000 0001
────────────────────────────────────────────
结果 0x01 → 0000 0001 (== 0x01,条件成立:TCB 是动态的)同理,第四步"防重复创建检查"使用 if (0 == (object_alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC)),即 bit0 为 0(TCB 是静态的)时才需要遍历链表检查是否重复;动态 TCB(bit0=1)是新 malloc 的,不会冲突,跳过检查。
第三步:分配/获取任务栈内存(含失败回滚)
#ifdef OS_USING_HEAP
if (OS_NULL == stack_begin)
{
stack_size = OS_ALIGN_UP(stack_size, OS_ARCH_STACK_ALIGN_SIZE);
stack_begin = OS_KERNEL_MALLOC_ALIGN(OS_ARCH_STACK_ALIGN_SIZE, stack_size);
object_alloc_type |= OS_KDATA_ALLOC_TYPE_DYNAMIC;
}
if ((OS_NULL == stack_begin) || (OS_NULL == task))
{
OS_KERN_LOG(KERN_ERROR, TASK_TAG, "Malloc failed, stack_begin(%p), task(%p)", stack_begin, task);
if (OS_KOBJ_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC))
{
if (OS_NULL != task)
{
OS_KERNEL_FREE(task);
}
}
/* If the space malloc fails, the return task must be NULL. */
task = OS_NULL;
if (OS_KDATA_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KDATA_ALLOC_TYPE_DYNAMIC))
{
if (OS_NULL != stack_begin)
{
OS_KERNEL_FREE(stack_begin);
stack_begin = OS_NULL;
}
}
}
else
#endif详细解释(本步骤内部可细分为三个环节):
环节 1:是否动态分配栈
if (OS_NULL == stack_begin)
{详细解释:
stack_begin参数是用户传入的任务栈起始地址。- 当
stack_begin == OS_NULL时,说明用户没有提供栈,需要内核动态分配,进入if分支。 - 当
stack_begin != OS_NULL时(静态创建),跳过整个分支,直接使用用户提供的栈地址(此时stack_size也保持用户传入的原始值)。
环节 2:对齐与分配栈内存
stack_size = OS_ALIGN_UP(stack_size, OS_ARCH_STACK_ALIGN_SIZE);
stack_begin = OS_KERNEL_MALLOC_ALIGN(OS_ARCH_STACK_ALIGN_SIZE, stack_size);
object_alloc_type |= OS_KDATA_ALLOC_TYPE_DYNAMIC;详细解释:
| 代码 | 作用 |
|---|---|
stack_size = OS_ALIGN_UP(stack_size, OS_ARCH_STACK_ALIGN_SIZE) | 先把用户给的栈大小向上对齐到架构要求的边界 |
stack_begin = OS_KERNEL_MALLOC_ALIGN(...) | 从堆中分配一块起始地址对齐的栈内存 |
object_alloc_type |= OS_KDATA_ALLOC_TYPE_DYNAMIC | 将栈标记为动态分配(置位 bit1) |
为什么需要对齐(OS_ALIGN_UP):
OS_ALIGN_UP 的定义如下(见 kernel/include/os_stddef.h):
#define OS_ALIGN_UP(size, align) (((size) + (align)-1) & ~((align)-1))它的作用是把 size 向上取整到 align 的整数倍。而 OS_ARCH_STACK_ALIGN_SIZE 是架构相关的栈对齐要求,常见取值如下(见各架构的 arch_task.h):
| 架构 | OS_ARCH_STACK_ALIGN_SIZE | 说明 |
|---|---|---|
| Cortex-M(armv7m/armv6m 等) | 8 | 栈指针按 8 字节(64-bit)对齐 |
| RISC-V | 16 | 栈指针按 128-bit 边界对齐 |
| sim(POSIX 模拟) | 16 * 1024 | 模拟线程要求 16K 对齐 |
具体计算示例:假设在 Cortex-M 平台(对齐值 8),用户传入 stack_size = 1002:
OS_ALIGN_UP(1002, 8)
= ((1002 + 8 - 1) & ~(8 - 1))
= (1009 & ~7)
= 1009 & 0xFFFFFFF8
= 1008可以看到,1002 被向上取整成了 1008(比 1002 大且是 8 的倍数)。这样既保证了栈满足对齐要求,又不会浪费过多空间。
再看一个已经是 8 的倍数的例子 stack_size = 1000:
OS_ALIGN_UP(1000, 8)
= ((1000 + 8 - 1) & ~7)
= (1007 & 0xFFFFFFF8)
= 1000结果仍是 1000,说明已经是对齐值整数倍的 size 不会被改动,OS_ALIGN_UP 只会把不足的部分向上补齐到下一个边界。
为什么需要对齐分配(OS_KERNEL_MALLOC_ALIGN):
#define OS_KERNEL_MALLOC_ALIGN(alignment, size) os_aligned_malloc(alignment, size) /* 见 os_kernel_internal.h */- 普通的
os_malloc只保证内存块按堆块头对齐,无法保证满足架构栈对齐要求。 os_aligned_malloc会专门返回一个起始地址满足alignment对齐要求的内存块,确保后续任务切换时栈指针(SP)满足架构约束。
环节 3:分配失败检测与回滚
if ((OS_NULL == stack_begin) || (OS_NULL == task))
{
OS_KERN_LOG(KERN_ERROR, TASK_TAG, "Malloc failed, stack_begin(%p), task(%p)", stack_begin, task);
if (OS_KOBJ_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC))
{
if (OS_NULL != task)
{
OS_KERNEL_FREE(task);
}
}
/* If the space malloc fails, the return task must be NULL. */
task = OS_NULL;
if (OS_KDATA_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KDATA_ALLOC_TYPE_DYNAMIC))
{
if (OS_NULL != stack_begin)
{
OS_KERNEL_FREE(stack_begin);
stack_begin = OS_NULL;
}
}
}详细解释:
进入失败分支的条件:只要 stack_begin 或 task 任一为 NULL 就进入本分支。这里的两种失败来源是:
| 失败来源 | 说明 |
|---|---|
task == OS_NULL | 第二步中 TCB 动态分配失败(OS_KERNEL_MALLOC 返回 NULL) |
stack_begin == OS_NULL | 本步中栈动态分配失败(os_aligned_malloc 返回 NULL) |
注意:判断条件用
||(或)而非&&,是因为只要任一内存没分配到,整个任务就无法创建,必须统一走回滚流程。
失败回滚的四步逻辑:
第 1 步:打印错误日志
OS_KERN_LOG(KERN_ERROR, TASK_TAG, "Malloc failed, stack_begin(%p), task(%p)", stack_begin, task);把失败的两个指针地址打印出来,方便开发调试定位是哪一块内存分配失败。
第 2 步:释放动态分配的 TCB
if (OS_KOBJ_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC))
{
if (OS_NULL != task)
{
OS_KERNEL_FREE(task);
}
}- 先用
object_alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC判断 bit0 是否为 1(TCB 是否是动态分配的)。 - 只有 TCB 是动态分配(bit0=1)时,才执行
OS_KERNEL_FREE(task)释放。 - 关键:若 TCB 是静态的(bit0=0,用户提供的),则跳过释放,绝不释放用户静态内存。
第 3 步:强制置空 task
/* If the space malloc fails, the return task must be NULL. */
task = OS_NULL;注释明确说明:只要内存分配失败,返回的任务必须是 NULL。这一句无论 task 之前是动态还是静态都执行,保证最后返回 NULL 表示创建失败。
第 4 步:释放动态分配的栈
if (OS_KDATA_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KDATA_ALLOC_TYPE_DYNAMIC))
{
if (OS_NULL != stack_begin)
{
OS_KERNEL_FREE(stack_begin);
stack_begin = OS_NULL;
}
}- 用
object_alloc_type & OS_KDATA_ALLOC_TYPE_DYNAMIC判断 bit1 是否为 1(栈是否是动态分配的)。 - 只有栈是动态分配(bit1=1)时,才释放栈并把
stack_begin置空。
具体回滚场景示例:
| 场景 | task 分配 | stack_begin 分配 | object_alloc_type | 回滚动作 |
|---|---|---|---|---|
| 场景 A:TCB 成功、栈失败 | 动态,非 NULL | NULL(失败) | 0x01(bit0=1) | 释放 TCB;stack_begin 已是 NULL 不释放 |
| 场景 B:TCB 失败、栈成功 | NULL(失败) | 动态,非 NULL | 0x02(bit1=1) | task 为 NULL 不释放;释放栈 |
| 场景 C:TCB 失败、栈失败 | NULL | NULL | 0x00 | 都不释放(本就都是 NULL) |
| 场景 D:TCB 静态、栈失败 | 静态,非 NULL | NULL | 0x00 | 不释放静态 TCB;栈为 NULL 不释放 |
从表格可以看出:无论哪种失败组合,都只会释放真正动态分配且成功的部分,用户静态提供的内存绝不会被误释放,从而避免内存泄漏和非法释放。
关键点:
- 整个第三步骤被
#ifdef OS_USING_HEAP包裹。未启用堆(静态创建)时,整个分支被编译期屏蔽,直接跳过,因为静态创建的栈和 TCB 都由用户提供,无需也不会进行动态分配。 - 通过
object_alloc_type的标记(bit0/bit1)逐一分清哪些内存是动态分配的,做到只释放真正动态分配的部分,不会误释放用户静态提供的内存。
第四步:防重复创建检查
exist = OS_FALSE;
os_spin_lock(&gs_os_task_resource_list_lock);
if (0 == (object_alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC))
{
os_list_for_each(pos, &gs_os_task_resource_list_head)
{
iter_task = os_list_entry(pos, os_task_t, resource_node);
if (iter_task == task)
{
task = OS_NULL;
OS_KERN_LOG(KERN_ERROR, TASK_TAG, "The task(addr: 0x%p, name: %s) has been exist",
iter_task,
iter_task->name);
exist = OS_TRUE;
break;
}
}
}详细解释(本步骤的核心是回答一个问题:当前要使用的 TCB 是不是已经被别的任务占用了?)
环节 1:为什么需要防重复检查
exist = OS_FALSE;
os_spin_lock(&gs_os_task_resource_list_lock);详细解释:
- 背景:任务创建完成后,TCB 会被挂入一个全局任务资源链表
gs_os_task_resource_list_head(见第五步),系统通过它统一管理所有任务。 - 问题:静态创建时,用户传入的是一个固定的静态 TCB 地址(如
OS_TASK_DEFINE(my_task_cb)定义的全局变量)。如果用户在代码中不小心用同一个 TCB 地址创建了两次任务,第二个任务会破坏第一个任务的数据,导致系统混乱。 - 目的:本步骤在把 TCB 挂入链表之前,先遍历链表检查一遍,确保没有重复使用同一个 TCB。
exist:一个标志,OS_FALSE表示"未发现重复",OS_TRUE表示"发现重复"。os_spin_lock():加自旋锁。因为全局资源链表可能被多个 CPU(SMP)或中断并发访问,需要锁保护,防止遍历过程中链表被其他代码修改导致错误。
环节 2:为什么只检查静态 TCB
if (0 == (object_alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC))
{
os_list_for_each(pos, &gs_os_task_resource_list_head)详细解释:
- 判断条件
0 == (object_alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC)等价于:bit0 == 0,即 TCB 是静态分配的。 - 只有静态 TCB 才需要遍历检查,原因如下:
| TCB 来源 | 是否检查 | 原因 |
|---|---|---|
| 静态(用户传入地址) | ✔ 需要检查 | 地址固定,可能被重复使用,必须检查 |
| 动态(第二步 malloc) | ✘ 跳过检查 | 是刚 malloc 出来的全新内存,地址不可能是已存在的任务,天然不会重复 |
动态 TCB 跳过检查既是因为逻辑上不可能重复,也是性能优化——避免每次都白白遍历整条链表。
环节 3:遍历链表查找重复 TCB
os_list_for_each(pos, &gs_os_task_resource_list_head)
{
iter_task = os_list_entry(pos, os_task_t, resource_node);
if (iter_task == task)
{
task = OS_NULL;
OS_KERN_LOG(KERN_ERROR, TASK_TAG, "The task(addr: 0x%p, name: %s) has been exist",
iter_task,
iter_task->name);
exist = OS_TRUE;
break;
}
}详细解释:
| 代码 | 作用 |
|---|---|
os_list_for_each(pos, &gs_os_task_resource_list_head) | 从头到尾遍历全局任务资源链表,pos 依次指向每个节点的链表指针 |
iter_task = os_list_entry(pos, os_task_t, resource_node) | 通过链表节点指针 pos 反推出它所属的 os_task_t 结构体(即某个已存在任务的 TCB) |
if (iter_task == task) | 判断链表中某个已有任务的 TCB 地址是否与当前要创建的 task 地址完全相同 |
os_list_entry 与 resource_node 的关系:
os_list_entry(pos, os_task_t, resource_node)- TCB 结构体
os_task_t中包含一个成员resource_node(见 4.2 任务控制块一节),它是挂入全局资源链表用的"节点"。 - 链表里存放的其实是各个 TCB 的
resource_node节点地址,而不是 TCB 的起始地址。 os_list_entry会根据resource_node在os_task_t中的偏移量,反向计算出整个 TCB 的起始地址。这样就拿到了"已存在任务的 TCB"指针iter_task。
找到重复后的处理(iter_task == task 成立):
| 动作 | 代码 | 说明 |
|---|---|---|
| 置空任务 | task = OS_NULL | 标记创建失败,保证最后返回 NULL |
| 打印错误日志 | OS_KERN_LOG(...) | 输出重复 TCB 的地址和名称,方便定位 |
| 置重复标志 | exist = OS_TRUE | 供第五步判断走"失败"分支 |
| 跳出循环 | break | 找到重复即可,无需继续遍历 |
具体遍历示例:
假设全局任务资源链表中已存在 2 个任务,它们的 TCB 地址分别为 0x20000000(task_a)和 0x20000040(task_b)。现在用户试图用 0x20000040(即 &my_task_cb)再次创建任务:
链表结构: head → [task_a: 0x20000000] → [task_b: 0x20000040] → NULL
第 1 次遍历:iter_task = 0x20000000,与 task=0x20000040 不同,继续
第 2 次遍历:iter_task = 0x20000040,与 task=0x20000040 相同 → 判定重复!
→ task = NULL,exist = TRUE,break 跳出最终第五步会走 exist == OS_TRUE 分支,释放已分配的内存并返回失败,从而保护了 task_b 不被破坏。
关键点:
- 本步骤只做检查、不改链表,真正"挂入链表"的操作在第五步完成。
- 一旦发现重复,
task被置为OS_NULL,后续第五步通过exist标志走失败分支,不会再执行_k_task_init()初始化,避免破坏已存在的任务。 - 若
exist仍为OS_FALSE(未发现重复),则进入第五步正常创建流程。
第五步:加入全局任务资源链表
if (OS_FALSE == exist)
{
os_list_add_tail(&gs_os_task_resource_list_head, &task->resource_node);
os_spin_unlock(&gs_os_task_resource_list_lock);
_k_task_init(task, name, entry, arg, stack_begin, stack_size, priority, object_alloc_type);
task->state = OS_TASK_STATE_INIT;
task->object_inited = OS_KOBJ_INITED;
}
else
{
os_spin_unlock(&gs_os_task_resource_list_lock);
#ifdef OS_USING_HEAP
if (OS_KDATA_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KDATA_ALLOC_TYPE_DYNAMIC))
{
OS_KERNEL_FREE(stack_begin);
stack_begin = OS_NULL;
}
#endif
}详细解释:本步骤根据第四步的结果(exist)分成两个分支:
- 若
exist == OS_FALSE(第四步未发现重复):执行if分支,正常完成任务的登记与初始化。 - 若
exist == OS_TRUE(第四步发现重复):执行else分支,回滚已分配的内存,创建失败。
分支一:成功路径(exist == OS_FALSE)
if (OS_FALSE == exist)
{
os_list_add_tail(&gs_os_task_resource_list_head, &task->resource_node);
os_spin_unlock(&gs_os_task_resource_list_lock);
_k_task_init(task, name, entry, arg, stack_begin, stack_size, priority, object_alloc_type);
task->state = OS_TASK_STATE_INIT;
task->object_inited = OS_KOBJ_INITED;
}详细解释(按执行顺序):
| 代码 | 作用 |
|---|---|
os_list_add_tail(&gs_os_task_resource_list_head, &task->resource_node) | 把当前任务的 resource_node 节点挂到全局任务资源链表的尾部 |
os_spin_unlock(&gs_os_task_resource_list_lock) | 释放第四步加的自旋锁 |
_k_task_init(...) | 初始化 TCB 各字段(详见第六步) |
task->state = OS_TASK_STATE_INIT | 设置任务状态为 INIT |
task->object_inited = OS_KOBJ_INITED | 标记对象已初始化 |
os_list_add_tail 到底做了什么:
os_list_add_tail(head, node) 是链表操作,把 node 插入到 head 指向的链表尾部。结合本场景:
head是全局任务资源链表头gs_os_task_resource_list_head。node是&task->resource_node,即当前任务 TCB 中的resource_node成员。
执行后,新任务的 resource_node 就被"登记"进了全局任务资源链表。例如链表中原本已有 2 个任务(task_a、task_b),现在新增 task_c:
加入前: head → [task_a] → [task_b] → NULL
加入后: head → [task_a] → [task_b] → [task_c: 当前新任务] → NULL为什么要把 TCB 登记进全局资源链表:
| 用途 | 说明 |
|---|---|
| 系统统一管理 | 内核可以遍历该链表获取所有已创建任务的信息 |
| shell 命令支持 | 例如 ps / list_task 等命令通过遍历此链表列出所有任务 |
| 销毁时查找 | 任务销毁时也需要从该链表移除(见 os_task_destroy) |
| 第四步检查的基础 | 正因为任务都会登记在这里,第四步才能遍历它做防重复检查 |
执行顺序的讲究(为什么 _k_task_init 在挂链表之后):
这一步的代码顺序是:①挂链表 → ②释放锁 → ③初始化 → ④设状态 → ⑤标记。要理解这个顺序,先要明白一个前提:
挂链表和释放锁之后、但
_k_task_init还没执行完之前,这段代码可以被"打断"。因为os_spin_unlock()已经释放了自旋锁,此时如果发生中断、或者_k_task_init内部触发了调度,CPU 会暂时离开os_task_create()去执行别的事情(比如另一个任务、或中断服务函数)。
打断之后,其他代码如果去遍历全局任务资源链表,会遇到什么情况?这取决于该任务在链表中的状态:
- "先挂链表再初始化"的顺序:此时该任务的
resource_node已经在链表里了,别的代码能看到这个任务的记录。虽然它的字段还没初始化完(object_inited还是未初始化状态),但至少不会出现"链表里少了一个任务"的混乱情况,后续查询时能识别出这是一个"正在创建、尚未就绪"的任务。 - 反过来(先初始化再挂链表):在"初始化进行中、还没挂链表"的这段时间里,如果被打断,别的代码遍历链表就完全看不到这个任务。假设这时系统在做任务统计、shell 列任务列表等,就会遗漏这个正在创建的任务,造成信息不一致。
类比帮助理解:可以把它想象成"先登记入册,再填写档案"。
- 先把名字登记进系统花名册(挂链表),这样别人查花名册时能看到"有这么个人";
- 然后再慢慢填档案(
_k_task_init初始化各字段); - 如果反过来先填档案再登记,那么填档案的这段时间里,花名册上查不到这个人,管理上就会出漏洞。
另一个考虑:锁的持有时间:
- 自旋锁是一种"忙等锁"——持有它的代码会占用 CPU 不放,其他想拿同一把锁的代码只能原地空转等待。
- 所以锁持有时间越短越好。本步骤先快速完成"挂链表"(一个链表插入操作),立刻释放锁,然后再去做耗时的
_k_task_init初始化(要复制名称、初始化多个链表节点等)。这样锁只保护了最短的临界区,其他代码不必长时间空等,并发性能更好。 - 假如把
_k_task_init放在持锁期间,锁就要一直被占着直到初始化完成,其他代码会长时间阻塞空转,性能明显下降。
初始化完成后:
task->state = OS_TASK_STATE_INIT:任务进入 INIT 状态(注意是 INIT,不是 READY,此时尚未加入就绪队列,调度器不会调度它)。task->object_inited = OS_KOBJ_INITED:标记对象已初始化,供后续检查任务是否有效。
分支二:失败路径(exist == OS_TRUE)
else
{
os_spin_unlock(&gs_os_task_resource_list_lock);
#ifdef OS_USING_HEAP
if (OS_KDATA_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KDATA_ALLOC_TYPE_DYNAMIC))
{
OS_KERNEL_FREE(stack_begin);
stack_begin = OS_NULL;
}
#endif
}详细解释:
| 代码 | 作用 |
|---|---|
os_spin_unlock(&gs_os_task_resource_list_lock) | 释放第四步加的自旋锁 |
OS_KERNEL_FREE(stack_begin) | 释放动态分配的栈内存 |
stack_begin = OS_NULL | 置空指针,防止悬空 |
为什么只需要释放栈?栈分配失败的情况到哪里去了?
要回答这个问题,必须先理清代码的整体控制流。回顾第三步,它的结构是 if / else:
#ifdef OS_USING_HEAP
if (OS_NULL == stack_begin)
{
... 分配栈 ...
}
if ((OS_NULL == stack_begin) || (OS_NULL == task)) // 【A】栈或 TCB 分配失败
{
... 释放已分配内存,task = OS_NULL ...
}
else // 【B】分配都成功,才走到这里
#endif
{
/* 第四步、第五步 都在这个 else 块里 */
exist = OS_FALSE;
...
}关键点:本分支(第五步的 else,即 exist == OS_TRUE)只可能在分支【B】里被走到。
- 如果栈分配失败(
stack_begin == NULL),会进入分支【A】,在那里释放内存并把task置NULL,然后直接跳过整个 else 块(第四、第五步都不会执行),最终return NULL结束函数。所以"栈分配失败"的情况已经在第三步的分支【A】处理完毕,根本到不了第五步。 - 能走到第五步,说明分支【B】成立,即 TCB 和栈都分配成功了(
stack_begin != NULL)。此时第五步发现的是另一种失败——TCB 地址重复(第四步检查出静态 TCB 已被占用)。
所以,本分支"只需要释放栈"的完整推理是:
| 判断对象 | 为什么不用/需要释放 |
|---|---|
TCB(task) | 第四步发现重复时 task 已被置 NULL,不纳入创建。且能到本分支的 TCB 必然是静态的(第四步只对静态 TCB 做重复检查),静态内存是用户提供的,不能释放。 |
栈(stack_begin) | 栈一定分配成功了,且可能是动态分配的(bit1=1)。任务创建失败后这份栈没人使用,若不释放就造成内存泄漏,所以必须释放。 |
| 栈是静态的情况 | 若栈是静态的(bit1=0),说明是用户提供的,OS_KDATA_ALLOC_TYPE_DYNAMIC 未置位,if 条件不成立,自然跳过释放,不会误释放用户内存。 |
小结:
- "栈分配失败"的兜底已经在第三步的分支【A】处理,不会遗留到这里。
- 本分支(第五步
else)处理的是"分配成功但 TCB 重复"的场景,所以只需要关心"动态分配的栈要不要释放",答案是:要释放,否则泄漏。 - 两层失败(分配失败、TCB 重复)各自由不同的分支处理,互不遗漏,且都保证了"只释放真正动态分配的内存,不误释放用户静态内存"。
补充说明:整个
OS_KERNEL_FREE(stack_begin)由#ifdef OS_USING_HEAP包裹,未启用堆时跳过(此时栈也必然是用户静态提供的,无需释放)。
关键点:
- 成功路径是"登记 + 初始化",失败路径是"回滚 + 清理",两者通过第四步的
exist标志严格区分。 - 成功路径把 TCB 登记进全局资源链表,使任务真正"纳入了系统的管理范围",后续才能被调度、被查询、被销毁。
- 无论成功还是失败,自旋锁都会在分支内正确释放,不会出现锁泄漏导致系统死锁。
第六步:TCB 字段初始化(_k_task_init())
if (OS_NULL != name)
{
(void)strncpy(task->name, name, OS_NAME_MAX);
task->name[OS_NAME_MAX] = '\0';
}
else
{
task->name[0] = '\0';
}
task->object_alloc_type = object_alloc_type;
task->err_code = OS_SUCCESS;
task->switch_retval = OS_SUCCESS;
task->backup_priority = priority;
task->current_priority = priority;
task->time_slice = OS_SCHEDULE_TIME_SLICE;
task->remaining_time_slice = OS_SCHEDULE_TIME_SLICE;
task->user_data = OS_NULL;
task->block_list_head = OS_NULL;
task->stack_begin = stack_begin;
task->stack_end = (void *)((uint8_t *)stack_begin + stack_size);
task->stack_top = os_hw_stack_init(entry, arg, stack_begin, stack_size, k_task_exit);
os_list_init(&task->task_node);
os_list_init(&task->tick_node);
#ifdef OS_USING_MUTEX
os_list_init(&task->hold_mutex_list_head);
#endif
#ifdef OS_USING_EVENT
task->event_set = 0U;
task->event_option = 0U;
#endif
task->swap_data = 0U;
task->cleanup = OS_NULL;
task->user_data = OS_NULL;
#ifdef OS_USING_CPU_MONITOR
task->usage_info.start_time = 0;
task->usage_info.total_time = 0;
#endif
#ifdef OS_USING_SMP
os_list_init(&task->join_node);
task->cpu_index = -1;
task->cpu_affinity = -1;
#endif详细解释(按字段分组):
| 字段组 | 字段 | 作用 |
|---|---|---|
| 名称 | name | 用 strncpy 复制任务名称,最多 OS_NAME_MAX 个字符并补 '\0' |
| 基础属性 | object_alloc_type | 记录 TCB/栈是否为动态分配 |
| 基础属性 | err_code / switch_retval | 错误码、切换返回值初始化为 OS_SUCCESS |
| 优先级 | backup_priority / current_priority | 备用优先级与当前优先级均设为入参 priority |
| 时间片 | time_slice / remaining_time_slice | 初始时间片与剩余时间片均设为 OS_SCHEDULE_TIME_SLICE |
| 用户数据 | user_data | 初始为 NULL |
| 阻塞 | block_list_head | 初始为 NULL |
| 栈 | stack_begin / stack_end | 栈起始地址、结束地址(stack_begin + stack_size) |
| 栈(核心) | stack_top | 由 os_hw_stack_init() 初始化,返回任务首次调度的 SP 值 |
| 链表节点 | task_node / tick_node | 就绪/阻塞队列节点、节拍队列节点 |
| 条件编译 | hold_mutex_list_head | OS_USING_MUTEX 时,持有互斥锁链表头 |
| 条件编译 | event_set / event_option | OS_USING_EVENT 时,事件集与事件选项 |
| 其他 | swap_data | 切换数据,初始为 0 |
| 其他 | cleanup / user_data | 清理回调、用户数据初始为 NULL |
| 条件编译 | usage_info | OS_USING_CPU_MONITOR 时,CPU 使用率统计清零 |
| 条件编译 | join_node / cpu_index / cpu_affinity | OS_USING_SMP 时,join 节点及 CPU 亲和性初始化 |
关键点 —— os_hw_stack_init()(任务能否启动的前提):
task->stack_top = os_hw_stack_init(entry, arg, stack_begin, stack_size, k_task_exit);- 该函数是架构相关代码,会根据 CPU 的栈结构,在任务栈空间上预先构造好任务的"首次运行上下文"。
- 它会把入口函数
entry、参数arg、任务退出处理函数k_task_exit预置到栈上。 - 返回的
stack_top就是任务首次被调度时的栈顶指针(SP),调度器从该地址恢复寄存器即可跳转到入口函数开始执行。
第七步:设置任务初始状态
task->state = OS_TASK_STATE_INIT;
task->object_inited = OS_KOBJ_INITED;详细解释:
task->state = OS_TASK_STATE_INIT:任务进入 INIT 状态。task->object_inited = OS_KOBJ_INITED:标记对象已初始化。
关键点:
- 此时任务是 INIT 而非 READY,尚未加入就绪队列,调度器不会调度它。
- 必须后续调用
os_task_startup()才会将状态转为 READY 并加入就绪队列(详见 4.2.1 任务状态机)。
第八步:返回任务 ID
return OS_TYPE_CONVERT(os_task_id, task);详细解释:
- 将
task指针转换为os_task_id类型返回。 - 创建成功时返回非
NULL的 TCB 指针;若中途失败(如内存不足、TCB 已存在),task已被置为OS_NULL,返回NULL。
注意:
os_task_create()不会在中断上下文中执行(内部有OS_ASSERT(OS_FALSE == os_is_irq_active())断言),也不应在中断禁用或调度器锁定的情况下调用。
二、静态创建任务
静态创建适用于资源受限、不允许动态内存分配的系统。
2.1 静态创建宏
/* 定义任务控制块 */
#define OS_TASK_DEFINE(name) os_task_dummy_t name
/* 定义任务栈 */
#define OS_TASK_STACK_DEFINE(name, size) \
OS_ALIGN(OS_ARCH_STACK_ALIGN_SIZE) uint8_t (name)[OS_ALIGN_UP(size, OS_ARCH_STACK_ALIGN_SIZE)]
/* 获取栈信息 */
#define OS_TASK_STACK_BEGIN_ADDR(name) (void *)&((name)[0])
#define OS_TASK_STACK_SIZE(name) sizeof(name)2.2 静态创建示例
下面通过一个完整的示例演示静态创建任务的四个步骤。静态创建的核心特征是:TCB 和任务栈都是用户预先定义的全局变量,不需要启用 OS_USING_HEAP。
#include <os_task.h>
/* 定义入口函数参数结构体(可选,用于传递业务数据) */
typedef struct
{
int led_pin; /* 控制的 LED 引脚 */
uint16_t period; /* 闪烁周期(ms) */
} led_param_t;
/* ========== 步骤 1:定义任务控制块 ========== */
/* OS_TASK_DEFINE(name) 宏定义: #define OS_TASK_DEFINE(name) os_task_dummy_t name
因此 OS_TASK_DEFINE(my_task_cb) 展开为一条普通变量声明:
os_task_dummy_t my_task_cb;
在本例中它写在函数外面(文件作用域),所以 my_task_cb 是一个全局变量。
它占用的是编译/链接阶段就分配好的静态存储区(.bss/.data),
而不是运行时从堆(heap)里 malloc 出来的,所以"不占堆"。
注意:os_task_dummy_t 是给用户看的"占位"类型,它的内存大小与内核
真正的 os_task_t 完全一致,内核会把这块内存当作真正的 TCB 来使用。 */
OS_TASK_DEFINE(my_task_cb);
/* ========== 步骤 2:定义任务栈(1024 字节,自动对齐) ========== */
/* OS_TASK_STACK_DEFINE(name, size) 宏展开为:
OS_ALIGN(OS_ARCH_STACK_ALIGN_SIZE)
uint8_t my_task_stack[OS_ALIGN_UP(1024, OS_ARCH_STACK_ALIGN_SIZE)];
即定义了一个按 OS_ARCH_STACK_ALIGN_SIZE 对齐的全局数组 my_task_stack,
栈内存同样在编译/链接阶段分配好,不占堆。 */
OS_TASK_STACK_DEFINE(my_task_stack, 1024);
/* ========== 步骤 3:任务入口函数 ========== */
void my_task_entry(void *arg)
{
led_param_t *param = (led_param_t *)arg; /* 取出传入的参数 */
while (1)
{
/* 业务逻辑:点亮/熄灭 LED */
gpio_pin_set(param->led_pin, 1);
os_task_msleep(param->period);
gpio_pin_set(param->led_pin, 0);
os_task_msleep(param->period);
}
}
/* ========== 步骤 4:创建并启动任务 ========== */
void app_init(void)
{
os_task_id tid; /* 任务 ID(即 TCB 指针) */
os_err_t ret;
led_param_t led_cfg = { .led_pin = 0x01, .period = 500 };
/* 4.1 创建任务:TCB 和栈都传入用户预先定义好的地址 */
tid = os_task_create(
&my_task_cb, /* 传入静态 TCB 地址 */
OS_TASK_STACK_BEGIN_ADDR(my_task_stack), /* 传入静态栈起始地址 */
OS_TASK_STACK_SIZE(my_task_stack), /* 传入静态栈大小 */
"my_task", /* 任务名称 */
my_task_entry, /* 入口函数 */
&led_cfg, /* 入口参数 */
5 /* 优先级(0 最高,越小优先级越高) */
);
/* 4.2 检查创建是否成功 */
if (tid == OS_NULL)
{
/* 创建失败(例如:该静态 TCB 已被占用、参数非法) */
os_kprintf("task create failed\r\n");
return;
}
/* 4.3 启动任务:将任务从 INIT 状态转为 READY 状态,加入就绪队列 */
ret = os_task_startup(tid);
if (ret != OS_SUCCESS)
{
os_kprintf("task startup failed\r\n");
return;
}
/* 启动成功后,任务会被调度执行,函数从这里返回 */
}2.3 静态创建示例逐步说明
对照上面的代码,静态创建的四个步骤解释如下:
| 步骤 | 代码 | 说明 |
|---|---|---|
| 1 | OS_TASK_DEFINE(my_task_cb) | 定义任务控制块。展开为 os_task_dummy_t my_task_cb;,在本例中写在文件作用域,是一个全局变量,TCB 内存由用户提供 |
| 2 | OS_TASK_STACK_DEFINE(my_task_stack, 1024) | 定义任务栈。展开为一个对齐的全局 uint8_t 数组,栈空间由用户提供 |
| 3 | my_task_entry(void *arg) | 任务入口函数,通过 arg 接收创建时传入的参数,通常是一个 while(1) 无限循环 |
| 4 | os_task_create(...) + os_task_startup(tid) | 用静态 TCB 和栈创建任务,成功后启动任务 |
2.4 关键概念澄清:为什么说"不占堆、静态分配"?
这一步的"静态创建",关键在于 TCB 和栈的内存来源。对照两种创建方式:
| 对比项 | 静态创建 | 动态创建 |
|---|---|---|
| TCB 内存 | 用户定义的全局变量(编译/链接时分配) | 内核运行时 os_malloc 从堆分配 |
| 栈内存 | 用户定义的全局数组(编译/链接时分配) | 内核运行时 os_aligned_malloc 从堆分配 |
是否需要 OS_USING_HEAP | 不需要 | 需要 |
"静态分配"是指什么时候分配内存?
- 静态创建:
OS_TASK_DEFINE(my_task_cb)和OS_TASK_STACK_DEFINE(my_task_stack, 1024)只是两条普通的变量/数组定义。编译器在编译阶段就知道它们的大小,链接器在链接阶段就把它们的空间划好(放在.bss/.data段),程序一启动这些内存就存在了,运行时不分配。 - 动态创建:传入
NULL时,内核在os_task_create()运行到那一刻才调用os_malloc从堆里找内存。
"不占堆"是什么意思?
- "堆(heap)"是程序运行时动态分配内存的区域(由
os_malloc/os_free管理)。 - 静态创建的 TCB 和栈是编译/链接期就固定在静态存储区里的,不经过堆的 malloc,所以"不占堆"。
"全局变量"的说法要如何理解?
- 宏
OS_TASK_DEFINE(name)本身只是展开成os_task_dummy_t name;,它没有加static关键字,也没有指定存储位置。 - 它究竟是"全局变量"还是"局部变量",取决于你把这个宏写在哪里:
- 写在函数外面(文件作用域)→ 全局变量(示例就是这种情况)
- 写在某个函数里面 → 局部变量(一般不会这样用,因为 TCB 需要长期存在)
- 所以严谨的说法是:本例中它是一个文件作用域的全局变量,而不是"宏本身就定义全局变量"。
为什么叫 dummy(占位)?
os_task_dummy_t是给用户侧看的"占位"结构体类型,它的内存布局、大小与内核真正的os_task_t完全一致。- 用户只需按
os_task_dummy_t定义一个变量来预留内存;内核在os_task_create()内部用OS_TYPE_CONVERT把这块用户内存当作真正的os_task_t来使用。这样既保证了用户无需了解内核内部 TCB 的细节,又保证了内存尺寸正确。
2.5 静态创建的关键点
- 内存来源:TCB(
my_task_cb)和栈(my_task_stack)都是编译/链接阶段分配的全局变量,不经过堆分配。因此无需启用OS_USING_HEAP,非常适合资源受限的 MCU 场景。 - 传入参数:
os_task_create()的前两个参数必须传入非 NULL 的静态地址(&my_task_cb和OS_TASK_STACK_BEGIN_ADDR(my_task_stack)),因为未启用堆时内核无法自动分配。 - 防重复创建:静态 TCB 地址固定,不能重复创建。如果同一个
my_task_cb被创建两次,第二次会因第四步的防重复检查而失败并返回NULL。 - 返回值检查:
os_task_create()返回NULL表示失败(如 TCB 被占用),os_task_startup()返回OS_SUCCESS表示启动成功。示例中对两者都做了检查,这是工程上推荐的做法。 arg的生命周期:示例中把led_cfg定义为app_init()内的局部变量。任务被调度后仍会访问arg,因此要确保led_cfg在任务运行期间有效。如果任务生命周期长于app_init(),建议用static修饰或动态分配,避免悬空指针。
三、动态创建任务
动态创建适用于内存充足、追求灵活性的场景,TCB 和任务栈都由内核在运行时从堆中自动分配,因此必须启用 OS_USING_HEAP。
3.1 动态创建示例
下面通过一个完整示例演示动态创建任务。与静态创建最大的区别是:不需要提前定义 TCB 和栈变量,os_task_create() 传 NULL 即可。
#include <os_task.h>
/* 定义入口函数参数结构体(可选,用于传递业务数据) */
typedef struct
{
int sensor_id; /* 传感器编号 */
uint16_t interval; /* 采集间隔(ms) */
} sensor_param_t;
/* 任务入口函数 */
void sensor_task_entry(void *arg)
{
sensor_param_t *param = (sensor_param_t *)arg;
while (1)
{
/* 业务逻辑:读取传感器并打印 */
os_kprintf("sensor[%d] collect, interval=%dms\r\n",
param->sensor_id, param->interval);
os_task_msleep(param->interval);
}
}
/* 创建并启动任务 */
void app_init(void)
{
os_task_id tid;
os_err_t ret;
/* TCB 和栈都传 NULL,内核从堆中自动分配 */
tid = os_task_create(
OS_NULL, /* TCB 由内核自动分配 */
OS_NULL, /* 栈由内核自动分配 */
2048, /* 栈大小(字节) */
"sensor_task", /* 任务名称 */
sensor_task_entry,/* 入口函数 */
OS_NULL, /* 入口参数(此处先传 NULL,见下方说明) */
10 /* 优先级(0 最高,越小优先级越高) */
);
/* 检查创建是否成功 */
if (tid == OS_NULL)
{
/* 创建失败(如堆内存不足) */
os_kprintf("task create failed\r\n");
return;
}
/* 启动任务 */
ret = os_task_startup(tid);
if (ret != OS_SUCCESS)
{
os_kprintf("task startup failed\r\n");
return;
}
/* 任务已就绪,会被调度执行 */
}3.2 动态创建的关键点
动态创建与静态创建的本质区别在于内存来源:
| 对比项 | 动态创建 | 静态创建 |
|---|---|---|
| TCB 内存 | 运行时 os_malloc 从堆分配 | 用户定义的全局变量 |
| 栈内存 | 运行时 os_aligned_malloc 从堆分配 | 用户定义的全局数组 |
是否需 OS_USING_HEAP | 需要 | 不需要 |
| 提前定义变量 | 不需要 | 需要 |
3.2.1 必须启用 OS_USING_HEAP
- 动态创建依赖堆内存管理器。若未定义
OS_USING_HEAP,os_task_create()中关于堆分配的代码会被编译期屏蔽,此时前两个参数必须传非 NULL 地址,否则断言失败(见 1.4 第一步)。
3.2.2 动态创建的好处
| 优点 | 说明 |
|---|---|
| 代码简洁 | 无需定义 TCB/栈变量,一行创建 |
| 灵活方便 | 任务个数和大小可在运行时动态决定,便于动态加载/销毁 |
| 节省内存 | 只在真正创建任务时才分配堆内存 |
3.2.3 动态创建的注意事项
| 注意事项 | 说明 |
|---|---|
| 堆内存不足 | 堆耗尽时 os_malloc 返回 NULL,os_task_create() 返回 NULL,必须检查返回值 |
需要 OS_USING_HEAP | 未启用堆则无法动态创建 |
arg 参数的生命周期 | 同静态创建,要保证传入的参数在任务运行期间有效 |
| 销毁时自动回收 | 动态创建的 TCB/栈在 os_task_destroy() 时由内核自动 os_free(见第五节) |
3.3 混合创建(静态 TCB + 动态栈)
混合创建指:TCB 用静态定义(OS_TASK_DEFINE),任务栈用动态分配(传 OS_NULL)。它在三种创建方式中兼顾了"确定性与灵活性"。
3.3.1 完整示例
#include <os_task.h>
/* 步骤 1:静态定义 TCB(全局变量,不占堆) */
OS_TASK_DEFINE(my_task_cb);
/* 步骤 2:任务入口函数 */
void my_task_entry(void *arg)
{
while (1)
{
/* 业务逻辑 */
os_task_msleep(1000);
}
}
/* 步骤 3:创建并启动任务 */
void app_init(void)
{
os_task_id tid;
os_err_t ret;
/* TCB 传静态地址,栈传 NULL(内核从堆自动分配) */
tid = os_task_create(
&my_task_cb, /* 静态 TCB 地址 */
OS_NULL, /* 动态分配栈 */
2048, /* 栈大小 */
"hybrid_task", /* 任务名称 */
my_task_entry, /* 入口函数 */
OS_NULL, /* 入口参数 */
5 /* 优先级 */
);
/* 检查创建是否成功 */
if (tid == OS_NULL)
{
os_kprintf("task create failed\r\n");
return;
}
/* 启动任务 */
ret = os_task_startup(tid);
if (ret != OS_SUCCESS)
{
os_kprintf("task startup failed\r\n");
return;
}
}3.3.2 为什么需要"混合创建"?
回顾静态创建和动态创建的优缺点:
| 方式 | 优点 | 缺点 |
|---|---|---|
| 完全静态创建 | 不需要堆、内存确定 | 栈大小编译期固定,无法按需调整;每个任务都要提前定义栈 |
| 完全动态创建 | 灵活、无需提前定义 | 完全依赖堆,堆耗尽时创建失败 |
| 混合创建 | 静态 TCB 占用小且确定;栈可动态按需分配 | 仍需要启用 OS_USING_HEAP(栈从堆分配) |
混合创建的两大好处:
| 好处 | 说明 |
|---|---|
| 确定性 + 灵活性兼顾 | TCB 静态分配(内存占用固定、启动即存在),栈动态分配(可按需分配大小) |
| 减少静态内存占用 | 有些任务的栈较大且只在特定情况下需要,动态分配可在需要时才占用堆,不必长期占用静态内存 |
典型应用场景:任务数量固定、但某个任务的栈大小可能随负载变化(需要较大的栈)时,用静态 TCB + 动态栈能既保证 TCB 稳定,又能灵活分配栈。
3.3.3 混合创建的内存分配标志
混合创建时,os_task_create() 内部的 object_alloc_type 标记如下(对应前面 1.4 第二步中的"示例 2"):
TCB 是静态的(第二步走 else) 不置位 → bit0 = 0
栈动态分配后(第三步) |= 0x02 → bit1 = 1
最终 object_alloc_type = 0x02(只有栈是动态分配的)3.3.4 销毁时的资源处理
任务销毁(os_task_destroy())时,内核会根据 object_alloc_type 的标记区分内存来源:
| 内存 | 来源 | 销毁时 |
|---|---|---|
TCB(my_task_cb) | 静态(bit0=0) | 不释放(用户提供的静态内存,内核不能释放) |
| 栈 | 动态(bit1=1) | 释放(内核调用 os_free 回收堆内存) |
小结:混合创建通过
object_alloc_type精确记录"栈是动态的、TCB 是静态的",销毁时就能只释放动态分配的栈,而绝不释放用户静态提供的 TCB,既避免了内存泄漏,也避免了非法释放。
四、os_task_startup() —— 启动任务
4.1 函数签名
os_err_t os_task_startup(os_task_id tid);4.2 功能说明
os_task_startup() 是任务从"创建"走向"运行"的最后一步。它将任务从 INIT 状态转为 READY 状态,并加入就绪队列。如果新任务的优先级高于当前任务,将立即触发一次任务调度。
| 参数 | 类型 | 说明 |
|---|---|---|
tid | os_task_id | 任务 ID,即 os_task_create() 返回的 TCB 指针 |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 启动成功 |
| 其他 | 启动失败(正常情况下通过断言提前拦截非法调用) |
4.3 完整源码
来源: kernel/source/os_task.c
os_err_t os_task_startup(os_task_id tid)
{
os_task_t *task;
OS_KERNEL_INIT();
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(OS_NULL != task);
OS_ASSERT(OS_KOBJ_INITED == task->object_inited);
OS_ASSERT(OS_TASK_STATE_INIT == task->state);
OS_KERNEL_ENTER();
task->state &= ~OS_TASK_STATE_INIT;
task->state |= OS_TASK_STATE_READY;
k_readyq_put(task);
OS_KERNEL_EXIT_SCHED();
return OS_SUCCESS;
}4.4 分步详解
第一步:准备与参数校验
os_task_t *task;
OS_KERNEL_INIT();
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(OS_NULL != task);
OS_ASSERT(OS_KOBJ_INITED == task->object_inited);
OS_ASSERT(OS_TASK_STATE_INIT == task->state);详细解释:
| 代码 | 作用 |
|---|---|
OS_KERNEL_INIT() | 确保内核已初始化,若未初始化则先执行初始化 |
OS_TYPE_CONVERT(os_task_t *, tid) | 把用户传入的 os_task_id(即 TCB 指针)转换为内核内部的 os_task_t * 类型 |
OS_ASSERT(OS_NULL != task) | 任务指针不能为空 |
OS_ASSERT(OS_KOBJ_INITED == task->object_inited) | 任务对象必须处于"已初始化"状态 |
OS_ASSERT(OS_TASK_STATE_INIT == task->state) | 任务状态必须是 INIT |
关键点:
- 最后一条断言很重要:
os_task_startup()只能启动 INIT 状态的任务。如果任务已启动过(处于 READY/RUNNING 等状态),断言会触发,防止重复启动。
第二步:状态转换(INIT → READY)
OS_KERNEL_ENTER();
task->state &= ~OS_TASK_STATE_INIT;
task->state |= OS_TASK_STATE_READY;详细解释:
| 代码 | 作用 |
|---|---|
OS_KERNEL_ENTER() | 进入内核临界区,禁止任务调度,保证状态转换的原子性 |
task->state &= ~OS_TASK_STATE_INIT | 清除 INIT 位(~OS_TASK_STATE_INIT 取反后与运算) |
| `task->state | = OS_TASK_STATE_READY` |
位运算计算:
OS_TASK_STATE_INIT = 0x0001,OS_TASK_STATE_READY = 0x0002。假设 state 当前为 0x0001(INIT):
task->state &= ~OS_TASK_STATE_INIT
state 0x0001 → 0000 0001
& ~0x0001(=0xFFFE)→ 1111 1110
──────────────────────────────
结果 0x0000 → 0000 0000 (清除 INIT)
task->state |= OS_TASK_STATE_READY
state 0x0000 → 0000 0000
| 0x0002 → 0000 0010
──────────────────────────────
结果 0x0002 → 0000 0010 (置位 READY)最终 state = 0x0002,即 READY 状态。
第三步:加入就绪队列
k_readyq_put(task);详细解释:
k_readyq_put()把任务插入到就绪队列中,调度器会从就绪队列中选取优先级最高的任务来运行。- 此时任务已具备被调度的条件,只等调度器选中。
第四步:触发调度并返回
OS_KERNEL_EXIT_SCHED();
return OS_SUCCESS;详细解释:
OS_KERNEL_EXIT_SCHED():退出临界区,并触发一次调度。如果新启动的任务优先级比当前运行的任务更高,会立即切换到新任务执行;否则继续运行当前任务。- 返回
OS_SUCCESS表示启动成功。
4.5 使用示例
os_task_id tid = os_task_create(...); /* 1. 创建任务(状态为 INIT) */
if (tid != OS_NULL)
{
os_task_startup(tid); /* 2. 启动任务(INIT -> READY) */
}启动前后的状态变化:
创建后: 任务状态 = INIT (尚未进入就绪队列,不会被调度)
启动后: 任务状态 = READY (已进入就绪队列,等待调度器选中运行)注意:
os_task_startup()与os_task_create()配套使用。创建后任务不会立即运行,必须调用os_task_startup()才会被调度器纳入调度。
五、os_task_destroy() —— 销毁任务
5.1 函数签名
os_err_t os_task_destroy(os_task_id tid);5.2 返回值
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 销毁成功 |
OS_BUSY | 任务持有互斥锁,无法销毁 |
| 其他 | 销毁失败 |
5.3 完整源码
来源: kernel/source/os_task.c
os_task_destroy() 的实现按单核(非 SMP)与多核(SMP)分支不同。下面给出的是完整源码(与项目一致):
os_err_t os_task_destroy(os_task_id tid)
{
os_task_t *current_task;
os_bool_t need_sched;
os_bool_t task_hold_mutex;
os_err_t ret;
os_task_t *task;
uint8_t object_alloc_type;
OS_KERNEL_INIT();
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(OS_NULL != task);
OS_ASSERT(OS_KOBJ_INITED == task->object_inited);
OS_ASSERT((task->state & OS_TASK_STATE_CLOSE) == OS_TASK_STATE_EMPTY);
OS_ASSERT(OS_FALSE == os_is_irq_active());
#ifndef OS_USING_HEAP
OS_ASSERT(OS_ALLOC_TYPE_STATIC == (task->object_alloc_type & OS_KDATA_ALLOC_TYPE_DYNAMIC));
OS_ASSERT(OS_ALLOC_TYPE_STATIC == (task->object_alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC));
#endif
need_sched = OS_FALSE;
ret = OS_SUCCESS;
OS_KERNEL_ENTER();
#if defined(OS_USING_MUTEX)
task_hold_mutex = !os_list_empty(&task->hold_mutex_list_head);
#else
task_hold_mutex = OS_FALSE;
#endif
if (OS_TRUE == task_hold_mutex)
{
ret = OS_BUSY;
}
else
{
/* Modify the status to prevent other tasks from performing operations on this task. */
task->object_inited = OS_KOBJ_DEINITED;
OS_KERNEL_EXIT();
os_spin_lock(&gs_os_task_resource_list_lock);
os_list_del(&task->resource_node);
os_spin_unlock(&gs_os_task_resource_list_lock);
OS_KERNEL_ENTER();
task->state |= OS_TASK_STATE_CLOSE;
#ifdef OS_USING_SMP
if (task->cpu_index == os_cpu_id_get())
{
OS_ASSERT(((task->state & OS_TASK_STATE_READY) != 0));
k_readyq_remove(task);
task->state &= ~OS_TASK_STATE_READY;
need_sched = OS_TRUE;
}
else if (task->cpu_index != -1)
{
k_readyq_remove(task);
task->state &= ~OS_TASK_STATE_READY;
current_task = _k_task_self();
k_readyq_remove(current_task);
current_task->state &= ~OS_TASK_STATE_READY;
current_task->state |= OS_TASK_STATE_BLOCK;
current_task->block_list_head = OS_NULL;
os_list_add_tail(&task->join_node, ¤t_task->task_node);
need_sched = OS_TRUE;
}
#else
_k_task_deinit(task);
current_task = _k_task_self();
if (task == current_task)
{
os_list_add_tail(&gs_os_task_recycle_list_head, &task->resource_node);
_k_wakeup_recycle_task();
need_sched = OS_TRUE;
}
#endif
else
{
_k_task_deinit(task);
OS_KERNEL_EXIT();
object_alloc_type = task->object_alloc_type;
if (OS_NULL != task->cleanup)
{
task->cleanup(task->user_data);
}
#ifdef OS_USING_HEAP
if (OS_KDATA_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KDATA_ALLOC_TYPE_DYNAMIC))
{
OS_KERNEL_FREE(task->stack_begin);
}
if (OS_KOBJ_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC))
{
OS_KERNEL_FREE(task);
}
#endif
OS_KERNEL_ENTER();
}
}
if (OS_TRUE == need_sched)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
return ret;
}销毁过程中还调用了辅助函数 _k_task_deinit()(位于 os_task.c),负责把任务从各种队列中摘除:
static void _k_task_deinit(os_task_t *task)
{
/* Task is at ready queue */
if ((task->state & OS_TASK_STATE_READY) != 0)
{
k_readyq_remove(task);
task->state &= ~OS_TASK_STATE_READY;
}
/* Task is not at ready queue */
else
{
/* Task is sleep state */
if ((task->state & OS_TASK_STATE_SLEEP) != 0)
{
k_tickq_remove(task);
task->state &= ~OS_TASK_STATE_SLEEP;
}
/* Task is block state */
if ((task->state & OS_TASK_STATE_BLOCK) != 0)
{
/* Remove from ipc list head */
os_list_del(&task->task_node);
task->state &= ~OS_TASK_STATE_BLOCK;
}
if ((task->state & OS_TASK_STATE_INIT) != 0)
{
task->state &= ~OS_TASK_STATE_INIT;
}
if ((task->state & OS_TASK_STATE_SUSPEND) != 0)
{
task->state &= ~OS_TASK_STATE_SUSPEND;
}
/* Here, if task is running state, do nothing. */
}
return;
}5.4 分步详解(以单核非 SMP 为例)
说明:下面的分步以最常见的单核(非 SMP) 编译路径为准。SMP 下的差异会在关键处单独说明。
第一步:准备与参数校验
OS_KERNEL_INIT();
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(OS_NULL != task);
OS_ASSERT(OS_KOBJ_INITED == task->object_inited);
OS_ASSERT((task->state & OS_TASK_STATE_CLOSE) == OS_TASK_STATE_EMPTY);
OS_ASSERT(OS_FALSE == os_is_irq_active());详细解释:
| 断言/语句 | 作用 |
|---|---|
OS_KERNEL_INIT() | 确保内核已初始化 |
OS_TYPE_CONVERT(os_task_t *, tid) | 把 os_task_id 转换为 os_task_t * |
OS_ASSERT(OS_NULL != task) | 任务指针非空 |
OS_ASSERT(OS_KOBJ_INITED == task->object_inited) | 任务必须处于已初始化状态 |
OS_ASSERT((task->state & OS_TASK_STATE_CLOSE) == OS_TASK_STATE_EMPTY) | 任务不能已经处于 CLOSE 状态(防止重复销毁) |
OS_ASSERT(OS_FALSE == os_is_irq_active()) | 不能在中断上下文中销毁任务 |
未启用堆(
OS_USING_HEAP)时,还会断言任务必须是静态分配的(因为动态分配依赖堆,未启用堆时只能销毁静态任务)。
为什么不能在中断上下文中销毁任务?
这条断言体现了 RTOS 设计的一个基本原则:需要"阻塞/切换上下文"的 API 不允许在中断中使用。销毁任务涉及多个不能在中断里进行的操作,具体原因如下:
| 原因 | 详细说明 |
|---|---|
| 1. 中断上下文没有"任务"概念,无法切换 | 中断服务函数(ISR)是 CPU 在处理异常时进入的特殊上下文,它不属于任何任务。销毁任务的本质是"结束一个任务、让出 CPU 并调度到别的任务",这需要任务上下文。在 ISR 中无法进行任务切换,也就无法完成销毁后的调度。 |
| 2. 会调用可能阻塞的操作 | os_task_destroy() 内部会使用 OS_KERNEL_ENTER/OS_KERNEL_EXIT(临界区)、os_spin_lock(自旋锁)、并可能触发任务调度(OS_KERNEL_EXIT_SCHED)。这些操作需要任务上下文支持,在 ISR 中执行会造成不可预知的结果。 |
| 3. 释放的内存可能正在被中断使用 | 若销毁一个任务,它动态分配的 TCB/栈会被 os_free 释放。但如果该任务当前正被抢占(其栈帧还挂在中断栈上或正被 ISR 使用),在中断里释放它会破坏正在使用的栈/内存,导致崩溃。 |
| 4. 破坏"当前任务"语义 | 销毁逻辑里有 current_task = _k_task_self() 并判断 task == current_task。但在 ISR 中,"当前任务"的语义是模糊的(可能是被中断打断的任务),无法正确判定"销毁的是不是当前任务",容易走错分支。 |
RTOS 的通用规则:在中断中只能调用"不阻塞、不切换"的 API(如 os_semaphore_post 这类仅置位/唤醒的接口)。凡是需要阻塞、需要上下文切换的 API(创建/销毁任务、获取信号量、等待消息等),都必须在任务上下文中调用。
正确的做法:如果确实需要在中断里销毁任务,应改为:在中断中发送一个信号(如释放信号量、发送消息、置事件标志),然后由任务上下文收到信号后调用 os_task_destroy() 来真正销毁任务。
第二步:检查是否持有互斥锁
need_sched = OS_FALSE;
ret = OS_SUCCESS;
OS_KERNEL_ENTER();
#if defined(OS_USING_MUTEX)
task_hold_mutex = !os_list_empty(&task->hold_mutex_list_head);
#else
task_hold_mutex = OS_FALSE;
#endif
if (OS_TRUE == task_hold_mutex)
{
ret = OS_BUSY;
}
else
{ ... }详细解释(本步骤的核心是回答一个问题:这个任务还拿着别的任务需要的互斥锁吗?):
环节 1:初始化局部变量
need_sched = OS_FALSE;
ret = OS_SUCCESS;
OS_KERNEL_ENTER();详细解释:
need_sched = OS_FALSE:初始默认"不需要调度",后面销毁当前任务时才置为OS_TRUE。ret = OS_SUCCESS:初始默认"销毁成功",后面若发现持有互斥锁才改为OS_BUSY。OS_KERNEL_ENTER():进入内核临界区,保护后续对互斥锁链表和任务状态的检查。
环节 2:判断是否持有互斥锁
#if defined(OS_USING_MUTEX)
task_hold_mutex = !os_list_empty(&task->hold_mutex_list_head);
#else
task_hold_mutex = OS_FALSE;
#endif
if (OS_TRUE == task_hold_mutex)
{
ret = OS_BUSY;
}
else
{ ... }详细解释:
hold_mutex_list_head 是什么?
- 它是 TCB 中的一个链表头,专门用来记录"这个任务当前持有了哪些互斥锁"。
- 当任务通过
os_mutex_lock()成功获取一个互斥锁时,内核会把该互斥锁的hold_node节点挂到这个链表上(见os_mutex.c中os_list_add_tail(¤t_task->hold_mutex_list_head, &mutex->hold_node))。 - 当任务
os_mutex_unlock()释放该互斥锁时,会把这个节点从链表中移除。 - 所以:链表为空 → 任务没持有任何互斥锁;链表非空 → 任务至少持有一个互斥锁。
判断逻辑逐句解释:
| 代码 | 作用 |
|---|---|
os_list_empty(&task->hold_mutex_list_head) | 判断互斥锁链表是否为空。空返回真(OS_TRUE),非空返回假(OS_FALSE) |
!os_list_empty(...) | 取反:链表非空时为真,即"任务持有互斥锁"为真 |
task_hold_mutex = !os_list_empty(...) | 把"是否持有互斥锁"的结果存入 task_hold_mutex |
if (OS_TRUE == task_hold_mutex) | 若持有互斥锁,走 OS_BUSY 分支 |
关于 #if defined(OS_USING_MUTEX):如果没有启用互斥锁功能,就不存在"持有互斥锁"的问题,直接置 task_hold_mutex = OS_FALSE,跳过检查。
环节 3:持有互斥锁时为何拒绝销毁
if (OS_TRUE == task_hold_mutex)
{
ret = OS_BUSY;
}详细解释:若任务持有互斥锁,立即返回 OS_BUSY,不允许销毁。原因是销毁持锁任务会带来严重问题:
问题一:互斥锁"悬空"(没人释放了)
- 互斥锁的正常生命周期是:获取 → 使用 → 释放。
- 如果持锁任务被销毁,它持有的互斥锁就永远不会被释放,变成"悬空"状态。其他所有等待这个锁的任务都会被永久阻塞。
问题二:可能引发死锁
- 用具体场景说明。假设有任务 A、任务 B,互斥锁
M:1. 任务 A 获取了互斥锁 M(A 的 hold_mutex_list_head 非空) 2. 任务 B 正在等待互斥锁 M(被阻塞) 3. 此时若销毁任务 A: - 互斥锁 M 永远不会被释放 - 任务 B 永远等不到 M,被永久阻塞(死锁) - 更糟的情况是互斥锁优先级继承被破坏:持锁任务可能已通过优先级继承提升了优先级,销毁它会让其他等待者的优先级继承关系错乱。
问题三:TCB/栈被释放,但锁的引用还在
- 任务持有互斥锁时,锁对象里的
owner(所有者)指针指向这个任务的 TCB。如果任务被销毁、TCB 被os_free,该指针就变成悬空指针,其他任务再访问这个锁时会发生不可预知的错误。
正确做法:要销毁一个持锁任务,必须先让该任务主动释放它持有的所有互斥锁(os_mutex_unlock),确认 hold_mutex_list_head 链表为空后,才能调用 os_task_destroy()。
第三步:标记去初始化并从全局资源链表移除
task->object_inited = OS_KOBJ_DEINITED;
OS_KERNEL_EXIT();
os_spin_lock(&gs_os_task_resource_list_lock);
os_list_del(&task->resource_node);
os_spin_unlock(&gs_os_task_resource_list_lock);
OS_KERNEL_ENTER();
task->state |= OS_TASK_STATE_CLOSE;详细解释:
| 代码 | 作用 |
|---|---|
task->object_inited = OS_KOBJ_DEINITED | 标记对象为"去初始化",防止其他任务再对它操作 |
os_list_del(&task->resource_node) | 从全局任务资源链表移除该任务(加自旋锁保护) |
task->state |= OS_TASK_STATE_CLOSE | 设置任务状态为 CLOSE |
关键点:
- 先标记
DEINITED再移出链表,是为了让该任务立刻"不可见、不可操作",避免并发访问正在销毁的任务。 - 此时任务已从系统的资源管理中移除,不再被 shell 等遍历到。
第四步:根据"销毁对象"分三条路径
_k_task_deinit(task);
current_task = _k_task_self();
if (task == current_task)
{
os_list_add_tail(&gs_os_task_recycle_list_head, &task->resource_node);
_k_wakeup_recycle_task();
need_sched = OS_TRUE;
}路径 A:销毁的是当前任务(自己销毁自己)
- 调用
_k_task_deinit(task)先做"去队列"处理。 - 由于当前任务正占用 CPU,不能立即释放它的栈/TCB(栈还在用),所以:
- 把任务的
resource_node挂到回收链表gs_os_task_recycle_list_head。 - 调用
_k_wakeup_recycle_task()唤醒 recycle-task,由它在合适的时机回收资源。 - 设置
need_sched = OS_TRUE,触发调度切换到其他任务。
- 把任务的
- 这正是"自己销毁自己"时
os_task_destroy()不会返回的原因——任务在调度切换后就结束了。
路径 B(非 SMP 下销毁其他任务):见下方第五步。
第五步:销毁非当前任务(单核 else 分支)
else
{
_k_task_deinit(task);
OS_KERNEL_EXIT();
object_alloc_type = task->object_alloc_type;
if (OS_NULL != task->cleanup)
{
task->cleanup(task->user_data);
}
#ifdef OS_USING_HEAP
if (OS_KDATA_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KDATA_ALLOC_TYPE_DYNAMIC))
{
OS_KERNEL_FREE(task->stack_begin);
}
if (OS_KOBJ_ALLOC_TYPE_DYNAMIC == (object_alloc_type & OS_KOBJ_ALLOC_TYPE_DYNAMIC))
{
OS_KERNEL_FREE(task);
}
#endif
OS_KERNEL_ENTER();
}详细解释(销毁的是其他任务,当前任务可安全地立即释放资源):
| 代码 | 作用 |
|---|---|
_k_task_deinit(task) | 从就绪/睡眠/阻塞队列中摘除该任务 |
task->cleanup(task->user_data) | 若注册了清理回调,执行清理回调(释放用户资源) |
OS_KERNEL_FREE(task->stack_begin) | 若栈是动态分配的(bit1=1),释放栈 |
OS_KERNEL_FREE(task) | 若 TCB 是动态分配的(bit0=1),释放 TCB |
关键点 —— 只释放动态分配的内存:
- 通过
object_alloc_type的 bit0/bit1 判断 TCB 和栈是否动态分配。 - 动态的才
os_free,静态的(用户提供)绝不释放。 - 这与创建时的内存标记(见 1.4 第二步)一一对应,保证销毁时精确回收。
第六步:收尾
if (OS_TRUE == need_sched)
{
OS_KERNEL_EXIT_SCHED();
}
else
{
OS_KERNEL_EXIT();
}
return ret;详细解释:
- 若
need_sched == OS_TRUE(销毁了当前任务,需要切换),调用OS_KERNEL_EXIT_SCHED()退出临界区并触发调度。 - 否则调用
OS_KERNEL_EXIT()仅退出临界区,不触发调度。 - 返回
ret:OS_SUCCESS表示销毁成功,OS_BUSY表示因持有互斥锁销毁失败。
5.5 _k_task_deinit() 详解
5.5.1 函数作用
_k_task_deinit() 是销毁任务时调用的去初始化辅助函数。它的核心作用是:把任务从它当前所在的所有内核队列中摘除,并清除对应的状态位,使该任务不再参与任何调度和阻塞,为后续释放资源做准备。
任务在运行过程中可能被挂到多种队列中(就绪队列、延时队列、IPC 阻塞队列等),销毁前必须逐一将其从这些队列中"摘"出来,否则会出现悬空节点,破坏链表。
5.5.2 完整源码
来源: kernel/source/os_task.c
static void _k_task_deinit(os_task_t *task)
{
/* Task is at ready queue */
if ((task->state & OS_TASK_STATE_READY) != 0)
{
k_readyq_remove(task);
task->state &= ~OS_TASK_STATE_READY;
}
/* Task is not at ready queue */
else
{
/* Task is sleep state */
if ((task->state & OS_TASK_STATE_SLEEP) != 0)
{
k_tickq_remove(task);
task->state &= ~OS_TASK_STATE_SLEEP;
}
/* Task is block state */
if ((task->state & OS_TASK_STATE_BLOCK) != 0)
{
/* Remove from ipc list head */
os_list_del(&task->task_node);
task->state &= ~OS_TASK_STATE_BLOCK;
}
if ((task->state & OS_TASK_STATE_INIT) != 0)
{
task->state &= ~OS_TASK_STATE_INIT;
}
if ((task->state & OS_TASK_STATE_SUSPEND) != 0)
{
task->state &= ~OS_TASK_STATE_SUSPEND;
}
/* Here, if task is running state, do nothing. */
}
return;
}5.5.3 分步详解
_k_task_deinit() 的整体结构是一个 if / else,先判断任务是否在就绪队列,再根据具体状态分情况处理。
总览:任务可能处于以下任一位置,需要分别处理:
| 任务所在队列 | 判断条件(状态位) | 处理动作 |
|---|---|---|
| 就绪队列 | READY | k_readyq_remove() 从就绪队列移除,清除 READY |
| 延时队列 | SLEEP | k_tickq_remove() 从节拍队列移除,清除 SLEEP |
| IPC 阻塞队列 | BLOCK | os_list_del() 从阻塞队列移除,清除 BLOCK |
| (无队列) | INIT | 仅清除 INIT 状态位 |
| (无队列) | SUSPEND | 仅清除 SUSPEND 状态位 |
| (运行中) | RUNNING | 不做任何处理(正在运行的任务由调度器负责) |
分支一:任务在就绪队列(READY)
if ((task->state & OS_TASK_STATE_READY) != 0)
{
k_readyq_remove(task);
task->state &= ~OS_TASK_STATE_READY;
}详细解释:
- 判断条件:
(task->state & OS_TASK_STATE_READY) != 0,即任务状态中 READY 位为 1,说明任务正在就绪队列中等待调度。 k_readyq_remove(task):把任务从就绪队列中移除,防止它再被调度器选中。task->state &= ~OS_TASK_STATE_READY:清除 READY 状态位。
为什么用 if / else 而不是多个独立的 if:一个任务同一时刻只会处于一个队列(就绪、延时、阻塞是互斥的),所以用 if / else 先判断"是否在就绪队列",是则走就绪分支;否则才进入 else 去处理其他状态。
分支二:任务不在就绪队列(else 分支)
else
{
/* Task is sleep state */
if ((task->state & OS_TASK_STATE_SLEEP) != 0)
{
k_tickq_remove(task);
task->state &= ~OS_TASK_STATE_SLEEP;
}
/* Task is block state */
if ((task->state & OS_TASK_STATE_BLOCK) != 0)
{
/* Remove from ipc list head */
os_list_del(&task->task_node);
task->state &= ~OS_TASK_STATE_BLOCK;
}
if ((task->state & OS_TASK_STATE_INIT) != 0)
{
task->state &= ~OS_TASK_STATE_INIT;
}
if ((task->state & OS_TASK_STATE_SUSPEND) != 0)
{
task->state &= ~OS_TASK_STATE_SUSPEND;
}
}详细解释(else 分支里用多个独立的 if,因为 SLEEP/BLOCK/INIT/SUSPEND 这些状态可以叠加存在,必须逐一判断并清除):
① 处理睡眠状态(SLEEP)
(task->state & OS_TASK_STATE_SLEEP) != 0:任务处于睡眠状态(调用了os_task_tsleep/os_task_msleep)。k_tickq_remove(task):把任务从节拍(延时)队列中移除,使其不再参与延时计时。task->state &= ~OS_TASK_STATE_SLEEP:清除 SLEEP 位。
② 处理阻塞状态(BLOCK)
(task->state & OS_TASK_STATE_BLOCK) != 0:任务处于阻塞状态(等待信号量/互斥锁/消息队列等 IPC 资源)。os_list_del(&task->task_node):把任务从阻塞它的 IPC 对象的等待队列中移除。task->state &= ~OS_TASK_STATE_BLOCK:清除 BLOCK 位。
③ 处理 INIT 状态
(task->state & OS_TASK_STATE_INIT) != 0:任务还处于 INIT 状态(创建后未启动)。- 清除 INIT 位(此时任务不在任何队列,无需摘除操作)。
④ 处理 SUSPEND 状态
(task->state & OS_TASK_STATE_SUSPEND) != 0:任务被挂起。- 清除 SUSPEND 位。
⑤ RUNNING 状态(else 分支末尾注释)
- 注释
/* Here, if task is running state, do nothing. */说明:如果任务正在运行(RUNNING),不做任何处理。 - 原因:正在运行的任务由调度器负责处理。销毁当前任务时,销毁流程会在调度切换点(
OS_KERNEL_EXIT_SCHED())由调度器完成剩余工作,此处无需手动摘除。
5.5.4 为什么需要 _k_task_deinit()
| 意义 | 说明 |
|---|---|
| 防止链表悬空 | 任务可能挂在就绪/延时/阻塞队列中,销毁前必须摘除,否则链表节点悬空,遍历链表时会崩溃 |
| 防止再次被调度 | 清除 READY 位并从就绪队列移除后,调度器不会再选中这个即将销毁的任务 |
| 防止误唤醒 | 从延时/阻塞队列移除后,节拍中断或资源释放不会再尝试唤醒这个已销毁的任务 |
| 为资源释放做准备 | 摘除所有队列关联后,才能安全地释放任务的栈和 TCB |
5.6 使用示例
/* 其他任务销毁 my_task */
os_task_id tid = os_task_get_id("my_task");
if (tid != OS_NULL)
{
os_task_destroy(tid); /* 销毁后资源被立即回收 */
}
/* 任务自己销毁自己 */
void self_destroy_task(void *arg)
{
/* 执行一些清理工作 */
os_task_destroy(os_get_current_task());
/* 不会执行到这里(任务已被销毁并触发调度) */
}销毁时资源回收小结:
| 场景 | TCB | 栈 | 清理回调 |
|---|---|---|---|
| 销毁其他任务 | 动态则释放 | 动态则释放 | 立即执行 |
| 自己销毁自己 | 由 recycle-task 异步回收 | 由 recycle-task 异步回收 | 由 recycle-task 执行 |
六、清理回调
6.1 概述
清理回调(cleanup callback) 是任务在被销毁时自动执行的用户自定义函数。它常用于释放任务动态分配的资源(内存、文件句柄、设备等),确保任务销毁时资源不会泄漏。
- 清理回调通过
os_task_set_cleanup_callback()注册到任务的 TCB 中。 - 销毁任务时,内核会自动调用该回调,并把任务的用户数据(user_data) 作为参数传入(见第七节)。
- 回调函数签名固定为
void (*cleanup)(void *user_data)。
6.2 函数签名
os_err_t os_task_set_cleanup_callback(os_task_id tid, void (*cleanup)(void *user_data));| 参数 | 类型 | 说明 |
|---|---|---|
tid | os_task_id | 要注册清理回调的任务 ID |
cleanup | void (*)(void *user_data) | 清理回调函数指针,销毁时被调用,参数为任务的用户数据 |
| 返回值 | 说明 |
|---|---|
OS_SUCCESS | 注册成功 |
6.3 完整源码
来源: kernel/source/os_task.c
os_err_t os_task_set_cleanup_callback(os_task_id tid, void (*cleanup)(void *user_data))
{
os_task_t *task;
OS_KERNEL_INIT();
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(OS_NULL != task);
OS_ASSERT(OS_KOBJ_INITED == task->object_inited);
OS_KERNEL_ENTER();
task->cleanup = cleanup;
OS_KERNEL_EXIT();
return OS_SUCCESS;
}6.4 分步详解
OS_KERNEL_INIT();
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(OS_NULL != task);
OS_ASSERT(OS_KOBJ_INITED == task->object_inited);
OS_KERNEL_ENTER();
task->cleanup = cleanup; /* 关键:把回调指针保存到 TCB 的 cleanup 字段 */
OS_KERNEL_EXIT();
return OS_SUCCESS;详细解释:
| 代码 | 作用 |
|---|---|
OS_KERNEL_INIT() | 确保内核已初始化 |
OS_TYPE_CONVERT(os_task_t *, tid) | 把任务 ID 转换为 os_task_t * |
OS_ASSERT(OS_NULL != task) | 任务指针非空 |
OS_ASSERT(OS_KOBJ_INITED == task->object_inited) | 任务必须处于已初始化状态 |
OS_KERNEL_ENTER() | 进入临界区,保证写入原子性 |
task->cleanup = cleanup | 把回调函数指针保存到 TCB 的 cleanup 字段 |
OS_KERNEL_EXIT() | 退出临界区 |
关键点:
- 该函数只负责注册(把指针存进 TCB),不会立即调用清理回调。
- 清理回调真正被调用是在任务销毁时:销毁流程中检查
task->cleanup != OS_NULL才调用(见第五节os_task_destroy()中task->cleanup(task->user_data))。
6.5 清理回调在销毁时如何被调用
在 os_task_destroy() 的"销毁其他任务"分支中,清理回调是这样被调用的:
if (OS_NULL != task->cleanup)
{
task->cleanup(task->user_data); /* 把用户数据作为参数传给清理回调 */
}流程说明:
销毁开始
│
├─ 检查 task->cleanup 是否非空(是否注册了回调)
│ ├─ 已注册 → 调用 task->cleanup(task->user_data) ← 执行清理逻辑
│ └─ 未注册 → 跳过
│
└─ 释放动态分配的栈/TCB- 回调的参数
task->user_data是任务通过os_task_set_user_data()设置的用户数据(见第七节)。 - 自己销毁自己时,清理回调由 recycle-task 在异步回收资源时执行。
6.6 使用示例
下面是一个完整的"动态分配资源 + 注册清理回调"示例:
#include <os_task.h>
/* 定义要管理的资源(如传感器配置) */
typedef struct
{
int sensor_id;
char name[16];
} sensor_cfg_t;
/* 清理回调:释放任务动态分配的资源 */
void my_cleanup(void *user_data)
{
sensor_cfg_t *cfg = (sensor_cfg_t *)user_data;
os_kprintf("cleanup task, free sensor[%d] %s\r\n", cfg->sensor_id, cfg->name);
if (cfg != OS_NULL)
{
os_free(cfg); /* 释放动态分配的配置结构体 */
}
}
/* 任务入口函数 */
void my_task(void *arg)
{
/* 动态分配资源 */
sensor_cfg_t *cfg = (sensor_cfg_t *)os_malloc(sizeof(sensor_cfg_t));
cfg->sensor_id = 1;
strncpy(cfg->name, "temp", sizeof(cfg->name) - 1);
/* 1. 把资源指针设为任务的用户数据 */
os_task_set_user_data(os_get_current_task(), cfg);
/* 2. 注册清理回调,确保任务销毁时自动释放 cfg */
os_task_set_cleanup_callback(os_get_current_task(), my_cleanup);
while (1)
{
/* 业务逻辑 */
os_task_msleep(1000);
}
/* 任务被销毁时,内核会调用 my_cleanup(cfg),自动释放 cfg */
}配合清理回调和用户数据的典型模式:
| 步骤 | 代码 | 说明 |
|---|---|---|
| 1 | os_malloc() | 任务动态分配资源 |
| 2 | os_task_set_user_data(..., cfg) | 把资源指针设为任务用户数据 |
| 3 | os_task_set_cleanup_callback(..., my_cleanup) | 注册清理回调 |
| 4 | 任务被销毁 | 内核自动调用 my_cleanup(cfg),释放资源 |
6.7 使用注意事项
| 注意事项 | 说明 |
|---|---|
| 只在任务销毁时调用 | 清理回调不会被周期调用,仅在任务销毁流程中执行一次 |
| 与用户数据配合 | 回调通过 user_data 参数拿到资源指针,通常先 os_task_set_user_data() 设置 |
| 回调参数判空 | 若 user_data 可能为空,回调内应做判空处理 |
| 清理回调不能是阻塞函数 | 它运行在内核销毁流程中,不应调用会阻塞的 API |
| 注册后生效 | 必须在任务销毁之前调用 os_task_set_cleanup_callback() 完成注册 |
七、用户数据
7.1 概述
用户数据(user_data) 是 TCB 中一个 void * 类型的字段(见 4.2 任务控制块中的 user_data 成员),它允许为任务关联任意类型的私有数据,在任务与外部(其他任务、中断、清理回调等)之间传递上下文信息。
通过 os_task_set_user_data() 设置、os_task_get_user_data() 获取。这两个 API 配合清理回调(见第六节)尤其有用:清理回调能拿到任务设置的用户数据,从而在销毁时释放对应的资源。
7.2 函数签名
os_err_t os_task_set_user_data(os_task_id tid, void *user_data);
void *os_task_get_user_data(os_task_id tid);| 函数 | 参数/返回值 | 说明 |
|---|---|---|
os_task_set_user_data() | tid:任务 ID;user_data:要关联的数据指针 | 为任务设置用户数据,返回 OS_SUCCESS |
os_task_get_user_data() | tid:任务 ID | 获取该任务已关联的用户数据指针 |
7.3 完整源码
来源: kernel/source/os_task.c
os_err_t os_task_set_user_data(os_task_id tid, void *user_data)
{
os_task_t *task;
OS_KERNEL_INIT();
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(OS_NULL != task);
OS_ASSERT(OS_KOBJ_INITED == task->object_inited);
OS_KERNEL_ENTER();
task->user_data = user_data;
OS_KERNEL_EXIT();
return OS_SUCCESS;
}
void *os_task_get_user_data(os_task_id tid)
{
os_task_t *task;
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(OS_NULL != task);
return task->user_data;
}7.4 分步详解
7.4.1 os_task_set_user_data()
OS_KERNEL_INIT();
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(OS_NULL != task);
OS_ASSERT(OS_KOBJ_INITED == task->object_inited);
OS_KERNEL_ENTER();
task->user_data = user_data;
OS_KERNEL_EXIT();
return OS_SUCCESS;详细解释:
| 代码 | 作用 |
|---|---|
OS_KERNEL_INIT() | 确保内核已初始化 |
OS_TYPE_CONVERT(os_task_t *, tid) | 把任务 ID 转换为 os_task_t * |
OS_ASSERT(OS_NULL != task) | 任务指针非空 |
OS_ASSERT(OS_KOBJ_INITED == task->object_inited) | 任务必须处于已初始化状态 |
OS_KERNEL_ENTER() | 进入临界区,保证写入的原子性 |
task->user_data = user_data | 把用户数据指针保存到 TCB 的 user_data 字段 |
OS_KERNEL_EXIT() | 退出临界区 |
return OS_SUCCESS | 返回成功 |
关键点:写入操作被 OS_KERNEL_ENTER/OS_KERNEL_EXIT 包裹,保证在多任务/中断并发环境下,user_data 字段的读写是原子、安全的。
7.4.2 os_task_get_user_data()
task = OS_TYPE_CONVERT(os_task_t *, tid);
OS_ASSERT(OS_NULL != task);
return task->user_data;详细解释:
- 转换为
os_task_t *,断言非空。 - 直接返回 TCB 的
user_data字段。 - 若从未设置过用户数据,
user_data在_k_task_init()中被初始化为OS_NULL,返回NULL。
7.5 使用示例
7.5.1 基本用法:为任务关联上下文数据
#include <os_task.h>
/* 定义用户数据结构体 */
typedef struct {
int sensor_id; /* 传感器编号 */
float threshold; /* 报警阈值 */
char name[16]; /* 名称 */
} sensor_ctx_t;
void app_init(void)
{
os_task_id tid;
sensor_ctx_t ctx = { .sensor_id = 1, .threshold = 25.0, .name = "temp" };
/* 1. 创建任务 */
tid = os_task_create(OS_NULL, OS_NULL, 2048, "sensor", sensor_task, OS_NULL, 5);
if (tid == OS_NULL)
{
return;
}
/* 2. 为任务关联用户数据 */
os_task_set_user_data(tid, &ctx);
/* 3. 启动任务 */
os_task_startup(tid);
}
/* 在别处(如其他任务/回调中)获取用户数据 */
void dump_task_user_data(os_task_id tid)
{
sensor_ctx_t *pctx = (sensor_ctx_t *)os_task_get_user_data(tid);
if (pctx != OS_NULL)
{
os_kprintf("Sensor: %s, ID: %d, threshold=%.1f\r\n",
pctx->name, pctx->sensor_id, pctx->threshold);
}
}7.5.2 典型场景:与清理回调配合释放资源
用户数据最常见的作用,是让清理回调能拿到任务动态分配的资源并释放:
void sensor_cleanup(void *user_data)
{
/* 清理回调拿到 user_data(即任务设置的用户数据) */
sensor_ctx_t *pctx = (sensor_ctx_t *)user_data;
if (pctx != OS_NULL)
{
os_free(pctx); /* 释放任务动态分配的资源 */
}
}
void sensor_task(void *arg)
{
/* 动态分配资源,并保存到用户数据 */
sensor_ctx_t *pctx = (sensor_ctx_t *)os_malloc(sizeof(sensor_ctx_t));
pctx->sensor_id = 2;
pctx->threshold = 30.0;
/* 注册清理回调(把 user_data 也一并设置好) */
os_task_set_user_data(os_get_current_task(), pctx);
os_task_set_cleanup_callback(os_get_current_task(), sensor_cleanup);
while (1)
{
/* 业务逻辑 */
os_task_msleep(1000);
}
}流程说明:
- 任务动态分配了
sensor_ctx_t资源。 - 用
os_task_set_user_data()把pctx关联到任务。 - 用
os_task_set_cleanup_callback()注册清理回调sensor_cleanup。 - 任务被销毁时,内核会调用
sensor_cleanup(task->user_data),即把之前设置的用户数据pctx传回给清理函数,从而在销毁时安全释放该资源。
7.6 使用注意事项
| 注意事项 | 说明 |
|---|---|
| 生命周期管理 | user_data 只保存指针,不管理指针指向内存的生命周期。设置的用户数据必须在任务销毁前始终有效 |
| NULL 判断 | os_task_get_user_data() 未设置时返回 NULL,使用前建议判空 |
| 与清理回调配合 | 用户数据最常见的用途是配合清理回调,在任务销毁时释放其指向的资源 |
本质是 void * | 可以是任意类型指针(结构体、数组、简单变量地址等),由用户自行负责类型转换 |
八、创建与销毁的注意事项
任务创建与销毁涉及内存管理、状态机、并发安全等多个方面,工程中应特别注意以下事项。按阶段分为创建阶段和销毁阶段两类。
8.1 创建阶段的注意事项
| # | 注意事项 | 详细说明 |
|---|---|---|
| 1 | 不允许在中断上下文中创建任务 | os_task_create() 内部有 OS_ASSERT(OS_FALSE == os_is_irq_active()) 断言。创建任务会动态分配内存、操作链表,这些都需要任务上下文,不能在中断中调用 |
| 2 | 静态创建时 TCB 不能重复使用 | 同一个静态 TCB 地址(如 &my_task_cb)只能创建一次任务。若重复创建,第四步的防重复检查会返回 NULL 并打印错误日志 |
| 3 | 动态创建必须启用 OS_USING_HEAP | 动态创建依赖堆内存管理。未启用堆时,os_task_create() 的前两个参数(TCB、栈)必须传非 NULL 地址,否则断言失败 |
| 4 | 必须检查返回值 | os_task_create() 返回 NULL 表示失败(堆不足、TCB 被占用、参数非法)。工程中必须检查返回值,避免使用无效的任务 ID |
| 5 | 栈大小必须大于 0 | os_task_create() 断言 stack_size > 0。栈大小要足够任务使用,过小会导致栈溢出 |
| 6 | 创建后必须 os_task_startup() 才运行 | 创建后任务处于 INIT 状态,不会被调度。必须调用 os_task_startup() 转 READY 才会被调度执行 |
| 7 | 静态 TCB/栈是全局变量 | 静态创建的 TCB 和栈是编译/链接期分配的全局变量,不占堆。适合资源受限、不使用动态内存的 MCU |
8.2 销毁阶段的注意事项
| # | 注意事项 | 详细说明 |
|---|---|---|
| 1 | 不允许在中断上下文中销毁任务 | os_task_destroy() 有断言检查。销毁任务会释放内存、可能触发调度,必须在任务上下文中调用 |
| 2 | 销毁持有互斥锁的任务返回 OS_BUSY | 任务持有互斥锁时销毁会失败。因为销毁持锁任务会导致互斥锁悬空、死锁。必须先让任务释放所有互斥锁 |
| 3 | 不能重复销毁 | os_task_destroy() 断言任务不能已经处于 CLOSE 状态。销毁已销毁的任务会触发断言 |
| 4 | 自己销毁自己时函数不会返回 | os_task_destroy(os_get_current_task()) 销毁当前任务后,会触发调度切换到其他任务,函数不会返回。其后代码不会执行 |
| 5 | 动态分配的资源在销毁时被回收 | 动态创建的 TCB 和栈,在销毁时会由内核自动 os_free。静态的(用户提供)不会被释放 |
| 6 | 清理回调用于释放用户资源 | 若任务动态分配了额外资源,应通过 os_task_set_cleanup_callback() 注册清理回调,在销毁时释放(见第六节) |
8.3 任务生命周期总结
结合本节内容,一个任务的完整生命周期如下:
os_task_create() os_task_startup() 调度器调度 os_task_destroy()
│ │ │ │
▼ ▼ ▼ ▼
(TCB/栈分配) → INIT ──────► READY ──────► RUNNING ──► CLOSE ──► 资源回收
│ │ │ │ │
│ │ │ └──(延时/阻塞/挂起)──► SLEEP/BLOCK/SUSPEND
│ │ │
│ └────── 必须先启动 ──────┘| 阶段 | 关键操作 | 注意事项 |
|---|---|---|
| 创建 | os_task_create() | 静态/动态、检查返回值 |
| 启动 | os_task_startup() | INIT → READY,必须调用 |
| 运行 | 调度器调度 | 任务在 READY/RUNNING/SLEEP/BLOCK 等状态间转换 |
| 销毁 | os_task_destroy() | 不能在中断中、持锁任务返回 OS_BUSY、配合清理回调 |
📝 本节小结
核心要点回顾
本节围绕任务的生命周期(创建 → 启动 → 运行 → 销毁)展开,核心要点如下:
1. 三种创建方式
| 方式 | TCB | 栈 | 是否需要 OS_USING_HEAP |
|---|---|---|---|
| 完全静态创建 | 全局变量 | 全局数组 | 不需要 |
| 完全动态创建 | 内核从堆分配 | 内核从堆分配 | 需要 |
| 混合创建 | 全局变量 | 内核从堆分配 | 需要 |
2. os_task_create() 内部 8 步流程
参数检查 → 分配/获取 TCB → 分配/获取栈(含失败回滚)→ 防重复检查 → 加入资源链表 → _k_task_init() 初始化 → 置 INIT 状态 → 返回任务 ID。
3. 启动任务
os_task_startup()将任务从 INIT 转为 READY,并加入就绪队列,使任务被调度器调度。- 创建后不调用它,任务不会运行。
4. 销毁任务
os_task_destroy()销毁任务并回收资源。- 持有互斥锁的任务返回
OS_BUSY,不能销毁。 - 自己销毁自己时函数不会返回,资源由 recycle-task 异步回收。
5. 清理回调与用户数据
- 清理回调:任务销毁时自动调用,用于释放用户动态分配的资源。
- 用户数据:为任务关联任意私有数据,通常配合清理回调使用。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
忘记调用 os_task_startup() | 创建后必须启动任务,否则任务不会运行 |
忽略 os_task_create() 返回值 | 必须检查是否为 NULL,避免使用无效任务 ID |
| 在中断中创建/销毁任务 | 这两个 API 只能在任务上下文中调用 |
| 销毁持锁任务 | 先释放互斥锁,否则返回 OS_BUSY |
| 重复创建同一静态 TCB | 同一 TCB 地址只能创建一次 |
| 用户数据生命周期管理 | 确保 user_data 指向的内存直到任务销毁前始终有效 |
下一步
任务创建与销毁是任务管理的基础。接下来请学习下一小节:
- 4.2.3 任务调度:了解任务就绪后如何被调度器选中运行、优先级抢占、时间片轮转