线程相关函数(2)

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

254.2.5线程相关函数(2)

#include <rtthread.h>                         // 引入 RT-Thread 的线程类型、错误码和线程控制接口。
#define DBG_TAG "main"                       // 把本文件的日志标签设置为 main。
#define DBG_LVL DBG_LOG                      // 允许输出调试级别及以上的日志。
#include <rtdbg.h>                            // 在日志配置宏之后引入 RT-Thread 日志接口。
int ret;                                      // 按课程源码保存空闲钩子注册或线程控制接口的返回值。
rt_thread_t th1_ptr, th2_ptr;                // 分别保存动态线程 th1 和 th2 的句柄。
void scheduler_hook_func(struct rt_thread *from, struct rt_thread *to) // 定义调度器钩子,接收被切出和将切入的线程控制块指针。
{                                             // 调度器钩子函数开始。
    rt_kprintf("the thread schedul from %s to %s \r\n", from->parent.name, to->parent.name); // 按课堂原文打印切换方向;schedul 的拼写也原样保留。
}                                             // 调度器钩子函数结束。
void idle_hook_func(void)                     // 定义无参数、无返回值的空闲线程钩子。
{                                             // 空闲线程钩子函数开始。
    rt_thread_t current_ptr;                  // 用来保存当前执行钩子的空闲线程句柄。
    current_ptr = rt_thread_self();            // 获取当前线程;该钩子运行时应得到空闲线程 tidle0。
    rt_kprintf("the thread of name is: %s \r\n", current_ptr->parent.name); // 打印当前空闲线程名称;只适合课堂观察,不宜正式使用。
}                                             // 空闲线程钩子函数结束。
void th1_printf(void *parameter)              // 定义 th1 的入口函数;本节没有使用入口参数。
{                                             // th1 入口函数开始。
    rt_thread_t current_ptr;                  // 用来保存当前正在运行线程的句柄。
    while (1)                                 // 让 th1 周期性地持续运行。
    {                                         // th1 循环体开始。
        current_ptr = rt_thread_self();        // 获取当前线程句柄,此处应为 th1。
        rt_kprintf("the thread of name is: %s \r\n", current_ptr->parent.name); // 打印 th1 的线程名称。
        rt_kprintf("In %s,func %s is running! the line is %d!\r\n", __FILE__, __FUNCTION__, __LINE__); // 输出当前源文件、函数名和代码行号。
        rt_thread_mdelay(1000);                // 让 th1 挂起约 1000 ms,使其他就绪线程能够运行。
    }                                         // th1 本轮循环结束。
}                                             // th1 入口函数结束。
void th2_printf(void *parameter)              // 定义 th2 的入口函数;本节同样没有使用入口参数。
{                                             // th2 入口函数开始。
    while (1)                                 // 让 th2 周期性地持续运行。
    {                                         // th2 循环体开始。
        rt_kprintf("In %s,func %s is running! the line is %d!\r\n", __FILE__, __FUNCTION__, __LINE__); // 输出 th2 当前所在的文件、函数和代码行。
        rt_thread_mdelay(1000);                // 让 th2 挂起约 1000 ms,使其他就绪线程能够运行。
    }                                         // th2 本轮循环结束。
}                                             // th2 入口函数结束。
int main(void)                                // RT-Thread 初始化完成后,主线程从 main 开始执行。
{                                             // main 函数开始。
    int count = 1;                            // 作为课程实验中延后启动 th2 的循环计数变量。
    th1_ptr = rt_thread_create("th1",         // 动态创建名称为 th1 的线程,并保存返回的线程句柄。
                               th1_printf,    // 指定 th1_printf 为 th1 的入口函数。
                               NULL,          // 本节不向 th1 传递入口参数。
                               1024,          // 由内核从堆中为 th1 分配 1024 字节线程栈。
                               20,            // 把 th1 的优先级设置为 20。
                               10);           // 把 th1 的初始时间片设置为 10 个系统 tick。
    if (th1_ptr == RT_NULL)                   // 返回空句柄表示动态创建 th1 失败。
    {                                         // th1 创建失败分支开始。
        LOG_E("th1_printf rt_thread_create failed!\r\n"); // 输出 th1 创建失败日志。
        return -RT_ENOMEM;                    // 按课程源码返回负的“内存不足”错误码。
    }                                         // th1 创建失败分支结束。
    rt_thread_startup(th1_ptr);               // 立即启动 th1,使其进入就绪队列。
    th2_ptr = rt_thread_create("th2",         // 动态创建名称为 th2 的线程,并保存返回的线程句柄。
                               th2_printf,    // 指定 th2_printf 为 th2 的入口函数。
                               NULL,          // 本节不向 th2 传递入口参数。
                               1024,          // 由内核从堆中为 th2 分配 1024 字节线程栈。
                               20,            // 把 th2 的优先级也设置为 20。
                               10);           // 把 th2 的初始时间片设置为 10 个系统 tick。
    if (th2_ptr == RT_NULL)                   // 返回空句柄表示动态创建 th2 失败。
    {                                         // th2 创建失败分支开始。
        LOG_E("th2_printf rt_thread_create failed!\r\n"); // 输出 th2 创建失败日志。
        return -RT_ENOMEM;                    // 按课程源码返回负的“内存不足”错误码。
    }                                         // th2 创建失败分支结束。
    // rt_thread_startup(th2_ptr);            // 课堂最终注释掉直接启动,使 th2 暂时保持初始状态。
    // rt_scheduler_sethook(scheduler_hook_func); // 课堂最终关闭调度器钩子注册,避免持续输出切换日志。
    // ret = rt_thread_idle_sethook(idle_hook_func); // 课堂最终关闭空闲钩子注册,避免空闲循环高速打印。
    // if (ret != RT_EOK)                     // 若启用注册,则检查空闲钩子是否添加成功。
    // {                                      // 空闲钩子注册失败分支开始。
    //     LOG_E("rt_thread_idle_sethook failed!\r\n"); // 输出空闲钩子注册失败日志。
    //     return ret;                        // 将空闲钩子注册错误码返回给上层。
    // }                                      // 空闲钩子注册失败分支结束。
    while (count++)                           // 每次判断时先使用 count 的旧值,再将 count 自增 1。
    {                                         // 主线程循环开始。
        LOG_D("Hello RT-Thread!");            // 输出主线程仍在运行的调试日志。
        rt_thread_mdelay(1000);                // 让主线程挂起约 1000 ms,并形成实验的时间基准。
        if (count == 4)                       // 第三轮循环的延时结束后,count 首次等于 4。
        {                                     // 延后启动 th2 的分支开始。
            ret = rt_thread_control(th2_ptr, RT_THREAD_CTRL_STARTUP, NULL); // 通过线程控制接口启动仍处于初始状态的 th2。
            if (ret != RT_EOK)                // 返回值不是 RT_EOK,表示启动 th2 失败。
            {                                 // th2 启动失败分支开始。
                LOG_E("th2_ptr rt_thread_control failed!\r\n"); // 输出线程控制失败日志。
                return ret;                   // 将线程控制接口返回的错误码返回给上层。
            }                                 // th2 启动失败分支结束。
        }                                     // 延后启动 th2 的分支结束。
    }                                         // 主线程循环结束。
    return RT_EOK;                            // 理论上的成功返回;正常运行时不会执行到这里。
}                                             // main 函数结束。

