FreeRTOS【18】系统运行监控

1.开发背景

       基于 FreeRTOS 的学习已经接近尾声,事实上看似没问题的任务可能存在分配合理的地方,某个线程任务长期占用控制器,甚至是线程任务的内存已经开始慢慢泄漏并且越界使用,如果全然不知可能导致程序崩溃。因此需要监控线程的内存和 MCU 使用情况。

        事实上 FreeRTOS 开放了这样监控接口,但是需要消耗额外的资源,比较适合资源 MCU 资源丰富一点的芯片。

        系统提供接口: vTaskGetRunTimeStats,参考运行时间统计 - FreeRTOS™

        系统提供接口:uxTaskGetSystemState,参考uxTaskGetSystemState() - FreeRTOS™

2.开发需求

        定期监控线程的 CPU 和堆栈使用情况。

3.开发环境

        window10 + MDK + STM32F429 + FreeRTOS10.3.1

4.实现步骤

 FreeRTOSConfig.h 配置

#define configUSE_TRACE_FACILITY                    (1)
#define configGENERATE_RUN_TIME_STATS               (1)
#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()    (xTickCount = 0)
#define portGET_RUN_TIME_COUNTER_VALUE()            (xTaskGetTickCount())   // 时间计数

        理论上应该使用更高的时钟去监控系统的行为,这里只是要粗略估算 CPU 的使用率,所以直接使用了系统时钟,计算比较粗糙。

4.1 实现编码

#ifndef APP_MONITOR_H
#define APP_MONITOR_H

#include <stdbool.h>

#include "appTaskConfig.h"

#define MONITOR_DEFAULT_CHECK_PERIOD_MS     (2000)

#define THRESHOLD_TASK_STACK_WARNING        (64)
#define THRESHOLD_SYSTEM_STACK_WARNING      (256)
#define THRESHOLD_RUN_TIME_PERCENT          (5)     // 5% ?

#define MONITOR_TASK_STACK                  stackMonitor    // 监控线程堆栈大小
#define MONITOR_TASK_PRIORITY               priorityMonitor // 监控线程优先级


/* 设置线程监控线程默认参数 */
#ifndef MONITOR_TASK_STACK
    #define MONITOR_TASK_STACK              (256)
#endif

#ifndef MONITOR_TASK_PRIORITY
    #define MONITOR_TASK_PRIORITY           (1)
#endif

void aMonitor_Init(void);
void aMonitor_SetCheckPeriod(unsigned int periodMs);
void aMonitor_Enable(bool isEnable);
void aMonitor_ShowSystemState(void);    // 未实现

#endif

#include "appMonitor.h"

#include <stdio.h>
#include <string.h>

#include "appLog.h"
#include "appTaskConfig.h"

/* 任务管理结构体 */
typedef struct
{
    unsigned int checkPeriodMs;
    TaskHandle_t taskMonitor;
    
    #define LIST_BUFF_DIV       (4)
    #define LIST_BUFF_SIZE      (1024)
    #define LIST_BUFF_LOG_SIZE  (LIST_BUFF_SIZE / LIST_BUFF_DIV)
    char pcWriteBuffer[LIST_BUFF_SIZE];
    
}Ctrl_t;

static Ctrl_t s_ctrl = {
    
    .checkPeriodMs  = MONITOR_DEFAULT_CHECK_PERIOD_MS,
    .taskMonitor    = NULL,
    
};
static Ctrl_t *p = &s_ctrl;

static void MonitorTask(void* pvParameters);


/** 
 * @brief  系统监控任务
 * @param  pvParameters 传入参数
 * @return void
 */
