《嵌入式软件的时间分析》-第四章分享 CPU负载计算实践
<div class='showpostmsg'><p>本章介绍了CPU负载计算相关原理,我们就来基于RTOS进行实践。</p><p> </p>
<p> </p>
<h1> </h1>
<p > </p>
<h1 > </h1>
<p > </p>
<p > </p>
<p > </p>
<p > </p>
<p > </p>
<p > </p>
<h1><b>一.前言</b></h1>
<p >这一篇我们来分享CPU负载计算的原理和实现,先介绍基本原理然后基于FreeRTOS的实现进行介绍。这里推荐一本书《嵌入式软件的时间分析——汽车行业领域的嵌入式软件理论,分析及实践》[英]皮特.格利瓦(Peter Gliwa)著。第四章介绍了相关的时间参数的定义,以及CPU负载的计算原理等。</p>
<p > </p>
<ul>
<li ><b>RTOS和前后台系统的低功耗程序架构</b></li>
</ul>
<p > </p>
<p >这里顺带简单提一下RTOS和前后台程序进行低功耗处理的一般方式,了解一般架构也有助于介绍CPU负载的计算原理,RTOS和前后台程序有点不一样。</p>
<p >对于RTOS在无任务需要处理时,会执行到最低优先级的空闲任务,在空闲任务中可以使CPU进入WFI/WFE等待事件或者等待中断的状态,此时CPU不再执行,在系统滴答定时器中断时或者其他事件中断时唤醒CPU继续运行处理,然后重复上述过程。 当然这种低功耗处理不是很低。如果要更低,可以在空闲任务时进入更低功耗的模式比如深度睡眠模式等。进入不同程度的睡眠模式则需要不同的唤醒方式,比如一般睡眠可以通过一些外设中断唤醒,深度睡眠一般可以通过外部中断或者RTC唤醒,唤醒后可以接着原来的地方运行(当然前面一般会进入对应的唤醒源的中断处理)。而更深度的掉电模式一般只能通过复位唤醒,此时就是相当于软重启了。</p>
<p >对于前后台程序一般的处理方式是,主循环作为类似空闲任务的角色,功能代码在中断中进行处理,这和RTOS有点不一样。 中断中功能逻辑处理完后则进入主循环进入低功耗,进入低功耗的方式和前面介绍的一样。唤醒后则进入中断处理任务,处理完又进入主循环重新进入低功耗。当然也可以实现简单的调度,在主循环中实现功能代码处理和低功耗处理,但是没有前面介绍的方式自然,因为前后台程序本来就是中断进入前台处理任务,天然的就和低功耗的需求模式对应(即平常不工作对应后台,需要工作时中断到前台执行对应前台处理任务)。</p>
<p > </p>
<ul>
<li ><b>CPU负载计算原理</b></li>
</ul>
<p >对于活动的CPU即使无事情要做,CPU也会一直执行指令,所以在RTOS中有一个空闲任务执行一个死循环不执行任何功能代码(支持低功耗处理的则在此进入低功耗模式)。而对于前后台程序也是类似,无需要处理任何事情时主循环也是一直会执行。当然上述是不考虑低功耗处理的情况,低功耗的处理模式见上一节说明。</p>
<p >我们简单的理解CPU负载即CPU执行有用业务代码的时间占总时间的比例,当然我们实测时这个总时间一般是抽取一定时间间隔来进行,我们假设这个间隔时间为t(0), 而这段时间内执行有效功能代码的时间是t(e),那么CPU负载U=t(e)/t(0),是一个0~1的百分比范围。</p>
<p >而整个时间要么执行功能代码,要么执行空闲代码,所以假设执行空闲代码是时间为t(i)则</p>
<p >t(e)+t(i)=t(0),</p>
<p >所以上述式子也可以为</p>
<p >U=1-t(i)/t(0).</p>
<p >下图比较好的说明了该公式的原理,书中以U=t(e)/t(0)来进行说明的,t(e)即各功能任务的执行时间和,t(0)-t(e)为空闲任务的执行时间。</p>
<p > </p>
<p >所以对应的计算U也有两种方式,一种是通过测量t(e)来进行,一种通过测量t(i)来进行,</p>
<p >前者需要累加所有功能任务的执行时间,而后者只需要测量空闲任务执行时间,相对而言后者更简单。对于RTOS,t(e)即各种任务执行的时间,需要累加所有任务的执行时间,其实会麻烦点,而t(i)即空闲任务的的执行时间,空闲任务只有一个这样比较好测量,所以使用U=1-t(i)/t(0),计算更方便。一般都是使用该方式计算的。</p>
<p > </p>
<p >而上述公式还有一个比较重要的参数即t(0)如何选取,选取较小则可能无法真实测量出来有效的CPU负载,因为可能这个时间内都是在执行功能代码或者都是执行空闲代码,测量出来CPU负载要不是0要不是1,选取较大则计算时间可能过长,是长时间的平均值,计时数据可能溢出,且不能实时反应当前状态。所以一般t(0)需要根据具体情况而定,比较常用的可以考虑各个周期任务的最小公倍数,有些功能任务变化很大的,比如汽车的安全相关的设备,可能在触发初始条件前都是大部分时候空闲的,而检测到触发条件比如碰撞之后,后续会高密度的执行一系列检测,判断等功能任务执行较密集,这种不同时间点变化很大。后面这部分时间就需要设置t(0)较小,才能真实反应当前状态,前期可以设置较大。</p>
<p > </p>
<p >对于前后台任务,如果是前面低功耗处理模式中介绍的中断中执行业务,主循环作为空闲的的模式,则测量主循环执行时间和总执行时间占比即可,把主循环执行时间作为空闲任务的执行时间。如果不是这种方式,那就根据实际测量空闲执行的时长即可。</p>
<p > </p>
<p >当然上面还有一个隐藏的参数没有介绍,就是计时的方式,一般使用硬件定时进行计时,这里也需要注意硬件定时器设置的精度高则计时时长短,反之设置计时时间长则精度差,所以也要平衡选取,考虑时间的溢出问题。 很多CPU会提供一个高位宽比如64位的计数器专门用于类似的效率统计,比如RISC-V内核就有64位的计数器作为该功能用,当然没有类似计数器的则可以使用通用硬件定时器。</p>
<p > </p>
<ul>
<li ><b>FreeRTOS中的CPU负载计算实现</b></li>
</ul>
<p >FreeRTOS中没有实时计算CPU负载的接口,但是提供了每个任务的当前总运行时间的参数,提供了每个任务运行以来的平均CPU占比。</p>
<p >所以我们可以基于以上信息,间接的的进行计算,即按照间隔时间t0去获取这段时间内,空闲任务执行时间的变化ti,1-ti/t0即近似CPU负载。</p>
<p > </p>
<p >实际以上记录的就是空闲任务的以t0间隔的实时CPU使用率,类似的其他任务也可以获取。</p>
<p >即我们可以获取每个任务的实时CPU使用率,然后根据空闲任务的实时CPU使用率计算整个CPU的负载。</p>
<p > </p>
<p > </p>
<h2><b>4.1 每个任务的运行时间统计</b></h2>
<p >我们先介绍下FreeRTOS每个任务的运行时间的记录原理,以及每个任务的CPU使用率的统计方法。</p>
<p > </p>
<p >FreeRTOS中每个任务的TCB中都有一个参数,在任务创建时调用prvInitialiseNewTask时初始化为0,当然相应的宏configGENERATE_RUN_TIME_STATS需要使能。</p>
<p >#if( configGENERATE_RUN_TIME_STATS == 1 )</p>
<p > uint32_t ulRunTimeCounter; /*< Stores the amount of time the task has spent in the Running state. */</p>
<p >#endif</p>
<p > </p>
<p > </p>
<p >以下接口可以获取空闲任务的执行时间参数,我们可以利用其来计算CPU负载。</p>
<p >#if( ( configGENERATE_RUN_TIME_STATS == 1 ) && ( INCLUDE_xTaskGetIdleTaskHandle == 1 ) )</p>
<p >TickType_t xTaskGetIdleRunTimeCounter( void )</p>
<p >{</p>
<p > return xIdleTaskHandle->ulRunTimeCounter;</p>
<p >}</p>
<p >#endif</p>
<p > </p>
<p >vTaskGetInfo中也可以获取指定任务的该参数,当然该函数需要使能宏configUSE_TRACE_FACILITY</p>
<p >#if ( configGENERATE_RUN_TIME_STATS == 1 )</p>
<p > {</p>
<p > pxTaskStatus->ulRunTimeCounter = pxTCB->ulRunTimeCounter;</p>
<p > }</p>
<p >#else</p>
<p > {</p>
<p > pxTaskStatus->ulRunTimeCounter = 0;</p>
<p > }</p>
<p >#endif</p>
<p > </p>
<p >任务执行时间的测量原理如下,在上下文切换时</p>
<p >即函数vTaskSwitchContext中如下代码,实现ulRunTimeCounter的累加,即累加该任务的执行时间。</p>
<p > </p>
<p >#if ( configGENERATE_RUN_TIME_STATS == 1 )</p>
<p > {</p>
<p >#ifdef portALT_GET_RUN_TIME_COUNTER_VALUE</p>
<p > portALT_GET_RUN_TIME_COUNTER_VALUE( ulTotalRunTime );</p>
<p >#else</p>
<p > ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();</p>
<p >#endif</p>
<p > </p>
<p > /* Add the amount of time the task has been running to the</p>
<p > accumulated time so far. The time the task started running was</p>
<p > stored in ulTaskSwitchedInTime. Note that there is no overflow</p>
<p > protection here so count values are only valid until the timer</p>
<p > overflows. The guard against negative values is to protect</p>
<p > against suspect run time stat counter implementations - which</p>
<p > are provided by the application, not the kernel. */</p>
<p > if ( ulTotalRunTime > ulTaskSwitchedInTime ) {</p>
<p > pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );</p>
<p > } else {</p>
<p > mtCOVERAGE_TEST_MARKER();</p>
<p > }</p>
<p > ulTaskSwitchedInTime = ulTotalRunTime;</p>
<p > }</p>
<p >#endif /* configGENERATE_RUN_TIME_STATS */</p>
<p > </p>
<p > </p>
<p >如下图所示,假设A切换到B执行,此时</p>
<p > ulTotalRunTime = portGET_RUN_TIME_COUNTER_VALUE();获取的是当前时刻,</p>
<p >ulTaskSwitchedInTim是记录的任务A开始执行时的时刻,是在上一次vTaskSwitchContext时更新的。pxCurrentTCB->ulRunTimeCounter += ( ulTotalRunTime - ulTaskSwitchedInTime );即累加当前A任务的执行时间。</p>
<p > </p>
<p >然后ulTaskSwitchedInTime = ulTotalRunTime;更新全局变量ulTaskSwitchedInTime为当前时刻,即下一个任务B开始执行的时刻。</p>
<p > </p>
<p >后续B的执行时间累加也是类似。</p>
<p > </p>
<p > </p>
<p > </p>
<p >当然获取时间戳需要实现接口</p>
<p >portGET_RUN_TIME_COUNTER_VALUE或portALT_GET_RUN_TIME_COUNTER_VALUE</p>
<p > </p>
<p >vTaskGetRunTimeStats可以打印每个任务执行时间占用总执行时间的比例。</p>
<p >需要配置对应的宏。#if ( ( configGENERATE_RUN_TIME_STATS == 1 ) && ( configUSE_STATS_FORMATTING_FUNCTIONS > 0 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )</p>
<p >这里打印的是运行以来总的比例,不是实时的占比。</p>
<p > </p>
<p >注意:以上实现,没有考虑定时器的溢出问题,</p>
<p >即(定时器绕回时( ulTotalRunTime - ulTaskSwitchedInTime )可能小于0.</p>
<p >所以依赖于接口</p>
<p >portALT_GET_RUN_TIME_COUNTER_VALUE或portGET_RUN_TIME_COUNTER_VALUE</p>
<p >必须不能溢出,必须保持单调递增,所以就需要合适的选择定时器的精度以兼顾总时长和精度。对于1us精度,32位最长时间只能记录1个多小时。</p>
<p > </p>
<h2 ><b>4.2 实现实时的CPU负载计算</b></h2>
<p > </p>
<p >获取当前时刻和空闲任务执行时长,隔一段时间在此获取当前时刻和空闲任务执行时长。</p>
<p >1 - 两次空闲任务时长差/当前时刻差即CPU负载, 注意获取当前时刻和空闲任务执行时长要保证其时刻一致性,所以使用临界段保护,避免获取完一个后调度执行其他任务回来时时间已经差了很多。</p>
<p > </p>
<p > </p>
<p >进入临界段</p>
<p >t00 = portGET_RUN_TIME_COUNTER_VALUE();</p>
<p >ti0=xTaskGetIdleRunTimeCounter();</p>
<p >退出临界段</p>
<p > </p>
<p >进入临界段</p>
<p >t01 = portGET_RUN_TIME_COUNTER_VALUE()</p>
<p >ti1=xTaskGetIdleRunTimeCounter();</p>
<p >退出临界段</p>
<p > </p>
<p >则计算的CPU负载百分比如下</p>
<p >U=100 - (ti1-ti0)*100/(t01-t00)</p>
<p > </p>
<p >我这里在某个任务中,每隔10ms进行一次计算</p>
<p > </p>
<p >#include "FreeRTOS.h"</p>
<p >#include "task.h"</p>
<p > </p>
<p > </p>
<p >任务循环</p>
<p >uint32_t t00,t01,ti0,ti1;</p>
<p >while(1)</p>
<p >{</p>
<p > Alloc_Critical();</p>
<p > Enter_Critical(); </p>
<p > t00 = portGET_RUN_TIME_COUNTER_VALUE();</p>
<p > ti0 = xTaskGetIdleRunTimeCounter();</p>
<p > Exit_Critical(); </p>
<p > </p>
<p > </p>
<p > //间隔20ms</p>
<p > </p>
<p > </p>
<p > Enter_Critical();</p>
<p > t01 = portGET_RUN_TIME_COUNTER_VALUE();</p>
<p > ti1= xTaskGetIdleRunTimeCounter();</p>
<p > Exit_Critical();</p>
<p > </p>
<p > printf("cpu:%d\r\n",100-(ti1-ti0)*100/(t01-t00));</p>
<p > </p>
<p >}</p>
<p >portmacro.h中定义获取时间戳的宏</p>
<p > </p>
<p >#if ( configGENERATE_RUN_TIME_STATS == 1 )</p>
<p >extern void vPortRunTimerInit(void);</p>
<p >extern uint32_t vPortGetRunTimerValue(void);</p>
<p >extern uint32_t timer_get_time(void);</p>
<p >#define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() vPortRunTimerInit()</p>
<p >#define portGET_RUN_TIME_COUNTER_VALUE() timer_get_time() </p>
<p >#endif</p>
<p > </p>
<p > </p>
<p >打印如下</p>
<p > </p>
<p > </p>
<p >还可以使用上位机进行可视化</p>
<p > </p>
<h1 ><b>五.总结</b></h1>
<p >前面我们介绍了CPU负载计算的原理实现等内容。</p>
<p > </p>
<p >按照我们以往的文章的思路,介绍完一个知识点后我们还需要扩展下,到目前为止我们介绍完了CPU负载相关的原理和实现。但是上述数据其实不够形象化,我们可以把上述实时的数据通过串口,或者其他方式导入到电脑进行分析,可视化处理等。</p>
<p >但是这样太复杂,效率不高,实时性不够,我们想能不能直接直观的看到CPU负载的实时情况呢,有! 我们之前已经分享过类似的工程实践方法了,参考文章</p>
<p >《<a href="https://mp.weixin.qq.com/s/C8UgVLY-011w4GWQa7N6hg" target="_blank">https://mp.weixin.qq.com/s/C8UgVLY-011w4GWQa7N6hg</a>超级精简系列之:高效且运行时间固定的内存池实现》即介绍了使用IO翻转的方式来实时观察执行时间。</p>
<p >我们也可以类似的使用IO翻转的方式来观察空闲任务的时间,即在进入空闲任务(主循环空闲周期)内翻转IO,退出时再翻转IO,这样就可以通过实时的IO状态来查看空闲的占空比,密度,波动情况等,非常直观,可以快速发现波动大,变化异常的地方。可以对关心的任务,每个任务使用一个IO,使用逻辑分析仪就可以实时观察多个任务的情况。</p>
<p > </p>
<p >当然以上其实没有考虑上下文切换,中断执行等时间,按照空闲任务执行时间计算时,因为是反的,算出来的CPU负载是包括这些时间的。如果是累加各任务的执行时间来算的则没有包括这些时间,算出来负载值要偏小。使用IO翻转方式时我们可以类似的使用IO观测中断执行时间,上下文切换时间等。</p>
<p > </p>
<p > </p>
<p> </p>
</div><script> var loginstr = '<div class="locked">查看本帖全部内容,请<a href="javascript:;" style="color:#e60000" class="loginf">登录</a>或者<a href="https://bbs.eeworld.com.cn/member.php?mod=register_eeworld.php&action=wechat" style="color:#e60000" target="_blank">注册</a></div>';
if(parseInt(discuz_uid)==0){
(function($){
var postHeight = getTextHeight(400);
$(".showpostmsg").html($(".showpostmsg").html());
$(".showpostmsg").after(loginstr);
$(".showpostmsg").css({height:postHeight,overflow:"hidden"});
})(jQuery);
} </script><script type="text/javascript">(function(d,c){var a=d.createElement("script"),m=d.getElementsByTagName("script"),eewurl="//counter.eeworld.com.cn/pv/count/";a.src=eewurl+c;m.parentNode.insertBefore(a,m)})(document,523)</script> <p>没有考虑上下文切换,中断执行等时间,按照空闲任务执行时间计算时,因为是反的,算出来的CPU负载是包括这些时间的</p>
<p>谢谢提醒</p>
<p>原来负载原理是这样的</p>
<p><img height="48" src="https://bbs.eeworld.com.cn/static/editor/plugins/hkemoji/sticker/facebook/lol.gif" width="48" />有些贴图是反的</p>
<p>谢谢分享</p>
<p>原来CPU里还有空闲任务,这个是不是所谓的空闲代码执行的时间</p>
<p>这是程序员设计发挥的地方吗?</p>
<p>CPU负载过高时有什么风险吗,可以动态调整空闲任务执行的频率和时长吗</p>
页:
[1]