一、线程切换的钩子函数

钩子函数是由应用提供、由内核在特定事件发生时回调的函数。应用不是在业务流程中直接调用钩子,而是先把函数地址登记给内核;当对应事件发生时,内核通过保存的函数指针调用它。

本节涉及的调度器钩子接口为:

void rt_scheduler_sethook(void (*hook)(struct rt_thread *from,
                                      struct rt_thread *to));

这里需要分清两层参数:

  • rt_scheduler_sethook() 的参数 hook 是函数指针,课程传入的是 scheduler_hook_func;
  • scheduler_hook_func() 自己的两个参数 from、to 不是函数指针,而是两个线程控制块指针,分别表示即将切出的线程和即将切入的线程。

这与创建线程时传入入口函数都使用了“回调函数指针”的思想,但两种回调的函数原型和触发时机并不相同,不能混用。

1.1 钩子函数作用

在本项目 RT-Thread 5.0.2 的单核调度器中,调用关系可以概括为:

应用调用 rt_scheduler_sethook(scheduler_hook_func)
                     │
                     ▼
内核保存 scheduler_hook_func 的函数地址
                     │
                     ▼
rt_schedule() 判断确实需要从 from 切换到 to
                     │
                     ▼
内核调用 scheduler_hook_func(from, to)
                     │
                     ▼