static void MonitorTask(void* pvParameters)
{
    vTaskDelay(1000);
    
    /* 遍历所有存在的线程最小堆栈大小 */
    UBaseType_t taskNumber = uxTaskGetNumberOfTasks();
    TaskStatus_t *pxTaskStatusArray = pvPortMalloc(taskNumber * sizeof(TaskStatus_t)); 
    uxTaskGetSystemState(pxTaskStatusArray, taskNumber, NULL);
    for (int i = 0; i < taskNumber; i++)
    {
        Log_Info("%s Space = %d Bytes\r\n", pxTaskStatusArray[i].pcTaskName,
                pxTaskStatusArray[i].usStackHighWaterMark);
    }
    vPortFree(pxTaskStatusArray);
    Log_Info("FreeRTOS Remain Space = %d Bytes\r\n", xPortGetFreeHeapSize());
    
    /* 监控系统内存使用情况 */
    for ( ; ; )
    {
        /* 间隔时间查询 */
        vTaskDelay(s_ctrl.checkPeriodMs / (1000 / configTICK_RATE_HZ));  // FreeRTOS 延时
        
        /* 查询总系统最小堆栈大小 */
        if (xPortGetMinimumEverFreeHeapSize() <= THRESHOLD_SYSTEM_STACK_WARNING)
        {
            Log_Warn("FreeRTOS Min Remain Space Warning, Space = %d Bytes\r\n",
                xPortGetMinimumEverFreeHeapSize());
        }
        
        /* 遍历所有存在的线程最小堆栈大小 */
        taskNumber = uxTaskGetNumberOfTasks();
        pxTaskStatusArray = pvPortMalloc(taskNumber * sizeof(TaskStatus_t)); 
        uxTaskGetSystemState(pxTaskStatusArray, taskNumber, NULL);
        for (int i = 0; i < taskNumber; i++)
        {
            if ((pxTaskStatusArray[i].usStackHighWaterMark * 4) <= THRESHOLD_TASK_STACK_WARNING)
            {
                Log_Warn("%s Stack Waring, Now Space = %d Bytes\r\n", 
                    pxTaskStatusArray[i].pcTaskName, pxTaskStatusArray[i].usStackHighWaterMark);
            }
            
            /* 线程使用率高打印 */
            float usedRate = (float)pxTaskStatusArray[i].ulRunTimeCounter / xTaskGetTickCount() * 100;
            if (usedRate >= THRESHOLD_RUN_TIME_PERCENT)
            {
                Log_Info("%s pcTaskName = %s, usedRate = %.2f%%\r\n",
                         __func__, pxTaskStatusArray[i].pcTaskName, usedRate);
            }
        }
        vPortFree(pxTaskStatusArray);
        
#if (1)
        /* 统计空闲时间 */
        static int timeCountMs = 0;
        timeCountMs += p->checkPeriodMs;
        if (timeCountMs >= 6 * 1000)       // 60 * 1000
        {
            timeCountMs = 0;
            memset(p->pcWriteBuffer, 0, LIST_BUFF_SIZE);
            vTaskGetRunTimeStats(p->pcWriteBuffer);
            
            int offset = 0;
            char sendData[LIST_BUFF_LOG_SIZE + 1] = {0};
            
            Log_Info("\r\n\r");
            Log(eLog_Info, "任务名         运行计数     使用率\r\n\r");
            for (int i = 0; i < LIST_BUFF_DIV; i++)
            {
                memcpy(sendData, p->pcWriteBuffer + offset, LIST_BUFF_LOG_SIZE);
                offset += LIST_BUFF_LOG_SIZE;
                Log(eLog_Info, "%s", sendData);
            }
            Log(eLog_Info, "---------------------------------------------\r\n");
        }
        
#endif
    
    }
}

/** 
 * @brief  系统监控的初始化
 * @param  void
 * @return void
 */
void aMonitor_Init(void)
{
    xTaskCreate(MonitorTask, "MonitorTask", MONITOR_TASK_STACK, 
            NULL, MONITOR_TASK_PRIORITY, &p->taskMonitor);
}

/** 
 * @brief  线程监控开启和关闭 
 * @param  isEnable 开启 = true / 关闭 = false
 * @return void
 */
void aMonitor_Enable(bool isEnable)
{
    if (isEnable)
    {
        vTaskResume(p->taskMonitor);
    }
    else
    {
        vTaskSuspend(p->taskMonitor);
    }
}

/** 
 * @brief  设置线程监控检测周期 
 * @param  periodMs 检测内存周期/ms
 * @return void
 */
void aMonitor_SetCheckPeriod(unsigned int periodMs)
{
    p->checkPeriodMs = periodMs;
}

/** 
 * @brief  显示系统运行状态 
 * @param  void
 * @return void
 */
void aMonitor_ShowSystemState(void)
{
    
}

4.2 结果显示 

4.3 代码分析

        代码中使用了 2 种方法获取任务的运行时间,不推荐使用 vTaskGetRunTimeStats,因为返回的数据是一大段字符串,很容易出现内存溢出,当然带来的好处就是显示清晰明了。

        通过跟踪源码发现 vTaskGetRunTimeStats 实际上调用的就是 uxTaskGetSystemState ,所以直接使用 uxTaskGetSystemState 获取成员状态即可。

