线程

XWOS的线程

概述

线程 是XWOS最基本的调度单位,在其他RTOS中可能称之为 任务 。 XWOS的线程,除了最基本的运行、睡眠、退出操作外,还支持冻结与解冻,迁移等操作。

XWOS线程的函数,是仿造 pthread 的函数设计的。

线程的状态

XWOS线程状态图
Photo: xwos.tech / CC-BY-SA-4.0

  • 待命(standby) :线程对象已被初始化,但未指定主函数;
  • 就绪(ready) :线程已加入到就绪队列中;
  • 运行(running) :线程正在运行,每个CPU中只可能存在一个线程正在运行;
  • 睡眠(sleeping) :线程正在睡眠;
  • 阻塞(blocking) :线程正在等待,可与睡眠态组合;
  • 可被冻结(freezable) :线程可被冻结;
  • 冻结(frozen) :线程已被冻结;
  • 退出(exiting) :线程即将结束;
  • 迁移(migrating) :线程正处于迁移到别的CPU的过程中;
  • 分离态(detached) :分离态的线程退出后由操作系统自动回收其内存资源;
  • 已连接(joined)连接态(joinable) 的线程被其他线程 join() 后的状态;
  • 不可被中断(uninterrupted) :线程的 阻塞睡眠 不可被中断。

线程的分离态与连接态

XWOS线程的分离态与连接态是参考 pthread 设计的:

  • 连接态(joinable) 线程需要由另一个线程调用 xwos_thd_join()xwos_thd_stop() 来回收其内存资源;
  • 分离态(detached) 的线程退出后,系统自动回收其资源。

线程对象与对象描述符

线程对象是 XWOS对象 struct xwos_object 的派生类 。 类似的,线程对象也用 线程对象描述符 xwos_thd_d 来解决有效性和身份合法性的问题。

线程对象描述符由 线程对象的指针标签 组成:

typedef struct {
        struct xwos_thd * thd; /**< 线程对象的指针 */
        xwsq_t tik; /**< 标签 */
} xwos_thd_d;

通过对象描述符引用对象时,首先检测 obj->magic 的值,是否为 0x58574F53U ,由此可确定指针 obj 指向一个有效的 XWOS的对象 。 然后对比标签 obj->tiktik 是否相等,由此可以确定对象的 身份 。 因为对象的 tik 是全局唯一的,当对象被释放后,它的 tik 会被析构函数析构为 0 。 当内存地址被重新构建为新的对象,那么它的 tik 一定与对象描述符的 tik 不一致。

线程的初始化与创建

线程属性

线程在创建或初始化时,可通过参数 struct xwos_thd_attr 设定其属性。 XWOS的线程属性参考 pthread 来实现,其结构体定义也与 pthread_attr_t 类似。

  • xwos_thd_attr::privileged :表示线程拥有系统特权。
    • 在ARMv6m/ARMv7m中,是通过 CONTROL 寄存器的 bit0(nPRIV) 来实现的;
    • 在Embedded PowerPC中,是通过 MSR 寄存器的 bit17(PR) 来实现的;
    • 在RISCV32中,是通过 MCAUSE 寄存器的 bit28bit29(MPP) 来实现的。
  • xwos_thd_attr::detached :表示线程是分离的,类似于POSIX线程的detached属性。
    • 分离态(detached) 的线程退出后,系统自动回收其资源;
    • 连接态(joinable) 线程需要由另一个线程调用 xwos_thd_join()xwos_thd_stop() 来回收其内存资源。
  • xwos_thd_attr::stack :表示线程的栈的首地址。
  • xwos_thd_attr::stack_size :表示线程的栈的字节数。
  • xwos_thd_attr::stack_guard_size :表示线程的警戒线位置。
    • 当栈指针sp增长超过了警戒线位置会触发 stackoverflow 警告,但这需要SOC的MPU或MMU来提供支持, 可以在线程创建后的HOOK函数 board_thd_postinit_hook() 来针对不同的SOC设置MPU或MMU。
    • XWOS内部也提供了一种基于 if...else... 的检测逻辑,但 stack overflow 后导致程序跑飞,可能没有机会运行检测逻辑的代码。
  • xwos_thd_attr::name :表示线程的名字,用于调试时的日志输出。
  • xwos_thd_attr::priority :表示线程的优先级,XWOS的优先级是数值越大,优先级越高。

静态初始化

  • 静态初始化: xwos_thd_init()
  • 静态 是指用户预先定义线程结构体对象,这些对象在编译期由编译器分配内存。
  • 静态初始化线程还需预先定义栈数组,作用域为全局。
  • 栈数组的首地址与大小,必须要满足CPU的ABI规则。例如ARM,就要求8字节对齐,因此在定义栈数组时需要使用 __xwcc__aligned(8) 来修饰,且大小是8的倍数。
  • 如果CPU内有L1Cache,应该使用 __xwcc_alignl1cache 来修饰栈数组,让其对其到L1Cache的缓存线上。

示例

