ARM处理器下的Fault异常 ARM处理器当发生异常事件后就会暂停当前程序的运行,处理器进入异常模式,响应一个来自处理器内核或者外设的中断请求。
这里想要处理的Fault异常就是ARM处理器多个异常中的一类,以Cortex-M7内核的ARM处理器为例,主要的Fault异常有HardFault、BusFault、UsageFault、MemManage。当发生这些异常就表示程序出现了比较严重的错误,进而导致“死机”。
嵌入式实时操作系统下发生Fault异常后会如何处理 常见的各种嵌入式实时操作系统(FreeRTOS、UCOSII/III等)都没有特别的对Fault进行处理,基本上都是按照默认处理方式来解决,也就是让处理器进入死循环。
1 2 3 4 void xxxFault_Handler (void ) { while (1 ); }
这种处理方式是无可厚非的,因为程序本身发生了致命的故障,暂停当前程序的运行或许能够避免程序发生更严重的错误。大多数单线程嵌入式软件(NoOS)都是这样处理的,一个地方的错误会导致整个系统崩溃。
但是在多任务环境下,RTOS提供了多线程运行的机制,线程之间是相对独立的运行。基于此,在RTOS中采用这种暂停处理器的方式来处理异常就不是最佳的解决方案了。例如有两个线程在运行(线程A和线程B),线程B在正常运行,线程A由于地址访问错误,导致触发HardFault异常,Fault异常会暂停整个处理器的运行,不但使发生错误的A线程停止了运行,线程B也被牵连导致运行停止。所以在RTOS中发生异常时想要将出现Fault异常而带来的损失降低到最小,也就是单独停止错误线程的运行而不结束整个系统的运行。
发生Fault异常后如何暂停异常线程 首先需要知道的是,异常并不总是由线程运行导致的,还有用户中断处理程序(IRQ)、RTOS内核调度等,在这些地方发生的异常目前看起来是比较难处理的,所以后面主要处理的就是在线程中发生的Fault异常。这里以FreeRTOS操作系统为例,对于其他的操作系统都能实现类似的解决方案,可以结合实际的处理器、操作系统平台进行修改移植,因为核心思想是一致的。 当发生Fault后不能简单的while(1)处理了,更优雅且安全的的实现要完成两步动作:
暂停错误线程的运行
将CPU的执行权限切换到正常线程中
清理异常线程 这是在FreeRTOS下的一个实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include "FreeRTOS.h" #include "task.h" #if (INCLUDE_xTaskGetIdleTaskHandle == 0) #warning "Unable to switch to a valid task!" #endif void * clean_fault_task (void ) { extern void * pxCurrentTCB; TaskHandle_t fault_task = pxCurrentTCB; if (xTaskGetSchedulerState() == taskSCHEDULER_NOT_STARTED){ return NULL ; } #if defined(INCLUDE_xTaskGetIdleTaskHandle) && (INCLUDE_xTaskGetIdleTaskHandle == 1) pxCurrentTCB = xTaskGetIdleTaskHandle(); #else return NULL ; #endif vTaskDelete(fault_task); return pxCurrentTCB; }
简单解释一下这段代码的含义。首先需要判断操作系统的任务调度有没有运行,如果操作系统还没有启动,这就说明异常不是在线程中触发的,这就不在解决范围内。然后找到一个可以切入的线程,而且它是处于就绪态的任务,显然最好的选择就是Idle线程了。通过xTaskGetIdleTaskHandle()获取到Idle线程的句柄,赋值给pxCurrentTCB。最后调用vTaskDelete()删除当前错误的线程,并返回新的当前线程就好了。
这里为什么需要先获取新的的线程再删除旧的错误线程呢?这就需要理解线程删除中发生了什么。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 if ( pxTCB == pxCurrentTCB ){ vListInsertEnd( &xTasksWaitingTermination, &( pxTCB->xStateListItem ) ); ++uxDeletedTasksWaitingCleanUp; traceTASK_DELETE( pxTCB ); portPRE_TASK_DELETE_HOOK( pxTCB, &xYieldPending ); } if ( pxTCB == pxCurrentTCB ){ configASSERT( uxSchedulerSuspended == 0 ); portYIELD_WITHIN_API(); }
这是FreeRTOS线程删除时是进行的一些操作,当判断为删除当前线程(pxCurrentTCB)是会进行的额外操作。FreeRTOS是不能直接删除线程自身的,它是将自己标记为预删除的状态,然后操作系统切换到Idle线程中时去清理这些需要被删除的线程,此外这里还会进行一次主动的线程调度,这是非常危险的,当线程出现错误后,如果继续进行常规调度流程,这可能会涉及到访问错误线程的栈空间,这是不可靠的(导致更严重的错误)。所以线程清理仅仅是让操作系统不再调度这个错误的线程,与原错误线程相关的操作降低到最少。
进行一次特别的线程调度 有何特别?这里的线程调度操作将没有切出线程,只有切入线程,因为本该切入的线程在刚刚前面那段代码中删除了。所以这里的线程调度就不能直接使用系统的API完成调度,这里需要自己实现一段线程调度的代码。这里以一段ARM汇编来实现这段调度程序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 bic r3 , lr , #7 cmn r3 , #8 beq .end bl clean_fault_taskcmp r0 , #0 beq .end ldr r0 , [r0 ]ldmia r0 !, {r4 -r11 , r14 }tst r14 , #0x10 it eqvldmiaeq r0 !, {s16 -s31 }msr psp, r0 isb mov r0 , #0 msr basepri, r0 bx r14 .end: b .end
这段汇编代码分为4个部分,第一段,这里是通过LR寄存器的值来判断触发异常是否是在普通的线程中,如果不是在线程中触发的异常将不能处理,直接跳转到最后的死循环中。第二段,调用前面实现的任务清理函数,清理掉错误线程并选择一个新的可运行线程(这里就是Idle线程),函数返回后,判断r0,如果无法清理错误线程或者没有可用的线程,仍然跳转到最后的死循环中。第三段,r0就是前面函数返回时传递过来的新的线程句柄,它存储的第一个字段就是该线程的栈,栈里面存储的内容结构涉及到FreeRTOS线程调度相关的内容。这里就简单说一说FreeRTOS的任务调度时的栈内结构,栈里面存储的是r4-r11寄存器,这些寄存器是ARM异常处理无法自动保存的。此外还有保存r14(lr)寄存器,它是用来判断栈内是否存储了浮点寄存器的,LR寄存器的bit4指示了线程是否在使用浮点寄存器。由于s0-s15以及浮点状态寄存器FPSR是直接由ARM异常自动完成存取了,所以这里还需要根据线程是否使用了浮点寄存器来存储s16-s31寄存器。
将待切入运行的线程相关的寄存器都恢复完毕后,就可以退出异常并转入到新的线程去执行了,也就是第四段代码的内容。
封装异常处理 将前面汇编代码进行封装,这样就能够处理多个Fault异常了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 #ifndef _FAULT_HANDLE_H #define _FAULT_HANDLE_H #if defined(__CC_ARM) #define FAULT_HANDLER() #elif defined(__GNUC__) #define FAULT_HANDLER() \ ({ \ __asm volatile ( \ " push {r0, r1, r2, r3, r12, lr} \n" \ \ " bic r3, lr, #7 \n" \ " cmn r3, #8 \n" \ " beq .end" END_LINE " \n" \ " \n" \ \ " bl clean_fault_task \n" \ " cmp r0, #0 \n" \ " beq .end" END_LINE " \n" \ " add sp, #24 \n" \ " \n" \ \ " ldr r0, [r0] \n" \ " ldmia r0!, {r4-r11, r14} \n" \ \ " tst r14, #0x10 \n" \ " it eq \n" \ " vldmiaeq r0!, {s16-s31} \n" \ \ " msr psp, r0 \n" \ " isb \n" \ " mov r0, #0 \n" \ " msr basepri, r0 \n" \ " bx r14 \n" \ " \n" \ ".end" END_LINE ": \n" \ " pop {r0, r1, r2, r3, r12, lr} \n" \ ); \ }) #define _STR2(x) #x #define _STR(x) _STR2(x) #define END_LINE _STR(__LINE__) #endif #endif
这段代码实现比前面的汇编多了一些内容,主要是要考虑堆栈平衡以及异常线程清理失败后的恢复线程的问题。前面处理失败是进入一个死循环中,这里没有进入死循环。处理失败后所有的寄存器会恢复到处理前的状态,这样能够为其它错误分析类的程序提供帮助,比如我前面有篇文章讲解了如何进行栈回溯,那段程序就能够衔接在这段程序前面或者后面,帮助分析异常问题。
这里需要特别提醒,栈平衡是非常重要的一点,尤其是使用汇编来操作栈空间时。要知道,bx lr后后面的代码是无法继续执行的,所以需要保证进入Fault时到执行bx lr时msp栈是平衡的。如果栈不平衡就会导致栈内存空间异常减小。 这段宏定义是内联汇编,当它嵌入到C函数后,函数头是否还有栈操作我不确定。可能不同的编译器情况不一样,但我用GCC时没有发现问题。如果你想要把这段代码移植到你的系统中,最好仔细核对栈平衡相关的内容。
相比于最开始的while(1),新的异常处理程序可以写为:
1 2 3 4 5 6 7 void xxxFault_Handler (void ) { FAULT_HANDLER(); while (1 ) { } }
这样当一个线程执行出现异常后能够自动停止并继续进行系统调度,保证了系统的持续运行。
总结 通过这种处理方式能够解决线程错误带来的死机问题,但是在实际的生产运行过程中,还需要其它辅助性质的程序来帮助程序更加稳固的运行,例如可以设计一个基于定时器的守护任务,当它监视到线程崩溃结束后自动重启它。这样才更加具有实际意义。
这段程序任然有不完美的地方,比如它不能处理非线程Fault异常,还有线程清理时是暂停它还是删除它?删除线程后相关资源是否及时回收?这些问题都需要结合实际项目进一步完善。
if (hexo-config('comment') && hexo-config('comment.enable') == true && hexo-config('comment.use')) {
if (hexo-config('comment.use') == "valine") {
@import "./valine.styl"
}
else if (hexo-config('comment.use') == "gitalk") {
@import "./gitalk.styl"
}
else if (hexo-config('comment.use') == "twikoo") {
@import "./twikoo.styl"
}
else if (hexo-config('comment.use') == "waline") {
@import "./waline.styl"
}
}
.comments-container {
display inline-block
width 100%
margin-top var(--component-gap)
.comment-area-title {
width 100%
color var(--text-color-3)
font-size 1.38rem
line-height 2
i {
color var(--text-color-3)
}
+keep-tablet() {
font-size 1.2rem
}
}
.configuration-items-error-tip {
display flex
align-items center
margin-top 1rem
color var(--text-color-3)
font-size 1rem
i {
margin-right 0.3rem
color var(--text-color-3)
font-size 1.2rem
}
}
.comment-plugin-fail {
display none
flex-direction column
align-items center
justify-content space-around
width 100%
padding 2rem
.fail-tip {
color var(--text-color-3)
font-size 1.1rem
}
.reload {
margin-top 1rem
}
}
.comment-plugin-loading {
flex-direction column
padding 1rem
color var(--text-color-3)
.loading-icon {
color var(--text-color-4)
font-size 2rem
}
.load-tip {
margin-top 1rem
color var(--text-color-4)
font-size 1.1rem
}
}
}