查看原文
其他

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

ZhengNian 嵌入式大杂烩 2021-01-31

什么是临界区?

在多线程实时系统中,多个线程操作/访问同一块区域(代码),这块代码就称为临界区

例如一项工作中的两个线程:一个线程从传感器中接收数据并且将数据写到共享内存中,同时另一个线程周期性的从共享内存中读取数据并发送去显示,下图描述了两个线程间的数据传递:


如果对共享内存的访问不是排他性的,那么各个线程间可能同时访问它,这将引起数据一致性的问题。

例如,在显示线程试图显示数据之前,接收线程还未完成数据的写入,那么显示将包含不同时间采样的数据,造成显示数据的错乱。

临界区的问题

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

把调度器锁住也能让当前运行的任务不被换出,直到调度器解锁。但和关闭中断有一点不相同的是,对调度器上锁,系统依然能响应外部中断,中断服务例程依然有可能被运行。

所以在使用调度器上锁的方式来做任务同步时,需要考虑好, 任务访问的临界资源是否会被中断服务例程所修改,如果可能会被修改,那么将不适合采用此种方式作为同步的方法。

以上就是本次的笔记分享,如有错误欢迎指出!谢谢。欢迎收藏、转发、在看~

参考资料:

《RT-Thread编程指南》
《RT-Thread 内核实现与应用开发实战指南》
《一起来学 RT-Thread 教程连载》


猜你喜欢:

STM32的ISP下载的原理是什么呢?

STM32串口IAP分享

C语言代码优化的一些技巧(四)

【RT-Thread笔记】内核对象模型

【RT-Thread笔记】线程的基本知识

【RT-Thread笔记】IO设备模型及PIN设备

【RT-Thread笔记】PIN设备中断实验

RT-Thread线下培训实例分享:基于i.MX RT1050的云接入


等你来撩:

聊天界面发送嵌入式大杂烩获取1T大杂烩资料包

聊天界面发送m获取往期笔记目录



    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存