序言

我的上一篇文章介绍了信号量的基础知识并利用这些基础知识进行了一个小实验以此来增进对信号量这个概念的感性认识。下面我们要介绍一种特殊得信号量:互斥信号量

在介绍互斥信号量之前,我们先来谈谈一种现象:优先级反转。优先级反转在可剥夺型内核中是比较常见的,在实时系统中不允许出现这种现象,因为这样会破坏任务的预期顺序,可能会导致严重的后果。

我们会利用实验来具体展示一下什么是优先级反转。

在介绍完优先级反转现象之后,我们会继续介绍这种现象在UCOSIII中的解决工具:互斥信号量,以及其具体的作用原理

接着又会有一个小实验来展示UCOSIII中的互斥信号量是如何解决优先级反转的问题的。

最后有一个小插曲,那就是UCOSIII中的内嵌信号量,我们对其进行简要地介绍。

优先级反转

什么是优先级反转,我们用一张图片来解释

先来解释一下这张图片的含义,有三个任务:任务H、任务M、任务L。优先级依此降低,下面为了简便我就叫它们H、M、L。下面是它们的执行过程

  1. H和M处于挂起状态,等待某个事件的发生,L正在运行
  2. 某一时刻L想要访问共享资源,在这之前它必须先获得对应该资源的信号量。
  3. L获得信号量并开始使用该共享资源
  4. 由于H优先级高,它等待的事件发生后便剥夺了L的CPU使用权
  5. H开始运行
  6. 但是H在运行过程中也要使用被L占用的资源,由于该资源目前还被L占用着,H只能被挂起等待L释放该信号量
  7. L继续运行,由于M优先级高于L,当M等待事件发生后,M剥夺了CPU的使用权。
  8. M开始处理自己的事情
  9. M执行完毕后,将CPU使用权归还给L。
  10. L继续执行
  11. 最终L完成所有任务并释放信号量,到此为止,由于UCOSIII直到有个高优先级的任务在等待这个信号量,故内核做任务切换。
  12. H得到该信号量并接着运行

如果你仔细对照着图理解了这个过程,那么一定会发现一个有趣的现象,H的优先级虽然高,但是它却因为无法获得信号量而不能及时地处理自己的事务,而是把大量的时间花在了等待一个低优先级的任务L上,更为糟糕的是,这个低优先级任务L又花了更多的时间等待另一个更高优先级的任务M上,最终的结果是一个最高优先级的任务不得不等两个低优先级的任务结束之后才能顺利处理自己的事情。

上面的情况就是所谓的优先级反转

优先级反转实验

为了验证我们对于优先级反转现象的构想,下面我们要进行一个小实验来确认这种现象的存在。

我们创建了4个任务,任务A用于创建B、C、D这三个任务,任务A还创建了一个初始值为1的信号量,任务B和任务D都请求信号量TEST_SEM,其中任务优先级从高到低分别为B、C、D。

先定义一个信号量

OS_SEM  TEST_SEM;       //定义一个信号量,用于任务同步

创建信号量

    //创建一个信号量
    OSSemCreate ((OS_SEM*   )&TEST_SEM,
                 (CPU_CHAR* )"SYNC_SEM",
                 (OS_SEM_CTR)1,
                 (OS_ERR*   )&err);

定义任务函数

//高优先级任务
void high_task(void *p_parg)
{
    u8 num;
    OS_ERR err;

    CPU_SR_ALLOC();
    POINT_COLOR = BLACK;
    OS_CRITICAL_ENTER();
    LCD_DrawRectangle(5,110,115,314);   //画一个矩形
    LCD_DrawLine(5,130,115,130);        //画线
    POINT_COLOR = BLUE;
    LCD_ShowString(6,111,110,16,16,"High Task");
    OS_CRITICAL_EXIT();
    while(1)
    {
        OSTimeDlyHMSM(0, 0, 0, 500, OS_OPT_TIME_PERIODIC, &err);
        num++;
        printf("high task Pend Sem\r\n");
        OSSemPend(&TEST_SEM, 0, OS_OPT_PEND_BLOCKING, 0, &err);
        printf("high task Running!\r\n");
        LCD_Fill(6, 131, 114, 313, lcd_discolor[num % 14]);
        LED1 = ~LED1;
        OSSemPost(&TEST_SEM, OS_OPT_POST_1, &err);
    }

}

//任务2的任务函数
void middle_task(void *p_arg)
{
    u8 num;
    OS_ERR err;
    CPU_SR_ALLOC();

    POINT_COLOR = BLACK;
    OS_CRITICAL_ENTER();
    LCD_DrawRectangle(125,110,234,314); //画一个矩形
    LCD_DrawLine(125,130,234,130);      //画线
    POINT_COLOR = BLUE;
    LCD_ShowString(126,111,110,16,16,"Middle Task");
    OS_CRITICAL_EXIT();
    while(1)
    {
        num++;
        printf("middle task Running!\r\n");
        LCD_Fill(126, 131, 223, 313, lcd_discolor[13 - num % 14]);
        LED0 = ~LED0;
        OSTimeDlyHMSM(0, 0, 1, 0, OS_OPT_TIME_PERIODIC, &err);
    }
}

