APP下载

mbedOS实时操作系统延时机制剖析

2023-08-05刘长勇王宜怀

长春师范大学学报 2023年6期
关键词:蓝灯绿灯线程

刘长勇,王宜怀

(1.武夷学院数学与计算机学院,福建 武夷山 354300;2.苏州大学计算机科学与技术学院,江苏 苏州 215006;3.武夷学院 认知计算与智能信息处理福建省高校重点实验室,福建 武夷山 354300)

在无操作系统的嵌入式系统运行过程中,常常会设计一个绿色指示灯,让它按某个固定的周期不停地闪烁(实际上就是按一定的时间频率不断切换亮暗),以此来表明系统正在运行,这个固定的周期就是延时,一般采用原地跑(空循环)的方式进行或通过定时器进行定时中断,在这种方式下程序仍然占用CPU的使用权,其他程序只能等待空循环结束或定时器中断执行完之后才能得以运行。而在带有实时操作系统(Real-time Operation System,RTOS)的嵌入式系统中,会使用延时函数进行延时,此时RTOS的内核会暂时把不需执行的任务放入延时队列中,让出CPU的使用权,并对线程进行调度。使用延时函数的延时操作并非停止其他操作的空跑等待,而是由内核通过延时队列管理这些延时任务,从而实现对任务的延时。目前,延时操作被广泛地应用于各个领域。张辉等[1]指出可以根据火面环境、星历、地形图以及火星车的工作能力和约束条件,通过延时指令序列控制火星车完成火星表面巡视探测任务。樊智勇等[2]设计了一种大数据量低延时航电中继系统,实现了将不同类型信号在多种设备之间进行远程异地低延时数据交换。岑伯维等[3]通过在串联支路、并联支路、孤立支路和复合支路上对延时进行建模,有效地降低了电子物联网边缘计算终端业务的时序逻辑结构对计算资源配置的影响。王旭等[4]提出了基于ARM设计时钟同步与触发单元对分布式测试系统进行同步的方法,通过延时触发输出,实现了亚微秒级时钟同步精度和精确同步触发功能。张华健等[5]结合Linux系统和开源机器人操作系统ROS,设计了一个低成本、可扩展、高性能的开源移动机器人,实现了在同一局域网内低延时的远程图像传输和控制。黄雨航等[6]基于可验证延时函数为基础的随机信标方法,结合Boneh-Lynn-Shacham(BLS)聚合签名算法,在区块链场景中生成公平且安全的随机数。李川[7]通过向注入器电子枪等相关设备发送可精确延时调节的触发信号,确保了HLS-II时序系统在Linux操作系统下可将直线加速器ns级长度的电子宏脉冲对准储存环220 ns长度范围内的任一相位的稳定。本文将分析RTOS延时函数的基本工作原理和RTOS的调度策略,剖析mbedOS实时操作系统延时函数的调度机制和关键代码,以意法半导体的STM32L431芯片为例进行mbedOS延时函数剖析实践,将线程响应延时函数的调度过程信息进行直观输出,对线程调度过程时序进行梳理与剖析,有助于读者更加透彻地理解mbedOS延时函数响应机制,也可为不同实时操作系统的延时函数的比较分析提供借鉴。

1 延时函数的基本原理

在RTOS中,为了避免程序空跑占用CPU,采用延时函数来实现对任务的延时管理。在任务中执行延时函数时,RTOS内核会将当前任务状态由激活态更改为阻塞态,并根据延时参数指示时间插入到延时队列的相应位置,该队列中的任务按照延时时长从小到大排序,每一个任务控制块都记录了自身需要的等待唤醒时间(等待唤醒时间=任务本身的延时时间-所有前驱结点的等待时间)。在任务延时期间,RTOS内核会调度其他就绪队列的任务运行,当所有任务都因延时进入延时队列后,此时RTOS内核会调度空闲任务运行。在空闲任务运行期间,SysTick定时器中断会每隔1个时间嘀嗒检测1次延时队列中的任务是否到期,若有到期的任务则将任务从延时队列移出,并将任务状态由阻塞态更改为就绪态,放入就绪队列中,等待RTOS的再次调度运行。在不同的RTOS中延时函数的基本工作原理是相同的,但延时函数的名称及参数会略有不同,如在mbedOS实时操作系统中使用sleep_for延时函数,其参数millisec为32位的整型,表示延时的嘀嗒数[8];在MQX实时操作系统中使用time_delay_ticks延时函数,其参数time_in_ticks为32位的整型,表示延时的嘀嗒数[9];在RT-Thread实时操作系统中使用rt_thread_sleep延时函数,其参数tick为32位的整型,表示延时的嘀嗒数[10-11]。

