本节来到核心模块任务控制块的学习,在源码中,该模块依赖于大量配置项、宏定义和移植层,在这里我们先实现其核心功能,部分配置项如单核/多核、是否打开禁用抢占、优先级集成等等配置,我们默认开启并实现其功能,部分功能依赖移植层,这里不考虑,待之后再实现。
以下为相关基础知识,之后实现其源码:
1. 优先级反转与优先级继承
1.1 例:优先级反转(Priority Inversion)
- 小张 拿到了打印机(cpu),先去打印一份报告(低优先级)
- 这时 王总 突然要紧急打印合同(高优先级)→ 但他得等小张用完打印机
- 更糟的是,小李(中优先级)这时候跑过来处理邮件,小李优先级比小张高,占用了打印机
- 结果:王总在等小张,但小张又抢不过小李,一直没法用完打印机 → 王总被间接卡住!
这就是 优先级反转:高优先级任务被低优先级任务拖慢,还被中优先级任务插队。
1.2 解决方案:优先级继承(Priority Inheritance)
操作系统发现王总在等小张,那需要让小张快点用完打印机
于是它做了一件事:
临时把小张的优先级提升到和王总一样高
这样:
- 小李(中优先级)再也插不了队
- 小张立刻继续打印,快速释放打印机
- 王总马上拿到打印机,完成任务
- 小张用完后,优先级自动降回原来水平
即 TCB 中这两个成员的作用:
cpp
// 优先级继承
uint32_t u32BasePrio; // 基础优先级
uint32_t u32MtxHeld; // 持有的互斥量数量
- u32BasePrio:记住本来的优先级是多少→ 即使临时升到 CEO 级别,也知道事情办完要降回去
- u32MtxHeld:记录现在拿着几个互斥量→ 只有当所有互斥量都还回去(u32MtxHeld== 0),才能把优先级降回 u32BasePrio。(比如小张同时拿了打印机+扫描仪,必须两个都还了才算完)
2. 任务通知(Task Notifications)
一种超轻量级通信机制,比信号量、队列更快。
cpp
// 任务通知
volatile uint32_t notifiedVal[CFG_TSK_NOTICE_SIZE]; // 任务通知值
volatile uint8_t notifyState[CFG_TSK_NOTICE_SIZE]; // 任务通知状态
#define tskNOT_WAITING_NOTIFICATION 0
#define tskWAITING_NOTIFICATION 1
#define tskNOTIFICATION_RECEIVED 2
**例:**温度监控
- 任务A(传感器任务):读到温度 = 35℃
- 任务B(报警任务):需要知道温度是否过高
**传统做法:**建一个队列,任务A往里写,任务B从里读 → 要分配内存,有开销。
用任务通知:
- 任务B说:"我等着你通知我" → 进入等待状态(notifyState[0] = tskWAITING_NOTIFICATION)
- 任务A直接调用相关函数
;- 把数字 35 写进任务B的 notifiedVal
[0] - 把任务B的状态改成 tskNOTIFICATION_RECEIVED
- 如果任务B正在等,就立刻唤醒它
- 把数字 35 写进任务B的 notifiedVal
任务B醒来后,读 notifiedVal[0] 就知道温度是 35℃。
3. 线程本地存储(Thread Local Storage, TLS)
3.1 用户级任务私有上下文管理
cpp
void* TLSPointers[ CFG_TLS_NUM ];
定义: 每个任务(线程) 拥有自己独立的私有数据指针数组,其他任务无法访问。
目的: 让不同任务在共享同一个函数或库时,能各自保存自己的上下文或状态,而不会互相干扰。
如 C++ 中的 thread_local 或 POSIX 的 pthread_key_t 。
例:
一个日志函数 log_write() 需要知道当前任务的日志级别。
如果用全局变量,所有任务会共用一个级别 → 冲突。
如果用 TLS,每个任务可设置自己的日志级别 → 安全。
3.2 C 库存储任务的 TLS 数据
cpp
uint8_t TLSBlock[64];
TLSBlock 是为每个任务预留的一块私有内存,专门供 C 标准库(如 newlib、glibc)或编译器在多任务环境下安全使用 errno、strtok、浮点上下文等需要每个任务独立副本的内部变量。
用户不需要也应该自己调用或使用。
例如:
- 如果任务 A 调用 fopen 失败,errno 被设为 2(No such file)。
- 此时任务切换到 B,B 也调用 fopen 失败,errno 被覆盖为 13(Permission denied)。
- 当切回任务 A 打印 errno 时,它看到的是 13 ,而不是自己应该看到的 2
这就是 多任务共享全局变量导致的数据污染。
TLSBlock[64]是 一块每个任务独有的内存,C 库把 errno、strtok 内部指针等都存进去,相关代码需要在移植层实现。
例:C 库如何找到这块内存?
- 当用 GCC + newlib(常见于 ARM Cortex-M),C 库会调用一个函数(如 __aeabi_read_tp())来获取"当前线程的 TLS 基地址"。
- 移植层(port layer)会重定向这个函数,让它返回 &curTCB->TLSBlock。
- 于是 C 库就知道当前任务的 errno 就在 TLSBlock + offset_of_errno 这个位置。
4. 核亲和性(Core Affinity)掩码
cpp
#if ( CFG_NUM_CORES > 1 && CFG_CORE_AFFINITY == 1 )
uint32_t coreAffinityMask;
#endif
例如:
- 4 核系统(Core 0, 1, 2, 3)
- 某个任务设置 coreAffinityMask = 0b00000101(即 bit0 和 bit2 为 1)
- 那么该任务只能在 Core 0 或 Core 2 上执行,不会被调度到 Core 1 或 3。