#define THD_PRIORITY XWOS_SKD_PRIORITY_DROP(XWOS_SKD_PRIORITY_RT_MAX, 1)

struct xwos_thd static_thd;
xwos_thd_d static_thdd;
__xwcc_aligned(8) xwstk_t static_thd_stack[512];

xwer_t thd_func(void * arg)
{
        /* ...线程函数... */
}

void some_function(void)
{
        xwos_thd_attr_init(&attr);
        attr.name = "static.thd";
        attr.stack = static_thd_stack;
        attr.stack_size = sizeof(static_thd_stack);
        attr.priority = THD_PRIORITY;
        attr.detached = false;
        attr.privileged = true;
        rc = xwos_thd_init(&static_thd, &static_thdd, &attr, thd_func, NULL);
}

动态创建

  • 动态创建: xwos_thd_create()
  • 动态 是指程序在运行时,通过内存分配函数申请内存,并在申请的内存上构造对象。
  • 动态方式创建的线程,栈内存也可以动态申请,其地址对齐问题由操作系统内核处理。
  • 动态方式创建的线程,栈内存也支持使用静态方式定义的数组。栈数组的首地址与大小,必须要满足CPU的ABI规则。 例如ARM,就要求8字节对齐,因此在定义栈数组时需要使用 __xwcc__aligned(8) 来修饰,且大小是8的倍数。
  • 如果CPU内有L1Cache,应该使用 __xwcc_alignl1cache 来修饰栈静态方式定义的数组,让其对其到L1Cache的缓存线上。
#define THD_PRIORITY XWOS_SKD_PRIORITY_DROP(XWOS_SKD_PRIORITY_RT_MAX, 1)

xwos_thd_d dynamic_thdd;

xwer_t thd_func(void * arg)
{
        /* ...线程函数... */
}

void some_function(void)
{
        struct xwos_thd_attr attr;
        xwer_t rc;

        xwos_thd_attr_init(&attr);
        attr.name = "dynamic.thd";
        attr.stack = NULL;
        attr.stack_size = 2048;
        attr.priority = THD_PRIORITY;
        attr.detached = false;
        attr.privileged = true;
        rc = xwos_thd_create(&dynamic_thdd, &attr, thd_func, NULL);
}

中断线程的阻塞态与睡眠态

但线程调用了会 阻塞睡眠 的函数而进入 阻塞态睡眠态 时,它会让出CPU,调度器会重新调度。 其他线程或上下文可以通过 xwos_thd_intr() 中断其 阻塞态睡眠态阻塞睡眠 的函数会以错误码 -EINTR ( -4 ) 返回。

线程的退出与返回值

线程的退出

线程退出通常有两种方式:

  • 主函数直接 return
xwer_t thd_func(void * arg)
{
        /* ...省略... */
        return rc;
}

此CAPI的用法类似于POSIX中的函数 pthread_exit() ,调用的线程会立即终止并抛出返回值。

xwer_t thd_func(void * arg)
{
        /* ...省略... */
        xwos_cthd_exit(XWOK); /* 线程在此处结束,并抛出返回值 */
        /* 后面的代码不再执行 ... */
}

线程分离

线程退出的行为,与属性 xwos_thd_attr::detached 有关:

  • 分离态(detached) 的线程退出后,系统自动回收其资源;
  • 连接态(joinable) 线程需要由另一个线程调用 xwos_thd_join()xwos_thd_stop() 来回收其内存资源。 若忘记调用,资源不会自动被回收。

通知线程退出

xwos_thd_quit() 可用于通知线程退出。 调用此CAPI,可为线程设置 退出状态 ,并中断线程的 阻塞状态睡眠状态

被通知退出的线程 正在调用的 阻塞和睡眠 的CAPI将以返回值 -EINTR 返回。除非 被通知退出的线程不可被中断 的。

线程自己可以通过 xwos_cthd_shld_stop()xwos_cthd_frz_shld_stop() 检测 退出状态

等待线程退出

若线程是 连接态(joinable) 的,其他线程可通过 xwos_thd_join() 等待线程结束并获取其返回值。 此CAPI调用后,操作系统还会回收线程的资源。

终止线程

xwos_thd_stop() 可终止线程并等待它退出。 此CAPI等价于 xwos_thd_quit() + xwos_thd_join()

线程自身检测 退出状态

线程自己可以通过 xwos_cthd_shld_stop() 检测 退出状态 。 可以作为线程循环的结束条件:

xwer_t thd_func(void * arg)
{
        while (!xwos_cthd_shld_stop()) {
            /* ...thread loop... */ ;
        }
}

线程自身的睡眠

XWOS内核提供多种线程睡眠方式:

  • xwos_cthd_sleep() :睡眠的时间的起点由此CAPI自己获取,这种方式只需告诉CAPI需要睡眠多少事件,使用简单,但精度较低。
  • xwos_cthd_sleep_to() :指定未来的某个时间点被唤醒,精度较高。
  • xwos_cthd_sleep_from():睡眠时间的起点和持续时间由调用者提供,时间起点可以是 过去 的时间点。

