FreeRTOS学习笔记-4-资源管理
本章目的
- 为什么,以及在什么时候有必要进行资源管理与控制。
- 什么是临界区。
- 互斥是什么意思。
- 挂起调度器有什么意义。
- 如何使用互斥量。
- 如何创建与使用守护任务。
- 什么是优先级反转,以及优先级继承是如何减小(但不是消除)其影响的。
1.互斥
访问一个被多任务共享,或是被任务与中断共享的资源时,需要采用”互斥”技术以
保证数据在任何时候都保持一致性。
最好的互斥方法(如果可能的话,
任何时候都当如此)还是通过精心设计应用程序,尽量不要共享资源,或者是每个资源
都通过单任务访问。
2.临界区与挂起调度器
基本临界区
基本临界区是指宏 taskENTER_CRITICAL()与 taskEXIT_CRITICAL()之间的代码区间。
临界区是提供互斥功能的一种非常原始的实现方法。临界区的工作仅仅是简单地把
中断全部关掉,或是关掉优先级在 configMAX_SYSCAL_INTERRUPT_PRIORITY 及
以下的中断。
/* 为了保证对PORTA寄存器的访问不被中断,将访问操作放入临界区。
进入临界区 */
taskENTER_CRITICAL();
/* 在taskENTER_CRITICAL() 与 taskEXIT_CRITICAL()之间不会切换到其它任务。 中断可以执行,也允许
嵌套,但只是针对优先级高于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断 – 而且这些中断不允许访问
FreeRTOS API 函数. */
PORTA |= 0x01;
/* 我们已经完成了对PORTA的访问,因此可以安全地离开临界区了。 */
taskEXIT_CRITICAL();
挂起(锁定)调度器
==基本临界区==保护一段代码区间不被==其它任务或中断==打断。
==挂起调度器实现的临界区==只可以保护一段代码区间不被==其它任务==打断,因为这种方式下,中断是使能的。
void vTaskSuspendAll( void );
//通过调用 vTaskSuspendAll()来挂起调度器。挂起调度器可以停止上下文切换而不用关中断。
//如果某个中断在调度器挂起过程中要求进行上下文切换,则个这请求也会被挂起,直到调度器被唤醒后才会得到执行。
唤醒调度器
portBASE_TYPE xTaskResumeAll( void );
//如果一个挂起的上下文切换请求在xTaskResumeAll()返回前得到执行,则函数返回 pdTRUE。
//在其它情况下, xTaskResumeAll()返回 pdFALSE。
使用示例
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();
}
}
3.互斥量 Mutex
互斥量是一种特殊的二值信号量,用于控制在两个或多个任务间访问共享资源。
一个任务想要合法地访问资源,其必须先成功地得到(Take)该资源对应的令牌(成为令牌持有者)。
当令牌持有者完成资源使用,其必须马上归还(Give)令牌。
互斥量与二值信号量的小区别
- 用于互斥的信号量必须归还。
- 用于同步的信号量通常是完成同步之后便丢弃,不再归还。
创建互斥量
xSemaphoreHandle xSemaphoreCreateMutex( void );
//返回非 NULL 值表示互斥量创建成功。返回值应当保存起来作为该互斥量的句柄.
获取和归还互斥量
除了中断函数,所有的信号量都可以通过xSemaphoreTake和xSemaphoreGive函数来获取和归还/给与。(这在前面的信号量中有说明)
互斥量使用示例
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();
}
}
static void prvPrintTask( void *pvParameters )
{
char *pcStringToPrint;
/* Two instances of this task are created so the string the task will send
to prvNewPrintString() is passed into the task using the task parameter.
Cast this to the required type. */
pcStringToPrint = ( char * ) pvParameters;
for( ;; )
{
/* Print out the string using the newly defined function. */
prvNewPrintString( pcStringToPrint );
/* 等待一个伪随机时间。注意函数rand()不要求可重入,因为在本例中rand()的返回值并不重要。但
在安全性要求更高的应用程序中,需要用一个可重入版本的rand()函数 – 或是在临界区中调用rand()
函数。 */
vTaskDelay( ( rand() & 0x1FF ) );
}
}
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( ;; );
}
互斥量的缺陷
避免优先级反转和死锁的最好方法就是在设计阶段就考虑到这种潜在风险,
这样设计出来的系统就不应该会出现死锁的情况。
1.”优先级反转”
高优先级的任务 2 竟然必须等待低优先级的任务 1 放弃对互斥量的持有权。
高优先级任务被低优先级任务阻塞推迟的行为被称为”优先级反转”。
如果把这种行为再进一步放大,当高优先级任务正等待信号量的时候,一个
介于两个任务优先之间的中等优先级任务开始执行——这就会导致一个高优先级任务
在等待一个低优先级任务,而低优先级任务却无法执行!
一般解决办法:优先级继承
优先级继承暂时地将互斥量持有者的优先级提升至所有等待此互斥量的任务所具
有的最高优先级。互斥量持有者在归还互斥量时,优先级会自动设置为其原
来的优先级。
这种实现假定一个任务在任意时刻只会持有一个互斥量。
2. 死锁
当两个任务都在等待被对方持有的资源时,两个任务都无法再继续执行,这种情况就被称为死锁。
任务 A 在等待一个被任务 B 持有的互斥量,而任务 B 也
在等待一个被任务 A 持有的互斥量。死锁于是发生,因为两个任务都不可能再执行下
去了。
4.守护任务
互斥量缺陷的解决办法
守护任务是对某个资源具有唯一所有权的任务。只有守护任务才可以直接访问其守
护的资源——其它任务要访问该资源只能间接地通过守护任务提供的服务。
采用守护任务的示例
采用了一个守护任务来管理对标准输出的访问。当一个任务想要往终端写信息的时候,其不能直接调用打印函数,而是将消息发送到守护任务。
守护任务使用了一个 FreeRTOS 队列来对终端实现串行化访问。该任务内部实现
不必考虑互斥,因为它是唯一能够直接访问终端的任务。
守护任务大部份时间都在阻塞态等待队列中有信息到来。当一个信息到达时,守护
任务仅仅简单地将收到的信息写到标准输出上,然后又返回阻塞态,继续等待下一条信
息地到来。
//守护任务
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. */
}
}
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 ) );
}
}
//心跳钩子函数
//需要设置 FreeRTOSConfig.h 中的常量 configUSE_TICK_HOOK 为 1。和使用函数原型 void vApplicationTickHook( void );
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;
}
}
/* 定义任务和中断将会通过守护任务输出的字符串。 */
static char *pcStringsToPrint[] =
{
"Task 1 ****************************************************\r\n",
"Task 2 ----------------------------------------------------\r\n",
"Message printed from the tick hook interrupt ##############\r\n"
};
/*-----------------------------------------------------------*/
/* 声明xQueueHandle变量。这个变量将会用于打印任务和中断往守护任务发送消息。 */
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( ;; );
}
守护任务的优先级低于打印任务——所以发送到守护任务的消息会一直保持在队
列中,直到两个打印任务都进入阻塞态。在一些情况下,需要给守护任务赋予一个较高
的优先级,消息就可以得到更快的处理——但这样做会由于守护任务的开销使得低优先
级任务被推迟,直到守护任务完成对受其保护的资源的访问。
问题1:
资源会被抢占,那任务在对队列读写时,会不会被中断和其他任务打断?
答案:不会,因为队列的特性是读写时会阻塞。
- 由于队列可以被多个任务写入,所以对单个队列而言,也可能有多个任务处于阻塞
状态以等待队列空间有效。这种情况下,一旦队列空间有效,只会有一个任务会被解除
阻塞,这个任务就是所有等待任务中优先级最高的任务。 - 由于队列可以被多个任务读取,所以对单个队列而言,也可能有多个任务处于阻塞
状态以等待队列数据有效。这种情况下,一旦队列数据有效,只会有一个任务会被解除
阻塞,这个任务就是所有等待任务中优先级最高的任务。
(任务可以设置读写队列阻塞超时时间)
问题2:
心跳钩子函数在读写队列时阻塞,不会不导致心跳中断程序阻塞?
答案:会有可能, 如果设置了阻塞超时时间,在时间范围内就会阻塞。如果没有设置,当读空队列和写满队列的时候,会xQueueSendToFrontFromISR和xQueueReceiveToFrontFromISR函数直接返回。