【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();
	}
}

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



文章作者: 杂烩君
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 杂烩君 !
  目录