一、临界区保护
1.1 临界区保护是什么
临界区保护是一个重要的概念,用于保护共享资源免受并发访问导致的数据损坏和不一致。临界区是指一段代码,这段代码访问共享资源(如全局变量、硬件设备等),并且在任何时候只能由一个线程执行,以确保操作的原子性和数据的一致性。
1.2 不使用临界区保护的问题
有些情况下一些特殊操作(比如 XIP 下 Flash 擦写、低功耗模式切换)不能被随意打断,或者一些共享数据区不能被无序访问(A 任务正在读,B 任务却要写),这时候就要用到临界区保护策略了。
比如,两个任务同时操作一个全局变量:
int counter = 0;
void task1(void) {
counter++; // 对共享变量操作
}
void task2(void) {
counter++;
}
对于汇编来说,counter++ 不是原子性操作。
LOAD R0, counter ; 把 counter 的值读入寄存器
ADD R0, #1 ; 加 1
STORE counter, R0 ; 把结果写回内存
假设我们的程序在执行 ADD R0 #1 时被切换任务会导致:
| 时间 | 任务1操作 | 任务2操作 | counter值 |
| T0 | 任务1读取 counter=0 | / | 0 |
| T1 | 任务1加1(R0=1) | / | 0 |
| T2 | 任务1被切换出去了! | / | 0 |
| T3 | / | 任务2读取 counter=0 | 0 |
| T4 | / | 任务2加1(R0=1),写回 counter=1 | 1 |
| T5 | / | 任务1恢复执行,把自己的结果(1)写回 counter | 1 |
结果:两次加法,最终结果是 1,而不是期望的 2,这就是操作系统的竞态条件。
所以,在执行这种非原子性的任务,需要临界区保护防止意外。
1.3 实现临界区保护
RTOS 内实现的方式很简单,通过禁用中断、使用互斥锁或信号量等方法即可。在这里我们将实现一下禁用中断的临界区保护。
我们只需要通过置位和复位 PRIMASK 寄存器即可实现临界段代码保护,代码如下:
//进入之前的临界区状态值
uint32_t tTaskEnterCritical (void)
{
__disable_irq();
}
//退出临界区,恢复之前的临界区状态
void tTaskExitCritical (void) {
__set_PRIMASK(0);
}
可是这样并不能嵌套保护,比如我们连续保护两次,这样在中间那层就开启了保护。
//进入之前的临界区
tTaskEnterCritical();
tTaskEnterCritical();
//此时我们退出临界区成功了 但是我们之前进入了两次 这里不应该成功退出
tTaskExitCritical();

