【STM32F4】【银杏科技ARM+FPGA】iCore3移植RT-Thread--内核之临界区保护
一、临界区的引入
在嵌入式系统中,通过传感器采集数据进行保存,其他线程间可能同时访问它,但是如果数据还未完成写入,访问到的数据明显会出错,通过对临界区保存,则可以避免此类问题的发生。
1. 基本概念
临界资源:一次仅允许一个(或者指定数量)线程访问的共享资源,可以是一个具体的硬件设备(如打印机、传真机等),也可以是一个变量,一个缓冲区。
临界区:在RT-Thread中,访问公共资源的一段代码成为临界区,每个线程中访问(操作)连接资源的那段代码成为临界区(Critical Section),每次只允许一个线程进入临界区,为了保护线程内的资源不会被其他线程抢占。
二、临界区的问题展示
在RT-Thread系统中,临界区往往是对全局变量的操作,以下我们展示一个对全局变量操作的多线程临界区问题(此工程名中critical_sample.c中):
#include <rtthread.h>
#include <rtdevice.h>
#include <board.h>
#define THREAD1_STACK_SIZE 512
#define THREAD1_THREAD_PRIORITY 25
#define THREAD1_TIMESLICE 2
#define THREAD2_STACK_SIZE 512
#define THREAD2_THREAD_PRIORITY 24
#define THREAD2_TIMESLICE 1
uint32_t value = 0;
/* thread1线程入口函数 -------------------------------------------------------------------------*/
static void thread1_entry(void *parameter)
{
uint16_t i = 0;
rt_kprintf("value = %d\n", value);
for (i = 0; i < 100; i++)
{
value++;
}
rt_kprintf("value = %d\n", value);
}
/* thread2线程入口函数 -------------------------------------------------------------------------*/
static void thread2_entry(void *parameter)
{
rt_thread_delay(1);
value++;
}
int critical_sample(void)
{
/* 定义线程句柄 */
rt_thread_t tid;
/* 创建动态test1线程 :优先级 25 ,时间片2个系统滴答,线程栈512字节 */
tid = rt_thread_create("thread1",
thread1_entry,
RT_NULL,
THREAD1_STACK_SIZE,
THREAD1_THREAD_PRIORITY,
THREAD1_TIMESLICE);
/* 创建成功则启动动态线程 */
if (tid != RT_NULL)
{
rt_thread_startup(tid);
}
/* 创建动态test1线程 :优先级 24 ,时间片1个系统滴答,线程栈512字节 */
tid = rt_thread_create("thread2",
thread2_entry,
RT_NULL,
THREAD2_STACK_SIZE,
THREAD2_THREAD_PRIORITY,
THREAD2_TIMESLICE);
/* 创建成功则启动动态线程 */
if (tid != RT_NULL)
{
rt_thread_startup(tid);
}
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(critical_sample, critical sample);
实验结果:
创建的两个线程都是对全局变量value进行+1操作,运行结果为:
我们期望的value的结果是100,而实际的value是101,原因是线程thread2的优先级高于thread1, thread2线程优先执行,thread2线程首先挂起1个时间片,thread2挂起期间内核调度执行thread1线程,在1个时间片之后,thread2线程被唤醒,此时thread2线程的优先级最高的,thread2线程打断thread1线程,所以最终临界区变量value的结果为101。
三、临界区保护
为解决此问题发生,RT-Thread通过在访问临界区的时候只允许个线程运行。通过禁止调度或者关闭中断的方法关闭系统调度,从而保护临界区。
3.1、关闭中断
因为线程的调度是建立在中断的基础上的,所有关闭中断以后,系统将不能再进行调度,线程自身也不会再被其他线程抢占。
void thread_entry(void *parameter)
{
rt_base_t level;
while(1)
{
/*关闭中断*/
level = rt_hw_interrupt_disable();
/*以下是临界区*/
……
/*关闭中断*/
rt_hw_interrupt_enable(level);
}
}
3.2、禁止调度
禁止调度就是把调度器锁住,不让其进行线程切换,从而保证当前运行的任务不被换出,直到调度器解锁。
void thread_entry(void *parameter) { while(1) { /*调度器上锁,上锁后将不再切换到其他线程,仅响应中断*/ rt_enter_critical(); /*以下进入临界区*/ …… /*调度器解锁*/ rt_exit_critical (); } }
同关闭中断不同的是,对调度器上锁,系统依然可以响应外部中断,中断服务例程依然有可能被运行。所以在使用调度器上锁的方式来做任务同步时,需要考虑任务访问的临界区资源是否会被中断服务例程所修改。
四、临界区保护实例
我们使用中断方式进行临界区保护。
#define THREAD_PRIORITY 20
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 10
/* 同时访问的全局变量 */
static rt_uint32_t cnt;
void thread_entry(void *parameter)
{
rt_uint32_t no;
rt_uint32_t level;
no = (rt_uint32_t) parameter;
while (1)
{
/* 关闭中断 */
level = rt_hw_interrupt_disable();
cnt += no;
rt_kprintf("protect thread[%d]'s counter is %d\n", no, cnt);
/* 恢复中断 */
rt_hw_interrupt_enable(level);
rt_thread_mdelay(3);
}
}
int interrupt_sample(void)
{
rt_thread_t thread;
/* 创建thread1线程 */
thread = rt_thread_create("thread1", thread_entry, (void *)10,
THREAD_STACK_SIZE,
THREAD_PRIORITY, THREAD_TIMESLICE);
if (thread != RT_NULL)
rt_thread_startup(thread);
/* 创建thread2线程 */
thread = rt_thread_create("thread2", thread_entry, (void *)20,
THREAD_STACK_SIZE,
THREAD_PRIORITY, THREAD_TIMESLICE);
if (thread != RT_NULL)
rt_thread_startup(thread);
return 0;
}
/* 导出到 msh 命令列表中 */
MSH_CMD_EXPORT(interrupt_sample, interrupt sample);
代码分析:设定两个线程使用了同一个入口函数,并传递了不同的参数,第一个是10,第二个是20,线程都是对全局变量cnt操作(cnt+=no)。如果没有临界区保护,当第一个线程运行到加数操作之前时间片到了,则线程二开始运行,数据将会有丢失。我们看本次实验结果: