读书笔记(5)—— kernel 自旋锁

自旋锁

自旋锁 Spinlock 是 Linux 内核中使用最广泛的同步原语。具有以下基本特征:
1,获取锁的过程(即上锁的过程)是自旋(自旋就是忙等的意思)的,不会引起当前进程睡眠和调度。也就是说,当前进程一直出于活动状态中。
2,持有自旋锁的临界区中不允许调度和睡眠,因为一旦发生调度,临界区什么时候能够继续运行是不确定的(什么时候解锁是不确定的),这会导致其他竞争者死锁。因此,自旋锁的加锁操作会禁止抢占,解锁操作时再恢复抢占。也因此,自旋锁既可以用于进程上下 文,又可以用于中断上下文
自旋锁的主要用途是多处理器之间的并发控制,适用于锁竞争不太激烈的场景。如果锁竞争非常激烈,那么大量的时间会浪费在加锁自旋上面,导致整体性能下降。
注意:竞争激烈的场景下使用自旋锁并不会导致逻辑错误,只是说自旋锁不是这种场景下的最优选择。在功能上,自旋锁分普通自旋锁和读写自旋锁两种。

(1)普通自旋锁

在 Linux-4.4.x 中,普通自旋锁的数据结构定义有三种:
spinlock_t、raw_spinlock_t、arch_spinlock_t,具体的代码实现请看内核中include/linux中的spinlock_type.h和spinlock.h。这里不做详细的介绍了。
自旋锁的实现是体系结构相关的,因此每种体系结构提供了自己的实现,即体系结构自
旋锁 arch_spinlock_t;raw_spinlock_t 是对 arch_spinlock_t 进行包装的原始通用自旋锁,根据不同的配置加入了一些附加信息;spinlock_t 是对 raw_spinlock_t 的进一步包装, 是普通通用自旋锁。原始通用自旋锁是符合原始自旋锁定义的,在临界区中不允许抢占,但这不符合实时内核的要求(实时内核是指带 PREEMPT_RT 增强的内核,要求在任意上下文中均可抢占)。从 2.6.33 版本开始,Linux 内核开始区分原始通用自旋锁和普通通用自旋锁,在不启用 PREEMPT_RT 的情况下,spinlock_t 等价于 raw_spinlock_t;在启用 PREEMPT_RT 的情况下,spinlock_t 等价于实时可抢占的自旋锁实现(主线内核暂未引入 PREEMPT_RT 的具体实现)。普通自旋锁的主要 API 有(锁类型前缀和 API 前缀一一对应,arch_前缀的 API 主要供内部使用):

/*静态定义一个名为 lock 的自旋锁*/
DEFINE_SPINLOCK(lock)/DEFINE_RAW_SPINLOCK(lock)

/*自旋锁初始化(设置为未锁状态)*/
spin_lock_init(lock)/raw_spin_lock_init(lock)

/*加锁操作,加锁成功后返回,否则一直自旋*/
spin_lock(lock)/raw_spin_lock(lock)/arch_spin_lock(lock)

/*解锁操作,解锁过程无竞争因此必然会成功*/
spin_unlock(lock)/raw_spin_unlock(lock)/arch_spin_unlock(lock)

/*尝试加锁操作,加锁成功返回 1,否则返回 0(立即返回不自旋)*/
spin_trylock(lock)/raw_spin_trylock(lock)/arch_spin_trylock(lock)

/*判定锁状态操作,未锁状态返回 0,已锁状态返回 1*/
spin_is_locked(lock)/raw_spin_is_locked(lock)/arch_spin_is_locked(lock)

