操作系统开发:(7) 优先级反转与继承、TLS 及核亲和性

本节来到核心模块任务控制块的学习,在源码中,该模块依赖于大量配置项、宏定义和移植层,在这里我们先实现其核心功能,部分配置项如单核/多核、是否打开禁用抢占、优先级集成等等配置,我们默认开启并实现其功能,部分功能依赖移植层,这里不考虑,待之后再实现。

以下为相关基础知识,之后实现其源码:

1. 优先级反转与优先级继承

1.1 例:优先级反转(Priority Inversion)

  1. 小张 拿到了打印机(cpu),先去打印一份报告(低优先级)
  2. 这时 王总 突然要紧急打印合同(高优先级)→ 但他得等小张用完打印机
  3. 更糟的是,小李(中优先级)这时候跑过来处理邮件,小李优先级比小张高,占用了打印机
  4. 结果:王总在等小张,但小张又抢不过小李,一直没法用完打印机 → 王总被间接卡住!

这就是 优先级反转:高优先级任务被低优先级任务拖慢,还被中优先级任务插队。

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正在等,就立刻唤醒它

任务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。
相关推荐
橘色的喵1 小时前
C++17 vs C 编译产物体积:工业嵌入式场景的实测与分析
c语言·c++·c++17
皮卡丘不断更2 小时前
让数据“开口说话”!SwiftBoot AI 智能看板 v0.1.8 震撼来袭
人工智能·系统架构·ai编程
爱编码的小八嘎2 小时前
第1章 程序点滴-1.4 开放性思维(2)
c语言
嵌入式×边缘AI:打怪升级日志2 小时前
环境监测传感器从设备程序设计(ADC采集与输出控制)
单片机·嵌入式硬件·fpga开发
lpfasd1233 小时前
Zig 简介:C 的现代化继任者
c语言·开发语言
张槊哲3 小时前
IIC时序图详解
单片机
Hello_Embed3 小时前
Modbus 传感器开发:STM32F030 串口编程
笔记·stm32·单片机·嵌入式·freertos·modbus
流云鹤3 小时前
2026牛客寒假算法基础集训营1(B C E G K L)
c语言·算法
你怎么知道我是队长3 小时前
C语言---排序算法9---堆排序法
c语言·算法·排序算法