准备并执行底层线程上下文切换

只有调度器选中的 to_thread 与当前线程不同时,配套源码才调用该钩子。一次 rt_schedule() 如果最后仍选择当前线程,不会因为“调用过调度器”就产生这次切换钩子。

本版本的 rt_scheduler_sethook() 没有返回值,只是替换内核保存的单个调度器钩子指针。因此:

  • 后一次注册会覆盖前一次注册,不是把多个函数追加到列表;
  • 传入 RT_NULL 可以清除当前函数指针;
  • 课程最终通过注释注册语句关闭实验输出,钩子函数虽然仍定义在源码里,但不会被内核调用。

调度器钩子的执行环境比普通线程函数严格得多。本项目的 scheduler_up.c 在关中断的调度临界区内、真正切换栈之前调用它。此时钩子必须非常短,不能延时、等待 IPC、触发再次调度,也不应执行耗时或可能阻塞的串口输出。

课程为了直观看到切换方向而在钩子中调用 rt_kprintf(),适合短时间教学观察,不是正式项目的推荐写法。高频打印会显著拉长关中断时间、扰动被测时序,严重时还可能引起串口重入或系统异常。正式跟踪通常只记录少量固定大小的数据或置标志,再由普通线程异步处理。

1.2 线程切换钩子函数作用

课程钩子通过:

from->parent.name
to->parent.name

读取两个线程控制块中内嵌内核对象的名称,于是串口能看到类似结果:

the thread schedul from th2 to tidle0
the thread schedul from tidle0 to th1
the thread schedul from th1 to tidle0
the thread schedul from tidle0 to main

其中 schedul 是课堂格式字符串中的拼写,并不是 RT-Thread 的接口名。输出频繁出现 tidle0,原因是 th1、th2 和 main 打印后都调用了 rt_thread_mdelay(1000);它们同时挂起的间隙里,最低优先级的空闲线程成为唯一可运行线程。

这类日志可以帮助确认:

  • 哪个线程被切出、哪个线程被切入;
  • 周期线程休眠后是否回到空闲线程;
  • 某线程是否长期得不到运行机会;
  • 实际切换序列是否符合优先级和阻塞关系。

日志只能反映启用钩子后的观测结果,且打印本身会改变系统时序,所以不能把一次输出顺序当成严格的实时性能测量。

二、空闲线程钩子函数

空闲线程是系统中优先级最低的兜底线程。本项目为单核 STM32F103,空闲线程名是 tidle0;当没有任何更高优先级线程处于就绪状态时,它会一直执行自己的无限循环。

配套 5.0.2 工程启用了 RT_USING_IDLE_HOOK,注册和删除接口分别为:

rt_err_t rt_thread_idle_sethook(void (*hook)(void));
rt_err_t rt_thread_idle_delhook(void (*hook)(void));