在最终实现(raw_spinlock_t/arch_spinlock_t 的实现)上面,普通自旋锁又可以 分为经典自旋锁(Classical Spinlock)、排队自旋锁(Ticket Spinlock)和快速排队自旋 锁(Queued Spinlock,简称 qspinlock)。经典自旋锁是最简单的实现,就是跑一个很小的紧循环反复检查锁的状态。
但经典自旋锁面临一个问题,多个竞争者抢锁时,谁会成功是随机的,这就有可能导致某些竞争者饿死的情况
排队自旋锁给每个竞争者分配了一张票据(Ticket),用来保证先开始抢锁的竞争者能够先拿到锁,相当于给所有的竞争者排了一 个队,提高了公平性。X86 从 Linux-2.6.25 开始支持排队自旋锁,MIPS 从 Linux-2.6.28 开始支持排队自旋锁。
Ticket Spinlock 虽然比 Classical Spinlock 先进,但存在一个 Cache 颠簸的性能问题。多个竞争者会反复将自旋锁所在的 Cache 行加载到本地 Cache, 这在竞争很激烈的情况下(尤其是存在跨节点访问的 NUMA 结构上面),性能会下降得很厉 害。Queued Spinlock 比 Ticket Spinlock 更好的地方在于多个竞争者在抢锁的时候首先在自己的本地副本上自旋,直到成功的时候才操作主锁,因而对 Cache 更加友好,提高了 性能。X86 从 Linux-4.2 开始已经支持快速排队自旋锁,而 MIPS 直到 Linux-4.13 才开始支持快速排队自旋锁。
自旋锁解决了多处理器之间的并发问题(并行执行问题),但并没有解决单处理器上由于中断而导致的并发问题(交错执行问题)。如果需要同时解决这两种并发问题,就需要结合自旋锁和开关中断(包括开关硬中断和软中断,而普通的进程抢占问题已经由自旋锁解决 了,自旋锁是禁止抢占禁止内核进程切换的,但是不能禁止中断的放生)。
普通自旋锁和开关中断结合使用衍生了一套新的 API:

/*加锁并关闭软中断(包括 tasklet),加锁成功后返回,否则一直自旋。*/
spin_lock_bh(lock)/raw_spin_lock_bh(lock)

/*解锁并打开软中断(包括 tasklet),解锁必然会成功*/
spin_unlock_bh(lock)/raw_spin_unlock_bh(lock)

/*加锁并关闭硬中断,加锁成功后返回,否则一直自旋*/
spin_lock_irq(lock)/raw_spin_lock_irq(lock)

/*解锁并打开硬中断,解锁必然会成功*/
spin_unlock_irq(lock)/raw_spin_unlock_irq(lock)

/*加锁并保存中断标志(同时保证关闭中断),加锁成功后返回,否则一直自旋*/
spin_lock_irqsave(lock, flags)/raw_spin_lock_irqsave(lock, flags)

/*解锁并恢复中断标志(同时可能打开中断),解锁过程无竞争因此必然会成功*/
spin_unlock_irqrestore(lock, flags)/raw_spin_unlock_irqrestore(lock, flags)

(2)读写自旋锁

普通自旋锁有一个缺点,就是对所有的竞争者不做区分。实际上,很多情况下有些竞争者并不会修改共享资源(只读不写),但普通自旋锁总是会限制只有一个内核路径持有锁, 而实际上这种限制是没有必要的。读写自旋锁进行了改进,允许多个读者同时持有读锁(允许多个读者同时进入读临界区),而只允许一个写者同时持有写锁(只允许一个写者同时进 入写临界区),当然也不允许读者和写者同时持有锁。换言之,读锁是共享锁,而写锁是互斥锁。与普通自旋锁相比,读写自旋锁更适合读者多,写者少的应用场景。
和普通自旋锁一样,读写自旋锁的数据结构也有三种:
rwlock_t、raw_rwlock_t、arch_rwlock_t(具体代码实现和相关的代码定义都在include/linux/rwlock_type.h和rwlock.h中,这里不做详细的代码介绍)。
和普通自旋锁一样,读写自旋锁的实现也是体系结构相关的,因此每种体系结构提供了 自己的实现,即体系结构自旋锁 arch_rwlock_t这是最基本的实现;raw_rwlock_t 的实现是对 arch_rwlock_t 进行包装后的原始通用自旋锁,根据不同的配置加入了一些附加信息;rwlock_t 是对 raw_rwlock_t 的进一步包装,是读写通用自旋锁。raw_rwlock_t 在临界区中不允许抢占, 不符合实时内核的要求(实时内核就是指带 PREEMPT_RT 增强的内核)。从 2.6.33 版本开始,Linux 内核开始区分 rwlock_t 和 raw_rwlock_t,在不启用 PREEMPT_RT 的情况下,rwlock_t 等价于 raw_rwlock_t;在启用 PREEMPT_RT 的情况下,rwlock_t 等价于实时可抢占的读写自旋锁实现(目前主线内核暂未引入 PREEMPT_RT 的具体实现)。
普通自旋锁的主要 API 有(锁类型前缀和 API 前缀一一对应,arch_前缀的 API 主要供内部使用):

