如果一个函数可以安全地被多个任务调用,或是在任务与中断中均可调用,则这个函数是可重入的。每个任务都单独维护自己的栈空间及其自身在的内存寄存器组中的值。如果一个函数除了访问自己栈空间上分配的数据或是内核寄存器中的数据外,不会访问其它任何数据,则这个函数就是可重入的。
可重入的函数:
```css
/* 一个参数被传递到函数中。要么是传递到堆栈或在CPU寄存器中。 两种方法都是安全的
每个任务维护自己的堆栈和自己的寄存器集值。 */
long lAddOneHundered( long lVar1 )
{
/* 这个函数范围变量也将被分配给堆栈或者一个寄存器,这取决于编译器和优化级别。 每一个
调用此函数的任务或中断将有自己的副本lVar2。 */
long lVar2;
lVar2 = lVar1 + 100;
/* */
return lVar2;
}
不可重入的函:
/* 在这种情况下,lVar1是一个全局变量,所以调用的每个任务
函数将访问变量的同一个副本。 */
long lVar1; //全局变量
long lNonsenseFunction( void )
{
/* 这个变量是静态的,所以不会在堆栈上分配。 每个任务
调用该函数的函数将访问相同的变量。 */
static long lState = 0;
long lReturn;
switch( lState )
{
case 0 : lReturn = lVar1 + 10;
lState = 1;
break;
case 1 : lReturn = lVar1 + 20;
lState = 0;
break;
}
}
访问一个被多任务共享,或是被任务与中断共享的资源时,需要采用”互斥”技术以保证数据在任何时候都保持一致性。这样做的目的是要确保任务从开始访问资源就具有排它性,直至这个资源又恢复到完整状态。
FreeRTOS 提供了多种特性用以实现互斥,但是最好的互斥方法(如果可能的话,任何时候都当如此)还是通过精心设计应用程序,尽量不要共享资源,或者是每个资源都通过单任务访问。
基本临界区
基本临界区是指宏 taskENTER_CRITICAL()与 taskEXIT_CRITICAL()之间的代码区间
/* 为了保证对PORTA寄存器的访问不被中断,将访问操作放入临界区。
进入临界区 */
taskENTER_CRITICAL();
/* 在taskENTER_CRITICAL() 与 taskEXIT_CRITICAL()之间不会切换到其它任务。 中断可以执行,也允许
嵌套,但只是针对优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断 – 而且这些中断不允许访问
FreeRTOS API 函数. */
PORTA |= 0x01;
/* 我们已经完成了对PORTA的访问,因此可以安全地离开临界区了。 */
taskEXIT_CRITICAL();
void vPrintString( const portCHAR *pcString )
{
/* 往stdout中写字符串,使用临界区这种原始的方法实现互斥。 */
taskENTER_CRITICAL();
{
printf( "%s", pcString );
fflush( stdout );
}
taskEXIT_CRITICAL();
/* 允许按任意键停止应用程序运行。实际的应用程序如果有使用到键值,还需要对键盘输入进行保护。 */
if( kbhit() )
{
vTaskEndScheduler();
}
}
临界区是提供互斥功能的一种非常原始的实现方法。临界区的工作仅仅是简单地把中断全部关掉,或是关掉优先级在 configMAX_SYSCAL_INTERRUPT_PRIORITY 及以下的中断——依赖于具体使用的 FreeRTOS 移植。抢占式上下文切换只可能在某个中断中完成,所以调用 taskENTER_CRITICAL()的任务可以在中断关闭的时段一直保持运行态,直到退出临界区。
临界区必须只具有很短的时间,否则会反过来影响中断响应时间。
在每次调用taskENTER_CRITICAL()之后,必须尽快地配套调用一个 taskEXIT_CRITICAL()。
从这个角度来看,对标准输出的保护不应当采用临界区,因为写终端在时间上会是一个相对较长的操作。
DOS 模拟器和 Open Watcom 处理终端输出时没有采用这种互斥方式,其库函数中是没有关中断的。本章中的示例代码会探索其它解决方案。
临界区嵌套是安全的,因为内核有维护一个嵌套深度计数。
临界区只会在嵌套深度为 0 时才会真正退出——即在为每个之前调用的 taskENTER_CRITICAL()都配套调用了 taskEXIT_CRITICAL()之后。
也可以通过挂起调度器来创建临界区。挂起调度器有些时候也被称为锁定调度器。
基本临界区保护一段代码区间不被其它任务或中断打断。 由挂起调度器实现的临界区只可以保护一段代码区间不被其它任务打断,因为这种方式下,中断是使能的。如果一个临界区太长而不适合简单地关中断来实现,可以考虑采用挂起调度器的方式。但是唤醒(resuming, or un-suspending)调度器却是一个相对较长的操作。所以评估哪种是最佳方式需要结合实际情况。
void vTaskSuspendAll( void );//挂起调度器
通过调用 vTaskSuspendAll()来挂起调度器。挂起调度器可以停止上下文切换而不用关中断。如果某个中断在调度器挂起过程中要求进行上下文切换,则个这请求也会被挂起,直到调度器被唤醒后才会得到执行。
在调度器处于挂起状态时,不能调用 FreeRTOS API 函数。
portBASE_TYPE xTaskResumeAll( void );
返回值 在调度器挂起过程中,上下文切换请求也会被挂起,直到调度器
被唤醒后才会得到执行。如果一个挂起的上下文切换请求在
xTaskResumeAll()返回前得到执行,则函数返回 pdTRUE。在其
它情况下,xTaskResumeAll()返回 pdFALSE。
嵌套调用 vTaskSuspendAll()和 xTaskResumeAll()是安全的,因为内核有维护一个嵌套深度计数。
调度器只会在嵌套深度计数为 0 时才会被唤醒——即在为每个之前调用的 vTaskSuspendAll()都配套调用了 xTaskResumAll()之后。
通过挂起调度器的方式来保护终端输出
void vPrintString( const portCHAR *pcString )
{
/* Write the string to stdout, suspending the scheduler as a method
of mutual exclusion. */
vTaskSuspendScheduler();
{
printf( "%s", pcString );
fflush( stdout );
}
xTaskResumeScheduler();
/* Allow any key to stop the application running. A real application that
actually used the key value should protect access to the keyboard input too. */
if( kbhit() )
{
vTaskEndScheduler();
}
}
互斥量是一种特殊的二值信号量,用于控制在两个或多个任务间访问共享资源。单词MUTEX(互斥量) 源于”MUTual EXclusion”。
在用于互斥的场合,互斥量从概念上可看作是与共享资源关联的令牌。一个任务想要合法地访问资源,其必须先成功地得到(Take)该资源对应的令牌(成为令牌持有者)。当令牌持有者完成资源使用,其必须马上归还(Give)令牌。只有归还了令牌,其它任务才可能成功持有,也才可能安全地访问该共享资源。一个任务除非持有了令牌,否则不允许访问共享资源。
虽然互斥量与二值信号量之间具有很多相同的特性,但互斥量用于互斥功能完全不同于二值信号量用于同步。两者间最大的区别在于信号量在被获得之后所发生的事情:
用于互斥的信号量必须归还。
用于同步的信号量通常是完成同步之后便丢弃,不再归还
互斥量是一种信号量。FreeRTOS 中所有种类的信号量句柄都保存在类型为xSemaphoreHandle 的变量中。互斥量在使用前必须先创建。创建一 个互斥量类型的信号量需要使用
xSemaphoreHandle xSemaphoreCreateMutex( void)
返回值 如果返回 NULL 表示互斥量创建失败。原因是内存堆空间不足导致
FreeRTOS 无法为互斥量分配结构数据空间。第五章提供更多关于内存
管理方面的信息。
返回非 NULL 值表示互斥量创建成功。返回值应当保存起来作为该互斥
量的句柄。
2.释放互斥信号量
xSemaphoreGive(MutexSemaphore);//MutexSemaphore上面那个函数创建之后的返回值
3.获取互斥信号量
xSemaphoreTake(MutexSemaphore,portMAX_DELAY);//MutexSemaphore上面那个函数创建之后的返回值,portMAX_DELAY延时的时间
int main( void )
{
/* 信号量使用前必须先创建。本例创建了一个互斥量类型的信号量。 */
xMutex = xSemaphoreCreateMutex();
/* 本例中的任务会使用一个随机延迟时间,这里给随机数发生器生成种子。 */
srand( 567 );
/* Check the semaphore was created successfully before creating the tasks. */
if( xMutex != NULL )
{
/* Create two instances of the tasks that write to stdout. The string
they write is passed in as the task parameter. The tasks are created
at different priorities so some pre-emption will occur. */
xTaskCreate( prvPrintTask, "Print1", 1000,
"Task 1 ******************************************\r\n", 1, NULL );
xTaskCreate( prvPrintTask, "Print2", 1000,
"Task 2 ------------------------------------------\r\n", 2, NULL );
/* Start the scheduler so the created tasks start executing. */
vTaskStartScheduler();
}
/* 如果一切正常,main()函数不会执行到这里,因为调度器已经开始运行任务。但如果程序运行到了这里,
很可能是由于系统内存不足而无法创建空闲任务。第五章会提供更多关于内存管理的信息 */
for( ;; );
}
static void prvNewPrintString( const portCHAR *pcString )
{
/* 互斥量在调度器启动之前就已创建,所以在此任务运行时信号量就已经存在了。
试图获得互斥量。如果互斥量无效,则将阻塞,进入无超时等待。xSemaphoreTake()只可能在成功获得互
斥量后返回,所以无需检测返回值。如果指定了等待超时时间,则代码必须检测到xSemaphoreTake()返回
pdTRUE后,才能访问共享资源(此处是指标准输出)。 */
xSemaphoreTake( xMutex, portMAX_DELAY );
{
/* 程序执行到这里表示已经成功持有互斥量。现在可以自由访问标准输出,因为任意时刻只会有一个任
务能持有互斥量。 */
printf( "%s", pcString );
fflush( stdout );
/* 互斥量必须归还! */
}
xSemaphoreGive( xMutex );
/* Allow any key to stop the application running. A real application that
actually used the key value should protect access to the keyboard too. A
real application is very unlikely to have more than one task processing
key presses though! */
if( kbhit() )
{
vTaskEndScheduler();
}
}
优先级反转
上图 也展现出了采用互斥量提供互斥功能的潜在缺陷之一。在这种可能的执行流程描述中,高优先级的任务 2 竟然必须等待低优先级的任务 1 放弃对互斥量的持有权。高优先级任务被低优先级任务阻塞推迟的行为被称为”优先级反转”。这是一种不合理的行为方式,如果把这种行为再进一步放大,当高优先级任务正等待信号量的时候,一个介于两个任务优先之间的中等优先级任务开始执行——这就会导致一个高优先级任务在等待一个低优先级任务,而低优先级任务却无法执行!这种最坏的情形在下图中进行展示。
优先级继承
FreeRTOS 中互斥量与二值信号量十分相似——唯一的区别就是互斥量自动提供了一个基本的”优先级继承”机制。优先级继承是最小化优先级反转负面影响的一种方案——其并不能修正优先级反转带来的问题,仅仅是减小优先级反转的影响。优先级继承使得系统行为的数学分析更为复杂,所以如果可以避免的话,并不建议系统实现对优先级继承有所依赖。
优先级继承暂时地将互斥量持有者的优先级提升至所有等待此互斥量的任务所具有的最高优先级。持有互斥量的低优先级任务”继承”了等待互斥量的任务的优先级。这种机制在下图中进行展示。互斥量持有者在归还互斥量时,优先级会自动设置为其原来的优先级。
由于最好是优先考虑避免优先级反转,并且因为 FreeRTOS 本身是面向内存有限的微控制器,所以只实现了最基本的互斥量的优先级继承机制,这种实现假定一个任务在任意时刻只会持有一个互斥量。
死锁是利用互斥量提供互斥功能的另一个潜在缺陷。Deadlock 有时候会被更戏剧性地称为”deadly embrace(抱死)”。(在linux线程那边有详细介绍一样的)
守护任务提供了一种干净利落的方法来实现互斥功能,而不用担心会发生优先级反转和死锁。
守护任务是对某个资源具有唯一所有权的任务。只有守护任务才可以直接访问其守护的资源——其它任务要访问该资源只能间接地通过守护任务提供的服务。
守护任务大部份时间都在阻塞态等待队列中有信息到来
心跳钩子函数(或称回调函数)由内核在每次心跳中断时调用。要挂接一个心跳钩子函数,需要做以下配置:
设置 FreeRTOSConfig.h 中的常量 configUSE_TICK_HOOK 为 1。
提供钩子函数的具体实现,具体见下图
void vApplicationTickHook( void );//无返回值,无参数
demo:
static char *pcStringsToPrint[] =
{
"Task 1 ****************************************************\r\n",
"Task 2 ----------------------------------------------------\r\n",
"Message printed from the tick hook interrupt ##############\r\n"
};
xQueueHandle xPrintQueue;
/*-----------------------------------------------------------*/
int main( void )
{
/* 创建队列,深度为5,数据单元类型为字符指针。 */
xPrintQueue = xQueueCreate( 5, sizeof( char * ) );
/* 为伪随机数发生器产生种子。 */
srand( 567 );
/* Check the queue was created successfully. */
if( xPrintQueue != NULL )
{
/* 创建任务的两个实例,用于向守护任务发送信息。任务入口参数传入需要输出的字符串索引号。这两
个任务具有不同的优先级,所以高优先级任务有时会抢占低优先级任务。 */
xTaskCreate( prvPrintTask, "Print1", 1000, ( void * ) 0, 1, NULL );
xTaskCreate( prvPrintTask, "Print2", 1000, ( void * ) 1, 2, NULL );
/* 创建守护任务。这是唯一一个允许直接访问标准输出的任务。 */
xTaskCreate( prvStdioGatekeeperTask, "Gatekeeper", 1000, NULL, 0, NULL );
/* Start the scheduler so the created tasks start executing. */
vTaskStartScheduler();
}
/* 如果一切正常,main()函数不会执行到这里,因为调度器已经开始运行任务。但如果程序运行到了这里,
很可能是由于系统内存不足而无法创建空闲任务。第五章会提供更多关于内存管理的信息 */
for( ;; );
}
static void prvPrintTask( void *pvParameters )
{
int iIndexToString;
/* Two instances of this task are created. The task parameter is used to pass
an index into an array of strings into the task. Cast this to the required type. */
iIndexToString = ( int ) pvParameters;
for( ;; )
{
/* 打印输出字符串,不能直接输出,通过队列将字符串指针发送到守护任务。队列在调度器启动之前就
创建了,所以任务执行时队列就已经存在了。并有指定超时等待时间,因为队列空间总是有效。 */
xQueueSendToBack( xPrintQueue, &( pcStringsToPrint[ iIndexToString ] ), 0 );
/* 等待一个伪随机时间。注意函数rand()不要求可重入,因为在本例中rand()的返回值并不重要。但
在安全性要求更高的应用程序中,需要用一个可重入版本的rand()函数 – 或是在临界区中调用rand()
函数。 */
vTaskDelay( ( rand() & 0x1FF ) );
}
}
static void prvStdioGatekeeperTask( void *pvParameters )
{
char *pcMessageToPrint;
/* 这是唯一允许直接访问终端输出的任务。任何其它任务想要输出字符串,都不能直接访问终端,而是将要
输出的字符串发送到此任务。并且因为只有本任务才可以访问标准输出,所以本任务在实现上不需要考虑互斥
和串行化等问题。 */
for( ;; )
{
/* 等待信息到达。指定了一个无限长阻塞超时时间,所以不需要检查返回值 – 此函数只会在成功收到
消息时才会返回。 */
xQueueReceive( xPrintQueue, &pcMessageToPrint, portMAX_DELAY );
/* 输出收到的字符串。 */
printf( "%s", pcMessageToPrint );
fflush( stdout );
/* Now simply go back to wait for the next message. */
}
}
void vApplicationTickHook( void )
{
static int iCount = 0;
portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;
/* Print out a message every 200 ticks. The message is not written out
directly, but sent to the gatekeeper task. */
iCount++;
if( iCount >= 200 )
{
/* In this case the last parameter (xHigherPriorityTaskWoken) is not
actually used but must still be supplied. */
xQueueSendToFrontFromISR( xPrintQueue,
&( pcStringsToPrint[ 2 ] ),
&xHigherPriorityTaskWoken );
/* Reset the count ready to print out the string again in 200 ticks
time. */
iCount = 0;
}
}
守护任务的优先级低于打印任务——所以发送到守护任务的消息会一直保持在队列中,直到两个打印任务都进入阻塞态。在一些情况下,需要给守护任务赋予一个较高的优先级,消息就可以得到更快的处理——但这样做会由于守护任务的开销使得低优先级任务被推迟,直到守护任务完成对受其保护的资源的访问。