空闲钩子的函数原型是 void hook(void),既没有形参,也没有返回值。课程不是“把线程名参数传给钩子”,而是在钩子内部调用 rt_thread_self(),再从返回的空闲线程句柄中读取名称。

2.1 示例

本工程 RT_IDLE_HOOK_LIST_SIZE 为 4,因此内核维护四个空闲钩子槽位。rt_thread_idle_sethook() 在关中断保护下寻找第一个空槽:

  • 找到空槽并保存函数指针时返回 RT_EOK;
  • 四个槽位都已占用时返回 -RT_EFULL;
  • rt_thread_idle_delhook() 找到并移除指定函数时返回 RT_EOK,找不到时返回 -RT_ENOSYS。

所以课程用全局变量 ret 接收注册结果并检查是否等于 RT_EOK 是必要的。顶部最终代码按视频保留 int ret;从接口语义上看,使用 RT-Thread 专门的 rt_err_t 类型会更清楚,但不能因此擅自改写课堂源码。

空闲钩子直接运行在空闲线程上下文中,并在空闲循环的每一轮被调用。它不是新的线程,也没有自己的独立栈,而是使用空闲线程栈。因此必须遵守以下限制:

  • 必须短小、非阻塞,不能调用 rt_thread_mdelay()、永久等待信号量等会挂起当前线程的接口;
  • 不能让空闲线程退出或失去就绪条件,否则系统可能没有兜底可运行线程;
  • 应避免长时间轮询、海量打印和大局部变量;
  • 只有明确不会阻塞、不会长时间占用 CPU 的轻量工作才适合放入其中,例如非常简单的状态统计或特定平台允许的看门狗维护。

课程最初的 IDLE_THREAD_STACK_SIZE 是 256 字节,向空闲钩子加入 rt_thread_self() 和 rt_kprintf() 后出现栈溢出提示,随后把配置改为:

#define IDLE_THREAD_STACK_SIZE 256*4

即 1024 字节。这里不能理解成“空闲线程打印得越快,栈会一轮轮累加直到溢出”。正常函数返回后,同一段栈空间会被复用;循环次数本身不会永久累积调用帧。真正原因是某一次 rt_kprintf() 调用链所需的峰值栈空间超过原来的 256 字节。增大到 1024 字节使本次实验不再报警,但高频串口打印仍会占用大量时间、淹没终端并干扰系统行为,正式代码应移除这种钩子工作负载,而不是只靠扩大栈掩盖设计问题。

视频里其他业务线程都休眠 1000 ms,所以绝大部分空档由空闲线程占用。空闲循环没有延时,钩子会以很高频率打印 tidle0,终端自然迅速被刷满。课程最终将空闲钩子注册代码注释掉,因而最终控制线程实验不会再执行该打印。

三、控制其他线程

3.1 控制其他线程功能

线程控制接口原型为:

rt_err_t rt_thread_control(rt_thread_t thread, int cmd, void *arg);

三个参数的准确含义是:

参数含义
thread要操作的目标线程句柄;不一定非得是“其他线程”,具体能否操作当前线程由命令语义决定
cmd控制命令,决定执行启动、关闭、改优先级或绑核等哪一种操作
arg命令专用参数;有的命令不用它,有的命令要求特定类型或特定编码,不能一概写成空指针

本项目 RT-Thread 5.0.2 源码中的主要命令如下:

命令本版本实现arg 的用法
RT_THREAD_CTRL_STARTUP直接调用 rt_thread_startup(thread),启动尚处于初始态的线程未使用,课程传 NULL
RT_THREAD_CTRL_CLOSE静态线程执行 detach,动态线程执行 delete,随后调度未使用;这是关闭/删除,不是可随时恢复的“暂停”
RT_THREAD_CTRL_CHANGE_PRIORITY在关中断保护下修改目标线程的当前优先级;若线程在就绪队列中,会先移出再按新优先级插入指向 rt_uint8_t 新优先级值的有效指针
RT_THREAD_CTRL_BIND_CPU仅在启用 RT_USING_SMP 时编译,用于限制线程可运行的 CPU本版本把 arg 数值转换成 CPU 编号,并不是课程本次实验使用的空参数