#if ( ( configGENERATE_RUN_TIME_STATS == 1 ) && ( configUSE_STATS_FORMATTING_FUNCTIONS > 0 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )

	void vTaskGetRunTimeStats( char *pcWriteBuffer )
	{
	TaskStatus_t *pxTaskStatusArray;
	UBaseType_t uxArraySize, x;
	uint32_t ulTotalTime, ulStatsAsPercentage;

		#if( configUSE_TRACE_FACILITY != 1 )
		{
			#error configUSE_TRACE_FACILITY must also be set to 1 in FreeRTOSConfig.h to use vTaskGetRunTimeStats().
		}
		#endif

		/*
		 * PLEASE NOTE:
		 *
		 * This function is provided for convenience only, and is used by many
		 * of the demo applications.  Do not consider it to be part of the
		 * scheduler.
		 *
		 * vTaskGetRunTimeStats() calls uxTaskGetSystemState(), then formats part
		 * of the uxTaskGetSystemState() output into a human readable table that
		 * displays the amount of time each task has spent in the Running state
		 * in both absolute and percentage terms.
		 *
		 * vTaskGetRunTimeStats() has a dependency on the sprintf() C library
		 * function that might bloat the code size, use a lot of stack, and
		 * provide different results on different platforms.  An alternative,
		 * tiny, third party, and limited functionality implementation of
		 * sprintf() is provided in many of the FreeRTOS/Demo sub-directories in
		 * a file called printf-stdarg.c (note printf-stdarg.c does not provide
		 * a full snprintf() implementation!).
		 *
		 * It is recommended that production systems call uxTaskGetSystemState()
		 * directly to get access to raw stats data, rather than indirectly
		 * through a call to vTaskGetRunTimeStats().
		 */

		/* Make sure the write buffer does not contain a string. */
		*pcWriteBuffer = ( char ) 0x00;

		/* Take a snapshot of the number of tasks in case it changes while this
		function is executing. */
		uxArraySize = uxCurrentNumberOfTasks;

		/* Allocate an array index for each task.  NOTE!  If
		configSUPPORT_DYNAMIC_ALLOCATION is set to 0 then pvPortMalloc() will
		equate to NULL. */
		pxTaskStatusArray = pvPortMalloc( uxCurrentNumberOfTasks * sizeof( TaskStatus_t ) ); /*lint !e9079 All values returned by pvPortMalloc() have at least the alignment required by the MCU's stack and this allocation allocates a struct that has the alignment requirements of a pointer. */

		if( pxTaskStatusArray != NULL )
		{
			/* Generate the (binary) data. */
			uxArraySize = uxTaskGetSystemState( pxTaskStatusArray, uxArraySize, &ulTotalTime );

			/* For percentage calculations. */
			ulTotalTime /= 100UL;

			/* Avoid divide by zero errors. */
			if( ulTotalTime > 0UL )
			{
				/* Create a human readable table from the binary data. */
				for( x = 0; x < uxArraySize; x++ )
				{
					/* What percentage of the total run time has the task used?
					This will always be rounded down to the nearest integer.
					ulTotalRunTimeDiv100 has already been divided by 100. */
					ulStatsAsPercentage = pxTaskStatusArray[ x ].ulRunTimeCounter / ulTotalTime;

					/* Write the task name to the string, padding with
					spaces so it can be printed in tabular form more
					easily. */
					pcWriteBuffer = prvWriteNameToBuffer( pcWriteBuffer, pxTaskStatusArray[ x ].pcTaskName );

					if( ulStatsAsPercentage > 0UL )
					{
						#ifdef portLU_PRINTF_SPECIFIER_REQUIRED
						{
							sprintf( pcWriteBuffer, "\t%lu\t\t%lu%%\r\n", pxTaskStatusArray[ x ].ulRunTimeCounter, ulStatsAsPercentage );
						}
						#else
						{
							/* sizeof( int ) == sizeof( long ) so a smaller
							printf() library can be used. */
							sprintf( pcWriteBuffer, "\t%u\t\t%u%%\r\n", ( unsigned int ) pxTaskStatusArray[ x ].ulRunTimeCounter, ( unsigned int ) ulStatsAsPercentage ); /*lint !e586 sprintf() allowed as this is compiled with many compilers and this is a utility function only - not part of the core kernel implementation. */
						}
						#endif
					}
					else
					{
						/* If the percentage is zero here then the task has
						consumed less than 1% of the total run time. */
						#ifdef portLU_PRINTF_SPECIFIER_REQUIRED
						{
							sprintf( pcWriteBuffer, "\t%lu\t\t<1%%\r\n", pxTaskStatusArray[ x ].ulRunTimeCounter );
						}
						#else
						{
							/* sizeof( int ) == sizeof( long ) so a smaller
							printf() library can be used. */
							sprintf( pcWriteBuffer, "\t%u\t\t<1%%\r\n", ( unsigned int ) pxTaskStatusArray[ x ].ulRunTimeCounter ); /*lint !e586 sprintf() allowed as this is compiled with many compilers and this is a utility function only - not part of the core kernel implementation. */
						}
						#endif
					}

					pcWriteBuffer += strlen( pcWriteBuffer ); /*lint !e9016 Pointer arithmetic ok on char pointers especially as in this case where it best denotes the intent of the code. */
				}
			}
			else
			{
				mtCOVERAGE_TEST_MARKER();
			}

			/* Free the array again.  NOTE!  If configSUPPORT_DYNAMIC_ALLOCATION
			is 0 then vPortFree() will be #defined to nothing. */
			vPortFree( pxTaskStatusArray );
		}
		else
		{
			mtCOVERAGE_TEST_MARKER();
		}
	}

