转载请标明是引用于 http://blog.csdn.net/chenyujing1234
欢迎大家拍砖!
参考书籍:<<Windows驱动开发技术详解>>
1、中断请求级
在Windows的时候,设计者将中断请求分别划分为软件中断和硬件中断,并将这些中断都映射成不同级别的中断请求级(IRQL)。
同步处理机制很大程序上依赖于中断请求级,本节对中断请求级做介绍。
1、1 中断请求(IRQ)与可编程中断控制器(PIC)
中断请求(IRQ)一般有两种,一种是外部中断,也就是硬件产生的中断;
另一种是由软件指令int n产生的中断。这里只介绍硬件产生的中断。
1、2 高级可编程控制器(APIC)
传统PC一般使用2片Intel 8259A中断控制器,然而,面在的X86计算机都是应用高级可编译控制器,
即Advanced Programmable Interrupt Controller(ACIC)
APIC兼容PIC,且APIC把IRQ的数据增加到了24个,我们可以用设备管理器查看这24个中断。
1、3 中断请求级(IRQL)
在APIC中,IRQ的数量增加到了24个,每个IRQ有各自的优先级别,正在运行的线程随时可被中断打断,
进入到中断处理程序。当优先级高的中断来临时,处在优先级低的中断处理程序,也会被打断,进入到更高级的中断处理函数。
Windows将中断进入了扩展,提出一个中断请求级(IRQL)的概念。其中规定了32个中断请求级别,分别是0-2级别为软件中断,3-31为硬件中断。
Windows将24个IRQ映射到了从DISPATCH_LEVEL到PROFILE_LEVEL之间,不同硬件的中断处理程序运行在不同的IRQL级别中。
硬件IRQL称为设备中断请求级,或称DIRQL。Windows大部分时间运行在软件中断级别中。当设备中断来临时,操作系统提升IRQL至DIRQL级别,
并且运行中断处理函数。
用户模式的代码是运行在最低优先级的PASSIVE_LEVEL级别。驱动程序的DriverEntry函数,派遣函数、AddDevice等函数一般是运行在PASSIVE_LEVEL级别,
它们在必要时可以申请进入DISPATCH_LEVEL级别。
Windows负责线程调度的组件是运行在DISPATCH_LEVEL级别,当前线程运行完时间片后,系统自动从PASSIVE_LEVEL级别升到DISPATCH_LEVL级别。
当线程切换完毕后,又从DISPATCH_LEVEL级别降到PASSIVE_LEVEL级别。
在内核模式下,可以通过调用KeGetCurrentIrql内核函数来得到当前的IRQL级别。
1、4 线程调试与线程优先级
在APP编程中,会听到线程优先级概念。线程优先级和IRQL是两个容易混淆的概念。所有的应用程序都运行在PASSIVE_LEVEL级别上,它的优先级别最低,
可以被其他IRQL级别的程序打断。
线程优先级是指线程是否有更多机会运行在CPU上,线程优先级高的线程有更多的机会被内核调用。
ReadFile内部创建IRP_MJ_READ,然后这个IRP被传递到驱动程序的派遣函数中,这时派遣函数运行于ReadFile所在的线程中,或者说ReadFile和派遣函数位于
同一个线程上下文中。
1、5 IRQL的变化
为了更好理解IRQL概念我们描述一个线程运行过程。这个线程在运行中,被一个中断打断,并且在中断服务执行时,
被更高级的中断打断,运行的过程如下图,线程运行分为以下几个阶段。
(1)阶段1:一个普通线程A正在运行;
(2)阶段2:这个时刻有一个中断发生,它的IRQL为0xD。CPU中断当前运行的线程A,将IRQL提升到0xD级别。
(3)阶段3:这时有一个更高级别的中断发生,它的IRQL是0x1A 。这时CPU将IRQL提升到0x1A级别。
(4)阶段4:这时候又有一个中断发生,但它的IRQL为0x18,低于上一个中断优先级。CPU不会理睬这个中断。
(5)阶段5:这时IRQL为0x1A 的中断结束,OS进入IRQL为0x18的中断服务。
(6)阶段6:这时IRQL为0x18中断结束,于是进入IRQL为0xD 的中断服务。
(7)阶段7:最后IRQL为0xD 的中断结束,操作系统恢复线程A。
线程运行在PASSIVE_LEVEL级别,这个时候OS随时可能将当前线程切换到别的线程。但是如果提升IRQL到DISPATCH_LEVEL级别,这时会不会
出现线程切换。这是一种很常用的处理机制,但这种方法只能使用于单CPU的系统。对于多CPU的系统,需要采用别的同步处理机制。
1、6 IRQL与内存分页
在使用内存分页时,可能会导致页故障。因为分页内存随时可能从物理内存交换到磁盘文件。读取不在物理内存中的分页时,会引发一个页故障,从而
执行这个异常的处理函数。异常处理函数会重新将磁盘文件的内容交换到物理内存中。
页故障允许出现在PASSIVE_LEVEL级别的程序中,但如果在DISPATCH_LEVEL或者更高级别IRQL的程序中会带来系统崩溃。
对于等于或高于DISPATCH_LEVEL级别的程序不能使用分页内存,必须使用非分页内存。驱动程序的StartIO全程、DPC例程、中断服务例程都运行在DISPATCH_LEVEL
或更高的IRQL,因为这些例程不能使用分页内存,否则会导致系统崩溃。
1、7 控制IRQL提升与降低
有些时候驱动程序中需要提升IRQL级别,在运行一段时间后,再降回原来的IRQL级别,这样做的目的一般基于同步处理的需要。
首先驱动程序需要知道当前状态是什么IRQL级别,可以通过KeGetCurrentIrql内核函数获取当前的IRQL级别。
然后驱动程序使用内核函数KeRaiseIrql将IRQL提高。KeRaiseIrql需要两个参数,第一个参数是提升后的IRQL级别,第二个参数保存提升前的IRQL级别。
最后,驱动程序在某个时刻需要将IRQL恢复到以前的IRQL级别,驱动程序可以调用KeLowerIrql内核函数。下面的代码演示了在驱动中如何提升与降低IRQL级别:
VOID RasiseIRQL_Test()
{
KIRQL oldirql;
// 确保当前IRQL等于或小于DISPATCH_LEVEL
ASSERT(KeGetCurrentIrql() <= DIPATCH_LEVEL);
// 提升IRQL到DISPATCH_LEVEL,并将先前的IRQL保存起来
KeRaiseIrql(DISPATCH_LEVEL, &oldirql);
//...
// 恢复到先前的IRQL
KeLowerIrql(oldirql);
}
2、自旋锁
自旋锁也是一种同步机制,它能保证某个资源只能被一个线程所拥有,这种保护被形象地称做“上锁”。
2、1 原理
在Windows内核中,有一种被称为自旋锁(Spin Lock)的锁,它可以用于驱动程序中的同步处理。初始化自旋锁时,处理解锁状态,
这时它可以被程序“获取”。“获取”后的自旋锁处理于锁定状态,不能再被“获取”。
如果自旋锁已被锁住,这时有程序申请“获取”这个锁,程序则处于“自旋”状态。所谓自旋状态,就是不停地询问是否可以“获取”自旋锁。
自旋锁不同于线程中的等待事件,在线程中如果等待某个事件(Event),操作系统会使这个线程进入休眠状态,CPU会运行其他线程;而自旋锁原理则不同,
它不会切换到别的线程,而是一直让这个线程“自旋”。因此对自旋锁占用时间不宜过长,否则会导致申请自旋锁的其他线程处于自旋,会浪费CPU时间。
驱动程序必须在低于或者等于DISPATCH_LEVEL的IRQL级别中使用自旋锁。
2、2 使用方法
自旋锁的作用是为使各派遣函数之间同步,尽量不要将自旋锁放在全局变量中,而应该将自旋锁放在设备扩展中。(可参考我的文章 <<NDIS网络数据监控程序NDISMonitor(1)-----驱动程序(编译过程与源码讲解)>>文中讲到的每个派遣函数的执行都使用了自旋锁)
自旋锁用KSPIN_LOCK数据结构表示。
typedef struct _DEVICE_EXTENSION
{
.....
KSPIN_LOCK My_SpinLock; // 在设备扩展中定义自旋锁
}DEVICE_EXTENSION, *PDEVICE_EXTENSION;
使用自旋锁首先需要对其进行初始化,可以使用KeInitializeSpinLock内核函数。一般是在驱动程序的DriverEntry或AddDevice函数中初始化自旋锁。
申请自旋锁可以使用内核函数KeAcquireSpinLock,它有两个参数,一个为自旋锁指针,第二个参数记录获得自旋锁以前的IRQL级别。
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
KIRQL oldirql;
KeACquireSpinLock(&pdx->My_SpinLock, &oldirql);
3、用户模式下的同步对象
3、1 用户模式下的信号灯
信号灯也是一种常见的同步对象,信号灯也有两种状态,一种是激发状态,另一种是未激发状态。信号灯内部有个计数器,可以理解信号灯内部有N个灯泡。
如果有一个灯泡亮着,就代表信号灯处于激发状态,如果全部熄灭,则代表信号灯处于未激发状态。使用信号灯前需要先创建信号灯,CreateSemaphore函数负责创建信号灯。它的声明如下:
WINBASEAPI
HANDLE
WINAPI
CreateSemaphoreA(
IN LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 安全属性
IN LONG lInitialCount, // 初始化计数个数
IN LONG lMaximumCount, // 计数器最大个数
IN LPCSTR lpName // 命名
);
其中,第二个参数lInitialCount在指明初始化时,计数器的值为多少。
第三个参数lMaximumCount指明该信号灯计数器的最大值是多少。如果初始值为0,则处于未激发状态;如果初始值为非零,则处于激发状态。
另外,可以使用期ReleaseSemaphore函数增加信号灯的计数器,其函数声明如下:
WINBASEAPI
BOOL
WINAPI
ReleaseSemaphore(
IN HANDLE hSemaphore,
IN LONG lReleaseCount,
OUT LPLONG lpPreviousCount
);
其中,第二个参数lReleaseCount是这次操作增加计数的数量;
第三个参数lpPreviousCount获得执行本操作之前计数的大小。
另外,对信号灯执行一次等待操作,就会减少一个计数,相当于熄灭一个灯泡。当计数为零时,也就是所有灯泡都熄灭时,当前线程进入睡眠状态,直到信号灯变为激发状态。
下面综合以上API,缩写了信号灯同步对象的使用方法:
#include <windows.h>
#include <process.h> /*_beginthread, _endthread*/
#include <stdio.h>
UINT WINAPI Thread(LPVOID para)
{
printf("Enter Thread1\n");
HANDLE *phSemaphore = (HANDLE*)para;
// 等待5s
Sleep(5000);
printf("Leave Thread1\n");
// 将信号灯计数器加1,使之处于激发状态
ReleaseSemaphore(*phSemaphore, 1, NULL);
return 0;
}
int main()
{
// 创建同步事件
HANDLE hSemaphore = CreateSemaphore(NULL, 2, 2, NULL);
// 此时信号灯计数为2, 处于触发状态
WaitForSingleObject(hSemaphore, INFINITE);
// 此时的信号灯计数为1,处于触发状态
WaitForSingleObject(hSemaphore, INFINITE);
// 此时的信号灯计数为0, 处于未触发状态
// 开启新线程,并将同步事件句柄指针传给新线程
HANDLE hThread1 = (HANDLE)_beginthreadex(NULL, 0, Thread1, &hSemaphore, 0, NULL);
// 等待事件激发
WaitForSingleObject(hSemaphore, INFINITE);
}
4、内核模式下的同步对象
在用户模式下,程序员无法获得真实的同步对象的指针,而是用一个句柄代表这个对象。在内核模式下,程序员可以获得真实同步对象的指针。
内核模式可以通过ObReferenceObjectByHandle函数将用户模式的同步对象句柄转化为对象指针。(eg <<NDIS网络数据监控程序NDISMonitor(1)-----驱动程序(编译过程与源码讲解)>>)
NTSTATUS
ObReferenceObjectByHandle(
IN HANDLE Handle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_TYPE ObjectType OPTIONAL,
IN KPROCESSOR_MODE AccessMode,
OUT PVOID *Object,
OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL
);
4、1 内核模式下的等待
有两个函数负责等待内核同步对象,分别是KeWaitForSingleObject和KeWaitForMultipleObjects函数。
NTKERNELAPI
NTSTATUS
KeWaitForSingleObject (
IN PVOID Object,
IN KWAIT_REASON WaitReason,
IN KPROCESSOR_MODE WaitMode,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER Timeout OPTIONAL
);
内核模式下的KeWaitForSingleObject 比用户模式下的WaitForSingleObject多了很多参数。
第一个参数:Object是一个同步对象的指针,注意这不是句柄;
第二个参数:WaitReason表示等待原因,一般设为Executive;
第三个参数:WaitMode是等待模式,说明这个函数在用户模式下等待还是内核模式下等待。
第四个参数:Alertable指明等待是否是“警惕”的,一般为FALSE;
第五个参数:等待的时间。如果为NULL,代表无限期的等待。
KeWaitForMultipleObjects负责在内核模式下等待多个同步对象。
NTKERNELAPI
NTSTATUS
KeWaitForMultipleObjects (
IN ULONG Count,
IN PVOID Object[],
IN WAIT_TYPE WaitType,
IN KWAIT_REASON WaitReason,
IN KPROCESSOR_MODE WaitMode,
IN BOOLEAN Alertable,
IN PLARGE_INTEGER Timeout OPTIONAL,
IN PKWAIT_BLOCK WaitBlockArray OPTIONAL
);
4、2 内核模式下开启多线程
内核函数PsCreateSystemThread负责创建新线程,该函数可以创建两种线程:一种是用户线程;一种是系统线程。
(1)用户线程属于当前进程中的的线程,当前进程指的是当前I/O操作的发起者。如果IRP_MJ_READ的派遣函数中调用PsCreateSystemThread函数创建用户线程,
新线程就属于调用ReadFile的进程。
(2)系统进程不属于当前用户进程,而属于系统进程。系统进程是OS中一个特殊的进程。每个进程的ID一般为4,我们可以通过任务管理器查看进程。
驱动程序的DriverEntry和AddDevice等函数都是被某个系统线程调用的。
NTKERNELAPI
NTSTATUS
PsCreateSystemThread(
OUT PHANDLE ThreadHandle,
IN ULONG DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle OPTIONAL,
OUT PCLIENT_ID ClientId OPTIONAL,
IN PKSTART_ROUTINE StartRoutine,
IN PVOID StartContext
);
第一个参数ThreadHandle:用于输出,这个参数得到新创建的线程句柄;
第二个参数DesiredAccess:创建的权限;
第三个参数ObjectAttributes:是该线程的属性,一般为NULL;
第四个参数ProcessHandle:指定的是创建用户线程还是系统线程。如果为NULL, 为创建系统线程;如果该值为一个进程句柄,则新创建的线程属于
这个指定的进程。DDK提供的宏NtCurrentProcess可以得到当前进程的句柄。
第六个参数StartRoutine:为新线程的运行地址;
第七个参数StartContext:为新线程接收的参数;
在内核模式下,创建的线程必须用函数PsTerminateSystemThread强制线程结束。否则该线程是无法自动退出的。
我们这里介绍一种方法可以方便地让线程知道自己属于哪个进程:
首先,使用IoGetCurrentProcess函数得到当前线程,IoGetCurrentProcess函数会得到一个PEPROCESS数据结构,PEPROCESS数据结构记录进程的信息,
其中包括进程名,遗憾的是微软没有在DDK定义PEPROCESS结构,可以利用微软的符号表分析这个结构,我们一般用Windbg查看这个结构。
方法可以参考我的文章:<<Window XP驱动开发(十九)Window驱动的内存管理>>中的1、4
VOID SystemThread(IN PVOID pContext)
{
KdPrint(("Enter Systemthread\n"));
PEPROCESS pEprocess = IoGetCurrentProcess();
PTSTR ProcessName = (PTSTR)((ULONG)pEprocess + 0x174);
KdPrint(("This Thrad run in %s prcess!\n", ProcessName));
KdPrint(("Leave SystemThread\n"));
// 结束线程
PsTerminateSystemThread(STATUS_SUCCESS);
}
VOID MyProcessThread(IN PVOID pContext)
{
KdPrint(("Enter MyProcessThread\n"));
// 得到当前进程
PEPROCESS pEProcess = IoGetCurrentProcess();
PTSTR ProcessName = (PTSTR)((ULONG)pEProcess + 0x174);
KdPrint(("This Thrad run in %s prcess!\n", ProcessName));
KdPrint(("Leave SystemThread\n"));
// 结束线程
PsTerminateSystemThread(STATUS_SUCCESS);
}
VOID CreateThread_Test()
{
HANDLE hSystemThread, hMyThread;
// 创建系统线程,该线程是System进程的线程
NTSTATUS status = PsCreateSystemThread(&hSystemThread, 0, NULL, NULL, NULL, SystemThread, NULL);
// 创建进程线程,该线程是用户进程的线程
status = PsCreateSystemThread(&hMyThread, 0, NULL, NtCurrentProcess(), NULL, MyProcessThread, NULL);
}
第一个创建的线程是系统线程,它属于系统进程;第二个创建的是用户线程。
4、3 内核模式下的事件对象
在应用程序中,程序员只能得到事件句柄,无法得到事件对象的指针;
在内核中,用KEVENT数据结构表示一个事件对象,在使用事件对象前,需要调用KeInitializeEvent对事件进行初始化。
NTKERNELAPI
VOID
KeInitializeEvent (
IN PRKEVENT Event,
IN EVENT_TYPE Type,
IN BOOLEAN State
);
第一个参数Event:是初始化事件对象的指针;
第二个参数Type:是事件的类型,有两类:一类是“通知事件”,对应参数是NotificationEvent;另一类是“同步事件”,对应参数是SynchronizationEvent;
第三个参数State:如果为真,事件对象的初始状态为激发状态,如果为假,初始状态为未激发状态;
如果创建的对象是“通知事件”,当事件变是激发状态时,程序员需要手动将其改回未激发状态;
如果创建的是“同步事件”,当事件对象为激发状态时,如遇到KeWaitForXX等函数,事件对象则自动变回未激发状态。
下面的例子首先创建一个事件对象,然后创建一个新线程,并将事件对象的指针传递给线程,主线程等待该事件,新线程在完成任务后,将事件设置为激发状态,
主线程继续:
VOID MyProcessThread(IN PVOID pContext)
{
KdPrint(("Enter MyProcessThread\n"));
// 获得事件指针
PKEVENT pEvent = (PKEVENT)pContext;
KeSetEvent(pEvent, IO_NO_INCREMENT, FALSE);
KdPrint(("Leave SystemThread\n"));
// 结束线程
PsTerminateSystemThread(STATUS_SUCCESS);
}
#pragma PAGEDCODE
VOID Test()
{
HANDLE hMyThread;
KEVENT kEvent;
// 初始化内核事件
KeInitializeEvent(&kEvent, NotificationEvent, FALSE);
// 创建系统线程,该线程是System进程的线程
NTSTATUS status = PsCreateSystemThread(&hMyThread, 0, NULL, NULL, NULL, MyProcessThread, &kEvent);
// 很重要,如果不等待,则MyProcessThread引用了本函数的栈上变量
// 函数退出,同时栈上变量被回收,MyProcessThread引用的参数会出现错误
KeWaitForSingleObject(&kEvent, Executive, KernelMode, FALSE, NULL);
}
4、4 驱动程序与应用程序交互的事件对象
如何在应用程序与驱动程序中共用一个事件对象?需要解决的一个问题是如何将用户模式下创建的事件传递给驱动程序。
解决办法是采用DeviceIoControl API函数。在用户模式下创建一个同步事件,然后用DeviceIoControl把事件句柄传递给驱动。
需要指出的是,句柄与进程是相关的,也就是意味着一个进程中的句柄只能在这个进程中有效。句柄相当于事件对象进程中的索引,
(1)通过这个索引OS会得到事件对象的指针。DDK提供了内核函数将句柄转化为指针,函数是ObReferenceObjectByHandle。
ObReferenceObjectByHandle函数在得到指针时,会为对象的指针维护一个计数。每次调用ObReferenceObjectByHandle会使计数加1。
(2)因此为计数平衡,在使用完ObReferenceObjectByHandle函数后,需要调用ObDereferenceObject函数,它使计数减1。
ObReferenceObjectByHandle会返回一个状态值,表明是否成功得到指针,下面我们来演示一下:
int main()
{
HANDLE hDevice =
CreateFile("\\\\.\\HelloDDK",
GENERIC_READ | GENERIC_WRITE,
0, // share mode none
NULL, // no security
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL ); // no template
if (hDevice == INVALID_HANDLE_VALUE)
{
printf("Failed to obtain file handle to device "
"with Win32 error code: %d\n",
GetLastError() );
return 1;
}
BOOL hRet;
DWORD dwOutput;
// 创建用户模式同步事件
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
// 创建辅助线程
HANDLE hThread1 = (HANDLE)_beginthreadex(NULL, 0, Thread1, &hEvent, 0, NULL);
// 将用户模式的句柄传递给驱动
hRet = DeviceIoControl(hDevice, IOCTL_TRANSMIT_EVENT, &hEvent, sizeof(hEvent), NULL, dwOutput
0, &dwOutput, NULL);
// 等待辅助线程结束
WaitForSingleObject(hThread1, INFINITE);
// 关闭各个句柄
CloseHandle(hDevice);
CloseHandle(hThread1);
CloseHandle(hEvent);
return 0;
}
NTSTATUS HelloDDKDeviceIOControl(IN PDEVICE_OBJECT pDevObj, IN PIRP pirp)
{
NTSTATUS status = STATUS_SUCCESS;
KdPrint(("Enter HelloDDKDeviceIOControl\n"));
// 获得当前IO堆栈
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pirp);
// 获得输入参数大小
ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength;
// 获得输出参数大不
ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength;
// 得到IOCTL码
ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
ULONG info = 0;
switch(code)
{
case IOCTL_TRANSMIT_EVENT:
{
KdPrint(("IOCTL_TEST\n"));
// 得到应用程序传递进来的事件
HANDLE hUserEvent = *(HANDLE*)pirp->AssociatedIrp.SystemBuffer;
PKEVENT pEvent;
// 由事件句柄得到内核事件数据结构
status = ObReferenceObjectByHandle(hUserEvent, EVENT_MODIFY_STATE, *ExEventObjectType, KernelMode, (PVOID*)*pEvent, NULL);
// 设置事件
KeSetEvent(pEvent, IO_NO_INCREMENT, FALSE);
// 减小引用计数
ObDereferenceObject(pEvent);
break;
}
default:
status = STATUS_INVALID_VARIANT;
}
// 设置IRP完成状态
pirp->IoStatus.Status = status;
// 设置IRP操作字节数
pirp->IoStatus.Information = info;
// 结束IRP请求
IoCompleteRequest(pirp, IO_NO_INCREMENT);
KdPrint(("Leave HelloDDKDeviceIOTCL\n"));
return status;
}
4、5 驱动程序与驱动程序交互事件对象
4、6 内核模式下的信号灯
在内核中还有另外一种同步对象,就是信号灯。和事件一样,信号灯在用户模式下和内核模式下是完全统一的,只不过操作方式不同。在用户模式下,信号灯对象用
句柄,而在内核模式下,信号灯对象用KSEMAPHORE数据结构表示。
在使用信号灯对象前,需要对信号灯进行初始化,使用内核函数KeInitializeSemaphore对信号灯对象初始化,声明如下:
NTKERNELAPI
VOID
KeInitializeSemaphore (
IN PRKSEMAPHORE Semaphore,
IN LONG Count,
IN LONG Limit
);
第一个参数Semaphore:这个参数获得内核信号灯对象指针;
第二个参数Count:这个参数是初始化时的信号灯计数;
第三个参数Limit:这个参数指明信号灯的上限值;
KeReadStateSemaphore函数可以读取信号灯当前的计数。
NTKERNELAPI
LONG
KeReadStateSemaphore (
IN PRKSEMAPHORE Semaphore
);
释放信号灯会增加信号灯计数,它对应的内核函数是KeReleaseSemaphore。程序员可以用这个函数指定增量值。获得信号灯可用KeWaitXX 系统函数,如果能获得,
就将计数减一,否则陷入等待。
NTKERNELAPI
LONG
KeReleaseSemaphore (
IN PRKSEMAPHORE Semaphore,
IN KPRIORITY Increment,
IN LONG Adjustment,
IN BOOLEAN Wait
);
下面的代码演示了如何在驱动中使用信号灯对象:
VOID MyProcessThread(IN PVOID pContext)
{
// 得到信号灯
PKSEMAPHORE pkSemaphore = (PKSEMAPHORE)pContext;
KdPrintf("Enter MyProcesssThread\n");
KeReleaseSemaphore(pkSemaphore, IO_NO_INCREMENT, 1, FALSE);
KdPrintf("Leave MyProcessThread\n");
// 结束线程
PsTerminateSystemThread(STATUS_SUCESS);
}
#pragam PAGEDCODE
VOID Test()
{
HANDLE hMyThread;
KSEMAPHORE kSemaphore;
// 初始化内核信号灯
KeInitializeSemaphore(&kSemaphore, 2, 2);
// 读取信号灯状态
LONG count = KeReadStateSemaphore(&kSemaphore);
KdPrint(("The Semaphore count is %d\n", count));
// 等待信号灯
KeWaitForSingleObject(&kSemaphore, Executive, KernelMode, FALSE, NULL);
// 读取信号灯状态
LONG count = KeReadStateSemaphore(&kSemaphore);
KdPrint(("The Semaphore count is %d\n", count));
KeWaitForSingleObject(&kSemaphore, Executive, KernelMode, FALSE, NULL);
// 读取信号灯状态
LONG count = KeReadStateSemaphore(&kSemaphore);
KdPrint(("The Semaphore count is %d\n", count));
// 创建系统线程,该线程是System进程的线程
NTSTATUS status = PsCreateSystemThread(&hMyThread, 0, NULL, NtCurrentProcess(), NULL,
MyProcessThread, &kSemaphore);
// 很重要,如果不等待,则SystemThread引用了本函数的栈上变量
// 当函数退出,同时栈上变量被回收,SystemThread引用的参数会出错
KeWaitForSingleObject(&kSemaphore, Executive, KernelMode, FALSE, NULL);
KdPrint(("After KeWaitForSingleObject\n"));
}
和事件对象一样,信号灯对象也可以在应用程序与驱动程序中交互。
4、7 内核模式下的互斥体
在内核中还有一种同步对象,就是互斥体对象。
互斥体在内核中的数据结构是KMUTEX,使用前需要初始化互斥体对象,可以使用KeInitializeMutex内核函数初始化互斥体对象,其声明如下:
NTKERNELAPI
VOID
KeInitializeMutex (
IN PRKMUTEX Mutex,
IN ULONG Level
);
第一个参数Mutex:这个参数可以获得内核互斥体对象指针;
第二个参数Level:保留值,一般设为0。
初始化后的互斥体对象,就可以使线程之间互斥了,获是互斥体对象用KeWaitXX 系列内核函数,释放互斥体使用KeReleaseMutex内核函数。
下面的例子演示了如何在驱动中使用互斥体对象。首先这个例子创建两个线程,为了保证线程间不并行运行,线程间使用了互斥体对象同步。
VOID MyProcessThread1(IN PVOID pContext)
{
PKMUTEX pkMutex = (PKMUTEX)pContext);
// 获是互斥体
KeWaitForSingleObject(pkMutex, Executive, KernelMode, FALSE, NULL);
KdPrint(("Enter MyProcesThread1\n"));
// 强迫停止50ms,模拟一段代码,模拟运行某段费时
KeStallExecutionProcessor(50);
KdPrint(("Leave MyProcessThread1\n"));
// 释放互斥体
KeReleaseMutex(pkMuext, FALSE);
// 结束线程
PsTerminateSystemThrad(STATUS_SUCCESS);
}
VOID MyProcessThread2(IN PVOID pContext)
{
PKMUTEX pkMutex = (PKMUTEX)pContext);
// 获是互斥体
KeWaitForSingleObject(pkMutex, Executive, KernelMode, FALSE, NULL);
KdPrint(("Enter MyProcesThread2\n"));
// 强迫停止50ms,模拟一段代码,模拟运行某段费时
KeStallExecutionProcessor(50);
KdPrint(("Leave MyProcessThread2\n"));
// 释放互斥体
KeReleaseMutex(pkMuext, FALSE);
// 结束线程
PsTerminateSystemThrad(STATUS_SUCCESS);
}
#pragma PAGEDCODE
VOID Test()
{
HANDLE hMyThread1, hMyThread2;
KMUTEX kMutex;
// 初始化内核互斥体
KeInitializeMutext(&kMutex, 0);
// 创建系统线程,该线程是System进程的线程
PsCreateSystemThread(&hMyThread1, 0, NULL, NtCurrentProcess(), NULL, MyProcessThread1, &kMutex);
PsCreateSystemThread(&hMyThread1, 0, NULL, NtCurrentProcess(), NULL, MyProcessThread2, &kMutex);
PVOID Pointer_Array[2];
// 得到对象指针
ObReferenceObjectByHandle(hMyThread1, 0, NULL, KernelMode, &Pointer_Array[0], NULL);
ObReferenceObjectByHandle(hMyThread2, 0, NULL, KernelMode, &Pointer_Array[1], NULL);
// 等待多个事件
KeWaitForMultipleObjects(2, Pointer_Array, WaitAll, Executive, KernelMode, FALSE, NULL, NULL);
// 减小引用计数
ObDereferenceObject(Pointer_Array[0]);
ObDereferenceObject(Pointer_Array[0]);
KdPrint(("After KeWaitForMultipleObjects\n"));
}
4、8 快速互斥体
快速互斥体(Fast Mutex)是DDK提供的另外一种内核同步对象,它的特征类似前面介绍的普通互斥体对象。快速互斥体和普通互斥体作用完全一样,
之所以称为快速互斥体,是因为执行的速度比普通互斥体速度快(这里指的是获取和释放的速度)。然而,快速互斥体比普通互斥体多了一个缺点,就是
不能递归地获取互斥体对象。递归获取指的是,已经获得互斥体的线程,可以再次获得这个互斥体,换句话说,互斥体只互斥其他线程,而不能互斥自己所在
的线程。但是快速互斥体则不允许出现递归的情况。
普通互斥体在内核中使用MUTEX数据结构描述的,而快速互斥体在内核中是用FAST_MUTEX数据结构描述的。
除此之外,对快速互斥体的初始化、获取和释放对应的内核函数也和普通互斥体不同。初始化快速互斥体的内核函数是ExInitializeFastMutex,获取快速互斥体的内核函数是
ExAcquireFastMutex,释放快速互斥体的内核函数是ExReleaseFastMutex。
下面的代码演示了如何在驱动程序中使用快速互斥体:
VOID MyProcessThread1(IN PVOID pContext)
{
PFAST_MUTEX pFastMutex = (PFAST_MUTEX)pContext);
// 获是快速互斥体
ExAcquireFastMutex(pFastMutex);
KdPrint(("Enter MyProcesThread1\n"));
// 强迫停止50ms,模拟一段代码,模拟运行某段费时
KeStallExecutionProcessor(50);
KdPrint(("Leave MyProcessThread1\n"));
// 释放互斥体
KeReleaseFastMutex(pFastMutex);
// 结束线程
PsTerminateSystemThrad(STATUS_SUCCESS);
}
VOID MyProcessThread2(IN PVOID pContext)
{
PFAST_MUTEX pFastMutex = (PFAST_MUTEX)pContext);
// 获是快速互斥体
ExAcquireFastMutex(pFastMutex);
KdPrint(("Enter MyProcesThread2\n"));
// 强迫停止50ms,模拟一段代码,模拟运行某段费时
KeStallExecutionProcessor(50);
KdPrint(("Leave MyProcessThread2\n"));
// 释放互斥体
KeReleaseFastMutex(pFastMutex, FALSE);
// 结束线程
PsTerminateSystemThrad(STATUS_SUCCESS);
}
#pragma PAGEDCODE
VOID Test()
{
HANDLE hMyThread1, hMyThread2;
FAST_MUTEX fastMutex;
// 初始化内核互斥体
KeInitializeFastMutext(&fastMutex, 0);
// 创建系统线程,该线程是System进程的线程
PsCreateSystemThread(&hMyThread1, 0, NULL, NtCurrentProcess(), NULL, MyProcessThread1, &kMutex);
PsCreateSystemThread(&hMyThread1, 0, NULL, NtCurrentProcess(), NULL, MyProcessThread2, &kMutex);
PVOID Pointer_Array[2];
// 得到对象指针
ObReferenceObjectByHandle(hMyThread1, 0, NULL, KernelMode, &Pointer_Array[0], NULL);
ObReferenceObjectByHandle(hMyThread2, 0, NULL, KernelMode, &Pointer_Array[1], NULL);
// 等待多个事件
KeWaitForMultipleObjects(2, Pointer_Array, WaitAll, Executive, KernelMode, FALSE, NULL, NULL);
// 减小引用计数
ObDereferenceObject(Pointer_Array[0]);
ObDereferenceObject(Pointer_Array[0]);
KdPrint(("After KeWaitForMultipleObjects\n"));
}
4、9 使用自旋锁进行同步
在驱动程序中,经常使用自旋锁作为一种有效的同步机制。例如,在应用程序打开一个设备后,有时需要开户多个线程去操作设备(例如,都调用ReadFile函数对设备进行
读取操作)。这时,IRP_MJ_READ的派遣函数也会并发执行。但是大部分设备没有能力响应并发的读请求,必须完成一个读请求后再完成一个读请求。这时需要进行同步处理,程序员可以选择采用前面介绍的事件、信号灯、互斥体等内核同步对象,但还有另外一种选择,也就是自旋锁。
对于要同步的代码,需要用同一把自旋锁进行同步。如果程序得到了自旋锁,其他程序希望获取自旋锁时,则不停地进入自旋状态。获得自旋锁的内核函数是KeAcquireSpinLock。直到自旋锁被释放后,另外的程序才能获取自旋锁,释放自旋锁的内核函数是KeReleaseSpinLock。
如果希望同步某段代码区域,需要在这段代码区域前获取自旋锁,在代码区域后释放自旋锁。在单CPU的系统中,获取自旋锁是通过提升IRQL实现的,而在多CPU系统中,
实现方法比较复杂,有兴趣的可以自己研究。
无法获得自旋锁的线程会不停地自旋,这会浪费很多CPU时间,因此需要同步的代码不能过长,换句话说就是占有自旋锁时间不能过长。
下面的代码模拟了应用程序创建一个设备后,同时开户多个线程对设备进行请求情况,这个例子采用的同步机制是使用自旋锁:
#include <windows.h>
#include <process.h>
#include <stdio.h>
#include <winioctl.h>
#include "..\NT_Driver\Ioctls.h"
UITN WINAPI Thread1(LPVOID pCOntext)
{
BOOL bRet;
DWORD dwOutpt;
// 发送IOCTL码
bRet = DeviceIoControl(*(PHANDLE)pContext, IOCTL_TEST1, NULL, 0, NULL, 0, &dwOutput, NULL);
return 0;
}
int main()
{
// 打开设备
HANDLE hDevice = Create("\\\\.\\HelloDDK",
GENERIC_READ|GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
// 判断是否成功打开设备句柄
if(hDevice == INVALID_HANDLE_VALUE)
{
printf("Failed to obtain file handle to device\n");
return 1;
}
HANDLE hThread[2];
// 开启两个新线程,每个线程执行DeviceIoControl
// 因此在IRP_MJ_DEVICE_CONTROL的派遣函数会并行进行
// 为了让派遣函数不并行运行,而是串行运行,必须进行同步处理!
// 本例在派遣函数中采用自旋锁进行同步处理
hThread[0] = (HANDLE)_beginthradex(NULL, 0, Thread1, &hDevice, 0, NULL);
hTrhead[1] = (HANDLE)_beginthreaex(NULL, 0, Thread2, &hDevice, 0, NULL);
// 等待两个进程全部运行完毕
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
// 关闭句柄
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
CloseHandle(hDevice);
return 0;
}
驱动程序的派遣函数需要进行同步处理,下面是示例代码:
NTSTATUS HelloDDKDeviceIOControl(IN PDEVICE_OBJECT pDevObj,
IN PIRP pIrp)
{
// 为了避免多个派遣函数并行运行,所以进行同步处理
// 此处采用自旋处理同步
// DeviceIoControl调用,来源自用户线程,因此处于PASSIVCE_LEVEL
ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL);
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
KIRQL oldirql;
KeAccequireSpinLock(&pdx->My_SpinLock, &oldirql); // 获是自旋锁
// A点===========================================================
// 从A点到B点为同步区域,不会被其他派遣函数
NTSTATUS status = STATUS_SUCCESS;
KdPrint(("Enter HeloDDKDeviceIOControl\n"));
// 使用自旋锁后,IRQL提升到DISPATCH_LEVEL
ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);
// 设置IRP完成状态
pIrp->IoStatus.Status = status;
// 设置IRP操作字节数
pIrp->IoStatus.Information = 0;
// 结束IRP请求
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
KdPrint(("Leave HelloDDKDeviceIOControl\n"));
// B点=========================================================
KeReleaseSpinLock(&pdx->My_SpinLock, oldirql); //
return status;
}
4、10 使用互锁操作进行同步
C语言中变量自增的语句,会被编译成一段汇编指令。例如,下面的代码在多线程环境中,就存在“条件竞争”问题,语句number++不是执行的最小单位,最小的执行单位
是汇编指令。每条汇编都有可能被打断。出现这个问题的原因是语句number++不是最小的执行单位。
int number = 0;
void Foo()
{
number++;
// 做一些事件....
number--;
}
为了让number++称为最小的执行单位,保证运行的原子性,可以采用很多种办法,例如,可以使用自旋锁,下面的代码是更改后的代码:
int number = 0;
void Foo()
{
// 获取自旋锁
KeAcquireSpinLock(..);
number++;
// 释放自旋锁
KeReleaseSpinLock(..);
// 做一些事件....
// 获取自旋锁
KeAcquireSpinLock(..);
number--;
// 释放自旋锁
KeReleaseSpinLock(..);
}
DDK提供了两类互锁操作来提供简单的同步处理,一类是InterLockedXX函数,另一类是ExInterLockedXX函数。
其中,InterLockedXX系列函数不需要程序员提供自旋锁,内部不会提升IRQL,因此InterLockedXX函数可以操作非分页的数据,也可以操作分页的数据。
而ExInterLockedXX需要程序员提供一个自旋锁,内部依靠这个自旋锁实现同步,所有ExInterLockedXX不能操作分页内存的数据。
下表列出了DDK提供的ExInterlockedXX系统互锁函数及功能。