/*静态定义一个名为 lock 的自旋锁*/
DEFINE_RWLOCK(lock)

/*自旋锁初始化(设置为未锁状态)*/
rwlock_init(lock)

/*加读锁操作,加锁成功后返回,否则一直自旋*/
read_lock(lock)/_raw_read_lock(lock)/arch_read_lock(lock)

/* 解读锁操作,解锁过程无竞争因此必然会成功*/
read_unlock(lock)/_raw_read_unlock(lock)/arch_read_unlock(lock)

/*加写锁操作,加锁成功后返回,否则一直自旋*/
write_lock(lock)/_raw_write_lock(lock)/arch_write_lock(lock)

/*解写锁操作,解锁过程无竞争因此必然会成功*/
write_unlock(lock)/_raw_write_unlock(lock)/arch_write_unlock(lock)
 
 /*尝试加读锁操作,加锁成功返回 1,否则返回 0(立即返回不自旋)*/
read_trylock(lock)/_raw_read_trylock(lock)/arch_read_trylock(lock)

/*尝试加写锁操作,加锁成功返回 1,否则返回 0(立即返回不自旋)*/
write_trylock(lock)/_raw_write_trylock(lock)/arch_write_trylock(lock) 。

在最终实现上,读写自旋锁也分为经典读写自旋锁 Classical rwlock 和排队读写自旋 锁 Q u e u e d rw l o c k , 简 称 q r w l o c k。 和普通自旋锁一样,Queued rwlock 公平性好、对 Cache 更友好、性能也更好。X86 从 Linux-3.16 开始已经支持 Queued rwlock,而 MIPS 直到 Linux-4.13 才开始支持 Queued rwlock。
和普通自旋锁一样,读写自旋锁也只是解决了多处理器之间的并发问题(并行执行问 题),但并没有解决单处理器上由于中断而导致的并发问题(交错执行问题)。如果需要同时 解决这两种并发问题,就需要结合读写自旋锁和开关中断(包括开关硬中断和软中断,而普通的进程抢占问题已经由自旋锁解决了)。所以读写自旋锁和开关中断结合使用也衍生了一套新 的 API,既解决了多个CPU间的并发页解决了单个CPU由于中断而导致的并发问题,其代码如下:

/*加读锁并关闭软中断(包括 tasklet),加锁成功后返回,否则一直自旋*/
read_lock_bh(lock)/_raw_read_lock_bh(lock)

/*解读锁并打开软中断(包括 tasklet),解锁过程无竞争因此必然会成功*/
read_unlock_bh(lock)/_raw_read_unlock_bh(lock)

/*加写锁并关闭软中断(包括 tasklet),加锁成功后返回,否则一直自旋*/
write_lock_bh(lock)/_raw_write_lock_bh(lock)

/*解写锁并打开软中断(包括 tasklet),解锁过程无竞争因此必然会成功*/
write_unlock_bh(lock)/_raw_write_unlock_bh(lock)

/* 加读锁并关闭硬中断,加锁成功后返回,否则一直自旋*/
read_lock_irq(lock)/_raw_read_lock_irq(lock)

/*解读锁并打开硬中断,解锁过程无竞争因此必然会成功*/
read_unlock_irq(lock)/_raw_read_unlock_irq(lock)

/*加写锁并关闭硬中断,加锁成功后返回,否则一直自旋*/
write_lock_irq(lock)/_raw_write_lock_irq(lock): 。

/*解写锁并打开硬中断,解锁过程无竞争因此必然会成功*/
write_unlock_irq(lock)/_raw_write_unlock_irq(lock)

/*加读锁并保存中断标志(同时保证关闭中断),加锁成功后返回,否则一直自旋*/
read_lock_irqsave(lock, flags)/_raw_read_lock_irqsave(lock, flags)

/*解读锁并恢复中断标志(同时可能打开中断),解锁过程无竞争因此必然会成功*/
read_unlock_irqrestore(lock, flags)/_raw_read_unlock_irqrestore(lock, flags)

/*加写锁并保存中断标志(同时保证关闭中断),加锁成功后返回,否则一直自旋*/
write_lock_irqsave(lock, flags)/_raw_write_lock_irqsave(lock, flags)

/*解写锁并恢复中断标志(同时可能打开中断),解锁过程无竞争因此必然会成功*/
write_unlock_irqrestore(lock, flags)/_raw_write_unlock_irqrestore(lock, flags)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值