<think>我们正在讨论如何为FreeRTOS中的监控任务(例如栈监控任务)分配合理的堆栈空间。 监控任务通常需要执行一些相对轻量级的操作,比如周期性地获取各个任务的状态信息(包括栈高水位线)并打印或记录。 但是,由于涉及到调用FreeRTOS的API(如uxTaskGetSystemState, uxTaskGetStackHighWaterMark等)以及可能使用printf等输出函数(这些函数可能消耗较多栈空间),我们需要谨慎分配。 步骤: 1. 估算基本需求:监控任务本身需要一定的栈空间来保存局部变量、函数调用返回地址等。特别要注意,调用uxTaskGetSystemState会分配一个TaskStatus_t数组,这个数组是在堆上分配的(通过pvPortMalloc),所以不会占用任务栈空间,但是处理这个数组时的循环和临时变量会占用栈空间。 2. 考虑最大深度函数调用:例如,调用uxTaskGetSystemState -> 然后循环处理每个任务,调用uxTaskGetStackHighWaterMark,再调用vprintf(如果使用日志输出)等。其中vprintf可能消耗较多栈空间。 3. 安全裕量:在计算出的最小值上增加安全裕量(比如20%-50%),以应对未来代码变动。 具体建议: - 首先,可以给监控任务分配一个初始值(例如在32位系统上分配1024字,即4096字节)。 - 然后,在任务中调用uxTaskGetStackHighWaterMark来监控它自身的高水位线,根据实际使用情况调整。 示例代码: 创建监控任务: xTaskCreate(vStackMonitorTask, "StackMonitor", 1024, NULL, tskIDLE_PRIORITY + 1, NULL); 在vStackMonitorTask中,定期检查自己的栈使用情况: UBaseType_t uxHighWaterMark = uxTaskGetStackHighWaterMark(NULL); // 如果这个值接近0,说明栈分配不足;如果剩余很多,则可以减少分配。 调整策略: - 如果高水位线小于100字(即剩余栈空间在历史最低点时还有100字),则考虑增加栈空间。 - 如果高水位线大于总分配栈空间的30%,则可以考虑减少栈空间(但要保留足够余量)。 注意事项: - 监控任务的优先级通常设置为较低(但高于空闲任务),避免影响关键任务。 - 监控任务的执行周期不宜太短,以免占用过多CPU。 另外,使用printf等函数时,可以考虑使用栈占用较小的实现(如使用semihosting或重定向到串口且使用简单的输出函数)。 总结:通过实际运行监控任务,观察其高水位线,然后调整栈空间大小,是最可靠的方法。 分配步骤: 1. 初始分配:根据经验值(例如1024字)分配。 2. 压力测试:在系统最繁忙时运行监控任务,确保其能正常工作。 3. 调整:根据高水位线调整栈大小,保留10%-20%的余量。 示例监控任务代码: ```c void vStackMonitorTask(void *pvParameters) { const TickType_t xDelay = pdMS_TO_TICKS(5000); // 每5秒监控一次 for(;;) { UBaseType_t uxArraySize = uxTaskGetNumberOfTasks(); TaskStatus_t *pxTaskStatusArray = pvPortMalloc(uxArraySize * sizeof(TaskStatus_t)); if(pxTaskStatusArray != NULL) { // 获取系统任务状态 uxArraySize = uxTaskGetSystemState(pxTaskStatusArray, uxArraySize, NULL); // 遍历每个任务,获取其栈高水位线 for(UBaseType_t i=0; i<uxArraySize; i++) { UBaseType_t uxHighWaterMark = uxTaskGetStackHighWaterMark(pxTaskStatusArray[i].xHandle); // 这里可以打印任务名和栈高水位线 printf("Task: %s, HWM: %u\n", pxTaskStatusArray[i].pcTaskName, uxHighWaterMark); } vPortFree(pxTaskStatusArray); } else { // 内存分配失败处理 } // 检查自身栈使用 UBaseType_t selfHWM = uxTaskGetStackHighWaterMark(NULL); printf("Monitor Task HWM: %u\n", selfHWM); vTaskDelay(xDelay); } } ``` 根据打印出的Monitor Task HWM,调整创建任务时的栈分配大小。 特殊考量: - 如果使用动态内存分配,注意pvPortMalloc可能失败,需要处理。 - 输出信息时,避免在中断中调用,且注意输出速度,避免阻塞太久。 §§相关问题§§ 1. 在FreeRTOS中,如何确定监控任务的优先级以避免影响系统实时性? 2. 除了栈空间,监控任务在分配堆空间时需要注意哪些问题? 3. 如何减少FreeRTOS监控任务中printf等输出函数对栈空间的消耗? 4. 当监控任务自身检测到栈空间接近耗尽时,应该采取什么应急措施? 5. 在资源受限的嵌入式系统中,如何平衡监控任务的运行频率和系统开销?</think>### 如何为FreeRTOS监控任务分配合理的堆栈空间 为监控任务分配堆栈空间需平衡资源消耗和可靠性,以下是系统化的分配方法: #### 一、基础计算方法 1. **栈空间组成公式**: ``` 总栈空间 = 基础开销 + API开销 + 数据缓冲区 + 安全裕量 ``` - 基础开销:任务控制块+函数调用帧(约100-150字) - API开销:`uxTaskGetSystemState()`等函数的栈消耗 - 数据缓冲区:临时存储任务状态数据的空间 - 安全裕量:推荐增加20-30%余量 2. **最小空间参考值**: | 功能复杂度 | 最小栈空间 (字) | 典型场景 | |------------------|-----------------|-------------------------| | 基本监控 | 256-384 | 仅记录高水位线 | | 中等诊断 | 384-512 | 包含任务状态快照 | | 高级分析 | 512-768 | 带日志输出的实时分析 | #### 二、动态分配策略 ```c // 在任务函数内动态调整栈 void vMonitorTask(void *pvParams) { // 初始空间评估 UBaseType_t initMark = uxTaskGetStackHighWaterMark(NULL); // 动态扩容机制 if(initMark < SAFETY_THRESHOLD) { vTaskDelay(pdMS_TO_TICKS(100)); UBaseType_t newSize = uxTaskGetStackHighWaterMark(NULL) * 1.5; vTaskSetStackSize(xTaskHandle, newSize); // 需自定义实现 } // 主监控循环 for(;;) { /* 监控操作 */ } } ``` #### 三、配置最佳实践 1. **缓冲区优化**: ```c // 使用静态缓冲区减少栈消耗 static TaskStatus_t taskStats[MAX_TASKS]; void vMonitorTask(void *pvParams) { uxTaskGetSystemState(taskStats, MAX_TASKS, NULL); // ... } ``` 2. **安全阈值设置**: ```c #define STACK_SAFETY_THRESHOLD(configMINIMAL_STACK_SIZE * 3) if(uxTaskGetStackHighWaterMark(NULL) < STACK_SAFETY_THRESHOLD) { vLogWarning("栈空间不足!"); // 触发预警 } ``` 3. **内存保护配置**(在FreeRTOSConfig.h): ```c #define configCHECK_FOR_STACK_OVERFLOW 2 // 启用深度检查 #define configSTACK_DEPTH_TYPE uint32_t // 确保足够位宽 ``` #### 四、分阶段调试流程 ```mermaid graph TD A[分配初始空间] --> B{运行基础测试} B -->|通过| C[压力测试] B -->|失败| D[增加30%空间] C --> E{高水位线 > 阈值?} E -->|是| F[优化代码结构] E -->|否| G[减少10%空间] G --> H[验证稳定性] H -->|稳定| I[确定为最终值] H -->|失败| D ``` #### 五、特殊场景处理 1. **RTOS感知调试器**: - 使用SystemView或Tracealyzer实时监测栈消耗 - 在IDE中直接查看栈使用热力图 2. **内存受限系统**: - 采用分时监控策略:每次循环只检查部分任务 - 使用`xTaskGetStaticBuffers()`减少动态分配 3. **多核系统**: ```c // ESP32双核示例 #if CONFIG_FREERTOS_UNICORE #define MONITOR_STACK 1536 // 单核 #else #define MONITOR_STACK 2048 // 双核需更大空间 #endif ``` > **关键提示**:最终栈大小应满足 `实测高水位线 > (总栈空间/3)` 且绝对剩余值 > 128字节
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值