void low_task(void *p_arg)
{
    static u32 times;
    OS_ERR err;
    while(1)
    {
        OSSemPend(&TEST_SEM,0,OS_OPT_PEND_BLOCKING,0,&err); //请求信号量
        printf("low task Running!\r\n");
        for(times=0;times<10000000;times++)
        {
            OSSched();                                      //发起任务调度
        }
        OSSemPost(&TEST_SEM,OS_OPT_POST_1,&err);
        OSTimeDlyHMSM(0,0,1,0,OS_OPT_TIME_PERIODIC,&err);   //延时1s
    }
}

实验运行过程分析

  1. high_task一开始就引发了任务调度,所以middle_task和low_task会被执行
  2. middle_task和low_task执行完成之后high_task继续执行,请求信号量,但是信号量已经被low_task请求没了,所以high_task被挂起。
  3. 在low_task释放信号量之前,下面就是middle_task和low_task的天下
  4. low_task循环进行任务调度,middle不断被执行。
  5. 在某一时刻low_task任务运行结束,释放了信号量,high_task终于得到了信号量,high_task任务运行
  6. high_task任务运行结束后,又开始了下一轮循环

实验结果如下图所示

可以看到,结果和我们的分析完全吻合

互斥信号量

为了解决优先级反转的问题,UCOSIII支持一种特殊的二进制信号量:互斥信号量,用它可以解决优先级反转的问题,下面这张图就是其作用原理

任务L一开始执行,请求互斥信号量,由于任务调度,任务H执行,请求互斥信号量,但发现互斥信号量被L请求了,所以UCOSIII就把任务L的优先级提到和任务H一样高,这样就会优先执行完任务L,接着执行任务H,最后才执行任务M。这里我们发现一件事情,虽然我们把任务M排到了最后,但是还是要先把任务L执行完,才能去执行任务H,所以这里有一个要求就是任务L要尽可能快速地执行完

下面是与互斥信号量相关的一些函数

OSMutexCreate()创建一个互斥信号量
OSMutexDel()删除一个互斥型信号量
OSMutexPend()等待一个互斥型信号量
OSMutexPendAbort取消等待
OSMutexPost()释放一个互斥型信号量

具体的含义和普通信号量差不多,这里就不再一一解释了

互斥信号量实验

我们在上一个实验的基础上将普通信号量换成互斥信号量,相关函数也都要换掉,这样就可以了,下面是实验结果

我们可以看到实验结果和我们的预期一样,但是你在实验的过程中会发现一种现象,那就是本来是显示"middle task Running!"的那段时间现在是一直在等待,这段时间其实是low_task一直在循环。所以我们要求low_task要尽可能地快速执行完。

任务内嵌信号量

之前我们要使用信号量都要在外面定义一个然后再创建它,接着才能使用。再UCOSIII中又一种特殊的信号量,叫做任务内嵌信号量。每个任务都自带一个信号量,任务自己可以请求这个信号量,让这个信号量的值减小,同时其它的任务也可以向这个任务的信号量Post,让这个信号量的值增加,这种功能不仅能够简化代码,而且比使用独立的信号量更加有效,任务信号量是内嵌再UCOSIII中的,相关代码再os_task.c中,任务内嵌信号量的相关函数如下

OSTaskSemPend()等待任务信号量
OSTaskSemPendAbort取消等待任务信号量
OSTaskSemPost()发布任务信号量
OSTaskSemSet()强行设置任务信号量计数

具体的每个函数的意思都差不多,这里就不介绍了

任务内嵌信号量实验

创建3个任务,任务start_task用于创建其它两个任务,任务task1_task主要用于扫描按键,当检测到KWY_UP按下以后就向任务task2_task发送一个任务信号量。任务task2_task请求任务信号量,当请求到任务信号量的时候更新一次屏幕指定区域的背景颜色。

这里我们不需要再创建信号量,所以只有两个任务的代码,如下所示

//任务1的任务函数
void task1_task(void *p_arg)
{
    u8 key;
    u8 num;
    OS_ERR err;
    while(1)
    {
        key = KEY_Scan(0);  //扫描按键
        if(key==WKUP_PRES)
        {
            OSTaskSemPost(&Task2_TaskTCB,OS_OPT_POST_NONE,&err);    //使用系统内建信号量向任务task2发送信号量
            LCD_ShowxNum(150,111,Task2_TaskTCB.SemCtr,3,16,0);      //显示信号量值
        }
        num++;
        if(num==50)
        {
            num=0;
            LED0=~LED0;
        }
        OSTimeDlyHMSM(0,0,0,10,OS_OPT_TIME_PERIODIC,&err);          //延时10ms
    }
}

//任务2的任务函数
void task2_task(void *p_arg)
{
    u8 num;
    OS_ERR err;
    while(1)
    {
        OSTaskSemPend(0,OS_OPT_PEND_BLOCKING,0,&err);       //请求任务内建的信号量
        num++;
        LCD_ShowxNum(150,111,Task2_TaskTCB.SemCtr,3,16,0);  //显示任务内建信号量值
        LCD_Fill(6,131,233,313,lcd_discolor[num%14]);       //刷屏
        LED1 = ~LED1;
        OSTimeDlyHMSM(0,0,1,0,OS_OPT_TIME_PERIODIC,&err);   //延时1s
    }
}

具体的程序运行结果,自己实验看看吧。

02-09 23:49