3.6 自旋锁

3.6.1 使用自旋锁

链表之类的结构总是涉及恼人的多线程同步问题,这时候就必须使用锁。这里只介绍最简单的自旋锁。

有些读者可能疑惑锁存在的意义。这和多线程操作有关。在驱动开发的代码中,大多是存在多线程执行环境的,也就是说,可能有几个线程在同时调用当前函数。

自旋锁是内核中提供的一种高IRQL锁,用同步以及独占的方式访问某个资源。

如下的代码可初始化获取一个自旋锁:

KeInitializeSpinLock函数没有返回值。下面的代码展示了如何使用自旋锁。在KeAcquireSpinLock和KeReleaseSpinLock之间的代码是只有单线程执行的,其他的线程会停留在KeAcquireSpinLock等候,直到KeReleaseSpinLock被调用。换句话说,只有一个线程能够获得自旋锁。此外,KIRQL是中断级别。KeAcquireSpinLock会提高当前中断级别,将旧的中断级别保存到这个参数中。

读者要注意的是,像下面这样的“加锁”代码是没有意义的,相当于没有加锁。

原因是my_spin_lock这个变量在堆栈中,每个线程来执行的时候都会重新初始化一个锁。只有所有的线程共用一个锁,锁才有意义。所以,锁一般不会定义成局部变量,可以使用静态变量、全局变量,或者分配在池(POOL)中(请参考本章的“内存分配”),下面给出一个正确写法的例子:

3.6.2 在双向链表中使用自旋锁

最后需要提及的是本章前面介绍过的双向链表。链表本身并不保证多线程安全性,所以常常需要用到自旋锁。从理论上讲,应该为链表准备自旋锁,并且在操作链表之前,调用KeAcquireSpinLock来获取锁,在操作完成之后,调用KeReleaseSpinLock来释放锁。但是LIST_ENTRY有一系列操作,这些操作并不需要使用者自己调用获取与释放锁,只需要为每个链表定义并初始化一个锁即可。

链表一旦完成了初始化,之后就可以采用一系列加锁的操作来代替普通操作。比如插入一个节点,普通操作的代码如下:

换成加锁的操作方式如下:

注意不同之处,这里增加了一个自旋锁类型的指针作为参数。在ExInterlockedInsertHeadList中,会自动使用这个自旋锁进行加锁。类似的还有一个加锁的Remove函数,用来移除一个节点,调用方式如下:

这个函数从链表中移除第一个节点,并返回到pRemoveEntry中。

3.6.3 使用队列自旋锁提高性能

上面介绍了如何使用自旋锁,相信读者已经对自旋锁的使用方法有了一定的理解,在此基础上,本节为读者再介绍另外一种自旋锁,称为“队列自旋锁”(Queued spin lock)。

队列自旋锁在Windows XP系统之后被引入,和普通自旋锁相比,队列自旋锁在多CPU平台上具有更好的性能表现,并且遵守“first-come first-served”原则,即:队列自旋锁遵守“谁先等待,谁先获取自旋锁”的原则。其过程和队列的“First in First out”特点非常类似,正是由于这个原因,这种自旋锁被称为“队列自旋锁”。

队列自旋锁的使用和普通自旋锁的使用方法基本一样,初始化自旋锁也是使用KeInitializeSpinLock函数,唯一不同的地方是在获取和释放自旋锁时需要使用新的函数:

下面介绍队列自旋锁的具体用法。队列自旋锁的初始化:

队列自旋锁的获取和释放:

从上面的代码可以看出,队列自旋锁的使用增加了一个KLOCK_QUEUE_HANDLE数据结构,这个数据结构唯一地表示一个队列自旋锁。

请读者务必牢记,普通自旋锁和队列自旋锁虽然都是使用KeInitializeSpinLock函数来初始化的,但是对于一个初始化后的自旋锁,要么按普通自旋锁方式来使用,要么按队列自旋锁方式来使用,绝对不能混用。