2 RTOS的调度策略分析

在剖析延时函数响应机制的过程中,会涉及任务的管理和调度机制。任务的管理主要涉及就绪队列和延时队列,就绪队列管理将运行的就绪态任务,延时队列管理因调用延时函数而被阻塞的任务。任务调度主要是完成对任务状态的切换、任务进出队列的管理以及上下文切换等工作。在mbedOS中任务也称为线程,对线程采用优先级抢占和时间片轮询的综合调度策略,调度策略是通过系统服务调用(Supervisor Call,SVC中断)、可挂起系统调用(Pendable Supervisor,PendSV中断)和定时器中断(SysTick中断)来实现的。

2.1 SVC中断

为实现用户程序对系统硬件的间接访问,RTOS内核往往会为用户提供系统服务函数,用户通过调用这些函数,触发SVC中断,实现对系统硬件的访问。在mbedOS中执行SVC指令时(如在线程中调用延时函数),将触发SVC中断服务程序,完成指定的功能。SVC中断被触发后一般会被立即执行,有较高的精确性,如图1所示。

图1 触发SVC中断的简单示例

2.2 SysTick中断

ARM Cortex-M系列内核中提供了一个24位的SysTick定时器,采用倒计时的方式计数,当减1计数到0时,可产生SysTick中断。mbedOS的内核时钟频率采用48 MHz,1个时间嘀嗒为1 ms,每个时间片为5个时间嘀嗒,当一个时间嘀嗒到时则产生一次SysTick中断,在中断服务程序中完成对线程的调度,如图2所示。

图2 触发SysTick中断的简单示例

3 mbedOS延时函数调度机制剖析

3.1 SVC中断触发的延时调度

当用户线程调用线程延时函数sleep_for后,会触发SVC中断,其内部函数调用顺序为ThisThread∶∶sleep_for→osDelay→__svcDelay→触发SVC中断服务程序SVC_Handler→实际调用svcRtxDelay→最终调用osRtxThreadWaitEnter。线程延时等待函数osRtxThreadWaitEnter的主要功能包括获取当前正在运行的线程、阻塞当前运行线程、将阻塞的线程根据延时时长插入延时队列中、获取当前优先级最高的就绪态线程并切换其状态为激活态,该函数执行流程如图3所示。

3.2 SysTick中断触发的延时调度

在mbedOS中,每一个时间嘀嗒(1 ms)执行一次SysTick_Handler中断服务程序,通过调用osRtxTick_Handler函数完成对线程的调度,在osRtxTick_Handler函数中主要实现对到期的线程按优先级抢占策略和时间片轮询策略进行调度。

其中涉及优先级抢占调度的关键代码如下:

if ((kernel_state == osRtxKernelRunning)

&& (thread_ready != NULL) &&(thread_ready->priority >thread_running->priority))

{

osRtxThreadListRemove(thread_ready);

osRtxThreadBlock(thread_running);

osRtxThreadSwitch(thread_ready);

}

代码分析:if语句判断就绪队列最高优先级的线程(thread_ready)的优先级,如果大于当前正在运行线程的优先级,则调用osRtxThreadListRemove函数从就绪队列取出thread_ready,然后调用osRtxThreadBlock函数阻塞当前正在运行的线程,调用osRtxThreadSwitch函数切换thread_ready状态为激活态准备运行,这就是优先级抢占调度策略。

其中涉及时间片轮询调度的关键代码如下:

if (osRtxInfo.thread.robin.tick == 0U) {

if (osRtxKernelGetState() == osRtxKernelRunning){

thread = osRtxInfo.thread.ready.thread_list;

if ((thread != NULL) &&(thread->priority == osRtxInfo.thread.robin.thread->priority))

{

osRtxThreadListRemove(thread);

osRtxThreadReadyPut(osRtxInfo.thread.robin.thread);

EvrRtxThreadPreempted(osRtxInfo.thread.robin.thread);

osRtxThreadSwitch(thread);

osRtxInfo.thread.robin.thread = thread;

osRtxInfo.thread.robin.tick= osRtxInfo.thread.robin.timeout;

}}}

代码分析:第一条if (osRtxInfo.thread.robin.tick == 0U)语句指出当时间片结束(时间片=0)时,取出就绪队列最高优先级的线程thread,接着继续执行if ((thread != NULL) &&(thread->priority == osRtxInfo.thread.robin.thread->priority)),该语句指明如果thread的优先级等于当前正在运行线程的优先级,则从就绪队列取出thread,调用osRtxThreadReadyPut函数将当前运行线程放入就绪队列,切换thread状态为激活态准备运行,且重新设置时间片为5个时间嘀嗒,这就是基于时间片的轮询调度策略。

