在多任务实时系统中,一项工作的完成往往可以通过多个任务协调的方式共同来完成,
例如一个任务从传感器中接收数据并且将数据写到共享内存中,同时另一个任务周期性的从共享内存中读取数据并发送去显示(如图 两个线程间的数据传递 )
如果对共享内存的访问不是排他性的,那么各个线程间可能同时访问它。这将引起数据一致性的问题,例如,在显示线程试图显示数据之前,传感器线程还未完成数据的写入,那么显示将包含不同时间采样的数据,造成显示数据的迷惑。
将传感器数据写入到共享内存的代码是接收线程的关键代码段;将传感器数据从共享内存中读出的代码是显示线程的关键代码段;这两段代码都会访问共享内存。正常的操作序列应该是在一个线程对共享内存块操作完成后,才允许另一个线程去操作。对于操作/访问同一块区域,称之为临界区。
任务的同步方式有很多种,其核心思想都是:在访问临界区的时候只允许一个(或一类)任务运行。
关闭中断也叫中断锁,是禁止多任务访问临界区最简单的一种方式,即使是在分时操作系统中也是如此。当中断关闭的时候,就意味着当前任务不会被其他事件打断(因为整个系统已经不再响应那些可以触发线程重新调度的外部事件),也就是当前线程不会被抢占,除非这个任务主动放弃了处理器控制权。关闭中断/恢复中断API接口由BSP实现,根据平台的不同其实现方式也大不相同。
关闭、打开中断接口由两个函数完成:
rt_base_t rt_hw_interrupt_disable(void);
这个函数用于关闭中断并返回关闭中断前的中断状态。
返回调用这个函数前的中断状态
- 恢复中断
void rt_hw_interrupt_enable(rt_base_t level);
这个函数“使能”中断,它采用恢复调用rt_hw_interrupt_disable()函数前的中断状态,进行“使能”中断状态,如果调用rt_hw_interrupt_disable() 函数前是关中断状态,那么调用此函数后依然是关中断状态。level参数是上一次调用rt_hw_interrupt_ disable()时的返回值。
参数 | 描述 |
---|---|
level | 前一次rt_hw_interrupt_disable返回的中断状态 |
使用开关中断进行线程间同步的例子代码如下例代码所示:
/* 代码清单:关闭中断进行全局变量的访问 */
#include <rtthread.h>
/* 同时访问的全局变量 */
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_hw_interrupt_enable(level);
rt_kprintf("thread[%d]'s counter is %d\n", no, cnt);
rt_thread_delay(no);
}
}
/* 用户应用程序入口 */
void rt_application_init()
{
rt_thread_t thread;
/* 创建t1线程 */
thread = rt_thread_create("t1", thread_entry, (void*)10,
512, 10, 5);
if (thread != RT_NULL) rt_thread_startup(thread);
/* 创建t2线程 */
thread = rt_thread_create("t2", thread_entry, (void*)20,
512, 20, 5);
if (thread != RT_NULL) rt_thread_startup(thread);
}
警告:由于关闭中断会导致整个系统不能响应外部中断,所以在使用关闭中断做为互斥访问临界区的手段时,首先必须需要保证关闭中断的时间非常短,例如数条机器指令。
使用中断锁来操作系统的方法可以应用于任何场合,且其他几类同步方式都是依赖于中断锁而实现的,可以说中断锁是最强大的和最高效的同步方法。只是使用中断锁最主要的问题在于,在中断关闭期间系统将不再响应任何中断,也就不能响应外部的事件。所以中断锁对系统的实时性影响非常巨大,当使用不当的时候会导致系统完全无实时性可言(可能导致系统完全偏离要求的时间需求);而使用得当,则会变成一种快速、高效的同步方式。
例如,为了保证一行代码(例如赋值)的互斥运行,最快速的方法是使用中断锁而不是信号量或互斥量:
/* 关闭中断*/
level = rt_hw_interrupt_disable();
a = a + value;
/* 恢复中断*/
rt_hw_interrupt_enable(level);
在使用中断锁时,需要确保关闭中断的时间非常短,例如上面代码中的a = a + value; 也可换成另外一种方式,例如使用信号量:
/* 获得信号量锁*/
rt_sem_take(sem_lock, RT_WAITING_FOREVER);
a = a + value;
/* 释放信号量锁*/
rt_sem_release(sem_lock);
这段代码在rt_sem_take 、rt_sem_release 的实现中,已经存在使用中断锁保护信号量内部变量的行为,所以对于简单如a = a + value;的操作,使用中断锁将更为简洁快速。
同中断锁一样把调度器锁住也能让当前运行的任务不被换出,直到调度器解锁。但和中断锁有一点不相同的是,对调度器上锁,系统依然能响应外部中断,中断服务例程依然能进行相应的响应。所以在使用调度器上锁的方式进行任务同步时,需要考虑好任务访问的临界资源是否会被中断服务例程所修改,如果可能会被修改,那么将不适合采用此种方式进行同步。RT-Thread提供的调度锁操作API为:
void rt_enter_critical(void); /* 进入临界区*/
调用这个函数后,调度器将被上锁。在锁住调度器期间,系统依然响应中断,如果中断唤醒了更高优先级的线程,调度器并不会立刻执行它,直到调用解锁调度器函数时才会尝试进行下一次调度。、
void rt_exit_critical(void); /* 退出临界区*/
当系统退出临界区的时候,系统会计算当前是否有更高优先级的线程就绪,如果有比当前线程更高优先级的线程就绪,将切换到这个高优先级线程中执行;如果无更高优先级线程就绪,将继续执行当前任务。
注: rt_enter_critical/rt_exit_critical可以多次嵌套调用,但每调用一次rt_enter_critical就必须相对应地调用一次rt_exit_critical退出操作,嵌套的最大深度是65535。
调度器锁能够方便地使用于一些线程与线程间同步的场合,由于轻型,它不会对系统中断响应造成负担;但它的缺陷也很明显,就是它不能被用于中断与线程间的同步或通知,并且如果执行调度器锁的时间过长,会对系统的实时性造成影响(因为使用了调度器锁后,系统将不再具备优先级的关系,直到它脱离了调度器锁的状态)。
信号量是一种轻型的用于解决线程间同步问题的内核对象,线程可以获取或释放它,从而达到同步或互斥的目的。信号量就像一把钥匙,把一段临界区给锁住,只允许有钥匙的线程进行访问:线程拿到了钥匙,才允许它进入临界区;而离开后把钥匙传递给排队在后面的等待线程,让后续线程依次进入临界区。
信号量工作示意图如图 信号量工作示意图所示,每个信号量对象都有一个信号量值和一个线程等待队列,信号量的值对应了信号量对象的实例数目、资源数目,假如信号量值为5,则表示共有5个信号量实例(资源)可以被使用,当信号量实例数目为零时,再申请该信号量的线程就会被挂起在该信号量的等待队列上,等待可用的信号量实例(资源)。
struct rt_semaphore
{
struct rt_ipc_object parent;/*继承自ipc_object类*/
rt_uint16_t value; /* 信号量的值 */
};
/* rt_sem_t是指向semaphore结构体的指针类型 */
typedef struct rt_semaphore* rt_sem_t;
rt_semaphore对象从rt_ipc_object中派生,由IPC容器所管理。信号量的最大值是65535。
创建信号量
当创建一个信号量时,内核首先创建一个信号量控制块,然后对该控制块进行基本的初始化工作,创建信号量使用下面的函数接口:
rt_sem_t rt_sem_create (const char* name, rt_uint32_t value, rt_uint8_t flag);
当调用这个函数时,系统将先分配一个semaphore对象,并初始化这个对象,然后初始化IPC对象以及与semaphore相关的部分。在创建信号量指定的参数中,信号量标志参数决定了当信号量不可用时,多个线程等待的排队方式。当选择FIFO方式时,那么等待线程队列将按照先进先出的方式排队,先进入的线程将先获得等待的信号量;当选择PRIO(优先级等待)方式时,等待线程队列将按照优先级进行排队,优先级高的等待线程将先获得等待的信号量。
参数 | 描述 |
---|---|
name | 信号量名称; |
value | 信号量初始值; |
flag | 信号量标志,取值可以使用如下类型: |
#define RT_IPC_FLAG_FIFO 0x00 /* IPC参数采用FIFO方式*/
#define RT_IPC_FLAG_PRIO 0x01 /* IPC参数采用优先级方式*/
创建成功返回创建的信号量控制块指针;否则返回RT_NULL。
创建信号量的例程如下例所示:
/*
- 程序清单:动态信号量
- 3. 这个例子中将创建一个动态信号量(初始值为0)及一个动态线程,在这个动态线程中
- 将试图采用超时方式去持有信号量,应该超时返回。然后这个线程释放一次信号量,
- 并在后面继续采用永久等待方式去持有信号量, 成功获得信号量后返回。
*/
#include
#include "tc_comm.h"
/* 指向线程控制块的指针 */
static rt_thread_t tid = RT_NULL;
/* 指向信号量的指针 */
static rt_sem_t sem = RT_NULL;
/* 线程入口 */
static void thread_entry(void* parameter)
{
rt_err_t result;
rt_tick_t tick;
/* 获得当前的OS Tick */
tick = rt_tick_get();
/* 试图持有一个信号量,如果10个OS Tick依然没拿到,则超时返回 */
result = rt_sem_take(sem, 10);
if (result == -RT_ETIMEOUT)
{
/* 判断是否刚好过去10个OS Tick */
if (rt_tick_get() - tick != 10)
{
/* 如果失败,则测试失败 */
tc_done(TC_STAT_FAILED);
rt_sem_delete(sem);
return;
}
rt_kprintf("take semaphore timeout\n");
}
else
{
/* 因为并没释放信号量,应该是超时返回,否则测试失败 */
tc_done(TC_STAT_FAILED);
rt_sem_delete(sem);
return;
}
/* 释放一次信号量 */
rt_sem_release(sem);
/* 继续持有信号量,并永远等待直到持有到信号量 */
result = rt_sem_take(sem, RT_WAITING_FOREVER);
if (result != RT_EOK)
{
/* 返回不正确,测试失败 */
tc_done(TC_STAT_FAILED);
rt_sem_delete(sem);
return;
}
/* 测试成功 */
tc_done(TC_STAT_PASSED);
/* 删除信号量 */
rt_sem_delete(sem);
}
int semaphore_dynamic_init()
{
/* 创建一个信号量,初始值是0 */
sem = rt_sem_create("sem", 0, RT_IPC_FLAG_FIFO);
if (sem == RT_NULL)
{
tc_stat(TC_STAT_END | TC_STAT_FAILED);
return 0;
}
/* 创建线程 */
tid = rt_thread_create("thread",
thread_entry, RT_NULL, /* 线程入口是thread_entry, 参数RT_NULL */
THREAD_STACK_SIZE, THREAD_PRIORITY, THREAD_TIMESLICE);
if (tid != RT_NULL)
rt_thread_startup(tid);
else
tc_stat(TC_STAT_END | TC_STAT_FAILED);
return 0;
}
#ifdef RT_USING_TC
static void _tc_cleanup()
{
/* 调度器上锁,上锁后,将不再切换到其他线程,仅响应中断 */
rt_enter_critical();
/* 删除线程 */
if (tid != RT_NULL && tid->stat != RT_THREAD_CLOSE)
{
rt_thread_delete(tid);
/* 删除信号量 */
rt_sem_delete(sem);
}
/* 调度器解锁 */
rt_exit_critical();
/* 设置TestCase状态 */
tc_done(TC_STAT_PASSED);
}
int _tc_semaphore_dynamic()
{
/* 设置TestCase清理回调函数 */
tc_cleanup(_tc_cleanup);
semaphore_dynamic_init();
/* 返回TestCase运行的最长时间 */
return 100;
}
/* 输出函数命令到finsh shell中 */
FINSH_FUNCTION_EXPORT(_tc_semaphore_dynamic, a dynamic semaphore example);
#else
/* 用户应用入口 */
int rt_application_init()
{
semaphore_dynamic_init();
return 0;
}
#endi
删除信号量
系统不再使用信号量时,可通过删除信号量以释放系统资源。删除信号量使用下面的函数接口:
rt_err_t rt_sem_delete (rt_sem_t sem);
调用这个函数时,系统将删除这个信号量。如果删除该信号量时,有线程正在等待该信号量,那么删除操作会先唤醒等待在该信号量上的线程(等待线程的返回值是-RT_ERROR),然后再释放信号量的内存资源。
初始化信号量
对于静态信号量对象,它的内存空间在编译时期就被编译器分配出来,放在数据段或ZI段上,此时使用信号量就不再需要使用rt_sem_create接口来创建它,而只需在使用前对它进行初始化即可。初始化信号量对象可使用下面的函数接口:
rt_err_t rt_sem_init (rt_sem_t sem, const char* name, rt_uint32_t value, rt_uint8_t flag);
当调用这个函数时,系统将对这个semaphore对象进行初始化,然后初始化IPC对象以及与semaphore相关的部分。在初始化信号量指定的参数中,信号量标志参数决定了当信号量不可用时,多个线程等待的方式。当选择FIFO方式时,那么等待线程队列将按照先进先出的方式排队,先进入的线程将先获得等待的信号量;当选择PRIO(优先级等待)方式时,等待线程队列将按照优先级进行排队,优先级高的等待线程将先获得等待的信号量。
参数 | 描述 |
---|---|
sem | 信号量对象的句柄; |
name | 信号量名称; |
value | 信号量初始值; |
flag | 信号量标志。 |
#define RT_IPC_FLAG_FIFO 0x00 /* IPC参数采用FIFO方式*/
#define RT_IPC_FLAG_PRIO 0x01 /* IPC参数采用优先级方式*/
/*
* 程序清单:静态信号量
*
* 这个例子中将创建一个静态信号量(初始值为0 )及一个静态线程,在这个静态线程中
* 将试图采用超时方式去获取信号量,应该超时返回。然后这个线程释放一次信号量,并
* 在后面继续采用永久等待方式去获取信号量, 成功获得信号量后返回。
*/
#include
#include "tc_comm.h"
/* 线程控制块及栈 */
static struct rt_thread thread;
static rt_uint8_t thread_stack[THREAD_STACK_SIZE];
/* 信号量控制块 */
static struct rt_semaphore sem;
/* 线程入口 */
static void thread_entry(void* parameter)
{
rt_err_t result;
rt_tick_t tick;
/* 获得当前的OS Tick */
tick = rt_tick_get();
/* 试图持有信号量,最大等待10个OS Tick后返回 */
result = rt_sem_take(&sem, 10);
if (result == -RT_ETIMEOUT)
{
/* 超时后判断是否刚好是10个OS Tick */
if (rt_tick_get() - tick != 10)
{
tc_done(TC_STAT_FAILED);
rt_sem_detach(&sem);
return;
}
rt_kprintf("take semaphore timeout\n");
}
else
{
/* 因为没有其他地方释放信号量,所以不应该成功持有信号量,否则测试失败 */
tc_done(TC_STAT_FAILED);
rt_sem_detach(&sem);
return;
}
/* 释放一次信号量 */
rt_sem_release(&sem);
/* 永久等待方式持有信号量 */
result = rt_sem_take(&sem, RT_WAITING_FOREVER);
if (result != RT_EOK)
{
/* 不成功则测试失败 */
tc_done(TC_STAT_FAILED);
rt_sem_detach(&sem);
return;
}
/* 测试通过 */
tc_done(TC_STAT_PASSED);
/* 脱离信号量对象 */
rt_sem_detach(&sem);
}
int semaphore_static_init()
{
rt_err_t result;
/* 初始化信号量,初始值是0 */
result = rt_sem_init(&sem, "sem", 0, RT_IPC_FLAG_FIFO);
if (result != RT_EOK)
{
tc_stat(TC_STAT_END | TC_STAT_FAILED);
return 0;
}
/* 初始化线程1 */
result = rt_thread_init(&thread, "thread", /* 线程名:thread */
thread_entry, RT_NULL, /* 线程的入口是thread_entry,参数是RT_NULL*/
&thread_stack[0], sizeof(thread_stack), /* 线程栈thread_stack */
THREAD_PRIORITY, 10);
if (result == RT_EOK) /* 如果返回正确,启动线程1 */
rt_thread_startup(&thread);
else
tc_stat(TC_STAT_END | TC_STAT_FAILED);
return 0;
}
#ifdef RT_USING_TC
static void _tc_cleanup()
{
/* 调度器上锁,上锁后,将不再切换到其他线程,仅响应中断 */
rt_enter_critical();
/* 执行线程脱离 */
if (thread.stat != RT_THREAD_CLOSE)
{
rt_thread_detach(&thread);
/* 执行信号量对象脱离 */
rt_sem_detach(&sem);
}
/* 调度器解锁 */
rt_exit_critical();
/* 设置TestCase状态 */
tc_done(TC_STAT_PASSED);
}
int _tc_semaphore_static()
{
/* 设置TestCase清理回调函数 */
tc_cleanup(_tc_cleanup);
semaphore_static_init();
/* 返回TestCase运行的最长时间 */
return 100;
}
/* 输出函数命令到finsh shell中 */
FINSH_FUNCTION_EXPORT(_tc_semaphore_static, a static semaphore example);
#else
/* 用户应用入口 */
int rt_application_init()
{
thread_static_init();
return 0;
}
#endif
脱离信号量
脱离信号量就是让信号量对象从内核对象管理器中移除掉。脱离信号量使用下面的函数
接口:
rt_err_t rt_sem_detach (rt_sem_t sem);
使用该函数后,内核先唤醒所有挂在该信号量等待队列上的线程,然后将该信号量从内核对象管理器中删除。原来挂起在信号量上的等待线程将获得-RT_ERROR 的返回值。
参数 | 描述 |
---|---|
sem | 信号量对象的句柄。 |
获取信号量
线程通过获取信号量来获得信号量资源实例,当信号量值大于零时,线程将获得信号量,并且相应的信号量值都会减1,获取信号量使用下面的函数接口:
rt_err_t rt_sem_take (rt_sem_t sem, rt_int32_t time);
在调用这个函数时,如果信号量的值等于零,那么说明当前信号量资源实例不可用,申请该信号量的线程将根据time参数的情况选择直接返回、或挂起等待一段时间、或永久等待,直到其他线程或中断释放该信号量。如果在参数time指定的时间内依然得不到信号量,线程将超时返回,返回值是-RT_ETIMEOUT。
参数 | 描述 |
---|---|
sem | 信号量对象的句柄; |
time | 指定的等待时间,单位是操作系统时钟节拍(OS Tick)。 |
无等待获取信号量
当用户不想在申请的信号量上挂起线程进行等待时,可以使用无等待方式获取信号量,无等待获取信号量使用下面的函数接口:
rt_err_t rt_sem_trytake(rt_sem_t sem);
这个函数与rt_sem_take(sem, 0) 的作用相同,即当线程申请的信号量资源实例不可用的时候,它不会等待在该信号量上,而是直接返回-RT_ETIMEOUT。
释放信号量
当线程完成资源的访问后,应尽快释放它持有的信号量,使得其他线程能获得该信号量。释放信号量使用下面的函数接口:
rt_err_t rt_sem_release(rt_sem_t sem);
当信号量的值等于零时,并且有线程等待这个信号量时,将唤醒等待在该信号量线程队列中的第一个线程,由它获取信号量。否则将把信号量的值加一。
/*
* 程序清单:生产者消费者例子
*
* 这个例子中将创建两个线程用于实现生产者消费者问题
*/
#include
#include "tc_comm.h"
/* 定义最大5个元素能够被产生 */
#define MAXSEM 5
/* 用于放置生产的整数数组 */
rt_uint32_t array[MAXSEM];
/* 指向生产者、消费者在array数组中的读写位置 */
static rt_uint32_t set, get;
/* 指向线程控制块的指针 */
static rt_thread_t producer_tid = RT_NULL;
static rt_thread_t consumer_tid = RT_NULL;
struct rt_semaphore sem_lock;
struct rt_semaphore sem_empty, sem_full;
/* 生产者线程入口 */
void producer_thread_entry(void* parameter)
{
rt_int32_t cnt = 0;
/* 运行100次 */
while( cnt < 100)
{
/* 获取一个空位 */
rt_sem_take(&sem_empty, RT_WAITING_FOREVER);
/* 修改array内容,上锁 */
rt_sem_take(&sem_lock, RT_WAITING_FOREVER);
array[set%MAXSEM] = cnt + 1;
rt_kprintf("the producer generates a number: %d\n",
array[set%MAXSEM]);
set++;
rt_sem_release(&sem_lock);
/* 发布一个满位 */
rt_sem_release(&sem_full);
cnt++;
/* 暂停一段时间 */
rt_thread_delay(50);
}
rt_kprintf("the producer exit!\n");
}
/* 消费者线程入口 */
void consumer_thread_entry(void* parameter)
{
rt_uint32_t no;
rt_uint32_t sum = 0;
/* 第n个线程,由入口参数传进来 */
no = (rt_uint32_t)parameter;
while(1)
{
/* 获取一个满位 */
rt_sem_take(&sem_full, RT_WAITING_FOREVER);
/* 临界区,上锁进行操作 */
rt_sem_take(&sem_lock, RT_WAITING_FOREVER);
sum += array[get%MAXSEM];
rt_kprintf("the consumer[%d] get a number:%d\n",
no, array[get%MAXSEM]);
get++;
rt_sem_release(&sem_lock);
/* 释放一个空位 */
rt_sem_release(&sem_empty);
/* 生产者生产到100个数目,停止,消费者线程相应停止 */
if (get == 100) break;
/* 暂停一小会时间 */
rt_thread_delay(10);
}
rt_kprintf("the consumer[%d] sum is %d \n ", no, sum);
rt_kprintf("the consumer[%d] exit!\n");
}
int semaphore_producer_consumer_init()
{
/* 初始化3个信号量 */
rt_sem_init(&sem_lock , "lock", 1, RT_IPC_FLAG_FIFO);
rt_sem_init(&sem_empty,"empty", MAXSEM, RT_IPC_FLAG_FIFO);
rt_sem_init(&sem_full , "full", 0, RT_IPC_FLAG_FIFO);
/* 创建线程1 */
producer_tid = rt_thread_create("producer",
producer_thread_entry, /* 线程入口是producer_thread_entry */
RT_NULL, /* 入口参数是RT_NULL */
THREAD_STACK_SIZE, THREAD_PRIORITY - 1, THREAD_TIMESLICE);
if (producer_tid != RT_NULL)
rt_thread_startup(producer_tid);
else
tc_stat(TC_STAT_END | TC_STAT_FAILED);
/* 创建线程2 */
consumer_tid = rt_thread_create("consumer",
consumer_thread_entry,/* 线程入口是consumer_thread_entry */
RT_NULL, /* 入口参数是RT_NULL */
THREAD_STACK_SIZE, THREAD_PRIORITY + 1, THREAD_TIMESLICE);
if (consumer_tid != RT_NULL)
rt_thread_startup(consumer_tid);
else
tc_stat(TC_STAT_END | TC_STAT_FAILED);
return 0;
}
#ifdef RT_USING_TC
static void _tc_cleanup()
{
/* 调度器上锁,上锁后,将不再切换到其他线程,仅响应中断 */
rt_enter_critical();
/* 删除线程 */
if (producer_tid != RT_NULL && producer_tid->stat != RT_THREAD_CLOSE)
rt_thread_delete(producer_tid);
if (consumer_tid != RT_NULL && consumer_tid->stat != RT_THREAD_CLOSE)
rt_thread_delete(consumer_tid);
/* 调度器解锁 */
rt_exit_critical();
/* 设置TestCase状态 */
tc_done(TC_STAT_PASSED);
}
int _tc_semaphore_producer_consumer()
{
/* 设置TestCase清理回调函数 */
tc_cleanup(_tc_cleanup);
semaphore_producer_consumer_init();
/* 返回TestCase运行的最长时间 */
return 100;
}
/* 输出函数命令到finsh shell中 */
FINSH_FUNCTION_EXPORT(_tc_semaphore_producer_consumer, producer and consumer example);
#else
/* 用户应用入口 */
int rt_application_init()
{
semaphore_producer_consumer_init();
return 0;
}
#endif