基于STM32的mbedOS信号量调度机制剖析
2023-11-02刘中华王宜怀刘长勇王浩波
刘中华 王宜怀 刘长勇 王浩波
1(苏州大学计算机科学与技术学院 江苏 苏州 215006)
2(武夷学院数学与计算机学院 福建 武夷山 354300)
0 引 言
随着嵌入式实时操作系统(RTOS)[1-2]不断发展,对于共享数据访问的不一致现象屡见不鲜,而多任务[3]的并发调度是造成这一现象的主要原因。面对操作系统的同步问题,1968年荷兰计算机科学家艾兹格·迪杰斯特拉提出了信号量(Semaphore)的概念[4-5],用来实现对操作系统的资源管理[6]和多任务调度。信号量机制在常用的RTOS中一直有被应用,无论是早期出现的MQX,还是之后陆续出现的诸如μC/OS、FreeRTOS及2014年Arm公司出品的mbedOS等RTOS中,信号量机制始终被保留并不断完善[7]。因此,充分理解信号量的调度机制,有助于开发人员设计出实时性强、稳定性好的RTOS。目前,有关操作系统的信号量机制剖析主要集中在Linux、FreeRTOS、VxWorks等操作系统,并且不同的RTOS中信号量的名称和实现细节不太一样,例如FreeRTOS有二进制信号量、计数信号量、互斥量和递归互斥量,mbedOS只有互斥信号量和计数信号量;FreeRTOS中信号量的创建通过队列实现,mbedOS通过构造结构体来创建信号量[8]。但对mbedOS中的信号量调度剖析方面缺乏资料。为此,本文对mbedOS中的信号量调度机制进行理论分析,重点剖析关键函数的实现原理并加以流程图分析,利用STM32L431RC芯片结合SD-mbedOS工程框架[9]作为软硬件环境,通过多个任务使用信号量机制的并行调度实验,将实验的整个调度流程以及当前所运行的时间通过printf函数[10]进行输出显示,最后对调度机制的理论执行时间和实际执行时间进行对比,从而分析mbedOS信号量调度机制的实时性。通过对信号量调度机制进行全面剖析并分析其实时性,有助于理解调度机制的执行流程,更加了解多任务的并发调度机制,同时也为分析其他RTOS的信号量调度机制提供了基础[11]。
1 信号量的含义及其应用场合
在RTOS中,信号量通常被定义成一个提供信号的非负整型变量,来保证在多任务并发的环境下,能使得操作系统不会发生冲突,稳定运行。在操作系统的信号量机制的管理下,对共享资源的访问同步问题都可以用信号量来实现。比如一个读取数据任务和一个写入数据任务要访问共享资源缓冲区的问题,就能通过三个信号量来实现:SEM_Read,允许任务对缓冲区进行读取操作;SEM_Write,允许任务对缓存区进行写入操作;SEM_Mutex,限制缓冲区的互斥访问。在同一时刻只能允许一个读/写任务访问缓冲区,对缓冲区进行访问之前必须先获取信号量SEM_Mutex,并且任务执行完成后需释放信号量[12]。在任何一个任务中,获取信号量和释放信号量是同时存在的,意味着在任务结束的时候,并不会占用信号量。在RTOS中,信号量的调度机制如图1所示。
图1 信号量调度的一般流程
正是信号量这种有序的特性,使得信号量能应用到很多场合:多任务之间的同步进行;对共享资源的访问;为了实现更好的性能而控制任务的并发数等。
2 RTOS信号量调度机制及其关键要素
在RTOS中的同步与通信机制中,与设置事件字来表达多种可能的情况相比,信号量是一种简单的同步手段。
2.1 RTOS信号量的调度机制
采用信号量作为任务与任务间或中断与任务间的同步与通信的方法时,则必定有任务(或中断)创建信号量,同时有任务等待获取信号量[13]。信号量的获取与释放必须在同一任务中,例如在mbedOS中,任务调用Wait()函数获取信号量,实际是通过判断信号量控制块结构体的Tokens变量来决定是否允许获取信号量;在FreeRTOS中则是调用xSemaphoreTake()函数来获取信号量,判断队列句柄xHandle中的uxMessageWaiting变量来决定是否允许获取信号量。在任务执行操作完成后,会将信号量释放,例如在mbedOS中通过调用Release()函数来释放信号量;在FreeRTOS中调用xSemaphoreGive()函数来释放信号量[14]。若当前等待队列存在任务因等待获取信号量而阻塞,则会将等待队列中的优先级最高的任务移入就绪队列等待调度。
2.2 信号量调度机制的关键要素
信号量作为RTOS中任务同步与通信的重要方法之一,其主要功能是实现任务之间的同步或多任务并发执行。在信号量调度机制过程中所涉及到的关键要素有信号量的创建、获取、释放、响应、调度等[15]。
(1) 信号量的创建:指明信号量的名称,初始化信号量控制块结构体并设置信号量数值的大小。
(2) 信号量的获取:指明哪个任务或中断中请求获取信号量,等待获取信号量的时间为多少。
(3) 信号量的释放:在任务获取到信号量并执行完相关操作之后,释放信号量,若信号量的等待队列不为空,则取出任务准备进行调度。
(4) 信号量的响应:当信号量被获取后,获取信号量的任务会继续往下执行,当操作完成后,会释放信号量。
(5) 信号量的调度:当有任务释放信号量时,会将等待队列中优先级最高的任务与正在运行的任务的优先级进行比较,判断是否需要重新进行任务调度。
3 mbedOS信号量调度机制理论剖析
mbedOS信号量机制首先从创建信号量开始,从程序开始运行到主任务执行app_init()函数后,会调用Semaphore()函数来创建信号量。任务可以分别调用Wait()函数和Release()函数来进行信号量的获取和释放[16]。
下面将着重分析信号量创建、信号量获取和信号量释放的过程以及函数调用。
3.1 信号量创建过程剖析
在mbedOS中使用信号量控制块结构体来描述信号量,数据结构如下:
typedef struct{
uint8_t
id;//信号量ID
uint8_t reserved_state;
//互斥量状态
uint8_t
flags;//信号量标志
uint8_t reserved;
const char
*name;//信号量名称
osRtxThread_t *thread_list;
//信号量等待队列
uint16_t
tokens;//当前信号量的数值
uint16_t max_tokens;
//信号量的最大数值
}osRtxSemaphore_t;
信号量创建函数调用顺序为Semaphore()→Constructor()→osSemaphoreNew()→_svcSemaphoreNew()→SVC_Handler()→svcRtxSemaphoreNew()。信号量创建的流程如图2所示。
图2 信号量创建的流程
信号量的创建调用Semaphore()函数,传入参数count表示创建信号量的数值大小。紧接着调用Constructor()函数,在该函数中初始化信号量属性结构体osSemaphoreAttr_t和信号量控制块结构体osRtxSemaphore_t。当初始化结构体后,调用osSemaphoreNew()函数来创建信号量。在任务模式下会调用_svcSemaphoreNew()函数,从而触发SVC中断,转而去执行SVC_Handler中断处理函数。然后实际执行的函数是svcRtxSemaphoreNew(),由该函数来执行信号量的创建。当信号量创建之后,会对信号量等待队列thread_list是否为空进行判断,若不为空,则说明存在任务等待获取信号量,则从信号量等待队列中取出优先级最高的任务放入就绪队列,等待调度[17]。
3.2 信号量获取过程剖析
信号量获取函数调用顺序为Wait()→OsSemaphoreAcquire()→_svcSemaphoreAcquire()→SVC_Handler()→svcRtxSemaphoreAcquire()。信号量获取的流程如图3所示。
图3 信号量获取的流程
信号量获取通过调用Wait()函数,传入参数millisec设置等待信号量的时间,在该函数中调用_wait()函数,然后再调用osSemaphoreAcquire()函数。由于处于任务模式下,则会调用_svcSemaphoreAcquire()函数,在该函数中会触发SVC中断,转而去执行SVC_Handler中断处理函数。而实际执行的是svcRtxSemaphoreAcquire()函数,在函数内部来判断Tokens的数值是否大于0,若大于0,则表示任务可以获取信号量,此时首先要屏蔽系统中断,然后对信号量的数值进行减一操作,否则可能会多任务访问信号量数据出现不一致;若不大于0,则会根据参数millisec进行阻塞当前任务或者返回获取信号量失败。
3.3 信号量释放过程剖析
信号量释放函数调用顺序为Release()→OsSemaphoreRelease()→_svcSemaphoreRelease()→SVC_Handler()→svcRtxSemaphoreRelease()。信号量释放的流程如图4所示。
图4 信号量释放的流程
信号量的释放和信号量获取的过程大致相同。首先调用Release()函数,请求释放信号量,然后会跳转到osSemaphoreRelease()函数。当前处于任务模式下,则会调用_svcSemaphoreRelease()函数,从而触发SVC中断。而实际调用的是svcRtxSemaphoreRelease()函数,在该函数的执行过程中,对信号量阻塞队列进行判断,若为空,则直接调用SemaphoreTokenIncrement()函数进行释放信号量;若不为空,则调用osRtxThreadListGet()函数唤醒队列中优先级最高的任务,重新进行任务调度。
4 mbedOS信号量调度机制实践分析
以ARM Cortex-M4为内核的STM32L431RC开发芯片结合意法半导体(ST)公司研发了STM32CubeIDE为开发环境对mbedOS中的信号量调度机制进行实践。STM32L431RC芯片为64引脚LQFP封装,Flash内存为256 KB(共有128个扇区),RAM内存为64 KB。在信号量调度机制的实践中,使用了printf打桩输出调试方法,对关键步骤进行文字输出,可以更好地了解整个程序的运行。
4.1 功能设计
在SD-mbedOS工程框架下创建工程实例,实例的功能是:创建了三个优先级相同的任务Td1、Td2和Td3,数值为2的信号量SP,按照Td1、Td2和Td3的顺序启动三个任务。在Td1任务中,先请求获取信号量,获取成功后Td1任务延时5 s;在Td2任务中,获取信号量成功后,延时2 s。在Td3任务中,获取信号量后,延时5 s,然后切换STM32L431RC芯片上的绿灯的亮暗。在信号量获取和释放的前后,输出当前系统的运行时间,以便算出实际执行时间。三个任务(Td1、Td2和Td3)的内存地址分别为0x200016BC、0x2000177C和0x2000183C。实例的功能流程如图5所示。
图5 实例工程的功能流程
4.2 调度过程剖析
结合实例对mbedOS中信号量机制的调度过程进行更细致的分析,将当前运行的任务、任务的状态、系统所执行的时间用printf函数的方式进行输出。
(1) 任务启动。芯片上电启动最后转到主任务函数中执行,先后启动三个任务,然后阻塞该函数的运行,由mbedOS负责对任务的调度运行。printf输出结果如下(下同):
Td1、Td2和Td3任务启动完成,同时阻塞主任务。
(2) Td1任务请求获取信号量。在主任务阻塞后,mbedOS从就绪队列中取出优先级最高的任务(此时为Td1)开始执行。任务启动后请求获取信号量,初始信号量数值为2,Td1任务获取信号量成功,信号量数值减2变为1。
Td1任务(200016BC)请求获取SP,当前时间:3.441 44 s。
SP=2!=0,表示当前任务(200016BC)可获取SP。
Td1任务获取SP成功,当前时间:3.445 627 s,延时5 s。
(3) Td2任务请求获取信号量。当前信号量SP的数值为1,Td2任务可以获取信号量SP。
Td2任务(2000177C)请求获取SP,当前时间:3.447 029 s。
SP=1!=0,表示当前任务(2000177C)可获取SP。
Td2任务获取SP成功,当前时间:3.461 119 s,延时2 s。
(4) Td3任务请求获取信号量。由于当前SP的数值为0,Td3任务请求获取信号量失败,会将Td3任务添加到信号量阻塞队列和延时等待队列中。
Td3任务(2000183C)请求获取SP,当前时间:3.452 028 s。
SP=0,表示当前任务(2000183C)获取SP失败。
将当前任务(2000183C)放入等待队列和SP阻塞队列,获取就绪队列中的任务,当前时间:3.466 121 s。
(5) Td2任务释放信号量。当信号量SP被Td1和Td2获取之后,在Td2任务延时2 s后会释放信号量,由于在信号量阻塞队列中有一个Td3任务等待获取信号量,因此,在Td2任务释放信号量之后,会将Td3任务从延时等待队列和信号量阻塞队列中取出,并放入就绪队列中准备运行。此时Td3任务已经获取到信号量,可以看成是Td2任务将信号量转移给Td3任务,当前信号量数值还是为0。
Td2任务释放SP,当前时间:8.053 098 s。
从等待队列和SP阻塞队列中获取等待SP的任务(2000183C),当前时间:8.055 977 s。
Td2任务释放SP成功,当前时间:8.056 314 s。
Td3任务获取SP成功,当前时间:8.062 021 s,延时5 s并切换绿灯亮暗。
(6) Td2任务开始新一轮的请求获取信号量。Td2任务释放信号量后,重新开始获取信号量SP,此时信号量被Td1任务和Td3任务占据,信号量数值为0。因此,Td2任务放入信号量阻塞队列和延时等待队列中,同时从就绪队列中取出Td1任务准备运行。
Td2任务(2000177C)请求获取SP,当前时间:11.505 674 s。
SP=0,表示当前任务(2000177C)获取SP失败。
将当前任务(2000177C)放入等待队列和SP阻塞队列,获取就绪队列中的任务,当前时间:11.519 806 s。
(7) Td1任务释放信号量。Td1任务延时5 s结束,释放信号量。此时信号量阻塞队列中有Td2任务在等待获取信号量,当Td1任务释放信号量之后,将Td2任务从延时等待队列和信号量阻塞队列中取出,并放入就绪队列中准备运行。
Td1任务释放SP,当前时间:16.074 655 s。
从等待队列和SP阻塞队列中获取等待SP的任务(2000177C),当前时间:16.082 538 s。
Td1任务释放SP成功,当前时间:16.083 962 s。
Td2任务获取SP成功,当前时间:16.090 021 s,延时2 s。
(8) Td1任务开始新一轮的请求获取信号量SP。Td1任务请求获取信号量SP,当前SP数值为0,将Td1任务放入延时等待队列和信号量阻塞队列中。
Td1任务(200016BC)请求获取SP,当前时间:19.533 285 s。
SP=0,表示当前任务(200016BC)获取SP失败。
将当前任务(200016BC)放入等待队列和SP阻塞队列,获取就绪队列中的任务,当前时间:19.547 460 s。
(9) Td2和Td3任务释放信号量。Td2任务延时结束后,释放信号量。同时将Td1任务从延时等待队列和信号量阻塞队列中移出,并放入就绪队列中运行。在Td2任务释放信号量后,Td3任务延时结束释放信号量(几乎可以看作同时),此时信号量数值为1,故Td2获取信号量成功,开始运行。
Td2任务释放SP,当前时间:21.834 034 s。
从等待队列和SP阻塞队列中获取等待SP的任务(200016BC),当前时间:21.836 s。
Td2任务释放SP成功,当前时间:21.837 424 s。
Td3任务释放SP,当前时间:21.838 129 s。
Td3任务释放SP成功,当前时间:21.841 097 s。
Td1任务获取SP成功,当前时间:21.843 022 s,延时5 s。
(10) Td1、Td2和Td3任务新一轮的请求获取信号量。此时开始的运行情况和之前一样,循环之前的过程。按照Td1、Td2和Td3的顺序反复获取信号量执行。任务的调度时序图如图6所示。
图6 基于信号量机制的任务调度时序图
4.3 调度性能剖析
任务信号量的获取和释放的理论时间是判断mbedOS中信号量机制的实时性好与坏的性能标准。在SD-mbedOS架构下,系统时钟频率为48 MHz,一个指令周期的时间为0.020 8 μs。以任务请求获取信号量为例,进行理论时间和实际执行时间的比较,在信号量数值不为0的情况下,任务申请获取信号量的机器指令有461条,机器指令的条数是以执行一条_NOP指令所花费的时间为基准,所有执行的机器指令都能在编译之后生成的.lst文件中找到,关键函数及其对应的机器码和汇编指令如表1所示。
表1 关键函数及其对应的机器码和汇编指令
根据计算得:信号量获取的理论时间为10.44 μs,而在单个任务执行的情况下,信号量获取(信号量数值不为0)的实际执行时间为14.8 μs,理论时间和实际时间的误差在微秒级别,误差在可接受的范围内。
在工程实例中,将任务请求获取信号量前后、释放信号量前后的系统运行时间输出。三个任务具体的调度时间如表2所示。
表2 信号量获取和释放的实际执行时间 单位:ms
表2中获取信号量I和获取信号量II分别表示:获取信号量时信号量数值不为0和为0,获取信号量II中的时间I表示信号量数值为0,将任务添加到相应队列的时间,时间II表示在任务添加到队列中后,到获取信号量成功的时间。释放信号量I和释放信号量II分别表示:释放信号量时信号量阻塞队列为空和不为空,而时间III表示从队列中移出任务的时间,时间IV表示其他时间。
表2中的时间是结合实例工程中三个任务的延迟时间计算的,由于实例中是多任务并发,并且系统的运行状态用printf方法进行输出,故信号量调度机制中的操作耗时较多。总的来看,信号量的获取和释放需要的时间很短,具有很好的实时性。
5 结 语
mbedOS的信号量调度机制是一个较为复杂的过程,其中涉及到多任务并发调度、任务对信号量的获取和释放、就绪队列和等待队列等的管理,其中的函数调用关系也较为复杂,触发到的中断函数有SVC中断和Systick中断等。本文重点剖析mbedOS中的信号量调度机制及其关键函数,加以流程图总结,通过多任务并发的调度实验,将调度过程中任务的切换、状态的变化、当前系统运行时间进行输出,给出时序图分析,进一步验证信号量调度机制理论分析的正确性,最后还对调度过程进行实时性能剖析,结果表明信号量调度机制的实时性能较好。通过对信号量调度机制的剖析,有助于更好地理解mbedOS的多任务并发机制,也为其他RTOS的信号量机制分析提供了基础。