4.2 使用LIST_ENTRY
Windows内核提供了一个双向链表结构LIST_ENTRY,此外还有一些其他的结构,比如SINGLE_LIST_ENTRY(单向链表),我们这里不作介绍。
LIST_ENTRY是一个双向链表结构,但直接使用它将毫无任何意义,通常的做法是我们自定义一个结构体,将LIST_ENTRY作为该结构体的一个子域,这样给予了我们最大限度的灵活性,因为我们的数据需求千差万别,可能是整数、字符串等等,但只要简单修改一下便可利用LIST_ENTRY轻松实现一个链表。如下所示:
typedefstruct_MYDATASTRUCT{
ULONGnumber;
LIST_ENTRYListEntry;
}MYDATASTRUCT,*PMYDATASTRUCT;
把它放在后面,比如微软提供的很多结构就不是将其放在第一个子域。
这时候,如果我们想获取节点的地址,需要有一个计算偏移的过程,DDK里面提供了一个宏CONTAINING_RECORD可以在指定结构中找到节点地址的指针。
4.3 使用自旋锁
链表之类的结构总是涉及到恼人的多线程同步问题,这时候就必须使用锁。本章只介绍最简单的自选锁。
有些开发人员可能疑惑锁存在的意义,其实这和多线程操作有关。在驱动开发的代码中,大多是存在于多线程执行环境的,就是说可能同时有多个线程操作一个变量,这时候就可能会引起不可预料的后果。
虽然,多线程并不是真正的并发,但操作链表的过程翻译成汇编指令后往往是由多条指令组成的,简单如intc=a+b;之类的代码也是由多条指令组成的,这就是说这些操作不具有原子性,通过反汇编查看一下就很容易明白。
如下的代码演示了如何使用自选锁:
KSPIN_LOCKmy_spin_lock;
Windows 下设备驱动程序的开发方法 2120080411 计算机应用 赖锡盛
13
KIRQLirql;
// 初始化
KeInitializeSpinLock(&my_spin_lock);
KeAcquireSpinLock(&my_spin_lock,&irql);
//dosomething …
KeReleaseSpinLock(&my_spin_lock,irql);
在使用的时候,我们把需要同步的代码加在KeAcquireSpinLock和KeReleaseSpinLock这两个函数之间,这样在一个线程操作完成调用KeReleaseSpinLock之前,其他线程只能在KeAcquireSpinLock前面等候。KIRQL是一个中断级,KeAcquireSpinLock函数会提高当前的中断级,目前我们忽略这个问题。
在下面的一段程序中,具体演示了链表和自旋锁的操作方法。
VOID
LinkListTest()
{
LIST_ENTRYlinkListHead; // 链表
PMYDATASTRUCTpData; // 节点数据
ULONGi=0; // 计数
KSPIN_LOCKspin_lock; // 自旋锁
KIRQL irql; // 中断级别
// 初始化
InitializeListHead(&linkListHead);
KeInitializeSpinLock(&spin_lock);
//向链表中插入10个元素
KdPrint(("[Test]Begininserttolinklist"));
// 锁定,注意这里的irql是个指针
KeAcquireSpinLock(&spin_lock,&irql);
for(i=0;i<10;i++)
{
pData=(PMYDATASTRUCT)ExAllocatePool(PagedPool,sizeof(MYDATASTRUCT));
pData->number=i;
InsertHeadList(&linkListHead,&pData->ListEntry);
}
// 解锁,注意这里的irql不是指针
KeReleaseSpinLock(&spin_lock,irql);
//从链表中取出所有数据并显示
KdPrint(("[Test]Beginremovefromlinklist\n"));
Windows 下设备驱动程序的开发方法 2120080411 计算机应用 赖锡盛
// 锁定
KeAcquireSpinLock(&spin_lock,&irql);
while(!IsListEmpty(&linkListHead))
{
PLIST_ENTRYpEntry=RemoveTailList(&linkListHead);
// 获取节点地址
pData=CONTAINING_RECORD(pEntry,MYDATASTRUCT,ListEntry);
// 读取节点数据
KdPrint(("[Test]%d\n",pData->number));
ExFreePool(pData);
}
// 解锁
KeReleaseSpinLock(&spin_lock,irql);
}
现在我们需要查看程序的运行效果,首先打开DbgView,设置过滤条件为“*[Test]*”,然后使用KmdManager加载此驱动并运行(最好在虚拟机中测试),注意查看DbgView的输出,如图4-1所示:
图4-1 链表和自旋锁的操作
需要注意的是,像上述代码中在函数中定义一个锁的做法是没有实际意义
14
Windows 下设备驱动程序的开发方法 2120080411 计算机应用 赖锡盛
15
的,因为它是一个局部变量,被定义在栈中,每当有线程调用该函数时,都会重新初始化一个锁,因此它就失去了本来的作用。
在实际的编程中,我们应该把锁定义为一个全局变量、静态(static)变量或者将其定义在堆中。不过在驱动中应该尽量避免使用全局变量,良好的做法是将需要全局访问的变量定义为设备扩展结构体DEVICE_EXTENSION的一个子域。
另外,我们还可以为每个链表都定义并初始化一个锁,在需要向该链表插入或移除节点时不使用前面介绍的普通函数,而是使用如下方法:
ExInterlockedInsertHeadList(&linkListHead,&pData->ListEntry,&spin_lock);
pData=(PMYDATASTRUCT)ExInterlockedRemoveHeadList(&linkListHead,&spin_lock);
此时在向链表中插入或移除节点时会自动调用关联的锁进行加锁操作,可以有效地保证多线程安全性。
Windows内核提供了一个双向链表结构LIST_ENTRY,此外还有一些其他的结构,比如SINGLE_LIST_ENTRY(单向链表),我们这里不作介绍。
LIST_ENTRY是一个双向链表结构,但直接使用它将毫无任何意义,通常的做法是我们自定义一个结构体,将LIST_ENTRY作为该结构体的一个子域,这样给予了我们最大限度的灵活性,因为我们的数据需求千差万别,可能是整数、字符串等等,但只要简单修改一下便可利用LIST_ENTRY轻松实现一个链表。如下所示:
typedefstruct_MYDATASTRUCT{
ULONGnumber;
LIST_ENTRYListEntry;
}MYDATASTRUCT,*PMYDATASTRUCT;
把它放在后面,比如微软提供的很多结构就不是将其放在第一个子域。
这时候,如果我们想获取节点的地址,需要有一个计算偏移的过程,DDK里面提供了一个宏CONTAINING_RECORD可以在指定结构中找到节点地址的指针。
4.3 使用自旋锁
链表之类的结构总是涉及到恼人的多线程同步问题,这时候就必须使用锁。本章只介绍最简单的自选锁。
有些开发人员可能疑惑锁存在的意义,其实这和多线程操作有关。在驱动开发的代码中,大多是存在于多线程执行环境的,就是说可能同时有多个线程操作一个变量,这时候就可能会引起不可预料的后果。
虽然,多线程并不是真正的并发,但操作链表的过程翻译成汇编指令后往往是由多条指令组成的,简单如intc=a+b;之类的代码也是由多条指令组成的,这就是说这些操作不具有原子性,通过反汇编查看一下就很容易明白。
如下的代码演示了如何使用自选锁:
KSPIN_LOCKmy_spin_lock;
Windows 下设备驱动程序的开发方法 2120080411 计算机应用 赖锡盛
13
KIRQLirql;
// 初始化
KeInitializeSpinLock(&my_spin_lock);
KeAcquireSpinLock(&my_spin_lock,&irql);
//dosomething …
KeReleaseSpinLock(&my_spin_lock,irql);
在使用的时候,我们把需要同步的代码加在KeAcquireSpinLock和KeReleaseSpinLock这两个函数之间,这样在一个线程操作完成调用KeReleaseSpinLock之前,其他线程只能在KeAcquireSpinLock前面等候。KIRQL是一个中断级,KeAcquireSpinLock函数会提高当前的中断级,目前我们忽略这个问题。
在下面的一段程序中,具体演示了链表和自旋锁的操作方法。
VOID
LinkListTest()
{
LIST_ENTRYlinkListHead; // 链表
PMYDATASTRUCTpData; // 节点数据
ULONGi=0; // 计数
KSPIN_LOCKspin_lock; // 自旋锁
KIRQL irql; // 中断级别
// 初始化
InitializeListHead(&linkListHead);
KeInitializeSpinLock(&spin_lock);
//向链表中插入10个元素
KdPrint(("[Test]Begininserttolinklist"));
// 锁定,注意这里的irql是个指针
KeAcquireSpinLock(&spin_lock,&irql);
for(i=0;i<10;i++)
{
pData=(PMYDATASTRUCT)ExAllocatePool(PagedPool,sizeof(MYDATASTRUCT));
pData->number=i;
InsertHeadList(&linkListHead,&pData->ListEntry);
}
// 解锁,注意这里的irql不是指针
KeReleaseSpinLock(&spin_lock,irql);
//从链表中取出所有数据并显示
KdPrint(("[Test]Beginremovefromlinklist\n"));
Windows 下设备驱动程序的开发方法 2120080411 计算机应用 赖锡盛
// 锁定
KeAcquireSpinLock(&spin_lock,&irql);
while(!IsListEmpty(&linkListHead))
{
PLIST_ENTRYpEntry=RemoveTailList(&linkListHead);
// 获取节点地址
pData=CONTAINING_RECORD(pEntry,MYDATASTRUCT,ListEntry);
// 读取节点数据
KdPrint(("[Test]%d\n",pData->number));
ExFreePool(pData);
}
// 解锁
KeReleaseSpinLock(&spin_lock,irql);
}
现在我们需要查看程序的运行效果,首先打开DbgView,设置过滤条件为“*[Test]*”,然后使用KmdManager加载此驱动并运行(最好在虚拟机中测试),注意查看DbgView的输出,如图4-1所示:
图4-1 链表和自旋锁的操作
需要注意的是,像上述代码中在函数中定义一个锁的做法是没有实际意义
14
Windows 下设备驱动程序的开发方法 2120080411 计算机应用 赖锡盛
15
的,因为它是一个局部变量,被定义在栈中,每当有线程调用该函数时,都会重新初始化一个锁,因此它就失去了本来的作用。
在实际的编程中,我们应该把锁定义为一个全局变量、静态(static)变量或者将其定义在堆中。不过在驱动中应该尽量避免使用全局变量,良好的做法是将需要全局访问的变量定义为设备扩展结构体DEVICE_EXTENSION的一个子域。
另外,我们还可以为每个链表都定义并初始化一个锁,在需要向该链表插入或移除节点时不使用前面介绍的普通函数,而是使用如下方法:
ExInterlockedInsertHeadList(&linkListHead,&pData->ListEntry,&spin_lock);
pData=(PMYDATASTRUCT)ExInterlockedRemoveHeadList(&linkListHead,&spin_lock);
此时在向链表中插入或移除节点时会自动调用关联的锁进行加锁操作,可以有效地保证多线程安全性。