4.2.9 任务钩子
4.2.9 任务钩子
📚 本节导读
学习时长: 约 15 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: 任务调度机制、回调函数概念
🎯 学习目标
- 理解任务钩子(Task Hook)的作用和配置方式
- 掌握
os_task_switch_hook_add()和os_task_switch_hook_delete()的用法 - 理解 SMP 和单核模式下钩子函数的签名差异
- 了解任务钩子的典型应用场景
一、任务钩子概述
1.1 什么是任务钩子?
任务钩子(Task Hook)是 OneOS 提供的一种扩展机制,允许用户在每次任务切换时执行自定义的回调函数。通过任务钩子,开发者可以在不修改内核代码的情况下,实现对任务切换行为的监控、追踪和分析。
1.2 配置使能
任务钩子功能默认关闭,需要通过 Kconfig 启用:
config OS_USING_TASK_HOOK
bool "Using task hook"
default n
help
Allows user to register hooks with task module.启用后,os_task.h 中会定义 OS_TASK_HOOK_CALL 宏和钩子函数类型。
注意:启用
OS_USING_CPU_MONITOR会自动启用OS_USING_TASK_HOOK,因为 CPU 监控依赖于任务切换钩子。
二、钩子函数签名
2.1 单核模式
typedef void (*os_task_switch_hook_t)(os_task_id from, os_task_id to);| 参数 | 说明 |
|---|---|
from | 切换前的任务(即将让出 CPU 的任务) |
to | 切换后的任务(即将获得 CPU 的任务) |
2.2 SMP 模式
typedef void (*os_task_switch_hook_t)(os_task_id from, os_task_id to, int32_t cpu);| 参数 | 说明 |
|---|---|
from | 切换前的任务 |
to | 切换后的任务 |
cpu | 发生切换的 CPU 核心编号 |
2.3 OS_TASK_HOOK_CALL 宏
#define OS_TASK_HOOK_CALL(func, argv) \
do \
{ \
if ((func) != OS_NULL) \
{ \
func argv; \
} \
} while (0)该宏在调用钩子函数前检查函数指针是否为空,确保安全调用。当 OS_USING_TASK_HOOK 未启用时,该宏展开为空操作。
三、API 函数
3.1 os_task_switch_hook_add() —— 添加任务切换钩子
os_bool_t os_task_switch_hook_add(os_task_switch_hook_t hook);| 参数 | 说明 |
|---|---|
hook | 要注册的任务切换钩子函数指针 |
| 返回值 | 说明 |
|---|---|
OS_TRUE | 添加成功 |
OS_FALSE | 添加失败(已有钩子注册) |
完整源码(kernel/source/os_task.c,与项目一致):
os_bool_t os_task_switch_hook_add(os_task_switch_hook_t hook)
{
if (OS_NULL == gs_os_task_switch_hook)
{
gs_os_task_switch_hook = hook;
return OS_TRUE;
}
else
{
return OS_FALSE;
}
}分步详解:
| 代码 | 作用 |
|---|---|
if (OS_NULL == gs_os_task_switch_hook) | 判断是否已有钩子注册(全局变量 gs_os_task_switch_hook 是否为空) |
gs_os_task_switch_hook = hook | 把钩子函数指针存入全局变量,完成注册 |
return OS_TRUE | 注册成功 |
else ... return OS_FALSE | 已有钩子,注册失败 |
关键点:
- OneOS 当前只支持注册一个任务切换钩子(用全局变量
gs_os_task_switch_hook保存单个指针)。 - 如果已有钩子注册,再次调用返回
OS_FALSE。如需多个功能,应在同一个钩子函数内实现分发。
3.2 os_task_switch_hook_delete() —— 删除任务切换钩子
os_bool_t os_task_switch_hook_delete(os_task_switch_hook_t hook);| 参数 | 说明 |
|---|---|
hook | 要删除的钩子函数指针(须与注册时相同) |
| 返回值 | 说明 |
|---|---|
OS_TRUE | 删除成功 |
OS_FALSE | 删除失败(钩子未找到) |
完整源码(kernel/source/os_task.c,与项目一致):
os_bool_t os_task_switch_hook_delete(os_task_switch_hook_t hook)
{
if (hook == gs_os_task_switch_hook)
{
gs_os_task_switch_hook = OS_NULL;
return OS_TRUE;
}
else
{
return OS_FALSE;
}
}分步详解:
| 代码 | 作用 |
|---|---|
if (hook == gs_os_task_switch_hook) | 判断传入的 hook 是否与已注册的钩子相同 |
gs_os_task_switch_hook = OS_NULL | 把全局钩子变量置空,完成删除 |
return OS_TRUE | 删除成功 |
else ... return OS_FALSE | 传入的钩子不匹配,删除失败 |
关键点:删除时必须传入与注册时完全相同的函数指针,否则匹配不成功返回 OS_FALSE。
四、钩子的调用时机
任务切换钩子在 os_task_switch_notify() 函数中被调用,该函数在每次任务切换时执行。
完整源码(kernel/source/os_task.c,与项目一致):
void os_task_switch_notify(void)
{
os_task_t *current_task;
os_task_t *next_task;
#ifdef OS_USING_SMP
int32_t current_cpu_index;
current_cpu_index = os_cpu_id_get();
current_task = g_os_current_task[current_cpu_index];
next_task = g_os_next_task[current_cpu_index];
#else
current_task = g_os_current_task;
next_task = g_os_next_task;
#endif
#ifdef OS_USING_OVERFLOW_CHECK
_k_task_stack_check(current_task, next_task);
#endif
#ifdef OS_USING_SMP
OS_TASK_HOOK_CALL(
gs_os_task_switch_hook,
(OS_TYPE_CONVERT(os_task_id, current_task), OS_TYPE_CONVERT(os_task_id, next_task), current_cpu_index));
#else
OS_TASK_HOOK_CALL(gs_os_task_switch_hook,
(OS_TYPE_CONVERT(os_task_id, current_task), OS_TYPE_CONVERT(os_task_id, next_task)));
#endif
}分步详解:
第一步:获取切换前后任务
#ifdef OS_USING_SMP
int32_t current_cpu_index;
current_cpu_index = os_cpu_id_get();
current_task = g_os_current_task[current_cpu_index];
next_task = g_os_next_task[current_cpu_index];
#else
current_task = g_os_current_task;
next_task = g_os_next_task;
#endif详细解释:
current_task:切换前的任务(即将让出 CPU)。next_task:切换后的任务(即将获得 CPU)。- 单核:直接取全局变量
g_os_current_task和g_os_next_task。 - SMP:先
os_cpu_id_get()获取当前 CPU 编号,再取该 CPU 对应的数组元素。
第二步:堆栈溢出检测(可选)
#ifdef OS_USING_OVERFLOW_CHECK
_k_task_stack_check(current_task, next_task);
#endif详细解释:若启用了 OS_USING_OVERFLOW_CHECK,在切换前检查 from/to 两个任务的栈是否溢出(_k_task_stack_check)。
第三步:调用任务切换钩子
#ifdef OS_USING_SMP
OS_TASK_HOOK_CALL(
gs_os_task_switch_hook,
(OS_TYPE_CONVERT(os_task_id, current_task), OS_TYPE_CONVERT(os_task_id, next_task), current_cpu_index));
#else
OS_TASK_HOOK_CALL(gs_os_task_switch_hook,
(OS_TYPE_CONVERT(os_task_id, current_task), OS_TYPE_CONVERT(os_task_id, next_task)));
#endif详细解释:
- 通过
OS_TASK_HOOK_CALL宏调用已注册的钩子gs_os_task_switch_hook(宏内部会先判空,空则不调用)。 - 参数用
OS_TYPE_CONVERT(os_task_id, ...)把os_task_t *转换为os_task_id。 - 单核:传
(from, to)两个参数。 - SMP:多传一个
current_cpu_index(发生切换的 CPU 编号)。
编译条件详解:为什么只有启用 OS_USING_OVERFLOW_CHECK 或 OS_USING_TASK_HOOK 时才编译?
这是一个编译期(预处理)联动机制,涉及"配置宏 → 构建系统 → 中间宏 → 源码条件编译"四个环节。下面逐步拆解。
环节 1:os_task_switch_notify() 定义受 OS_TASK_SWITCH_NOTIFY 控制
在 os_task.c 中,函数定义被条件编译包裹:
#ifdef OS_TASK_SWITCH_NOTIFY
void os_task_switch_notify(void)
{
...
}
#endif /* OS_TASK_SWITCH_NOTIFY */只有定义了 OS_TASK_SWITCH_NOTIFY 宏,这个函数才会被编译。
环节 2:调用点(架构汇编)同样受该宏控制
os_task_switch_notify() 是在任务切换的架构汇编代码中被调用的(位于保存完 from 任务现场之后、切换上下文之前)。例如 arch/arm/armv8m/no-trustzone/gcc/arch_task_switch_gcc.S:
update_from_task_info
STR R3, [R2, #0] ; Update g_os_current_task->stack_top
; ...
#if defined (OS_TASK_SWITCH_NOTIFY)
PUSH {R1, R12}
BL os_task_switch_notify ; 在切换现场之间调用钩子通知
POP {R1, R12}
#endif
switch_to_task调用点同样被 OS_TASK_SWITCH_NOTIFY 包裹。所以函数定义和调用点必须同时生效。
环节 3:OS_TASK_SWITCH_NOTIFY 宏从哪里来?—— 构建系统自动定义
这个宏不是手动写在源码里的,而是由构建系统根据配置自动传给编译器。在 kernel/weave.yaml 中:
cppdefines:
- OS_TASK_SWITCH_NOTIFY ? {is_define('OS_USING_OVERFLOW_CHECK') or is_define('OS_USING_TASK_HOOK')}
asdefines:
- OS_TASK_SWITCH_NOTIFY ? {is_define('OS_USING_OVERFLOW_CHECK') or is_define('OS_USING_TASK_HOOK')}即:只要定义了 OS_USING_OVERFLOW_CHECK 或 OS_USING_TASK_HOOK 任意一个,构建系统就会自动加 -DOS_TASK_SWITCH_NOTIFY(cppdefines 给 C 代码,asdefines 给汇编代码)。
环节 4:两个配置宏从哪里来?—— Kconfig 菜单配置
OS_USING_OVERFLOW_CHECK:Debug → Check菜单下,"Using stack overflow checking",默认 y。OS_USING_TASK_HOOK:Debug → Check菜单下,"Using task hook",默认 n。- 关键联动:
OS_USING_CPU_MONITOR(CPU 监控)会select OS_USING_TASK_HOOK,所以启用 CPU 监控会自动启用任务钩子,进而自动启用os_task_switch_notify()。
整个联动机制流程:
Kconfig 配置 weave.yaml 构建系统 源码条件编译
─────────── ───────────────── ─────────────
OS_USING_OVERFLOW_CHECK = y ──► ├─► 定义 -DOS_TASK_SWITCH_NOTIFY ──► os_task.c 的
OS_USING_TASK_HOOK = y ──► │ │ os_task_switch_notify()
(或 CPU_MONITOR select 它) └─► 定义汇编宏 OS_TASK_SWITCH_NOTIFY ─► 架构 .S 中的调用点
(asdefines)为什么这样设计?
os_task_switch_notify()的代价是:每次任务切换都要额外执行一次函数调用(在切换的热路径上)。如果任务钩子和栈溢出检测都不需要,编译进这个函数会白白增加切换开销。- 所以内核用
OS_TASK_SWITCH_NOTIFY这个中间宏,把"是否需要这个通知函数"从两个独立的配置项(overflow check、task hook)统一起来——两者都不用就不编译,用任意一个就编译,实现按需裁剪,避免不必要的性能开销。
五、使用示例
5.1 任务切换追踪
#include <os_task.h>
/* 任务切换钩子函数 */
void my_task_switch_hook(os_task_id from, os_task_id to)
{
const char *from_name = "NONE";
const char *to_name = "NONE";
if (from != OS_NULL)
{
from_name = os_task_get_name(from);
}
if (to != OS_NULL)
{
to_name = os_task_get_name(to);
}
os_kprintf("Switch: %s → %s\r\n", from_name, to_name);
}
/* 注册钩子 */
void app_init(void)
{
os_task_switch_hook_add(my_task_switch_hook);
}5.2 CPU 使用率统计
#include <os_task.h>
#include <os_clock.h>
typedef struct {
const char *name;
os_tick_t total_ticks;
os_tick_t last_switch_time;
os_bool_t is_active;
} task_cpu_stat_t;
#define MAX_TASK_STAT 10
static task_cpu_stat_t g_task_stats[MAX_TASK_STAT];
static int g_stat_count = 0;
/* 记录任务切换时的 CPU 使用情况 */
void cpu_stat_hook(os_task_id from, os_task_id to)
{
os_tick_t now = os_tick_get_value();
int i;
/* 更新 from 任务的运行时间 */
for (i = 0; i < g_stat_count; i++)
{
if (g_task_stats[i].is_active)
{
g_task_stats[i].total_ticks += (now - g_task_stats[i].last_switch_time);
g_task_stats[i].is_active = OS_FALSE;
break;
}
}
/* 标记 to 任务开始运行 */
for (i = 0; i < g_stat_count; i++)
{
if (os_task_get_id(g_task_stats[i].name) == to ||
(g_task_stats[i].name != OS_NULL &&
strcmp(os_task_get_name(to), g_task_stats[i].name) == 0))
{
g_task_stats[i].is_active = OS_TRUE;
g_task_stats[i].last_switch_time = now;
return;
}
}
/* 新任务,添加到统计列表 */
if (g_stat_count < MAX_TASK_STAT)
{
g_task_stats[g_stat_count].name = os_task_get_name(to);
g_task_stats[g_stat_count].total_ticks = 0;
g_task_stats[g_stat_count].last_switch_time = now;
g_task_stats[g_stat_count].is_active = OS_TRUE;
g_stat_count++;
}
}5.3 任务执行时间监控
#include <os_task.h>
/* 监控任务切换间隔,检测是否存在长时间运行的任务 */
void execution_monitor_hook(os_task_id from, os_task_id to)
{
static os_tick_t last_switch = 0;
os_tick_t now = os_tick_get_value();
os_tick_t elapsed;
if (last_switch != 0)
{
elapsed = now - last_switch;
/* 如果上一次切换间隔超过阈值,发出警告 */
if (elapsed > 100) /* 100 tick = 1s(假设 100Hz) */
{
os_kprintf("WARNING: %s ran for %lu ticks!\r\n",
os_task_get_name(from), elapsed);
}
}
last_switch = now;
}5.4 注销钩子
/* 停止监控 */
void stop_monitoring(void)
{
os_task_switch_hook_delete(cpu_stat_hook);
}六、应用场景
| 场景 | 说明 |
|---|---|
| CPU 使用率监控 | 统计每个任务占用的 CPU 时间,用于性能分析和优化 |
| 任务切换追踪 | 记录任务切换序列,用于调试任务调度相关问题 |
| 执行时间监控 | 检测是否存在长时间占用 CPU 的任务,防止饥饿 |
| 性能基准测试 | 测量任务切换频率和开销 |
| 系统行为分析 | 分析任务间的协作模式,验证系统设计是否合理 |
| 堆栈溢出检测 | 内核已内置 OS_USING_OVERFLOW_CHECK,在钩子调用前检查栈溢出 |
七、注意事项
钩子函数执行时间要短:钩子函数在每次任务切换时都被调用,执行时间过长会显著增加上下文切换开销,影响系统实时性。
不要在钩子中调用阻塞 API:钩子函数运行在内核临界区中,不应调用任何可能导致阻塞或切换的 API(如
os_task_tsleep()、os_sem_wait()等)。只支持一个钩子:当前版本只支持注册一个任务切换钩子函数。如果需要多个功能,可以在钩子函数内部实现分发逻辑。
性能开销:启用任务钩子会增加每次任务切换的开销。在性能敏感的系统中,应谨慎使用。
SMP 注意事项:在 SMP 模式下,钩子函数可能被多个 CPU 核心同时调用,需要确保钩子函数内部的数据访问是线程安全的。
📝 本节小结
核心要点回顾
本节围绕任务钩子机制展开,核心要点如下:
1. 任务钩子是什么
- 一种扩展机制:允许在每次任务切换时执行自定义回调函数,无需修改内核代码即可监控、追踪任务调度。
2. 配置使能
- 默认关闭,通过 Kconfig 的
OS_USING_TASK_HOOK启用。 - 启用
OS_USING_CPU_MONITOR会自动启用它(CPU 监控依赖任务切换钩子)。
3. 钩子函数签名
| 模式 | 签名 |
|---|---|
| 单核 | void (*)(os_task_id from, os_task_id to) |
| SMP | void (*)(os_task_id from, os_task_id to, int32_t cpu) |
4. 两个管理 API
os_task_switch_hook_add():注册钩子(只支持一个)。os_task_switch_hook_delete():删除钩子(须传入相同函数指针)。
5. 调用时机
- 钩子在
os_task_switch_notify()中调用,每次任务切换时执行。 - 宏
OS_TASK_HOOK_CALL调用前自动判空,安全调用。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 注册多个钩子 | 只支持一个,多注册返回 OS_FALSE;多功能应在单个钩子内分发 |
| 钩子函数执行过长 | 每次切换都调用,过长会显著增加切换开销,影响实时性 |
| 在钩子中调用阻塞 API | 钩子运行在内核临界区,禁止调用 os_task_tsleep()、os_sem_wait() 等 |
| 删除钩子时函数指针不匹配 | 必须传入与注册时完全相同的函数指针 |
| 忽略 SMP 并发 | SMP 下钩子可能被多核同时调用,钩子内数据访问要线程安全 |
忘记启用 OS_USING_TASK_HOOK | 未启用时 OS_TASK_HOOK_CALL 为空操作,钩子不生效 |
下一步
任务管理相关小节已全部介绍完毕。接下来请学习下一章:
- 4.3 任务同步与通信:了解信号量、互斥锁、消息队列等任务间同步与通信机制