前言
本文主要分为以下三个部分:
- 简单介绍临界区的概念以及保护机制,并说明单核系统与多核系统的相关设计差异
- 讲述
701-142/701-143beta2-音箱SDK
开发中碰到的两个共性问题案例(其均是多核系统下的临界区保护设计缺陷所引起的) - 总述多核系统下的线程调度策略,以及其临界区保护设计
临界区概念简述
临界区代码: 涉及共享资源(如外设、全局变量/指针、buffer等)访问的代码区
以下情况,可能会导致数据不一致,或不可预知的问题
- 一个线程在执行临界区代码中,被更高优先级线程抢占执行
- 一个线程在执行临界区代码中,被中断抢占执行
- 一个中断在执行临界区代码中,被更高优先级中断抢占执行
- cpu0与cpu1双核,同时在执行临界区代码
因此,在多线程、多核心编程开发中,尤其要注意临界区保护设计!
重点:在嵌入式开发中,相当一部分的概率性问题,是由于临界区保护设计缺陷所引起的!
另,与临界区相关的几个概念如下:
不可重入函数: 如果函数在未完成执行前被再次调用,会导致数据不一致或其它不可预知的问题。此类型函数有如下特征:
- 函数内部使用静态变量
- 函数内部修改全局变量(访问、修改 其它线程会使用到的变量)
- 存在操作依赖于外部状态、外部变量
实际开发中,存在相当多的不可重入函数,必须要在上述第二第三点处,添加临界区保护。
线程安全函数: 可以被多线程同时调用的函数,如memcpy
、memset
等C库函数,如可以反复同时调用而不影响系统正常的通用API
- 不使用函数内部静态变量
- 通常由外界调用者传入其私有参数指针,避免数据竞争
临界区保护机制
禁用中断(local_irq_disable): 适用于单核系统的中断与线程间保护
- 禁用硬件中断访问共享资源,同时也禁用了系统调度器进行线程调度
- 但在多核系统中,特指禁用注册在当前核的中断,而不是禁用全局中断
自旋锁(spin_lock): 专门为多核系统设计的一种临界区保护方式
- 自旋等待:当前cpu核 查询到 锁被另一个cpu核持有,其会在原地等待,直到锁被释放
- 不会触发系统调度器调度,避免了线程上下文切换的开销
- 适用于持有时间非常短的临界区锁,因为长时间的自旋会浪费CPU资源
互斥锁(mutex): 多线程间保护
- 不能用于中断,只能用于线程与线程之间的临界区保护
信号量(Sem):
原子操作(Atomic):
多核系统-临界区保护设计缺陷案例分析
中断与线程访问共享指针变量——概率死机
701-142/143-iis输入-相关缺陷代码描述
以上IIS-DMA-IN
是在alink_isr
中断内调用的,以上禁用中断代码为通过audio_dec
线程调用的
alink_isr
中断,注册在cpu0audio_link.c ->
request_irq(IRQ_ALINK0_IDX, 3, alink0_dma_isr, 0);
,即注册在cpu->core0
- 从
task_table.c
可知,线程audio_dec
运行在cpu1核(固定核心调度的前提下)
问题: 在配置IIS输入48KHz,压测出现了概率死机,打印如下,底层判断为野指针访问断言:
问题原因分析
死机原因解析为:
- 解码线程运行在
cpu1
核,执行local_irq_disable
禁用cpu1对应的中断 - 解码线程进入临界区,将
audio->track
指针变量释放,但还没重新赋值 - 此时注册在
cpu0
核的alink_iis_dma
中断函数触发,并行访问audio->track
指针 —— 导致了野指针访问断言死机
示意图如下:
解决方案
改为spin_lock
自旋锁,锁住多核,保证audio->track
指针变量的访问安全
中断与线程操作共享fifo——发射声音卡顿
701-142/143-发射器-相关缺陷代码描述
缺陷代码位于IIS输入-蓝牙发射
音频流之间的audio_bt_emitter_hw.c->audio_bt_emitter_hw_output()
节点
此节点位于 MIXER 后,蓝牙发射sbc编码前,是一个用
硬件定时器回调
模拟硬件发射的fifo
临界区
解码线程的音频数据流写入bt_emitter_fifo
定时器中断触发从bt_emitter_fifo
取数,推送到编码线程
以下为该音频节点临界区设计缺陷代码节选:
问题原因分析
- 没有对fifo操作做临界区保护
audio_bt_emitter_hw_output
不止被一个线程独立调用,存在概率重入风险
线程在操作fifo,写数据、更新写指针过程中, 硬件中断触发读取临界区fifo,误将刚写入的数据清零 —— 导致了发射出去的音频,有不间断的零数据。
解决方案
部分修改示意如下:
- 非线程安全函数,添加防重入设计
- 多核下,中断与线程涉及的临界区fifo代码,添加自旋锁保护
总结
JL701N-多核系统的调度策略
……暂略
临界区保护设计要点
从单核系统上移植功能代码到多核系统,要着重注意修改临界区保护方式
尽量避免设计不可重入函数,对于可能被多线程共同调用的不可重入函数,一定要加临界区保护措施
异步通信,尤要谨慎注意设计
如,线程某重要操作,依赖于异步定时器所检测的事件。优先考虑通过RTOS-信号量或事件来处理,而不是共享全局变量的设计方式
临界区代码设计避免执行耗时过长
自旋锁一定要成对使用、禁止嵌套调用,否则会出现死锁现象,谨慎、规范设计
思考与拓展
JL701N-实时系统启动调度过程
……
先启动cpu0,再启动cpu1
系统调度器时基中断依赖于cpu0,禁用了cpu0,也相当于禁用了全局线程调度,但不能禁止cpu1起中断
固定cpu核心调度
701-143beta2版本默认是随机核心调度
1 | // 底层定义了 weak 弱函数,上层重写函数可决定核心调度方式 |