自旋锁

自旋锁

IRQL概念仅能解决单CPU上的同步问题,在多处理器平台上,它不能保证你的代码不被运行在其它处理器上的代码所干扰。一个称为自旋锁(spin lock)的原始对象可以解决这个问题。为了获得一个自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试并设置(test-and-set)某个内存变量,由于它是原子操作,所以在该操作完成之前其它CPU不可能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行。如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“测试并设置(test-and-set)”操作,即开始“自旋”。最后,锁的所有者通过重置该变量释放这个自旋锁,于是,某个等待的test-and-set操作向其调用者报告锁已释放。

关于自旋锁有两个明显的事实。第一,如果一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁,则该CPU将死锁(deadlock)。自旋锁没有与其关联的“使用计数器”或“所有者标识”;锁或者被占用或者空闲。如果你在锁被占用时获取它,你将等待到该锁被释放。如果碰巧你的CPU已经拥有了该锁,那么用于释放锁的代码将得不到运行,因为你使CPU永远处于“测试并设置”某个内存变量的自旋状态。

关于自旋锁的另一个事实是,CPU在等待自旋锁时不做任何有用的工作,仅仅是等待。所以,为了避免影响性能,你应该在拥有自旋锁时做尽量少的操作,因为此时某个CPU可能正在等待这个自旋锁。

关于自旋锁还存在着一个不太明显但很重要的事实:你仅能在低于或等于DISPATCH_LEVEL级上请求自旋锁,在你拥有自旋锁期间,内核将把你的代码提升到DISPATCH_LEVEL级上运行。在内部,内核能在高于DISPATCH_LEVEL的级上获取自旋锁,但你和我都做不到这一点。

使用自旋锁

为了明确地使用一个自旋锁,首先要在非分页内存中为一个KSPIN_LOCK对象分配存储。然后调用KeInitializeSpinLock初始化这个对象。接着,当代码运行在低于或等于DISPATCH_LEVEL级上时获取这个锁,并执行需要保护的代码,最后释放自旋锁。例如,假设你的设备扩展中有一个名为QLock的自旋锁,你用它来保护你专用IRP队列的访问。你应该在AddDevice函数中初始化这个锁:

typedef struct _DEVICE_EXTENSION {
  ...
  KSPIN_LOCK QLock;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;

...

NTSTATUS AddDevice(...)
{
  ...
  PDEVICE_EXTENSION pdx = ...;
  KeInitializeSpinLock(&pdx->QLock);
  ...
}

在驱动程序的其它地方,假定就在某种IRP的派遣函数中,你在某些必须的队列操作代码周围获取了(并很快释放)该自旋锁。注意这个函数必须存在于非分页内存中,因为在某个时期它会执行在提升的IRQL上。

NTSTATUS DispatchSomething(...)
{
  KIRQL oldirql;
  PDEVICE_EXTENSION pdx = ...;
  KeAcquireSpinLock(&pdx->QLock, &oldirql);				<--1
  ...
  KeReleaseSpinLock(&pdx->QLock, oldirql);				<--2
}
  1. KeAcquireSpinLock获取自旋锁时,它也把IRQL提升到DISPATCH_LEVEL级上。
  2. KeReleaseSpinLock释放自旋锁时,它也把IRQL降低到原来的IRQL级上。

如果你知道代码已经处在DISPATCH_LEVEL级上,你可以调用两个专用函数来获取自旋锁。这个技术适合于DPC、StartIo,和其它执行在DISPATCH_LEVEL级上的驱动程序例程:

KeAcquireSpinLockAtDpcLevel(&pdx->QLock);
...
KeReleaseSpinLockFromDpcLevel(&pdx->QLock);

你可能感兴趣的:(工作,struct,测试,存储,扩展,extension)