实现 RTOS 操作系统 【二】临界区保护、调度锁、优先级

一、临界区保护

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=00
T4/任务2加1(R0=1),写回 counter=11
T5/任务1恢复执行,把自己的结果(1)写回 counter1

结果:两次加法,最终结果是 1,而不是期望的 2,这就是操作系统的竞态条件。

所以,在执行这种非原子性的任务,需要临界区保护防止意外。

1.3 实现临界区保护

RTOS 内实现的方式很简单,通过禁用中断、使用互斥锁或信号量等方法即可。在这里我们将实现一下禁用中断的临界区保护。

Cortex‐M3 权威指南 41 页

我们只需要通过置位和复位 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); 
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值