内核同步机制之spinlock介绍

1. 背景

在内核中,共享对象的互斥处理向来是个非常麻烦的问题,这种麻烦是源自于 linux 内核中复杂的并发机制,这种并发有可能是单核上多进程(线程)之间的伪并发、也可能是 SMP 架构中的真实并发,再考虑到中断和下半部机制的情况,并发编程变得比较复杂。

在并发编程中,对共享数据加锁,这是一个最基本的共识,在 Linux 内核中支持各种各样的锁机制,以满足各种各样的并发情况,并没有哪一种锁机制能适用于所有的并发情况,即使有,那么这种锁机制肯定只是能用,而不是适用。毕竟,生活经验告诉我们:兼容性和针对性之间只能二选其一,兼容性带来更广的应用范围,而针对性带来更高的效能发挥。

在所有的锁机制中,spin lock 和 mutex lock 是应用最为广泛的,对于 mutex 而言,如果一个进程请求某个临界区的资源,当请求不到时陷入睡眠状态,直到临界区资源重新开放可用时再进行处理,这是相当实用的做法,节省了 CPU 的运行资源。

但是在某些情况下,mutex lock 并不合适:

一方面,从微观的角度来说,进程的切换尽管耗时很少,通常在 us 级别,如果程序员确定进入临界区的时间要小于这个时间,比如仅仅是置一个标志位,进程切换的开销相对来说就是不划算的,同时,随着硬件的发展,各种缓存技术的应用,进程切换的开销就不能只放在进程的 switch 过程了,后续还有一些隐性的开销,比如缓存和 TLB 的重新载入。

另一方面,通常也是开发者不使用 mutex lock 的原因,就是在中断上下文中不能睡眠,这是一个硬性限制,这个时候就需要使用 spinlock 来实现共享对象的互斥访问。

2. 自旋锁特性

自旋锁在同一时刻只能被一个内核代码路径持有。如果另外一个内核代码路径试图获取一个被持有的自旋锁,那么该内核代码路径需要一直忙等待,知道自旋锁持有者释放该锁。如果锁没有被其他内核代码路径持有(或者称为锁争用),那么可以立即获得该锁。自旋锁的特性如下。

  • 忙等待的锁机制。操作系统中锁的机制分为两类,一类是忙等待,另一类是睡眠等待。自旋锁属于前者,当无法获取自旋锁时会不断进行尝试,知道获取锁为止;
  • 同一时刻只能有一个内核代码路径可以获得该锁;
  • 要求自旋锁持有者尽快完成临界区的执行任务。如果临界区中的执行时间过长,在锁外面忙等待的CPU比较浪费,特别是自旋锁临界区里不能睡眠;
  • 自旋锁可以在中断上下文中使用。

3. spinlock 涉及的并发情况

在分析 spinlock 之前,先考虑一个问题:如果让你来实现 linux 中的 spinlock,你会怎么做?

本质上来说,全局对象需要使用锁来保护的原因是并发的产生,如果是单一的执行流,不需要考虑使用锁来做数据保护,所以第一个需要思考的问题是:spinlock 的使用过程中会有哪些并发的产生?

  • 内核的抢占
  • 中断的抢占
  • 下半部的抢占
  • SMP 架构中的并发

3.1 内核抢占

linux 默认支持内核抢占,这个特性使得当前进程的执行可以被其它内核进(线)程抢占执行,这种抢占并不是无时不刻都在进行的,而是存在一些抢占点,比如从中断处理函数返回到内核或用户空间,比如调用 preempt_enable 使能内核抢占,因此可能出现这样的情况:

内核进程 A 正在执行,并获取了一个 spinlock,此时发生了中断并跳转到中断服务程序中,在中断中一个更高优先级的进程 B 被唤醒,在中断返回时 B 会抢占 A 并执行,如果这时候 B 也请求同一个 spinlock,问题就来了:B 因为请求不到 spinlock 而一直自旋,而占用 spinlock 的 A 因为被 B 抢占而得不到执行,无法释放 spinlock。

因此,对于自旋锁而言,内核抢占是一个明显的风险点,作为最简单的处理,需要在使用自旋锁之前,就禁止内核抢占。

3.2 中断抢占

毫无疑问,中断的优先级肯定是比普通进程的优先级要高,在没有禁止中断的情况下,一旦有外设中断来到,CPU 就会直接跳转到中断向量处执行系统的中断代码以及我们定义的中断处理函数,这一步是硬件操作,软件上无法修改其行为。

同样的,如果进程 A 执行的时候占用了 spinlock,此时发生了中断,同时中断中也需要请求同一个 spinlock,也会造成中断请求不到锁,而进程 A 无法释放锁的情况,而且中断中长时间的自旋所带来的后果更加严重。

那么,作为处理,是不是需要在请求自旋锁之前也禁止本地中断呢?

实际上并不用,中断和内核抢占有本质上的区别。需要注意的是,内核管理所有的硬件,向应用层提供操作接口,是一个服务的提供者,通常需要支持并发访问,而这些并发访问是无法针对预估的,因为对于内核来说它们都是一样的,因此,对于某个驱动程序 A 而言,开发者并不知道同时会有多少个进程对其发起请求,完全可能存在用户进程 X 和 Y 同时发起系统调用向 A 请求服务,这就造成了 X 和 Y 可能同时请求到 A 中的同一个 spinlock,这种情况下如果 X 和 Y 相互抢占,就会导致上述的问题,除了禁止抢占,没有更好的办法。