如果线程只是想让调度器在同优先级的就绪队列中重新调度一下,可以通过调用 xwos_cthd_sleep()

线程的冻结与解冻

线程自身冻结

线程的冻结,是用来支持内核的一些特殊功能的,用户不能随意冻结线程。 在以下情况,XWOS内核要求线程进入冻结状态:

  • 系统准备进入低功耗模式。如果此时线程还在运行,很有可能因其正在访问硬件资源、 占用锁,导致系统关闭硬件、清理资源时发生异常。因此线程需要运行到一个特殊的点后冻结,这个点就是 冻结点 。 线程进入冻结点前,需要返回到最外层的主函数中,并释放掉所有的锁和硬件资源。
  • 线程迁移至另一个CPU。线程迁移时,也需要返回至最外层的冻结点,保证不能占用当前CPU的任何资源。

线程可以通过 xwos_cthd_shld_frz() 检测 可被冻结 状态。 一旦检测到 可被冻结 状态,就需要调用 xwos_cthd_freeze() 冻结自己。

示例:

xwer_t thd_func(void * arg)
{
        /* ...省略... */
        while (!xwos_cthd_shld_stop()) { /* 判断线程是否需要退出 */
                rc = do_sth(/* ... */); /* 线程在内部阻塞在某个同步对象或锁上 */
                if (-EINTR == rc) { /* 当线程需要冻结,阻塞/睡眠将被中断会以-EINTR返回 */
                        if (xwos_cthd_shld_frz()) { /* 判断是否需要冻结 */
                                release_resource(); /* 释放资源... */
                                xwos_cthd_freeze(); /* 冻结 */
                                /* 线程解冻后,从这里继续执行。*/
                                /* 如果线程发生了迁移,线程在另一个CPU上也是从此处开始运行。*/
                                acquire_resource(); /* 重新获取资源... */
                        } else {
                                /* 处理其他原因导致的中断... */
                        }
                }
        }
        /* ...省略... */
}

如果线程冻结之前不需要释放任何资源,可以使用 xwos_cthd_frz_shld_stop() 。 此CAPI等价于 xwos_cthd_shld_frz() + xwos_cthd_freeze() + xwos_cthd_shld_stop()

线程循环:

xwer_t thd_func(void * arg)
{
        bool wasfrz;
        /* ...省略... */
        while (!xwos_cthd_frz_shld_stop(&wasfrz)) { /* 通过wasfrz可以获知线程是否被冻结过 */
                /* ...线程循环... */;
        }
        /* ...省略... */
}

解冻

线程的解冻不由用户来操作,系统完成特殊功能后会自动对线程进行解冻:

  • 系统退出低功耗模式时
  • 线程迁移操作已经完成

线程的迁移

在多核系统中,XWOS的线程只会在某个CPU上被调度,XWOS内核并不会自动对线程做均衡处理,但支持将线程迁移到另一个CPU上。

迁移流程

  • 假定条件:线程正在CPU-A上,准备迁移到CPU-B上
  • 流程:
    • 用户在任意CPU的任意上下文调用CAPI: xwos_thd_migrate()
      • 系统向CPU-A发送调度器服务中断,提出 迁移出 的申请;
      • CPU-A切换至调度器服务中断,向线程设置冻结标志,并中断线程的阻塞态和睡眠态,然后退出中断上下文;
      • CPU-A中线程被重新调度,并运行到冻结点;
      • 线程在冻结点向CPU-A发送调度器服务中断,执行 冻结 操作;
      • 线程冻结后,CPU-A向CPU-B申请调度器服务中断,提出 迁移进 的申请;
      • CPU-B切换至调度器服务中断,把线程加入到自己的调度器中,解除线程的冻结状态,并加入就绪列表中;
      • 迁移完成,线程开始在CPU-B中调度。

线程的本地存储

C11 标准之后引入线程本地存储(TLS),XWOS支持关键字 _Thread_local ( C99 )、 thread_local ( C2X ) , 以及 gcc 以及 clang 编译器扩展的关键字 __thread

如果使用 C99 以前的标准,用户可以通过:

线程对象的生命周期管理

线程对象的基类是 XWOS对象 struct xwos_object 。 线程对象也有两组生命周期管理的CAPI:

  • 使用 对象指针 访问生命周期管理的CAPI:需要确保调用CAPI时,对象一定是有效的,且不存在 释放-又被申请 为另一个对象的情况。

    • xwos_thd_grab() :增加引用计数。
    • xwos_thd_put() :减少引用计数,当引用计数减少为 0 时,调用垃圾回收函数释放对象。
  • 使用 对象描述符 访问生命周期管理的CAPI:用户无法确保对象一定有效或无法确保对象不会变成另一个对象时使用。

    • xwos_thd_acquire() :通过对象描述符确定对象有效且合法,再增加引用计数。
    • xwos_thd_release() :通过对象描述符确定对象有效且合法,再减少引用计数。 当引用计数减少为 0 时,调用垃圾回收函数释放对象。

CAPI参考