rtdef.h 还定义了 RT_THREAD_CTRL_INFO,但配套 5.0.2 的 rt_thread_control() 没有对应处理分支。其 default 分支又会直接落到末尾返回 RT_EOK,所以不能仅凭宏存在或返回成功就推断某条命令在当前构建中真正完成了操作,必须结合版本源码和配置确认。

本课程使用的 STM32F103 是单核 Cortex-M3,工程没有启用 RT_USING_SMP,因此绑核分支不会编译进来。CPU 亲和性在多核系统上可以减少线程跨核迁移并改善缓存局部性或时延抖动,但它不保证程序一定更快,也不会让线程脱离调度器:线程仍可能在所绑定的核心上被更高优先级线程抢占或发生普通上下文切换。实际是否改善 PCIe 等大吞吐任务,需要用目标硬件和负载测量,不能把“绑核必然不丢速率”当成通用结论。

RT_THREAD_CTRL_STARTUP 也不是“重新启动任意旧线程”。它最终调用 rt_thread_startup(),该接口要求目标线程仍处于 RT_THREAD_INIT 初始状态。已经启动、正在运行、挂起或已经关闭的线程不能靠重复发送该命令无条件重启。

3.2 示例

本节实验的关键步骤是:

  1. rt_thread_create() 成功创建 th2,使其处于初始态;
  2. 注释 rt_thread_startup(th2_ptr),所以 th2 暂时不进入就绪队列,也不会打印;
  3. 注释调度器钩子和空闲钩子的注册,避免它们的大量输出干扰观察;
  4. 主线程每轮先打印,再休眠 1000 ms;
  5. 当 count == 4 时调用 rt_thread_control(th2_ptr, RT_THREAD_CTRL_STARTUP, NULL);
  6. 检查返回值,成功后 th2 才进入就绪调度。

视频对启动时刻先后出现“约 4 秒”和“约 2 秒”的口头判断,最终画面标注为 3 秒。按 C 语言后缀自增规则逐轮计算如下:

进入 while (count++) 前条件使用的旧值进入循环体后的 count休眠后 count == 4
11,条件为真2否
22,条件为真3否
33,条件为真4是,启动 th2

因此,从主循环第一次开始计时,到第三次 rt_thread_mdelay(1000) 返回,名义上约经过 3 秒,随后才执行启动命令。实际墙钟时间还会叠加启动、打印和调度开销,不能把它当作精确定时器。

主线程优先级为 10,th2 优先级为 20。rt_thread_startup() 虽然会请求调度,但刚启动的 th2 优先级低于仍在运行的主线程,所以不会在控制接口返回前强行抢占主线程;主线程很快进入下一轮并再次调用延时后,th2 才获得 CPU 并打印。串口从此持续出现 th2_printf 输出,说明延后启动成功。

条件使用 == 4,正常情况下只匹配这一轮,不会每秒重复启动 th2。这也很重要,因为对已经启动的线程再次执行 RT_THREAD_CTRL_STARTUP 不符合 rt_thread_startup() 的状态前提。课程的 while (count++) 仍存在有符号 int 最终溢出的未定义行为;本笔记忠实保留源码,但实际永久循环应采用明确、无溢出副作用的控制方式。

核对依据:本项目配套 RT-Thread 5.0.2 的 applications/main.c、rt-thread/src/scheduler_up.c、rt-thread/src/idle.c、rt-thread/src/thread.c、rt-thread/include/rtdef.h 与 rtconfig.h,以及 RT-Thread 官方线程管理文档、RT-Thread v5.0.2 scheduler_up.c、RT-Thread v5.0.2 idle.c、RT-Thread v5.0.2 thread.c和RT-Thread v5.0.2 rtdef.h。