自旋锁spinlock:Linux内核提供的一种用于保护临界资源的机制,特别是在多核系统中存在 进程与进程/进程与中断/中断与中断之间的并发访问场景。spinlock使用的是一种“忙等”机制,相对信号量获取如果阻塞会让出cpu行为,spinlock会一直占用cpu持续等待直到获取成功,优点是由于没有发生sche上下文切换,适用与中断,同时在频繁访问临界区效率更高,约束是临界区不能休眠。
1. spin_lock/spin_unlock:
(1)行为:lock 1、禁止内核抢占preempt_disable(); 2、获取自旋锁;unlock:1、释放自旋锁 2、打开内核抢占。
(2)适合场景:进程/软中断使用。 由于不会禁止本地中断,效率更高,但是也约束了不能在中断中访问并发访问。
2. spin_lock_irq/spin_unlock_irq
(1)行为:lock1、关闭本地中断 2、禁止内核抢占preempt_disable(); 3、获取自旋锁;unlock:1、释放自旋锁 2、使能本地中断 3、打开内核抢占
(2)适合场景:进程/软中断/中断 使用。默认unlock会打开本地中断,使用在本地中断常开的场景。
3. spin_lock_irqsave/spin_unlock_irqrestore
(1) 行为:lock1、记录当前中断开关状态到flag,关闭本地中断 2、禁止内核抢占preempt_disable(); 3、获取自旋锁;unlock:1、释放自旋锁 2、通过flag恢复本地中断状态 3、打开内核抢占
(2) 适合场景:进程/软中断/中断 使用。最安全的接口,相应的要注意flag在lock到unlock过程中不能被重写修改。
基础规则:
1、锁在使用前必须进行初始化
可使用SPIN_LOCK_UNLOCKED在定义事静态初始化或者使用spin_lock_init(XX)进行初始化。(同时也要注意 不要在锁初始化后 使用过程 再出现并发初始化问题)
2、lock和unlock必须配对使用
特别注意一些异常处理分支,特别是隐含有return等流程变化的宏函数,是否引入未释放锁的问题。
3、lock到unlock之间必须是原子操作,不能休眠
常见可能休眠的函数:sleep、wait_event、down、vmalloc、kmalloc(非GFP_ATOMIC)、copy_from_user
4、spin_lock_irqsave flag必须使用局部变量
为防止lock到unlock过程被修改引起中断状态恢复异常,必须使用局部变量。
5、避免同时获取多个锁
如果无法避免必须保证所有代码以相同的顺序获取锁。
进阶建议:
6、最小化锁保护原则,注意是否有过保护或漏保护(性能建议)
锁的目的是保护临界资源访问,但同时锁本身会关闭对应cpu核的抢占和中断响应,要避免过保护,特别是在对性能有较高要求的产品设计中。不要出现过长耗时的锁的保护,避免上锁后 进行如打印或者复杂业务运算 后 再释放锁。从设计上讲,如果能通过可重入以及流程控制等设计 避免或减少锁的引入,才是最优的设计。
7、避免多实例场景一把锁保护一切的粗暴写法(性能建议)
特别针对多实例场景,要分析清楚那些是实例内部的保护,那些是实例间的保护,高性能场景如果全部实例的资源都用同一把锁保护 将会从设计引入的性能瓶颈。适当选择合理锁类型(如读写锁优化) 能 优化性能。
8、在设计代码层次时,锁的使用一定要在统一的层次上(架构建议)
在编码过程,一定要非常明确 在哪一层添加锁,并且在函数中合理的体现出来,常见的锁层次:模块对外调用接口实现(无锁)--内部实现(带锁层-保护临界资源读写访问)--内部临界资源运算(无锁)。如果层次混乱,后续的维护和review非常难定位分析。
1、加锁之前没有进行初始化引起死锁,流程保护不完整引起。
2、A-A死锁,同一cpu获取锁后未释放重复获取同一把锁。由于加锁层次不清楚,连续获取同一把锁,或者递归加锁。
3、ABBA死锁,两个cpu按照不同顺序获取多把锁引起死锁,设计问题。
4、加锁/释放缩 不配对引起长期持有锁挂死尝试获取锁的cpu。常见是获取缩后异常分支返回,宏函数返回没有unlock。
5、加锁后休眠/阻塞 引起挂死,特别是再锁中 访问系统函数 或者 外部模块函数 一定要非常明确 里面是不可休眠的。
6、spinlock的flag使用全局变量,或者多处连续使用引起unlock时状态被改写,引发诡异死锁/中断响应问题。
7、锁初始化流程设计异常,出现锁使用过程再次被初始化问题 引起死锁。
一般情况 ,出现锁问题 会引起CPU对应核被挂起,但是常常锁死后很难有明确的异常打印,经常会出现受害者打印(该锁上的线程长时间未被唤醒报错)。对于此类死锁常见分析锁常见的思路:
1、尝试打开内核一些debug锁的配置项目,内核能尽可能打印出一些锁可能出现问题常见的信息。
2、想办法明确锁的位置,把多个模块的线程和硬件中断 绑定到制定的cpu核上,通过压测确认每一次死在哪个核心上判断其所在业务模块进行针对性review或者调试验证。
3、使用仿真器 进行死锁环境debug,往往死锁状态下总线还是正常的 连接仿真器可以挨个调试cpu确认 死在哪一个核心上,此时其汇编指令往往相对明显,存在等待自旋锁指令。同时还能通过 通用配合汇编指令 和 当前核心通用寄存器 状态 确定锁的某个成员地址。那么就可以尝试通过分析地址段 所在驱动模块,或者 反汇编内核 来定位位置。同时也可以通过仿真器调试断点 或者 linux 断点debug工具等 尝试debug该地址 定位锁位置。
锁的问题 设计阶段分析 和 编码后的review,要优先与测试和debug,更容易发现和解决问题;review锁 常见流程:
1、先明确锁要保护的内容是什么,判断 其临界资源 和 锁的力度划分是否合理。
2、确定锁定义的位置,和 锁初始化位置,用于判断流程上是否可能出现访问到未初始化锁或者使用过程再次被初始化的可能。3、review锁的每个使用位置,判断锁的获取和释放是否成对,锁的过程中是否有休眠,是否有多把锁同时获取的顺序问题,锁的范围是否合理,锁的使用层次是否统一。
4、搜索锁所保护的临界资源访问的接口,判断是否有漏锁的情况。