1. 线程概述
线程是操作系统能够进行运算调度的最小单位. 它被包含在进程之中, 是进程的实际运作单位. 一条线程指的是进程中的一个单一顺序的控制流, 一个进程可以并发多个线程, 每条线程并行执行不同的任务. 在多核或多 CPU, 或支持 Hyper-threading 的 CPU 上使用多线程程序设计的好处是显而易见的, 即提高了程序的执行吞吐率. 在单个 CPU 单核的计算机上,使用多线程技术, 也可以吧进程中负责 I/O 处理, 人机交互而常被阻塞的部分与密集计算的部分分开来执行, 编写专门的 workhorse 线程执行密集计算, 从而提高程序的执行效率.
分以下几部分概述创建, 调度和删除独立可执行线程的内核服务
- 生命周期
- 调度机制
- 自定义数据
- 系统线程
- 工作队列线程
- 配置选项
- API 参考
2. 生命周期(Lifecycle)
线程是用于应用程序处理的内核对象, 它太长或太复杂, ISR 无法执行.
2.1. 概念(Concepts)
一个应用程序可以创建任意多个线程. 每个线程由一个线程 id 引用, 该 id 在线程创建时分配.
线程有以下几个关键属性:
- 栈空间 : 线程栈所需的一段内存空间. 堆栈的大小可根据线程处理实际需要进行调整. 存在用于创建和处理内存堆栈区域的特殊宏.
- 线程控制块 : 用于线程元数据的私有内核簿记(bookkeeping). 是结构体 struct k_thread 的一个实例.
- 入口函数 : 线程启动时调用的函数. 该函数最多可接受 3 个参数值.
- 调度优先级 : 它指示内核调度程序如何分配 CPU 时间给线程.
- 线程可选项 : 允许线程在特定的环境下接受内核的特殊处理.
- 启动延时 : 指定内核在启动线程支付那个应该等待多长时间.
- 执行模式 : 可以是 管理模式 或者 用户模式. 默认情况下, 线程运行在管理模式下, 该模式下的线程可以访问特权 CPU 指令, 整个内存地址空间和外设. 用户模式下线程可访问特权中的一部分. 取决于配置选项 CONFIG_USERSPACE.
2.2. 创建线程(Thread Creation)
线程必须创建之后才能使用. 内核初始化线程控制块和堆栈部分的一端. 线程堆栈的剩余部分通常未初始化.
启动延时设置为 K_NO_WAIT 时表明内核将立即启动线程执行. 否则, 将设置一个超时时间以用于内核在超时时间到期时启动执行线程. 例如, 允许线程使用的硬件可用时启动线程.
内核允许在线程开始执行前取消延时启动. 如果线程已经启动了, 则取消请求是无效的. 已经成功取消延时启动的线程必须重新创建才能使用.
2.3. 终止线程(Thread Termination)
线程一旦启动, 将永远执行. 但是, 线程可以通过其入口函数返回来同步结束其执行. 称之为线程终止.
终止的线程负责在返回之前使用它拥有的任何共享资源(如互斥锁和动态分配的内存), 因为内核不会自动回收它们.
Note : 内核目前没有对应用程序重新创建终止线程的能力做出任何声明.
2.4. 中止线程(Thread Aborting)
线程可以通过执行 aborting 异步结束. 如果线程触发致命错误错误(如 : 引用空指针), 内核将自动中止该线程.
线程也可以被其它线程(或它自己)调用 k_thread_abort() 中止. 然而, 通常采用发信号给线程, 让线程自己结束执行.
线程终止时, 内核不会自动回收该线程锁拥有的共享资源.
Note : 内核目前没有对应用程序重新创建中止线程的能力做出任何声明.
2.5. 挂起线程(Thread Suspension)
如果线程被挂起, 它将在一段不确定的时间内暂停执行. 函数 k_thread_suspend() 用于挂起包括调用线程在内的任何线程, 对已经处于挂起的线程再次挂起时不会产生任何效果.
线程一旦挂起, 则不会被调度, 除非另一个线程调用函数 k_thread_resume() 取消挂起.
Note : 线程可以使用函数 k_sleep() 阻止其执行. 然而, 这不同于挂起线程, 因为睡眠时间到了之后线程自动变为可执行.
2.5. 线程的选项(Thread Options)
内核支持一小系列线程选项, 以允许线程在特殊情况下被特殊对待. 这些与线程相关联的选项在线程创建时就被指定了.
不需要任何线程选项的线程的线程可选项的值为 0. 如果线程需要可选项, 可通过名字指定, 使用 '|' 支持多个线程可选项.
支持以下线程可选项 :
- K_ESSENTIAL : 将线程标记为必须线程(essential thread). 如果该线程终止或中止, 则内核认为发生致命系统错误. 默认情况下, 线程不会被标记为必须线程.
- K_FP_REGS 和 K_SSE_REGS : 这两个是 X86 相关的选项, 标记线程使用 CPUs 浮点寄存器和 SSE 寄存器. 在调度这样的线程时, 内核执行额外的步骤保存和恢复这些寄存器的内容. 默认情况下,调度线程时, 内核不会保存和恢复这些寄存器的值.
- K_USER : 如果 CONFIG_USERSPACE 使能, 线程在用户模式下创建, 该标记的线程可访问特权中的一部分. 参见 User Mode. 否则, 该 Flag 什么也不做.
- K_INHERIT_PERMS : 如果 CONFIG_USERSPACE 使能, 这个线程将继承所有父线程所拥有的所有内核对象权限, 除父线程对象外.
2.6. 实现(Implementation)
2.6.1. 创建线程
线程是通过定义它自己的堆栈空间和线程控制块, 然后再调用函数 k_thread_create() 创建的. 栈空间必须使用 K_THREAD_STACK_DEFINE 定义, 以确保在内存中正确的设置.
线程的创建函数返回线程 id, 该 id 用来引用线程.
以下代码创建了立刻启动的线程.
#define MY_STACK_SIZE 500
#define MY_PRIORITY 5
extern void my_entry_point(void *, void *, void *);
K_THREAD_STACK_DEFINE(my_stack_area, MY_STACK_SIZE);
struct k_thread my_thread_data;
k_tid_t my_tid = k_thread_create(&my_thread_data, my_stack_area,
K_THREAD_STACK_SIZEOF(my_stack_area),
my_entry_point,
NULL, NULL, NULL,
MY_PRIORITY, 0, K_NO_WAIT );
除了以上创建方式, 也可以调用 K_THREAD_DEFINE 在编译时创建一个线程. 该宏自动定义堆栈空间, 控制块和线程 id 变量. 示例如下, 跟上述创建效果一样.
#define MY_STACK_SIZE 500
#define MY_PRIORITY 5
extern void my_entry_point(void *, void *, void *);
K_THREAD_DEFINE(my_tid, MY_STACK_SIZE,
my_entry_point, NULL, NULL, NULL,
MY_PRIORITY, 0, K_NO_WAIT);
2.6.1.1. 用户模式的约束(User Mode Constraints)
仅在 CONFIG_USERSPACE 使能时, 并且一个用户线程创建一个新的线程时, 该节才适用. API k_thread_create() 仍然可以使用, 但是有以下限制必须满足, 否则调用线程将被终止.
- 调用线程必须对子线程和堆栈参数都具有授予权限(permissions granted); 两者都由内核作为内核对象跟踪.
- 子线程和堆栈对象必须处于未初始化状态, 如: 它当前没有运行, 堆栈内存未使用.
- 堆栈大小参数必须等于或小于声明时的堆栈对象的边界.
- 线程可选项必须选择 K_USER, 用户线程仅能创建其它的用户线程.
- 线程可选项必须不能选择 K_ESSENTIAL, 用户线程不能作为必须线程.
- 子线程的优先级必须是一个有效的优先级, 等于或低于父优先级.
2.6.1.2. 删除权限(Dropping Permissions)
如果 CONFIG_USERSPACE 使能时, 运行在 管理模式 的线程可以使用 k_thread_user_mode_enter() API 进入用户模式. 这是一个单向的操作, 并且会复位和清零线程栈空间. 这个线程将被标记为非必要的(non-essential).
2.6.2. 终止线程
线程可以通过入口函数返回来终止自己. 示例代码如下:
void my_entry_point(int unused1, int unused2,