而中断不一样,中断服务程序是可知的,或者说开发者是可以明确地知道中断服务程序和普通程序会不会竞争同一个锁,从而进行针对性的处理,如果中断服务程序中会和普通程序中竞争同一个 spinlock,毫无疑问进程在使用 spinlock 的时候需要禁止中断,但是如果在中断中没有使用到 spinlock,就没有必要禁止中断,白白浪费系统响应性能。

所以,这个过程是可预知、可控制的,如果你的中断程序中使用到与进程竞争的 spinlock,就是用 spin_lock_irq 这一类接口,否则就使用 spin_lock 这种普通接口即可。

可能有的朋友就要问了,在 SMP 系统中,同一个中断是不是会在不同的 CPU 上并发执行,实际上这个不需要担心,目前 SMP 架构上的 linux 实现,是不支持中断的并发的。同时,如果是不同中断处理函数之间使用了同一个 spinlock,也不用担心,新版的(>2.6) linux 是不支持中断嵌套的,也就是中断执行的过程是关中断的。

3.3 下半部的抢占

中断下半部的优先级是高于普通进程的,因此同样会出现和中断一样的情况:中断下半部和普通进程发生竞争而导致死锁的问题。

由于下半部和中断一样同样可以方便地和普通进程区分开来,因此对于中断下半部的处理方式和中断一样:如果中断下半部中存在与普通进程之间的 spinlock 竞争关系,就需要禁止下半部,反之则不用。

中断下半部的调度点有:硬件中断处理程序退出时和调用 local_bh_enable 重新使能下半部时,所以禁止中断并不能完全禁止下半部的执行,因此下半部也有相对应的独立的接口:spin_lock_bh 和 spin_unlock_bh。同样的,如果普通进程和下半部之间没有 spinlock 的竞争,就使用普通的 spin_lock 和 spin_unlock。当然,如果你的程序完全不需要考虑性能,直接使用 spin_lock_bh 或者 spin_lock_irq 也是可以的。

相对于中断不存在并发执行而言,下半部是不是存在并发执行的情况呢?首先,下半部不会在同一个 CPU 上被下半部抢占,但是 softirq 支持在不同的 CPU 上并发执行,这类情况并不需要担心,比如 CPU0 上执行 softirq 请求了 spinlock,CPU1 同时运行该 softirq 请求 spinlock,但是这并不会造成死锁,实际情况是 CPU0 的 softirq 很快地执行完临界区代码释放 spinlock,从而 CPU1 上的 softirq 得以继续执行。

3.4 SMP 架构中的并发

SMP(Symmetrical Multi-Processing) 架构与 UP(Unique Processing) 最大的区别在于:SMP 实现了真正的并发执行,而不像单核下的伪并发。

实际上,对于 SMP 中的并发,和单核下的并发有本质的区别,单核下的 spinlock 并发问题来自于当一个执行流获取到锁,另一个执行流不能抢占当前执行流并同时尝试获取该锁,这会导致原本的执行流无法执行而新的执行流无法获取锁的情况。而在多核下,是实实在在的并发,只要两个同时请求锁的执行流在不同的 CPU 上,后请求的等待前一个请求的释放锁就可以了。

单核下锁的机制基本上是通过防止并发的方式实现,比如禁中断、禁抢占,单核下的禁止等于系统性地禁止。而多核下不一样,linux 并不提供系统性的禁止行为,比如禁止整个系统上所有 CPU 的中断,并发是必然存在的。

正因为是实实在在的并发,这也会带来另一个问题:多核下需要实现加解锁操作的原子性或者独占性,首先需要了解的是,即使是一个变量的加操作,通常都包括取指、操作、写回这三个步骤,假设 CPU0 对锁的操作正进行到操作步骤,准备写回,同时 CPU1 也在进行操作,并准备写回,这时候 CPU0 或者 CPU1 的其中一个操作结果可能会丢失,从而产生错误的逻辑。

所以,对于多核下的 spinlock,需要考虑锁操作的原子性和独占性。

4. spinlock 的特性总结

根据上面的分析,一个完善的 spinlock 实现应该有以下的特点:

1、申请锁时关内核抢占,释放锁时开启内核抢占

2、如果中断或者进程中出现 spinlock 的竞争,需要进程中的锁操作需要关中断,中断中不需要关中断(默认就是关状态)。

3、如果中断下半部或者进程中出现 spinlock 的竞争,需要进程中的锁关闭中断下半部

4、在多核系统下,加解锁的操作函数需要具备原子性或者独占性。

实际上,这也是目前 linux 中 spinlock 的实现所考虑的并发处理,接下来就来看看 spinlock 的源代码实现。

5. spinlock 的源码实现

因为 spinlock 的实现涉及到单核、多核的区分,对于多核的支持是架构相关,同时内核支持死锁的检测以及 debug 支持,所以源码的定义带有多层的宏选项,非常地绕,在这里基于 arm SMP 平台,在不支持死锁检测(lockdep)和 debug 的情况下对 spinlock 进行分析。

通用spinlock 相关的文件结构如下:

kernel$ find ./ -name spinlock*.h
./tools/include/linux/spinlock.h
./include/linux/spinlock_types.h
./include/linux/spinlock.h  // Linux通用的头文件
./include/linux/spinlock_api_up.h
./include/linux/spinlock_types_up.h
./include/linux/spinlock_api_smp.h
./include/linux/spinlock_up.h
./include/asm-generic/spinlock.h
./arch/arm/include/asm/spinlock_types.h  // arm32 架构的头文件
./arch/
  • 8
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值