【RT-Thread笔记】临界区问题及IPC机制

什么是临界区?

在多线程实时系统中,多个线程操作/访问同一块区域(代码),这块代码就称为临界区。 例如一项工作中的两个线程:一个线程从传感器中接收数据并且将数据写到共享内存中,同时另一个线程周期性的从共享内存中读取数据并发送去显示,下图描述了两个线程间的数据传递:

如果对共享内存的访问不是排他性的,那么各个线程间可能同时访问它,这将引起数据一致性的问题。例如,在显示线程试图显示数据之前,接收线程还未完成数据的写入,那么显示将包含不同时间采样的数据,造成显示数据的错乱。

临界区的问题

在 RT-Thread 里面,这个临界段最常出现的就是对全局变量的操作。下面我们以一个全局变量为例演示多线程系统中的临界区问题:

#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>

#define  TEST1_STACK_SIZE         512
#define  TEST1_THREAD_PRIORITY     25
#define  TEST1_TIMESLICE           2

#define  TEST2_STACK_SIZE         512
#define  TEST2_THREAD_PRIORITY     24
#define  TEST2_TIMESLICE           1

uint32_t gulTmp = 0;

/* test1线程入口函数 -------------------------------------------------------------------------*/
static void test1_thread_entry(void *parameter)
{
    uint16_t i = 0;

    rt_kprintf("gulTmp = %d\n", gulTmp);
    for (i = 0; i < 10000; i++)
    {
        gulTmp++;
    }
    rt_kprintf("gulTmp = %d\n", gulTmp);
}

/* test2线程入口函数 -------------------------------------------------------------------------*/
static void test2_thread_entry(void *parameter)
{
    rt_thread_delay(1);
    gulTmp++;
}

/* 主函数 ----------------------------------------------------------------------------------*/
int main(void)
{
    /* 定义线程句柄 */
    rt_thread_t tid;

    /* 创建动态test1线程 :优先级 25 ,时间片2个系统滴答,线程栈512字节 */
    tid = rt_thread_create("test1_thread",
                  test1_thread_entry,
                  RT_NULL,
                  TEST1_STACK_SIZE,
                  TEST1_THREAD_PRIORITY,
                  TEST1_TIMESLICE);

    /* 创建成功则启动动态线程 */
    if (tid != RT_NULL)
    {
        rt_thread_startup(tid);
    }

    /* 创建动态test1线程 :优先级 24 ,时间片1个系统滴答,线程栈512字节 */
    tid = rt_thread_create("test2_thread",
                  test2_thread_entry,
                  RT_NULL,
                  TEST2_STACK_SIZE,
                  TEST2_THREAD_PRIORITY,
                  TEST2_TIMESLICE);

    /* 创建成功则启动动态线程 */
    if (tid != RT_NULL)
    {
        rt_thread_startup(tid);
    }

    return 0;
}

创建两个测试线程test1、test2,这两个线程都有对全局变量gulTmp进行+1操作,编译、下载、运行得到的结果为:

我们期望gulTmp的结果为10000,而实际得到的gulTmp的值却为10001,那是因为test2线程的优先级高于test1线程,因此test2线程优先执行,test2线程首先挂起1个时间片,test2挂起期间内核调度执行test1线程,在1个时间片之后,test2线程被唤醒,此时test2线程的优先级最高的,test2线程打断test1线程,所以最终临界区变量gulTmp的结果为10001。

从以上结果中可以看到, 当公共资源在多个线程中公用时,如果缺乏必要的保护错误,最后的输出结果可能与预期的结果完全不同。

IPC机制

为了解决这样的问题,RT-Thread引入了IPC机制 (Inter-Process Communication):

其核心思想都是: 在访问临界区的时候只允许一个 (或一类) 线程运行。进入/退出临界区的方法有:关闭中断调度器上锁。 我们可通过这两种简单的途径来禁止系统调度,防止线程被打断,从而保证临界区不被破坏。

1、关闭中断

线程中关闭中断保护临界区的结构如下:

void test1_thread_entry(void* parameter)
{ 
    rt_base_t level;

    while(1)
    {
        /* 关闭中断*/
        level = rt_hw_interrupt_disable();
        /* 以下是临界区*/
        . . . .
        /* 关闭中断*/
        rt_hw_interrupt_enable(level);
    }
}

所有线程的调度都是建立在中断的基础上的,拿 CM3 核来举例:在 cm3 处理器上,所有的调度条件满足后(不管是在任务还是在中断中)系统会触发pendsv 中断, 在 pensv 中断中去执行调度工作。所以,当我们关闭中断后,系统将不能再进行调度,线程自身也自然不会被其他线程抢占了。

2、调度器上锁

锁住调度器以保护临界区的结构如下:

void test1_thread_entry(void* parameter)
{
    . . .
    while(1)
    {
        /* 调度器上锁,上锁后,将不再切换到其他线程,仅响应中断 */
        rt_enter_critical();
        /* 以下进入临界区 */
        . . . .
        /* 调度器解锁 */
        rt_exit_critical();
    }
}

把调度器锁住也能让当前运行的任务不被换出,直到调度器解锁。但和关闭中断有一点不相同的是,对调度器上锁,系统依然能响应外部中断,中断服务例程依然有可能被运行。所以在使用调度器上锁的方式来做任务同步时,需要考虑好, 任务访问的临界资源是否会被中断服务例程所修改,如果可能会被修改,那么将不适合采用此种方式作为同步的方法。


我的个人博客:https://zhengnianli.github.io/

我的微信公众号:嵌入式大杂烩

我的CSDN博客:https://blog.csdn.net/zhengnianli


 上一篇
【RT-Thread笔记】FAL软件包的使用:FLASH分区管理 【RT-Thread笔记】FAL软件包的使用:FLASH分区管理
什么是分区管理FLASH分区管理是怎么一回事呢?我们可以以个人电脑来做类比,我们的电脑通常都分有很多个盘符: 这些都是我们硬盘的分区,我这里装了两块硬盘,512GB的机械硬盘+128GB的固态硬盘,共分C~H六个分区,我这里的C盘和H盘是
2019-12-16
下一篇 
C语言、嵌入式位操作精华技巧大汇总 C语言、嵌入式位操作精华技巧大汇总
对于ST的芯片的使用,大家平时在学习、工作中大多使用库函数的方式来开发吧?我之前也是用库函数来进行配置,最近发现直接配置寄存器有时候好像更容易些,而且可读性也不会很差。下面分享关于寄存器配置的一些笔记: 一、位操作简单介绍首先,以下是按位运
2019-11-27
  目录