《嵌入式软件的时间分析》读书活动:5 第五章读书笔记-软件时间分析方法
<p id="u13b52fd9">第五章是本书的重点,也是本书篇幅占比最大的一个章节。 主要介绍了在不同的开发阶段使用的各种软件时间分析方法。</p><p id="ubcf890e0">目录如下:</p>
<pre>
<code>第5章 软件时间分析方法 69
5.1 概览及在不同层面上的分类 69
- 5.1.1 通信层级 69
- 5.1.2 调度层级 70
- 5.1.3 代码层级 71
5.2 术语定义 72
- 5.2.1 追踪 72
- 5.2.2 分析、时间测量和(再次)追踪 72
5.3 静态代码分析 73
- 5.3.1 基础功能和工作流 73
- 5.3.2 用例 75
- 5.3.3 静态代码分析的限制 76
- 5.3.4 静态代码分析专家访谈 78
5.4 代码仿真 80
- 5.4.1 功能与工作流 80
- 5.4.2 用例 81
- 5.4.3 静态代码仿真的限制 82
- 5.4.4 与代码仿真领域专家的访谈 83
5.5 运行时间测量 86
- 5.5.1 基本功能和工作流 86
- 5.5.2 用例 95
- 5.5.3 运行时间测量的限制 96
5.6 基于硬件的追踪 97
- 5.6.1 基本功能和工作流 97
- 5.6.2 用例 99
- 5.6.3 基于硬件的追踪的限制 101
- 5.6.4 基于硬件的追踪专家访谈 102
5.7 基于软件方法的追踪 108
- 5.7.1 基本功能和工作流 108
- 5.7.2 用例 115
- 5.7.3 基于测量的追踪的限制 115
- 5.7.4 基于测量的追踪领域专家访谈 116
5.8 调度模拟 121
- 5.8.1 基本功能和工作流 121
- 5.8.2 用例 124
- 5.8.3 调度模拟的限制 124
- 5.8.4 调度模拟专家访谈 125
5.9 静态调度分析 127
- 5.9.1 基本功能和工作流 128
- 5.9.2 用例 129
- 5.9.3 静态调度分析的限制 131
- 5.9.4 静态调度分析专家访谈 131
5.10 使用进化算法进行优化 135
5.11 时间分析方法在 V-Model中的应用 137</code></pre>
<h1 data-lake-index-type="2" id="TJnX2">1. 概念及分类</h1>
<ul>
<li data-lake-index-type="0" id="uacb1d57f">通信层级: 通信层级的时间通常与网络总线上的经过时间有关。关注的重点一般是是通信层级上<strong>端到端</strong>的时间(例如,从传感器到执行器),或者从软件中的一个事件到服务器上另一个事件的时间差;</li>
<li data-lake-index-type="0" id="u9408ba8b">调度层级: 调度层级(RTOS 层级)的时间会影响操作系统所有与时间有关的行为。因此,调度层级也被称为操作系统层级或 RTOS 层级;</li>
<li data-lake-index-type="0" id="u4cd425b6">代码层级: 对代码层级的元素执行时间分析时,重点是元素的处理和所需的时间。代码层级的中央时间参数是净运行时间(CET,核心执行时间)。</li>
</ul>
<h1 data-lake-index-type="2" id="fheUy">2. 术语</h1>
<h2 data-lake-index-type="2" id="agO6x">2.1. 追踪</h2>
<p id="u669fb8c6">追踪是指在存储器中记录事件并附加时间戳的过程。在以后的某个时间点,此追踪存储内容可用于重现原始事件发生时的情况。</p>
<h2 data-lake-index-type="2" id="xD1fC">2.2. 分析、时间测量和(再次)追踪</h2>
<p id="u70c87a89">分析(Profiling)是指访问运行中嵌入式系统或模拟的时间参数的过程。显示 了两种分析方法,其中一种方法是执行运行时间测量,以直接确定所需的时间参数;另一 种方法则是通过追踪间接进行。在追踪时,最初仅收集追踪数据,然后便可从中提取时间 参数。</p>
<h1 data-lake-index-type="2" id="EdPLk">3. 静态代码分析</h1>
<p id="u3a00ea33">本章重点讲解了软件时间的静态代码分析,主要研究如何确定特定代码片段(例如函数)可能的最大内核执行时间。</p>
<p id="uc2ed3cde">可能的最大CET是指WCET(最坏情况执行时间),实际应表述为 WCCET(最坏情况核心执行时间)。</p>
<h2 data-lake-index-type="2" id="V3x3Q">3.1. 分析流程</h2>
<p id="u9e256dde">首先,静态代码分析读取可执行文件并将其反汇编,通过反汇编的代码,可以推导出控制流和函数调用树(函数的调用关系),此分析还能确定最大循环迭代次数。</p>
<p id="ud91f661e">收集的数据合并在一起,可能的最大运行时间将沿着控制流累加。因为可执行文件包含所有机器指令和数据的存储地址,所以此分析甚至可以考虑缓存和流水线对运行时的影响。但是此分析还需要非常精确的处理器模型。</p>
<p id="u0fc2f34a">由此可见,该分析方式对工程师的能力要求极高,所以一般都是使用工具进行分析。</p>
<div style="text-align: center;"></div>
<p id="u701833fe">书中提到了一个工具AbsInt,可以用来做静态最大执行时间的分析。我搜索了一下,这个工具有30天的试用,但是没有提供直接的下载链接,而是填写pdf提交申请,下载比较麻烦,所以就没有尝试。</p>
<h2 data-lake-index-type="2" id="rcMFx">3.2. 静态分析的限制</h2>
<p id="ucdafc136">静态代码的分析还是有很多困难的,如下:</p>
<ul>
<li data-lake-index-type="0" id="u4ebf859a">间接函数调用: 在某些情况下,静态分析将无法确定此参数可以取哪些值。在这种情况下,函数调用树并不完整。</li>
<li data-lake-index-type="0" id="uaf65aff2">递归: 递归深度不好确定。</li>
<li data-lake-index-type="0" id="u7ab99560">循环上界:无法确定最大循环迭代次数,计算 WCET 需要此数据。一般使用一个可能过大的值进行分析,从而导致明显的高估。</li>
<li data-lake-index-type="0" id="u701a441d">注释:用户必须通过提供其他信息来手动阐明上述三个“绊脚石”。也就是说,必须注释代码(由代码开发者提供)。</li>
<li data-lake-index-type="0" id="ua1f1aa8e">运行模式和互斥代码:一般一套程序都会有几种不同的工作模式(比如飞机的“在地面上”、“飞行中”),静态分析的时候各个模式的代码会有交叉,导致分析困难</li>
<li data-lake-index-type="0" id="u19e02f06">高估: 即使可以完全完成分析并注释应用程序工作模式,分析结果通常WCET仍会出乎意料的高。造成这种情况的原因之一可能是高估,即报告的 WCET 上界与实际 WCET 之间 的差异很大。</li>
<li data-lake-index-type="0" id="u3926a93a">中断、多核和暂时性错误: 静态代码分析是一种代码层级的方法,因此不涉及任何调度方面。存储器 接口上的中断或访问冲突(多核设备上经常发生)将不予考虑。 为确保完整性,静态代码分析也将忽略暂时性错误。</li>
</ul>
<h2 data-lake-index-type="2" id="EVtkG">3.3. 代码仿真</h2>
<p id="u1b636930">代码仿真器可以在 PC(x86 架构)上为任何处理器执行机器代码,PC 模拟了其他处理器。</p>
<p id="u7b2ce109">目标处理器的编译器也被称为交叉编译器(Cross-compiler)。</p>
<p id="u05910d5c">代码仿真通常涉及少部分代码的检查,例如单个函数或算法。仿真器由可以模拟目标处理器的软件组成,可以执行为目标处理器生成的可执行文件。为了使单元测试更接近真实目标系统,可以针对目标处理器对其进行编译,然后再使用代码仿真器执行。</p>
<p id="u210613bd">静态代码仿真的限制:仿真过程如果没有使用“正确的”变量,可能与真实情况出现很大的区别。</p>
<p id="u98f17629">》》文中通过作者和代码仿真专家的访谈,说明了代码仿真的介绍和一些可能会遇到的问题,受益匪浅。</p>
<h2 data-lake-index-type="2" id="L52zj">3.4. 运行时间测量</h2>
<ol>
<li data-lake-index-type="0" id="u96c91eae">测试方式一:普通IO口来测试</li>
</ol>
<p id="u62d122e4">传统的时间测量方式是通过IO口的翻转来测量,方法很简单:</p>
<ul>
<li data-lake-index-type="0" id="udbaacf66">初始化的时候设置某个IO口为高电平;</li>
<li data-lake-index-type="0" id="u64e9c232">在需要测试的代码前面将此IO口设置为低电平;</li>
<li data-lake-index-type="0" id="u9fb360ff">然后在测试代码之后将此IO口设置为高电平;</li>
<li data-lake-index-type="0" id="u1ea51e89">然后通过示波器或者逻辑分析仪采集IO口由高变低,然后低电平持续的时间就显示待测代码段执行的时间。</li>
</ul>
<p id="u269181f0">个人总结(非本书内容):这种方式虽然简单,但是这种方式测量的时候需要考虑到IO口翻转的执行执行时间,如果待测试的代码本来就执行很短的时间,则可能会导致测试结果不准确。</p>
<ol start="2">
<li data-lake-index-type="0" id="udcb43a3c">不使用IO口测试(使用硬件定时器)</li>
</ol>
<p id="ub2a37a9b">使用IO口来测试也存在一些弊端,比如不能自动化测试、需要示波器或者逻辑分析仪辅助,相对繁琐。还可以使用硬件定时器来做测试。</p>
<p id="u6a43628a">测试方式:</p>
<ul>
<li data-lake-index-type="0" id="u1effd5cf">待测试代码之前获取定时器的counter值;</li>
<li data-lake-index-type="0" id="uf4a6b296">待测试代码之后再次捕获定时器的counter值;</li>
<li data-lake-index-type="0" id="u06494bba">然后两个值作差,通过定时器的频率就可以算出执行时间。</li>
</ul>
<p id="uf971b36d">这种方式需要减去定时器值获取本身的开销,即单独调用值获取函数,记录消耗的counter数,然后在总时间中减去2倍此counter。</p>
<p id="u9abd20cc">此方式测量还需要考虑定时器是否溢出,一般使用无符号变量可以允许一次溢出,但是如果多次溢出就不适用,需要考虑减小定时器的频率,或者更换位宽更宽的定时器。</p>
<ol start="3">
<li data-lake-index-type="0" id="uc5e6c541">OSEK PreTaskHook/PostTaskHook</li>
</ol>
<p id="ud779881f">OSEK 引入了一种运行时间测量机制,也可用于 AUTOSAR CP,即PreTaskHook/PostTaskHook 机制。在调用任务的应用程序代码之前,PreTaskHook 运行,任务结束时,PostTaskHook 运行。</p>
<p id="u534f54f6">但是也存在一些缺点,比如,不能知道是哪个任务启动的,而且hook函数会被频繁调用,测试出来结果准确度不行。</p>
<ol start="4">
<li data-lake-index-type="0" id="ub388f58c">空循环计数器测量CPU负载</li>
</ol>
<p id="u61d30ba2">吐槽:这个方式看了大半天才看懂,咋说呢?感觉像是机翻,着实有点不好理解,如果换成国人常用的表述则好理解很多。</p>
<p id="ud90ad611">方法如下:</p>
<ul>
<li data-lake-index-type="0" id="ub4363d4d">这种方式在测量前需要脚本参数,即关闭全局中断、看门狗、各种监控成都等一切可能会打断代码运行的功能;</li>
<li data-lake-index-type="0" id="u2a0d6bfc">在空闲循环中让计数器值自增,经过一段时间得到的值为Z0(这是一个标准值,也是基准值,也就是在空闲循环中将一个变量自增);</li>
<li data-lake-index-type="0" id="u4402c9ad">然后开始正产功能运行(中断打开,看门狗打开...)</li>
<li data-lake-index-type="0" id="u8eb0dc89">统计空闲函数执行的时间Z(变量自增后的大小)</li>
<li data-lake-index-type="0" id="u351baaa2">CPU负载率就是1-(Z/Z0)。</li>
</ul>
<ol start="5">
<li data-lake-index-type="0" id="udcc95ef0">“性能计数器”进行测量</li>
</ol>
<p id="ucbb6f097">很多处理器提供的硬件功能都可以在代码执行期间确定与时间密切相关的参数,其中包括缓存未命中的数量和多个核(同时)访问共享存储器时的冲突次数以及流水线推迟次数。</p>
<p id="ucab99507">这个需要根据特定的处理器来实现。</p>
<ol start="6">
<li data-lake-index-type="0" id="u53c51715">ping进行测量</li>
</ol>
<p id="u122ec824">使用网络诊断工具ping,可以测量简单请求从客户端发送出去、经过网络到达主机再返回的响应时间。</p>
<h1 data-lake-index-type="2" id="FDgtZ">4. 基于硬件的追踪</h1>
<p id="u5a67b4d3">现在的处理器一般都提供调试器可以用于监控指令()追踪的运行,比如TRACE32,书中只做了相关知识的介绍,具体软件使用还需要参考工具手册。</p>
<p id="u1dbc16f9">追踪逻辑的确切工作方式取决于使用的处理器,但都有一个共同点:只有少数决定性事件记录在追踪存储器中,而执行的大多数指令都是重构时被插值进来的。</p>
<p id="ufd86a377">基于硬件的追踪提供了一种解决方法:可以在运行期间观测要检查的软件(即无须停止)并记录执行路径。与基于软件的追踪不同,该方法不需要修改软件就能实现这一目的。</p>
<p id="u91b79c27">除了“调试”用例以外,基于硬件的追踪还特别适用于运行时间分析。利用此方法,可以确定追踪期间所执行函数的所有时间参数。通常会有一个视图显示按CPU负载排序的所有已执行函数,这对于运行时间优化特别有用。</p>
<p id="u6336067a">本章节还描述了作者和硬件追踪专家的访谈记录,看了很受启发。</p>
<p id="u35d1abac"> </p>
<div style="text-align: center;"></div>
<p id="u1b0031dd"> </p>
<h1 data-lake-index-type="2" id="gwVCW">5. 基于软件方法的追踪</h1>
<p id="ud514d3d8">下面这张图说明了基于硬件方法的追踪、基于软件方法和外部存储器追踪数据缓存的追踪、单纯基于软件方法的追踪。</p>
<div style="text-align: center;"></div>
<p id="u1e42ebf8">基于软件追踪(外部追踪数据存储器):追踪数据的获取基于软件测量。与外部追踪硬件的连接可以通过所有可用的接口来实现(SPI,以太网等)。此方式的时间戳可以由追踪软件本身生成(开销大)并与其他信息一起传输到外部,也可以由外部硬件生成(准确度会有影响)。</p>
<p id="ua0dd0af7">基于纯软件的追踪:处理器的RAM来存储这些追踪数据,然后通过其他方式传输到外部。</p>
<p id="u14c7462b">追踪测量的细节注意事项还有很多,以及对不同的对象的追踪,书中都有描述,还需细细品味。</p>
<h1 data-lake-index-type="2" id="m3dhv">6. 调度模拟</h1>
<p id="u399e9755">调度模拟是对操作系统组织任务和中断的执行逻辑进行模拟。</p>
<p id="u7f74b0d7">调度模拟流程:</p>
<ul>
<li data-lake-index-type="0" id="ub297e645">必须为特定的调度方法配置模拟,即必须选择用于模拟的操作系统;</li>
<li data-lake-index-type="0" id="ub5e0ad3c">创建任务和中断,并定义与调度相关的参数。最重要的参数是优先级,其他参数涉及任务多次激活或“可抢占”设置。</li>
</ul>
<div style="text-align: center;"></div>
<p id="u5fceb54d">调度模拟工作方式:</p>
<p id="udb4c96fc">模拟期间将根据激活模式激活任务并触发中断。对于任务或中断的每次模拟执行,将根据指定的分布以及BCET和WCET随机确定运行时间(CET)。如果存在由另一个任务或中断造成的中断,则也会映射到模拟中,同样使用随机选择的CET。通常会生成与调度相关的所有事件的追踪图表(至少是所有任务的激活、开始和结束,以及所有中断的开始和结束)。追踪数据能够实现可视化,时间参数也可以根据追踪数据计算得出。</p>
<p id="u73213ffc">作者也记录了和调度模拟专家的访谈对话,很有帮助,其中提到了一个工具"TA Tool Suite",可用于调度模拟。</p>
<div style="text-align: center;"></div>
<h1 data-lake-index-type="2" id="ht8WR">7. 静态调度分析</h1>
<p id="u3476562f">静态调度分析是在调度层级执行,它是一种按“数学方法”确定最坏情况时间参数的机制,无须借助模拟、测量或追踪。此处所说的时间参数是指调度层级的时间参数,尤其是响应时间。</p>
<p id="uc720586e">静态调度分析也可以在通信层级进行。比如:可以通过这样的方式验证和优化CAN总线通信,以确保尽管总线负载明显高于广泛使用的40%,但是所有报文仍能在各自的截止时间内完成传输。</p>
<p id="u4c6136ee">如下说明了调度分析的工作原理:</p>
<div style="text-align: center;"></div>
<div style="text-align: center;"></div>
<p id="u7438641f">文中提到了一个工具:INCHRON 的 chronVAL,可以用于静态调度分析。</p>
<h1 data-lake-index-type="2" id="IVh9s">8. 使用进化算法进行优化</h1>
<p id="ubce1a3c1">进化算法用于解决调度可能非常复杂,至于无法轻松计算时间参数(例如任务的响应时间RT)的问题。</p>
<div style="text-align: center;"></div>
<p id="ub0311e0a">过程:</p>
<ul>
<li data-lake-index-type="0" id="ubb3dd9cf">首先指定优化目标,例如最小化任务的响应时间;</li>
<li data-lake-index-type="0" id="u1b6dfe6a">定义自由度,即在优化过程中可能更改的参数。这些参数可能包括周期性务的偏移或某些任务的优先级;</li>
<li data-lake-index-type="0" id="ue4e2146f">最后便是启动实际的优化。简单地说,就是先随机改变形成自由度的参数,然后进分析,并考虑对优化目标的最终影响。追踪参数修改,以确保达到优化目标,然后重新开始整个过程。随机修改参数类似于进化中的突变。成功修改后的“基因组成”将逐渐占上风,经过几代之后,配置将不断完善,并且越来越接近优化目标。如果优化目标得以充实现,或者超过了先前定义的时间,则会停止演化。</li>
</ul>
<h1 data-lake-index-type="2" id="pAnKj">9. 总结</h1>
<p id="u968c1e60">静态代码分析:静态代码分析需要已编译完成并链接的代码。但是,也可以把要测试的函数链接到测试应用程序(例如,通过单元测试)。对于静态代码分析,分析工具必须支持所用的处理器。</p>
<p id="uf5005a02">代码仿真:代码仿真与静态代码分析的情况类似,只是它还可以在比函数层级更高的层级。</p>
<p id="u17756e3d">运行时间测量:只要具备附带所需处理器和相应编译器工具链的评估版,就可以在PIL(Processor In the Loop,处理器在环)测试中执行运行时间测量。此方法可以在后续开发过程中以及最终产品中使用,以便在常规运行中监视运行时间。</p>
<p id="u47f73413">基于硬件的追踪:也需要具备处于运行状态的处理器。使用此方法时,会在代码层级和调度层级执行广泛的分析。如果情况允许,还可以将这些分析扩展到 HIL(Hardware In the Loop,硬件在环)环境,甚至扩展到最终的产品环境。</p>
<p id="ua11c7522">基于软件方法的追踪:严格地说,追踪可以与运行时间测量同时开始,但是调度通常是分析的关键方面。因此,这种方法的前提条件是有一个可用的系统并且此系统上已经在运行操作系统。</p>
<p id="u1bfe2d8f">调度模拟:此项分析在很大程度上与处理器、编译器、硬件或软件的可用性无关。只需大致了解系统的情况,就足以在较高的层级进行模拟、分析和优化。可以将后续开发过程中增加的信息添加到模型中,从而使其随着时间的推移越来越详细。</p>
<p id="u8f192512">静态调度分析:静态调度分析与调度模拟的情况非常类似,只是分析的侧重点有所不同,因为它更明确地考虑了最坏情况。但是,静态调度分析无法提供对系统正常行为的分析。</p>
<p id="u51159db4">该章节描述了各种事件分析方法,很多方法也是第一次听说,还介绍了一些时间分析的工具,一些章节记录了和相关专家的对话,总的来说,这个章节收益颇丰。</p>
<p id="u11dba934"> </p>
页:
[1]