内核同步机制
Windows内核提供了多种同步机制来处理并发。
从使用场景来进行划分比较简单,这些同步机制可以分为:
-
无锁实现,适用高IRQL无法wait对象,竞争不激烈:自旋锁
-
同一时间只能一个线程执行:互斥量(Mutex)
-
单写多读:执行体,获取独占锁时使用时使用临界区禁用用户和普通内核APC
-
固定个数线程执行:信号量(Semaphore)
-
通知所有线程/单个线程:事件(Event)
-
简单原子操作:互锁函数,CPU内联函数(特殊CPU指令)实现
自旋锁
自旋锁原理就是通过循环CPU提供的原子指令compare and swap实现的无锁机制。
当处于IRQL 2或者更高时,无法使用KeWaitForSingleObject,这时就可以使用自旋锁。
// 提升IRQL到DPC(2)并获得自旋锁
KIRQL oldIrql = KeAcquireSpinLockRaiseToDpc(&KSPIN_LOCK);
// 释放自旋锁,并降低IRQL
KeReleaseSpinLock(&KSPIN_LOCK,oldIrql);
除了这一组外,还有针对其它IRQL的申请和释放函数。
这里有个问题是,为什么要提升IRQL,因为自旋锁主要依赖的是CPU的compare and swap的原子指令,不提升IRQL也是能正常使用的。
为什么要提升IRQL
假设我们有两个处理器,处理器A和处理器B,且它们都在执行内核代码。现在有两个线程,线程1在处理器A上运行,线程2在处理器B上运行。它们都需要访问一个共享资源(比如一个链表),这个资源由一个SpinLock来保护。
-
「线程1(处理器A)获取锁」: -
线程1获取了SpinLock,并开始操作共享资源。 -
由于我们不提升IRQL,因此处理器A仍然可以被中断。
-
-
「线程1被中断」: -
在操作共享资源的过程中,处理器A上发生了一个中断(例如,一个定时器中断),操作系统将线程1挂起,转而去处理这个中断。 -
由于线程1持有了SpinLock,这个锁还没有被释放。
-
-
「线程2(处理器B)尝试获取锁」: -
同时,线程2在处理器B上试图获取相同的SpinLock。 -
由于SpinLock已经被线程1持有,线程2进入了自旋状态,等待线程1释放锁。
-
-
「线程1的中断服务程序(ISR)尝试获取锁」: -
处理器A上的中断服务程序(ISR)可能会尝试获取同一个SpinLock来访问共享资源(例如,链表中的某个节点)。 -
由于锁被线程1持有且线程1已被挂起,这将导致中断服务程序陷入等待,从而阻塞了中断处理。
-
-
「优先级反转与死锁」: -
处理器A无法继续,因为它在处理中断,而中断服务程序也在等待锁。 -
处理器B也无法继续,因为它的线程2在等待线程1释放锁。 -
这就可能导致死锁,整个系统的这部分功能变得无法响应,可能会导致系统崩溃或某些功能失效。
-
从上面例子可以看出,这里的提升IRQL的自旋锁主要是用于执行在不同IRQL上的线程之间的同步,如果说竞争自旋锁保护资源的线程都处于同一IRQL(比如都处于IRQL 0),就可以不提升,因为不会存在上述情况。
互斥量(Mutex)/快速互斥量(Fast_Mutex)
// 普通互斥量
ExInitializeMutex
KeWaitForSingleObject
KeReleaseMutex
// 快速互斥量
ExInitializeFastMutex
ExAcquireFastMutex
ExReleaseFastMutex
快速互斥量比普通互斥量性能更高、会提升IRQL到APC,且只能无限等待,不能指定时间等待。
执行体
互斥量只允许有一个线程存取共享资源,这对于单写多读的场景并不怎么高效。
// 执行体
ExInitializeResourceLite
// 读
ExAcquireResourceSharedLite/ExReleaseResourceLite
// 写
ExEnterCriticalRegionAndAcquireResourceExclusive/ExReleaseResourceAndLeaveCriticalRegion
写操作时,需要调用ExEnterCriticalRegion禁用内核APC,所以这两个操作总是一起。
信号量
// 信号量
KeInitializeSemaphore
KeWaitForSingleObject
信号量很简单,当内部值大于0时,KeWaitForSingleObject将返回,信号量-1。如果值到0,则信号量变为无信号,直到其它线程调用KeReleaseSemaphore释放信号量了,让信号量增加。
事件
// 事件
KeInitializeEvent
KeWaitXxx
KeSetEvent
KeResetEvent/KeClearEvent
事件很简单,要么真要么假,即要么有信号,要么无信号。
在初始化时有两种类别:
-
通知事件:释放所有在该事件上等待的线程 -
同步事件:释放最多一个线程后事件变为无信号状态
总结
综合场景使用不同的同步机制,对于内核开发来说,除了信号量,其它的都挺常用的。
本文使用 markdown.com.cn 排版