我们必须避免这个问题,解决办法如下:
//进入之前的临界区状态值
uint32_t tTaskEnterCritical (void)
{
uint32_t primask = __get_PRIMASK();
__disable_irq(); // CPSID I
return primask;
}
//退出临界区,恢复之前的临界区状态
void tTaskExitCritical (uint32_t status) {
__set_PRIMASK(status);
}
至此,用我们保护时先获得当前的状态,退出保护时恢复原先的状态就可以了。
二、调度锁保护
2.1 调度锁是什么
在 RTOS 中,调度器(scheduler)是负责决定谁能运行的那部分内核逻辑。调度锁是用来暂时禁止任务切换的机制。也就是说,锁定调度器后,即使有更高优先级的
任务变成就绪状态,系统也不会立刻切换过去,而是等解锁之后再切换。
主要是为了保护关键操作 (防止任务的链表被打断) 比如:
void UpdateTaskList(void)
{
RemoveTask(oldTask);
InsertTask(newTask);
}
假设我们正好刚改完任务的 prev->next 链表操作,但是没有更改 next->prev 的链表操作时,任务就被调度了。此时我们遍历这个链表就会遇到野指针、无法找到节点等情况。
void UpdateTaskList(void)
{
vTaskSuspendAll(); // 锁调度器,防止切换
RemoveTask(oldTask);
InsertTask(newTask);
xTaskResumeAll(); // 解锁调度器
}
这样的话就确保我们任务的链表修改修改完毕后,再进行调度。
2.2 调度锁的实现
调度锁的功能是让 RTOS 停止调度,我们只需要一个变量即可实现。
// 调度锁计数器
uint8_t schedLockCount;
下面的 255 意味着我们的调度锁最多嵌套 255 层。
//禁止任务调度函数
void tTaskSchedDisable (void)
{
uint32_t status = tTaskEnterCritical();
if (schedLockCount < 255)
{
schedLockCount++;
}
tTaskExitCritical(status);
}
///使能任务调度函数
void tTaskSchedEnable (void)
{
uint32_t status = tTaskEnterCritical();
if (schedLockCount > 0)
{
if (--schedLockCount == 0)
{
tTaskSched();
}
}
tTaskExitCritical(status);
}
三、任务优先级
3.1 任务优先级是什么
是决定多个任务谁先运行、谁后运行的一个核心机制。每个任务 (Task) 在创建时都会被赋予一个 优先级 (Priority)。
RTOS 调度器根据这个优先级来决定:优先级高的任务先执行。优先级低的任务后执行。
假设 RTOS 有三个任务 TankA (优先级高)、TankB (优先级高)、TankC (优先级中)。时间片设置为 10ms,则 CPU 执行情况如下:
时间线 →
|<--10ms--> |<--10ms--> |<--10ms--> | ...
TaskA TaskB TaskA
也就是当 TankA 和 TankB 都处于就绪态的时候,任务调度会在他们两个之前切换,并不会切换到 TankC 中。
当 TankA 和 TankB 都处于 RTOS 延时,此时 TaskC 才开始调度:
时间线 →
|<--10ms--> |<--10ms--> |<--10ms--> | ...
TaskC TaskC TaskC
这便是 RTOS 的多任务优先级的逻辑。
3.2 图数据结构
3.2.1 为什么要使用图
bitmap = bit + map 直译就是位的映射表
也就是说,bitmap 是一种用位 (bit) 来表示多个状态或对象对应关系的数据结构。
比如有 32 个任务,每个任务有是否就绪两种状态,那就可以用 32 个 bit 来映射它们的状态。
| 任务号 | 状态 | bitmap 位 |
| 0 | 就绪 | bit0=1 |
| 1 | 未就绪 | bit1=0 |
| 2 | 就绪 | bit2=1 |
| ... | ... | ... |
使用图数据结构可以帮我们快速找到优先级最高的任务。比如我们规定优先级数字越小优先级越高,此时只要找到 1 出现最低位就可以了。
比如 0011 0010 我们发现,优先级1、4、5 的任务有处于就绪状态的,我们就可以就可以调度这些任务。我们发现优先级最大的任务是优先级 1,此时就可以调度所有优先级为 1 的任务。
3.2.2 图数据结构定义
其中位图是数据结构本质是一个 uint32_t 的变量。任务优先级中使用图数据结构的目的是返回最小的位值。
// 位图类型
typedef struct
{
uint32_t bitmap;
}tBitmap;
3.2.2 图的操作函数
#include "tLib.h"
/**********************************************************************************************************
** Function name : tBitmapInit
** Descriptions : 初始化bitmap将所有的位全清0
** parameters : 无
** Returned value : 无
***********************************************************************************************************/
void tBitmapInit (tBitmap * bitmap)
{
bitmap->bitmap = 0;
}
/**********************************************************************************************************
** Function name : tBitmapPosCount
** Descriptions : 返回最大支持的位置数量
** parameters : 无
** Returned value : 最大支持的位置数量
***********************************************************************************************************/
uint32_t tBitmapPosCount (void)
{
return 32;
}
/**********************************************************************************************************
** Function name : tBitmapSet
** Descriptions : 设置bitmap中的某个位
** parameters : pos 需要设置的位
** Returned value : 无
***********************************************************************************************************/
void tBitmapSet (tBitmap * bitmap, uint32_t pos)
{
bitmap->bitmap |= 1 << pos;
}
/**********************************************************************************************************
** Function name : tBitmapClear
** Descriptions : 清除bitmap中的某个位
** parameters : pos 需要清除的位
** Returned value : 无
***********************************************************************************************************/
void tBitmapClear (tBitmap * bitmap, uint32_t pos)
{
bitmap->bitmap &= ~(1 << pos);
}
/**********************************************************************************************************
** Function name : tBitmapGetFirstSet
** Descriptions : 从位图中第0位开始查找,找到第1个被设置的位置序号
** parameters : 无
** Returned value : 第1个被设置的位序号
***********************************************************************************************************/
uint32_t tBitmapGetFirstSet (tBitmap * bitmap)
{
static const uint8_t quickFindTable[] =
{
/* 00 */ 0xff, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 10 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 20 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 30 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 40 */ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 50 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 60 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 70 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 80 */ 7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 90 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* A0 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* B0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* C0 */ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* D0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* E0 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* F0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0
};
if (bitmap->bitmap & 0xff)
{
return quickFindTable[bitmap->bitmap & 0xff];
}
else if (bitmap->bitmap & 0xff00)
{
return quickFindTable[(bitmap->bitmap >> 8) & 0xff] + 8;
}
else if (bitmap->bitmap & 0xff0000)
{
return quickFindTable[(bitmap->bitmap >> 16) & 0xff] + 16;
}
else if (bitmap->bitmap & 0xFF000000)
{
return quickFindTable[(bitmap->bitmap >> 24) & 0xFF] + 24;
}
else
{
return tBitmapPosCount();
}
}
其中位图是数据结构本质是一个 uint32_t 的变量。任务优先级中使用任务图数据结构的目的是返回最小的位值。
其中:
uint32_t tBitmapGetFirstSet (tBitmap * bitmap);
是最核心的函数,它可以返回我们所有任务中处于就绪态,并且优先级的任务。
比如对于二进制 0011 1010 来说,返回 1,因为最小被置位的 bit 在从右往左第二个。
此时我们就可以调度所有任务优先级为 1 的函数。
3.3 通过图实现优先级
// 任务优先级的标记位置结构
tBitmap taskPrioBitmap;
// 所有任务的指针数组:简单起见,只使用两个任务
tTask * taskTable[TINYOS_PRO_COUNT];
之后我们在 tTaskInit 初始化任务时,将图置位。
void tTaskInit (tTask * task, void (*entry)(void *), void *param, uint32_t prio, uint32_t * stack)
{
//....之前的代码没有变,忽略
taskTable[prio] = task; // 填入任务优先级表
tBitmapSet(&taskPrioBitmap, prio); // 标记优先级位置中的相应位
}
tTaskHighestReady 函数会返回当前优先级最低的任务。
tTask * tTaskHighestReady (void)
{
uint32_t highestPrio = tBitmapGetFirstSet(&taskPrioBitmap);
return taskTable[highestPrio];
}
在调度函数 tTaskSched 中,我们始终将 nextTask 指向当前优先级最低的函数。
void tTaskSched (void)
{
tTask * tempTask;
// 进入临界区,以保护在整个任务调度与切换期间,不会因为发生中断导致currentTask和nextTask可能更改
uint32_t status = tTaskEnterCritical();
// 如何调度器已经被上锁,则不进行调度,直接退bm
if (schedLockCount > 0)
{
tTaskExitCritical(status);
return;
}
// 找到优先级最高的任务,如果其优先级比当前任务的还高,那么就切换到这个任务
tempTask = tTaskHighestReady();
if (tempTask != currentTask)
{
nextTask = tempTask;
tTaskSwitch();
}
// 退出临界区
tTaskExitCritical(status);
}
1725

被折叠的 条评论
为什么被折叠?