4 mbedOS延时函数响应实践分析

2014年ARM公司推出了mbedOS,它是一种专为物联网(IoT)中的“物体”设计的开源嵌入式实时操作系统(Real-Time Operating System,RTOS)[12],具备一般RTOS的基本功能,在物联网设备平台[13]、物联网应用[14]、通信技术与安全访问服务机制[15]、协议栈与 IP网络组件[16]等方面得到广泛应用。现针对mbedOS实时操作系统,给出具体延时响应的实践分析。

mbedOS的延时响应测试工程采用SD-mbedOS工程框架[17],在Kinetis Design Studio 3.0.0 IDE集成开发环境和基于Cortex-M4内核的STM32微控制器上进行测试。STM32片内Flash大小为256 KB,用于中断向量表和程序代码等的存储;片内RAM大小为32 KB,用于各类变量的存储。

4.1 功能设计与流程分析

测试工程的功能是由主线程创建三个优先级相同的用户线程,即绿灯线程、红灯线程和蓝灯线程,这三个线程分别实现每隔8 s绿灯切换亮暗、每隔4 s红灯切换亮暗和每隔2 s蓝灯切换亮暗,测试工程的执行流程如图4所示。

图4 测试工程执行流程

当mbedOS启动和主线程的执行函数app_init运行结束后,先后共创建了3个系统线程和3个用户线程[18],其相关信息如表1所示。此时,在就绪队列的线程按优先级从高到低排列依次为绿灯线程、红灯线程、蓝灯线程和空闲线程,mbedOS内核开始对线程进行调度。由于就绪队列的第一个线程是Green_Thread,它优先得到激活运行。Green_Thread线程实现每隔8 s控制一次绿灯的亮暗状态,当Green_Thread线程的执行函数run_greenlight调用延时函数sleep_for执行延时8 s时,会触发SVC中断,暂时剥夺该线程对CPU的使用权,将该线程放入延时队列中。接着mbedOS内核依次调度运行Red_Thread线程的执行函数run_redlight和Blue_Thread线程的执行函数run_bluelight,当它们执行到延时函数sleep_for时也依次被放入延时队列中,最后空闲线程得到调度运行。在mbedOS内核调度线程的期间,SysTick中断会每隔1 ms查看延时队列中的线程是否到期,对到期的线程会按优先级抢占策略和时间片轮询策略进行调度运行。

表1 线程信息一览表

4.2 基于优先级相同和延时机制的线程调度时序

根据以上分析,基于优先级相同和延时机制的用户线程调度时序情况如图5所示,图中给出了表示各对象的有效运行时间,实线箭头表示线程进入队列,虚线箭头表示从队列取线程。

图5 基于优先级相同和延时机制的用户线程调度时序图

4.3 执行流程分段解析

基于测量的确定性时序分析技术指出,可以利用在实际处理器硬件上执行感兴趣的程序来获得所观察的数据[19]。程序插桩技术也表明可以在源程序中添加一些语句来获取程序执行时的动态信息[20]。printf调试方法是应用最广泛的调试手段之一,是一种动态分析方法,具有简单、直观等优点。在嵌入式系统开发过程中可将printf函数封装成printf调试控件用来输出调试过程信息[21],在CPU仿真中可输出调试信息[22],在计算机视觉中输出中间结果[23]。本文采用基于时序图的printf调试方法对延时函数执行流程进行输出,通过结果解析和理解延时函数的响应机制以及线程调度过程。延时函数响应过程可以归纳总结为以下11个步骤,由于篇幅有限,只给出与蓝灯线程有关的输出信息。另外,地址20004530表示蓝灯线程,地址200045F0表示绿灯线程,地址200046B0表示红灯线程,地址20004260表示空闲线程,地址8005A41表示缺省处理函数DefaultISR,地址2000303C表示延时队列,地址2000302C表示就绪队列。

4.3.1 线程启动

在主线程的执行函数中创建并启动三个用户线程运行,然后主线程被阻塞。接着mbedOS内核对这些线程按照优先级抢占策略进行调度,对优先级相同的线程按时间片轮询策略进行调度,首先调度绿灯线程运行。printf输出如下信息:

0-1.MCU启动

0-2.启动绿灯线程

0-3.启动红灯线程

0-4.启动蓝灯线程

3-1.当前运行的线程=200045F0 (绿灯)开始

