阳 丽
核间自旋锁的使用和问题定位方法
阳 丽
(萍乡学院 信息与计算机工程学院,江西 萍乡 337000)
多核CPU在不同的内核间同步,通常采用核间自旋锁的方法来保证多核之间互斥。核间自旋锁的使用频度需要根据CPU的核处理能力和用户程序对CPU总体处理能力的要求来进行权衡。如果核间自旋锁的使用过于频繁或核上加锁周期过长,就会导致CPU的单位时间资源使用率过高,使用户代码功能的执行效率降低。为解决核间自旋锁在使用过程中遇到的问题,文章提出的解决方案是在为核间自旋锁加锁和解锁过程增加相应的调试信息,然后针对不同情况进行扫描队列或新增核间自旋锁。
核间自旋锁;多核CPU;核处理能力
核间自旋锁是为了实现保护共享资源而提出的一种锁机制。由于用户程序在多核CPU上执行时,有些需要在各个核之间进行数据的交互,并且数据交互的方式是使用核间共享内存的方式实现的,因此被读写的核间共享内存就成为了临界资源,不允许有其他的任务访问该资源。对临界资源互斥访问最常见的方法有使用互斥信号量或自旋锁[1]。
信号量是一种睡眠锁,如果有一个任务试图获得一个已经被占用的信号量时,信号量就会将其推进到一个等待队列中,然后让其睡眠。当持有信号量的进程将信号量释放后,处于等待队列中的那个任务将被唤醒,并获得该信号量。而一个自旋锁就是一个互斥设备,它只能有两个值:“锁定”和“解锁”[2]。如果自旋锁可用,则“锁定”位被设置,而代码继续进入临界区;相反,如果自旋锁被其他进程争用,则代码进入忙循环并重复检查这个锁,直到该自旋锁可用为止,这个循环就是自旋锁的“自旋”[3]。一个被争用的自旋锁使得请求它的线程在等待自旋锁重新可用时自旋,因此特别浪费处理器资源,自旋锁不应该被长时间持有,该锁比较适用于锁使用者保持锁时间比较短的情况[4]。但是信号量机制的独有睡眠—唤醒过程比自旋锁有更大的时间开销,可以应用于临界区大的情况,自旋锁的效率远高于信号量机制。
本文针对自旋锁在使用过程中遇到的问题提出了一种为防止核间自旋锁死锁的故障定位和解决方案。
在用户多任务程设计中,为了减少系统内存碎片和降低内存申请的时间开销,共享内存中的一块区域被规划成UB池,其作用就是为了实现对频繁申请内存的高效的管理为,可以很大程度地减少内存碎片,同时也能缩减申请时间。实时的任务使用这种方法相当的高效[5],其中有一块UB池用于核间使用。因此获取UB和释放UB的过程中都会涉及临界资源的操作,影响共享内存的读写操作。
根据上文描述的临界资源保护机制可知,使用互斥信号量会到导致等待锁的任务休眠,而自旋锁是自旋等待。因此,对于带有门铃中断DoorBell并要求处理时间比较短的用户程序而言,使用自旋锁将会是最好的选择[3],而且自旋锁会尽可能地减少线程的阻塞。
使用自旋锁也会导致新的问题,自旋锁最多只能被一个可执行的线程持有,如果一个执行线程试图获得一个被争用的自旋锁,那么该线程就会一直进行忙循环—旋转—等待锁重新可用[6]。当系统进行测试使用自旋锁来实现对核间共享UB的互斥进行保护时,如果不能准确地控制其自旋锁进行保护,各个关联CPU的核将会进入死锁状态,并且系统不能提供任何异常打印。如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用CPU做无用功,同时有大量线程在竞争一个锁,导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其他需要CPU的线程又不能获取到CPU,造成CPU的浪费,这种情况下需要关闭自旋锁[7]。
针对前文描述情况,假设已知用户程序故障的问题是由于增加了核间自旋锁后导致的,省略了其他问题导致的定位过程,下文将主要描述如何定位自旋锁导致核间死锁的原因。在定位核间死锁前,首先需要在加锁和解锁函数中增加调试信息。调式信息结构体为:
typedef struct
{
WORD32 index;
/*调试信息次数*/
WORD32 flag;
/*加解锁标志,0-unlock;1-lock*/
WORD32 line;
/*调用加解锁函数的行号*/
WORD32 isSucc
;/*加解锁是否成功标志,0-succ;1-fail*/
Char func[32];
/*调用加解锁的函数名*/
}LockLog;
添加系统自动记录该自旋锁的详细使用过程的功能,当死锁出现后,自动停止记录,保留现场,手工解锁后将记录导出进行分析。当出现死锁场景时,如果每次导出的死锁现场记录都是一致的,可以根据情况分析,典型的错误打印如下例LOG输出所示:
I = 22, index = 6789022, func = DoorBellDataSend, line = 8572, action = 1, result = 0
I = 23, index = 6789023, func = DoorBellDataSend, line = 8572, action = 0, result = 0
I = 24, index = 6789024, func = DoorBellDataSend, line = 8572, action = 1, result = 0
I = 25, index = 6789025, func = ScanAndDispatchMsg, line = 4392, action = 0, result = 0
I = 26, index = 6789026, func = DoorBellDataSend, line = 8572, action = 1, result = 0
I = 27, index = 6789027, func = DoorBellDataSend, line = 8572, action = 0, result = 0
I = 28, index = 6789028, func = Macshd_writeMsg, line=15703, action=1, result=0
I = 29, index = 6789029, func = Macshd_writeMsg, line=15703, action=0, result=0
I = 30, index = 6789030, func = ReadRng, line = 6772, action = 1, result = 0
I = 31, index = 6789031, func = DoorBellDataSend, line = 8572, action = 1, result = 1
I = 32, index = 6789032, func = OSSSendMsg, line = 3614, action = 1, result = 1
此LOG记录了系统对公共UB自旋锁的使用过程,其中I表示本次记录在内存中的位置;index表示操作锁的顺序号;func表示触发本次操作的函数;line表示代码所处的行号;action表示动作,0表示解锁动作,1表示加锁动作;result表示action的结果,0表示成功,1表示失败。
按执行次序可以得到如下初步结论:
(1)第index=6789030次之前对锁的操作都是正常的,所有的加锁与解锁按顺序进行且正常返回。
(2)第index=6789030次,用户程序ReadRng()函数加锁成功,但是第index=6789031次不是这个ReadRng()函数的解锁动作,而是一次未成功DoorBellDataSend()函数的加锁动作。
(3)接着第index=6789032次OSSSendMsg()函数也尝试加锁,结果也没有成功。
至此,可以判断系统已经处于死锁状态。
深入分析最后三次自旋锁的操作,发现死锁跟最后一次序号为index=6789032的操作没有直接的联系,死锁主要是由第index=6789030次和第index=6789031次操作造成的,下面结合函数功能进行详细说明。
ReadRng()是一个处理普通用户任务的函数,该函数存放在处理器CPU-5中,它的功能是用来处理DSP发送过来的消息内容;而DoorBellDataSend()函数是位于DSP传送过来的DOORBELL()函数的中断处理程序中,优先级更高,也是在处理器CPU-5中运行。由于ReadRng()函数和DoorBellDataSend()函数都需要运行CPU-5,所以当前者获取了自旋锁时,后者又尝试获得该自旋锁,就得不到该自旋锁[8]。而自旋锁的一个特性就是当同一个CPU核心连续两次获得同一个自旋锁的时候会造成死锁,这种死锁方式被称为自死锁,也是最常见的自旋锁死锁形式。
最后一条记录中的OSSSendMsg()函数的功能是组织消息内容发送给DPS,发送过程中虽然有加锁动作,但因为该任务是在CPU-8内核中运行,具有独立性,所以假如该自旋锁被CPU-5内核释放,CPU-8内核的死锁是可以马上恢复的。
根据自旋锁的特点和以上的故障分析,要解决这个死锁问题,则必须要求在任何一个CPU核上都不能陷入自死锁。当前示例中的程序使用了三个CPU核,其中除了CPU-5可能会存在上文描述的这种中断导致自死锁的情况,其他两个CPU核都不会存在锁定被中断的场景,因此只要解决CPU-5上产生的上述问题就可以了。
死锁的处理策略主要包括死锁的预防、死锁的避免、死锁的检测和解除三种。
死锁的预防是通过设置某些限制条件,去破坏产生死锁的四个必要条件(互斥、请求与保持、不抢占、循环等待)中的一个或多个条件,来预防死锁的发生。这种方法容易实现从而被广泛使用,但由于所实施的限制条件往往过于严格,因而可能导致系统资源利用率和吞吐量降低。
避免死锁是在资源的动态分配过程中,采用某种方法去防止系统进入不安全状态,从而避免死锁,而不需要事先采取各种限制措施去破坏产生死锁的四个必要条件。这种方法施加的限制条件较弱,并且有一定的专用性。
死锁的检测与解除是指当检测到系统中已发生死锁时,需要将进程从死锁状态中解脱出来,常用的实施方法是撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转换为就绪状态继续运行。死锁的检测和解除措施,有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度非常大[9]。因此,在考虑到实现成本后,本文将从死锁的预防和避免入手提出相应的通用解决方案。
此方法首先要修改用户程序的状态机逻辑,避免在中断服务程序中使用自旋锁,接着创建一个新的队列,保存要执行的任务,并加入结点的锁定状态。然后开启一个单独CPU核运行一个扫描队列的任务,当DOORBELL中断到来时,中断服务程序仅执行写队列操作,接着通知对应的CPU核去处理DOORBELL中断。与此同时,中断服务程序检测前驱结点的锁定状态,如果锁定状态为False,则说明该线程是队列中的第一个线程;如果锁定状态为True,则说明该线程前面已经有线程获取了自旋锁,当前线程需要阻塞自旋,然后通知对应的CPU-5核取处理。
队列锁的优点是设计空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O=(L+n)。其次队列锁能对自旋锁进行优化,因为队列锁中每次只有一个线程与外部的线程进行争抢,一次争抢不会产生大量失败,同时失败的线程会进入到队列休眠,等待前驱节点将其唤醒,这样提高了CPU的利用率。由于队列内每次只有一个线程会争抢自旋锁,因此每次最多触发一次cache失效。
虽然不提倡中断服务程序中使用自旋锁,但仍然有设计者需要实现某些特定需求的用户程序,只能在中断服务程序中使用自旋锁,这时通用的方法就不能解决该问题了。
针对中断情况的解决方法是避免带有中断的任务获取到自旋锁,根据上文中的程序案例特点,另外分配一块公共UB池,对应该UB池再加一个新的自旋锁进行保护。采取这种方法后DOORBELL()函数就不会在前驱结点的锁定域上等待,而是在自身结点的锁定域上等待,从而不会出现如上文中CPU-5连续两次获得同一自旋锁的情况,它将会在其他CPU上完成。
另外,该UB池在DOORBELL()函数处理时进行申请,仅用于DOORBELL()函数触发的UB申请,网络消息处理触发的UB申请流程还是沿用之前的机制,而释放UB池则放在OSSSendMsg()函数即CPU-2核上执行。因为即使CPU-5被占用,这时DOORBELL()函数中断到来,CPU-5由于获取不到自旋锁而进入停等状态。但是当CPU-2释放完UB池时,占用自旋锁的线程会释放该自旋锁,因此CPU-5会马上恢复[10]。虽然根据用户程序的健壮性可能会出现功能上的错误,但不会出现导致系统死机的情况。而这个方法的优点在于对原有逻辑的改动较少,并且保留了用户程序中必要的中断处理逻辑。
目前为了提高硬件的处理能力,用户往往选用基于多核的CPU。多核的CPU可以同时运行多项任务,但任务之间往往需要具有一定的同步关系,以及多个并行任务对同一资源的操作也需要使用信号量或自旋锁来解决。信号量和自旋锁的使用经常会带来执行效率降低和死锁问题。本文对于防止核间自旋锁的死锁提供了一种定位和解决方法。使用本文描述的通用解决方案对系统改动最小,并可以满足一般用户的使用要求。针对中断任务,虽然不能完全解决死锁问题,但在保证系统健壮性的同时,减少了用户对程序的改动,保证了用户程序的正常逻辑执行。
[1] 李娟, 任晓瑞. 一种机载嵌入式对称多处理机系统互斥策略[J]. 电子科技, 2013, 26(4): 60–64.
[2] 许璐璐. 支持对称多核处理器的嵌入式实时操作系统研究与实现[D]. 北京: 中国航天科技集团公司第一研究院, 2016.
[3] 李天佑. 基于三层特权级的操作系统安全体系结构的研究[D]. 北京: 北京交通大学, 2014.
[4] William S. 操作系统精髓与设计原理: 第3版[M]. 北京:清华大学出版社, 1998: 213.
[5] 虞保忠, 郝继锋. 多核操作系统自旋锁技术研究[J]. 航空计算技术, 2017, 47(4): 36–40.
[6] 王月, 李杰.嵌入式多核系统的可调优先级自旋锁[J]. 单片机与嵌入式系统应用, 2021, 21(12):46–51
[7] 吕锦柏, 崔萍. 一种多核CPU访问资源时自旋锁的实现方法: CN201710376711.2[P]. 2017-05-25.
[8] 李亚爽, 姬希娜, 王振, 等. Nucleus PLUS自旋锁测试方法研究[J]. 电子技术应用, 2018, 44(1): 12–16
[9] 于璠, 王振国. 一种自旋锁抢占调度算法选择方法及装置: CN201310705505.3[P]. 2013-12-19.
[10] 张文盛, 侯整风. 一种Linux内核自旋锁死锁检测机制的设计与实现[J]. 合肥学院学报, 2012, 22(2): 56–60.
The Use of Inter-Core Spin Locks and Problem Location Methods
YANG Li
(School of Information and Computer Engineering, Pingxiang University, Ping Xiang, Jiangxi 337000, China)
In different inter-core synchronization technologies in multi-core CPU, the inter-core spin lock method is often inevitably used. The frequency of the usage of inter-core spin locks needs to be weighed according to the CPU’s core processing capability and the user program’s requirements on the CPU’s overall processing capability. If the inter-core spin lock is used too frequently or the lock period on the core is too long, the CPU’s resource utilization per unit time will be too high and the execution efficiency of user code functions will be reduced. For this reason, the paper proposed to add corresponding debugging information to the inter-core spin locking and unlocking process, and to use the solution of scanning queue or adding inter-core spinlock for different situations.
inter-core spin lock; multicore CPU; core processing power
TP316
A
2095-9249(2021)06-0073-04
2021-10-26
阳丽(1980—),女,江西萍乡人,讲师,硕士,研究方向:计算机应用、软件工程、数据库。
〔责任编校:吴侃民〕