3-2.当前运行的线程=200045F0 (绿灯)调用延时等待函数

sleep_for->osDelay->__svcDelay->svcRtxDelay

4.3.2 绿灯线程延时8 s

绿灯线程调用延时函数ThisThread∶∶sleep_for(8000)延时8 s,放弃CPU的控制权进入延时队列,取出红灯线程激活运行。

4.3.3 红灯线程延时4 s

红灯线程调用延时函数ThisThread∶∶sleep_for(4000)延时4 s,放弃CPU的控制权进入延时队列,取出蓝灯线程激活运行。

4.3.4 蓝灯线程延时2 s

蓝灯线程调用延时函数ThisThread∶∶sleep_for (2000)延时2 s,放弃CPU的控制权进入延时队列,取出空闲线程激活运行。printf输出如下信息:

4.当前运行线程=20004530

4-1.调用osRtxThreadWaitEnter前延时队列=2000303C中的线程: 0->200046B0->200045F0

4-2.调用osRtxThreadWaitEnter前就绪队列=2000302C中的线程: 20004260->0->0

5-1.调用osRtxThreadWaitEnter->osRtxThreadDelayInsert将当前运行线程=20004530放到延时队列

5-2.调用osRtxThreadWaitEnter->osRtxThreadListGet从就绪队列获取优先级最高的线程=20004260

5-3.调用osRtxThreadWaitEnter->osRtxThreadSwitch将线程=20004260设置为激活态准备运行

4-3.调用osRtxThreadWaitEnter后延时队列: 20004260->200046B0->200045F0

4-3-1.线程延时时间: 2000->1995->3990

6.调用wait结束

4.3.5 运行空闲线程

空闲线程不做任何事情,只为了让CPU不停止运行。

4.3.6 优先级抢占调度激活蓝灯线程

在mbedOS调度期间每隔1 ms会产生一次SysTick中断,当延时达到2 s时,会从延时队列中将蓝灯线程移出,其状态由阻塞态更改为就绪态,放入就绪队列中,按优先级抢占策略激活蓝灯线程运行。printf输出如下信息:

7-1.从延时队列移出到期线程=20004530

7-2.将移出的线程=20004530放入就绪队列中

8-1.从就绪队列(2000302C)获取线程(20004530),该线程的优先级=24>当前运行线程(20004260)的优先级=1,优先级抢占

8-2.将当前运行线程(20004260) 放到就绪队列中(即阻塞当前线程)

8-3.设置线程(20004530)为激活态

4.3.7 蓝灯线程结束

当蓝灯延时结束后,蓝灯反转,接着又开始新一轮的延时等待。printf输出如下信息:

3-3.当前运行的线程=20004530 (蓝灯)反转

3-4.当前运行的线程=20004530 (蓝灯)结束

4.3.8 优先级抢占调度激活红灯线程

在mbedOS调度期间每隔1 ms会产生一次SysTick中断,当延时达到4 s时,会从延时队列中将红灯线程移出,其状态由阻塞态更改为就绪态,放入就绪队列中,按优先级抢占策略激活红灯线程运行。

4.3.9 红灯线程结束

当红灯延时结束后,红灯反转,接着又开始新一轮的延时等待。

4.3.10 优先级抢占调度激活绿灯线程

在mbedOS调度期间每隔1 ms会产生一次SysTick中断,当延时达到8 s时,会从延时队列中将绿灯线程移出,其状态由阻塞态更改为就绪态,放入就绪队列中,按优先级抢占策略激活绿灯线程运行。

4.3.11 绿灯线程结束

当绿灯延时结束后,绿灯反转,接着又开始新一轮的延时等待。

5 结语

在RTOS中线程通过使用延时函数,线程会让出CPU的使用权,使CPU能更好地为其他线程服务,提高了嵌入式系统的实时性。本文分析了延时函数的原理和RTOS调度策略,给出优先级抢占策略和时间片轮询策略的关键代码,解析了mbedOS延时函数的调度机制,构建了一个测试工程对mbedOS延时函数进行详细剖析。通过基于时序图的printf调试方法,将mbedOS延时函数的整个执行过程进行显示输出,有助于读者快速理解延时函数的使用和调度机理,为分析比较其他实时操作系统提供借鉴。

猜你喜欢

蓝灯绿灯线程
为什么红灯停,绿灯行
浅谈linux多线程协作
红灯停,绿灯行
基于上下文定界的Fork/Join并行性的并发程序可达性分析*
Linux线程实现技术研究
一路绿灯 一路关爱
么移动中间件线程池并发机制优化改进
红灯与绿灯