看流星社区

 找回密码
 注册账号
查看: 2044|回复: 0

Window XP驱动开发(二十二) 驱动程序的同步处理

[复制链接]

该用户从未签到

发表于 2017-6-1 17:25:40 | 显示全部楼层 |阅读模式
转载请标明是引用于view
plaincopy






VOIDRasiseIRQL_Test()
{
KIRQLoldirql;
//确保当前IRQL等于或小于DISPATCH_LEVEL
ASSERT(KeGetCurrentIrql()<=DIPATCH_LEVEL);
//提升IRQL到DISPATCH_LEVEL,并将先前的IRQL保存起来
KeRaiseIrql(DISPATCH_LEVEL,&amp;oldirql);
//...
//恢复到先前的IRQL
KeLowerIrql(oldirql);
}



2、自旋锁

自旋锁也是一种同步机制,它能保证某个资源只能被一个线程所拥有,这种保护被形象地称做“上锁”。

2、1 原理

在Windows内核中,有一种被称为自旋锁(Spin Lock)的锁,它可以用于驱动程序中的同步处理。初始化自旋锁时,处理解锁状态,

这时它可以被程序“获取”。“获取”后的自旋锁处理于锁定状态,不能再被“获取”。

如果自旋锁已被锁住,这时有程序申请“获取”这个锁,程序则处于“自旋”状态。所谓自旋状态,就是不停地询问是否可以“获取”自旋锁。

自旋锁不同于线程中的等待事件,在线程中如果等待某个事件(Event),操作系统会使这个线程进入休眠状态,CPU会运行其他线程;而自旋锁原理则不同,

它不会切换到别的线程,而是一直让这个线程“自旋”。因此对自旋锁占用时间不宜过长,否则会导致申请自旋锁的其他线程处于自旋,会浪费CPU时间。

驱动程序必须在低于或者等于DISPATCH_LEVEL的IRQL级别中使用自旋锁。

2、2 使用方法

自旋锁的作用是为使各派遣函数之间同步,尽量不要将自旋锁放在全局变量中,而应该将自旋锁放在设备扩展中。(可参考我的文章 <<NDIS网络数据监控程序NDISMonitor(1)-----驱动程序(编译过程与源码讲解)>>文中讲到的每个派遣函数的执行都使用了自旋锁)

自旋锁用KSPIN_LOCK数据结构表示。



[cpp]view
plaincopy






typedefstruct_DEVICE_EXTENSION
{
.....
KSPIN_LOCKMy_SpinLock;//在设备扩展中定义自旋锁
}DEVICE_EXTENSION,*PDEVICE_EXTENSION;





使用自旋锁首先需要对其进行初始化,可以使用KeInitializeSpinLock内核函数。一般是在驱动程序的DriverEntry或AddDevice函数中初始化自旋锁。

申请自旋锁可以使用内核函数KeAcquireSpinLock,它有两个参数,一个为自旋锁指针,第二个参数记录获得自旋锁以前的IRQL级别。



[cpp]view
plaincopy






PDEVICE_EXTENSIONpdx=(PDEVICE_EXTENSION)pDevObj->DeviceExtension;
KIRQLoldirql;
KeACquireSpinLock(&amp;pdx->My_SpinLock,&amp;oldirql);




3、用户模式下的同步对象
3、1 用户模式下的信号灯

信号灯也是一种常见的同步对象,信号灯也有两种状态,一种是激发状态,另一种是未激发状态。信号灯内部有个计数器,可以理解信号灯内部有N个灯泡。

如果有一个灯泡亮着,就代表信号灯处于激发状态,如果全部熄灭,则代表信号灯处于未激发状态。使用信号灯前需要先创建信号灯,CreateSemaphore函数负责创建信号灯。它的声明如下:



[cpp]view
plaincopy






WINBASEAPI
HANDLE
WINAPI
CreateSemaphoreA(
INLPSECURITY_ATTRIBUTESlpSemaphoreAttributes,//安全属性
INLONGlInitialCount,//初始化计数个数
INLONGlMaximumCount,//计数器最大个数
INLPCSTRlpName//命名
);



其中,第二个参数lInitialCount在指明初始化时,计数器的&#20540;为多少。

第三个参数lMaximumCount指明该信号灯计数器的最大&#20540;是多少。如果初始&#20540;为0,则处于未激发状态;如果初始&#20540;为非零,则处于激发状态。

另外,可以使用期ReleaseSemaphore函数增加信号灯的计数器,其函数声明如下:



[cpp]view
plaincopy






WINBASEAPI
BOOL
WINAPI
ReleaseSemaphore(
INHANDLEhSemaphore,
INLONGlReleaseCount,
OUTLPLONGlpPreviousCount
);


其中,第二个参数lReleaseCount是这次操作增加计数的数量;

第三个参数lpPreviousCount获得执行本操作之前计数的大小。

另外,对信号灯执行一次等待操作,就会减少一个计数,相当于熄灭一个灯泡。当计数为零时,也就是所有灯泡都熄灭时,当前线程进入睡眠状态,直到信号灯变为激发状态。

下面综合以上API,缩写了信号灯同步对象的使用方法:



[cpp]view
plaincopy






#include<windows.h>
#include<process.h>/*_beginthread,_endthread*/
#include<stdio.h>


UINTWINAPIThread(LPVOIDpara)
{
printf("EnterThread1\n");
HANDLE*phSemaphore=(HANDLE*)para;
//等待5s
Sleep(5000);
printf("LeaveThread1\n");
//将信号灯计数器加1,使之处于激发状态
ReleaseSemaphore(*phSemaphore,1,NULL);
return0;
}

intmain()
{
//创建同步事件
HANDLEhSemaphore=CreateSemaphore(NULL,2,2,NULL);
//此时信号灯计数为2,处于触发状态
WaitForSingleObject(hSemaphore,INFINITE);
//此时的信号灯计数为1,处于触发状态
WaitForSingleObject(hSemaphore,INFINITE);
//此时的信号灯计数为0,处于未触发状态
//开启新线程,并将同步事件句柄指针传给新线程
HANDLEhThread1=(HANDLE)_beginthreadex(NULL,0,Thread1,&amp;hSemaphore,0,NULL);
//等待事件激发
WaitForSingleObject(hSemaphore,INFINITE);
}








4、内核模式下的同步对象

在用户模式下,程序员无法获得真实的同步对象的指针,而是用一个句柄代表这个对象。在内核模式下,程序员可以获得真实同步对象的指针。

内核模式可以通过ObReferenceObjectByHandle函数将用户模式的同步对象句柄转化为对象指针。(eg <<NDIS网络数据监控程序NDISMonitor(1)-----驱动程序(编译过程与源码讲解)>>)



[cpp]view
plaincopy






NTSTATUS
ObReferenceObjectByHandle(
INHANDLEHandle,
INACCESS_MASKDesiredAccess,
INPOBJECT_TYPEObjectTypeOPTIONAL,
INKPROCESSOR_MODEAccessMode,
OUTPVOID*Object,
OUTPOBJECT_HANDLE_INFORMATIONHandleInformationOPTIONAL
);





4、1 内核模式下的等待

有两个函数负责等待内核同步对象,分别是KeWaitForSingleObject和KeWaitForMultipleObjects函数。



[cpp]view
plaincopy






NTKERNELAPI
NTSTATUS
KeWaitForSingleObject(
INPVOIDObject,
INKWAIT_REASONWaitReason,
INKPROCESSOR_MODEWaitMode,
INBOOLEANAlertable,
INPLARGE_INTEGERTimeoutOPTIONAL
);


内核模式下的KeWaitForSingleObject 比用户模式下的WaitForSingleObject多了很多参数。

第一个参数:Object是一个同步对象的指针,注意这不是句柄;

第二个参数:WaitReason表示等待原因,一般设为Executive;

第三个参数:WaitMode是等待模式,说明这个函数在用户模式下等待还是内核模式下等待。

第四个参数:Alertable指明等待是否是“警惕”的,一般为FALSE;

第五个参数:等待的时间。如果为NULL,代表无限期的等待。


KeWaitForMultipleObjects负责在内核模式下等待多个同步对象。



[cpp]view
plaincopy






NTKERNELAPI
NTSTATUS
KeWaitForMultipleObjects(
INULONGCount,
INPVOIDObject[],
INWAIT_TYPEWaitType,
INKWAIT_REASONWaitReason,
INKPROCESSOR_MODEWaitMode,
INBOOLEANAlertable,
INPLARGE_INTEGERTimeoutOPTIONAL,
INPKWAIT_BLOCKWaitBlockArrayOPTIONAL
);





4、2 内核模式下开启多线程

内核函数PsCreateSystemThread负责创建新线程,该函数可以创建两种线程:一种是用户线程;一种是系统线程。

(1)用户线程属于当前进程中的的线程,当前进程指的是当前I/O操作的发起者。如果IRP_MJ_READ的派遣函数中调用PsCreateSystemThread函数创建用户线程,

新线程就属于调用ReadFile的进程。

(2)系统进程不属于当前用户进程,而属于系统进程。系统进程是OS中一个特殊的进程。每个进程的ID一般为4,我们可以通过任务管理器查看进程。



驱动程序的DriverEntry和AddDevice等函数都是被某个系统线程调用的。



[cpp]view
plaincopy






NTKERNELAPI
NTSTATUS
PsCreateSystemThread(
OUTPHANDLEThreadHandle,
INULONGDesiredAccess,
INPOBJECT_ATTRIBUTESObjectAttributesOPTIONAL,
INHANDLEProcessHandleOPTIONAL,
OUTPCLIENT_IDClientIdOPTIONAL,
INPKSTART_ROUTINEStartRoutine,
INPVOIDStartContext
);


第一个参数ThreadHandle:用于输出,这个参数得到新创建的线程句柄;

第二个参数DesiredAccess:创建的权限;

第三个参数ObjectAttributes:是该线程的属性,一般为NULL;

第四个参数ProcessHandle:指定的是创建用户线程还是系统线程。如果为NULL, 为创建系统线程;如果该&#20540;为一个进程句柄,则新创建的线程属于

这个指定的进程。DDK提供的宏NtCurrentProcess可以得到当前进程的句柄。

第六个参数StartRoutine:为新线程的运行地址;

第七个参数StartContext:为新线程接收的参数;



在内核模式下,创建的线程必须用函数PsTerminateSystemThread强制线程结束。否则该线程是无法自动退出的。

我们这里介绍一种方法可以方便地让线程知道自己属于哪个进程:

首先,使用IoGetCurrentProcess函数得到当前线程,IoGetCurrentProcess函数会得到一个PEPROCESS数据结构,PEPROCESS数据结构记录进程的信息,

其中包括进程名,遗憾的是微软没有在DDK定义PEPROCESS结构,可以利用微软的符号表分析这个结构,我们一般用Windbg查看这个结构。

方法可以参考我的文章:<<Window XP驱动开发(十九)Window驱动的内存管理>>中的1、4



[cpp]view
plaincopy






VOIDSystemThread(INPVOIDpContext)
{
KdPrint(("EnterSystemthread\n"));
PEPROCESSpEprocess=IoGetCurrentProcess();
PTSTRProcessName=(PTSTR)((ULONG)pEprocess+0x174);
KdPrint(("ThisThradrunin%sprcess!\n",ProcessName));
KdPrint(("LeaveSystemThread\n"));
//结束线程
PsTerminateSystemThread(STATUS_SUCCESS);

}

VOIDMyProcessThread(INPVOIDpContext)
{
KdPrint(("EnterMyProcessThread\n"));
//得到当前进程
PEPROCESSpEProcess=IoGetCurrentProcess();
PTSTRProcessName=(PTSTR)((ULONG)pEProcess+0x174);
KdPrint(("ThisThradrunin%sprcess!\n",ProcessName));

KdPrint(("LeaveSystemThread\n"));
//结束线程
PsTerminateSystemThread(STATUS_SUCCESS);

}

VOIDCreateThread_Test()
{
HANDLEhSystemThread,hMyThread;
//创建系统线程,该线程是System进程的线程
NTSTATUSstatus=PsCreateSystemThread(&amp;hSystemThread,0,NULL,NULL,NULL,SystemThread,NULL);
//创建进程线程,该线程是用户进程的线程
status=PsCreateSystemThread(&amp;hMyThread,0,NULL,NtCurrentProcess(),NULL,MyProcessThread,NULL);
}


第一个创建的线程是系统线程,它属于系统进程;第二个创建的是用户线程。


4、3 内核模式下的事件对象

在应用程序中,程序员只能得到事件句柄,无法得到事件对象的指针;

在内核中,用KEVENT数据结构表示一个事件对象,在使用事件对象前,需要调用KeInitializeEvent对事件进行初始化。



[cpp]view
plaincopy






NTKERNELAPI
VOID
KeInitializeEvent(
INPRKEVENTEvent,
INEVENT_TYPEType,
INBOOLEANState
);


第一个参数Event:是初始化事件对象的指针;

第二个参数Type:是事件的类型,有两类:一类是“通知事件”,对应参数是NotificationEvent;另一类是“同步事件”,对应参数是SynchronizationEvent;

第三个参数State:如果为真,事件对象的初始状态为激发状态,如果为假,初始状态为未激发状态;

如果创建的对象是“通知事件”,当事件变是激发状态时,程序员需要手动将其改回未激发状态;

如果创建的是“同步事件”,当事件对象为激发状态时,如遇到KeWaitForXX等函数,事件对象则自动变回未激发状态。

下面的例子首先创建一个事件对象,然后创建一个新线程,并将事件对象的指针传递给线程,主线程等待该事件,新线程在完成任务后,将事件设置为激发状态,

主线程继续:



[cpp]view
plaincopy






VOIDMyProcessThread(INPVOIDpContext)
{
KdPrint(("EnterMyProcessThread\n"));
//获得事件指针
PKEVENTpEvent=(PKEVENT)pContext;
KeSetEvent(pEvent,IO_NO_INCREMENT,FALSE);

KdPrint(("LeaveSystemThread\n"));
//结束线程
PsTerminateSystemThread(STATUS_SUCCESS);

}
#pragmaPAGEDCODE
VOIDTest()
{
HANDLEhMyThread;
KEVENTkEvent;
//初始化内核事件
KeInitializeEvent(&amp;kEvent,NotificationEvent,FALSE);
//创建系统线程,该线程是System进程的线程
NTSTATUSstatus=PsCreateSystemThread(&amp;hMyThread,0,NULL,NULL,NULL,MyProcessThread,&amp;kEvent);
//很重要,如果不等待,则MyProcessThread引用了本函数的栈上变量
//函数退出,同时栈上变量被回收,MyProcessThread引用的参数会出现错误
KeWaitForSingleObject(&amp;kEvent,Executive,KernelMode,FALSE,NULL);
}


4、4 驱动程序与应用程序交互的事件对象

如何在应用程序与驱动程序中共用一个事件对象?需要解决的一个问题是如何将用户模式下创建的事件传递给驱动程序。

解决办法是采用DeviceIoControl API函数。在用户模式下创建一个同步事件,然后用DeviceIoControl把事件句柄传递给驱动。

需要指出的是,句柄与进程是相关的,也就是意味着一个进程中的句柄只能在这个进程中有效。句柄相当于事件对象进程中的索引,

(1)通过这个索引OS会得到事件对象的指针。DDK提供了内核函数将句柄转化为指针,函数是ObReferenceObjectByHandle。

ObReferenceObjectByHandle函数在得到指针时,会为对象的指针维护一个计数。每次调用ObReferenceObjectByHandle会使计数加1。

(2)因此为计数平衡,在使用完ObReferenceObjectByHandle函数后,需要调用ObDereferenceObject函数,它使计数减1。

ObReferenceObjectByHandle会返回一个状态&#20540;,表明是否成功得到指针,下面我们来演示一下:



[cpp]view
plaincopy






intmain()
{

HANDLEhDevice=
CreateFile("\\\\.\\HelloDDK",
GENERIC_READ|GENERIC_WRITE,
0,//sharemodenone
NULL,//nosecurity
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);//notemplate

if(hDevice==INVALID_HANDLE_VALUE)
{
printf("Failedtoobtainfilehandletodevice"
"withWin32errorcode:%d\n",
GetLastError());
return1;
}

BOOLhRet;
DWORDdwOutput;
//创建用户模式同步事件
HANDLEhEvent=CreateEvent(NULL,FALSE,FALSE,NULL);
//创建辅助线程
HANDLEhThread1=(HANDLE)_beginthreadex(NULL,0,Thread1,&amp;hEvent,0,NULL);
//将用户模式的句柄传递给驱动
hRet=DeviceIoControl(hDevice,IOCTL_TRANSMIT_EVENT,&amp;hEvent,sizeof(hEvent),NULL,dwOutput
0,&amp;dwOutput,NULL);
//等待辅助线程结束
WaitForSingleObject(hThread1,INFINITE);
//关闭各个句柄
CloseHandle(hDevice);
CloseHandle(hThread1);
CloseHandle(hEvent);

return0;
}

NTSTATUSHelloDDKDeviceIOControl(INPDEVICE_OBJECTpDevObj,INPIRPpirp)
{
NTSTATUSstatus=STATUS_SUCCESS;
KdPrint(("EnterHelloDDKDeviceIOControl\n"));
//获得当前IO堆栈
PIO_STACK_LOCATIONstack=IoGetCurrentIrpStackLocation(pirp);
//获得输入参数大小
ULONGcbin=stack->arameters.DeviceIoControl.InputBufferLength;
//获得输出参数大不
ULONGcbout=stack->arameters.DeviceIoControl.OutputBufferLength;
//得到IOCTL码
ULONGcode=stack->arameters.DeviceIoControl.IoControlCode;
ULONGinfo=0;
switch(code)
{
caseIOCTL_TRANSMIT_EVENT:
{
KdPrint(("IOCTL_TEST\n"));
//得到应用程序传递进来的事件
HANDLEhUserEvent=*(HANDLE*)pirp->AssociatedIrp.SystemBuffer;
PKEVENTpEvent;
//由事件句柄得到内核事件数据结构
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(("LeaveHelloDDKDeviceIOTCL\n"));
returnstatus;
}




4、5 驱动程序与驱动程序交互事件对象

4、6 内核模式下的信号灯

在内核中还有另外一种同步对象,就是信号灯。和事件一样,信号灯在用户模式下和内核模式下是完全统一的,只不过操作方式不同。在用户模式下,信号灯对象用

句柄,而在内核模式下,信号灯对象用KSEMAPHORE数据结构表示。

在使用信号灯对象前,需要对信号灯进行初始化,使用内核函数KeInitializeSemaphore对信号灯对象初始化,声明如下:



[cpp]view
plaincopy






NTKERNELAPI
VOID
KeInitializeSemaphore(
INPRKSEMAPHORESemaphore,
INLONGCount,
INLONGLimit
);



第一个参数Semaphore:这个参数获得内核信号灯对象指针;

第二个参数Count:这个参数是初始化时的信号灯计数;

第三个参数Limit:这个参数指明信号灯的上限&#20540;;

KeReadStateSemaphore函数可以读取信号灯当前的计数。



[cpp]view
plaincopy






NTKERNELAPI
LONG
KeReadStateSemaphore(
INPRKSEMAPHORESemaphore
);



释放信号灯会增加信号灯计数,它对应的内核函数是KeReleaseSemaphore。程序员可以用这个函数指定增量&#20540;。获得信号灯可用KeWaitXX 系统函数,如果能获得,

就将计数减一,否则陷入等待。



[cpp]view
plaincopy






NTKERNELAPI
LONG
KeReleaseSemaphore(
INPRKSEMAPHORESemaphore,
INKPRIORITYIncrement,
INLONGAdjustment,
INBOOLEANWait
);


下面的代码演示了如何在驱动中使用信号灯对象:



[cpp]view
plaincopy






VOIDMyProcessThread(INPVOIDpContext)
{
//得到信号灯
PKSEMAPHOREpkSemaphore=(PKSEMAPHORE)pContext;
KdPrintf("EnterMyProcesssThread\n");
KeReleaseSemaphore(pkSemaphore,IO_NO_INCREMENT,1,FALSE);
KdPrintf("LeaveMyProcessThread\n");
//结束线程
PsTerminateSystemThread(STATUS_SUCESS);
}

#pragamPAGEDCODE
VOIDTest()
{
HANDLEhMyThread;
KSEMAPHOREkSemaphore;
//初始化内核信号灯
KeInitializeSemaphore(&amp;kSemaphore,2,2);
//读取信号灯状态
LONGcount=KeReadStateSemaphore(&amp;kSemaphore);
KdPrint(("TheSemaphorecountis%d\n",count));
//等待信号灯
KeWaitForSingleObject(&amp;kSemaphore,Executive,KernelMode,FALSE,NULL);
//读取信号灯状态
LONGcount=KeReadStateSemaphore(&amp;kSemaphore);
KdPrint(("TheSemaphorecountis%d\n",count));
KeWaitForSingleObject(&amp;kSemaphore,Executive,KernelMode,FALSE,NULL);
//读取信号灯状态
LONGcount=KeReadStateSemaphore(&amp;kSemaphore);
KdPrint(("TheSemaphorecountis%d\n",count));
//创建系统线程,该线程是System进程的线程
NTSTATUSstatus=PsCreateSystemThread(&amp;hMyThread,0,NULL,NtCurrentProcess(),NULL,
MyProcessThread,&amp;kSemaphore);
//很重要,如果不等待,则SystemThread引用了本函数的栈上变量
//当函数退出,同时栈上变量被回收,SystemThread引用的参数会出错
KeWaitForSingleObject(&amp;kSemaphore,Executive,KernelMode,FALSE,NULL);
KdPrint(("AfterKeWaitForSingleObject\n"));
}



和事件对象一样,信号灯对象也可以在应用程序与驱动程序中交互。

4、7 内核模式下的互斥体

在内核中还有一种同步对象,就是互斥体对象。

互斥体在内核中的数据结构是KMUTEX,使用前需要初始化互斥体对象,可以使用KeInitializeMutex内核函数初始化互斥体对象,其声明如下:



[cpp]view
plaincopy






NTKERNELAPI
VOID
KeInitializeMutex(
INPRKMUTEXMutex,
INULONGLevel
);



第一个参数Mutex:这个参数可以获得内核互斥体对象指针;

第二个参数Level:保留&#20540;,一般设为0。

初始化后的互斥体对象,就可以使线程之间互斥了,获是互斥体对象用KeWaitXX系列内核函数,释放互斥体使用KeReleaseMutex内核函数。

下面的例子演示了如何在驱动中使用互斥体对象。首先这个例子创建两个线程,为了保证线程间不并行运行,线程间使用了互斥体对象同步。



[cpp]view
plaincopy






VOIDMyProcessThread1(INPVOIDpContext)
{
PKMUTEXpkMutex=(PKMUTEX)pContext);
//获是互斥体
KeWaitForSingleObject(pkMutex,Executive,KernelMode,FALSE,NULL);
KdPrint(("EnterMyProcesThread1\n"));
//强迫停止50ms,模拟一段代码,模拟运行某段费时
KeStallExecutionProcessor(50);
KdPrint(("LeaveMyProcessThread1\n"));
//释放互斥体
KeReleaseMutex(pkMuext,FALSE);
//结束线程
PsTerminateSystemThrad(STATUS_SUCCESS);
}


VOIDMyProcessThread2(INPVOIDpContext)
{
PKMUTEXpkMutex=(PKMUTEX)pContext);
//获是互斥体
KeWaitForSingleObject(pkMutex,Executive,KernelMode,FALSE,NULL);
KdPrint(("EnterMyProcesThread2\n"));
//强迫停止50ms,模拟一段代码,模拟运行某段费时
KeStallExecutionProcessor(50);
KdPrint(("LeaveMyProcessThread2\n"));
//释放互斥体
KeReleaseMutex(pkMuext,FALSE);
//结束线程
PsTerminateSystemThrad(STATUS_SUCCESS);
}

#pragmaPAGEDCODE
VOIDTest()
{
HANDLEhMyThread1,hMyThread2;
KMUTEXkMutex;
//初始化内核互斥体
KeInitializeMutext(&amp;kMutex,0);
//创建系统线程,该线程是System进程的线程
PsCreateSystemThread(&amp;hMyThread1,0,NULL,NtCurrentProcess(),NULL,MyProcessThread1,&amp;kMutex);
PsCreateSystemThread(&amp;hMyThread1,0,NULL,NtCurrentProcess(),NULL,MyProcessThread2,&amp;kMutex);
PVOIDPointer_Array[2];
//得到对象指针
ObReferenceObjectByHandle(hMyThread1,0,NULL,KernelMode,&ampointer_Array[0],NULL);
ObReferenceObjectByHandle(hMyThread2,0,NULL,KernelMode,&ampointer_Array[1],NULL);
//等待多个事件
KeWaitForMultipleObjects(2,Pointer_Array,WaitAll,Executive,KernelMode,FALSE,NULL,NULL);
//减小引用计数
ObDereferenceObject(Pointer_Array[0]);
ObDereferenceObject(Pointer_Array[0]);
KdPrint(("AfterKeWaitForMultipleObjects\n"));
}




4、8 快速互斥体



快速互斥体(Fast Mutex)是DDK提供的另外一种内核同步对象,它的特征类似前面介绍的普通互斥体对象。快速互斥体和普通互斥体作用完全一样,

之所以称为快速互斥体,是因为执行的速度比普通互斥体速度快(这里指的是获取和释放的速度)。然而,快速互斥体比普通互斥体多了一个缺点,就是

不能递归地获取互斥体对象。递归获取指的是,已经获得互斥体的线程,可以再次获得这个互斥体,换句话说,互斥体只互斥其他线程,而不能互斥自己所在

的线程。但是快速互斥体则不允许出现递归的情况。



普通互斥体在内核中使用MUTEX数据结构描述的,而快速互斥体在内核中是用FAST_MUTEX数据结构描述的。

除此之外,对快速互斥体的初始化、获取和释放对应的内核函数也和普通互斥体不同。初始化快速互斥体的内核函数是ExInitializeFastMutex,获取快速互斥体的内核函数是

ExAcquireFastMutex,释放快速互斥体的内核函数是ExReleaseFastMutex。

下面的代码演示了如何在驱动程序中使用快速互斥体:



[cpp]view
plaincopy






VOIDMyProcessThread1(INPVOIDpContext)
{
PFAST_MUTEXpFastMutex=(PFAST_MUTEX)pContext);
//获是快速互斥体
ExAcquireFastMutex(pFastMutex);
KdPrint(("EnterMyProcesThread1\n"));
//强迫停止50ms,模拟一段代码,模拟运行某段费时
KeStallExecutionProcessor(50);
KdPrint(("LeaveMyProcessThread1\n"));
//释放互斥体
KeReleaseFastMutex(pFastMutex);
//结束线程
PsTerminateSystemThrad(STATUS_SUCCESS);
}


VOIDMyProcessThread2(INPVOIDpContext)
{
PFAST_MUTEXpFastMutex=(PFAST_MUTEX)pContext);
//获是快速互斥体
ExAcquireFastMutex(pFastMutex);
KdPrint(("EnterMyProcesThread2\n"));
//强迫停止50ms,模拟一段代码,模拟运行某段费时
KeStallExecutionProcessor(50);
KdPrint(("LeaveMyProcessThread2\n"));
//释放互斥体
KeReleaseFastMutex(pFastMutex,FALSE);
//结束线程
PsTerminateSystemThrad(STATUS_SUCCESS);
}

#pragmaPAGEDCODE
VOIDTest()
{
HANDLEhMyThread1,hMyThread2;
FAST_MUTEXfastMutex;
//初始化内核互斥体
KeInitializeFastMutext(&amp;fastMutex,0);
//创建系统线程,该线程是System进程的线程
PsCreateSystemThread(&amp;hMyThread1,0,NULL,NtCurrentProcess(),NULL,MyProcessThread1,&amp;kMutex);
PsCreateSystemThread(&amp;hMyThread1,0,NULL,NtCurrentProcess(),NULL,MyProcessThread2,&amp;kMutex);
PVOIDPointer_Array[2];
//得到对象指针
ObReferenceObjectByHandle(hMyThread1,0,NULL,KernelMode,&ampointer_Array[0],NULL);
ObReferenceObjectByHandle(hMyThread2,0,NULL,KernelMode,&ampointer_Array[1],NULL);
//等待多个事件
KeWaitForMultipleObjects(2,Pointer_Array,WaitAll,Executive,KernelMode,FALSE,NULL,NULL);
//减小引用计数
ObDereferenceObject(Pointer_Array[0]);
ObDereferenceObject(Pointer_Array[0]);
KdPrint(("AfterKeWaitForMultipleObjects\n"));
}



4、9 使用自旋锁进行同步

在驱动程序中,经常使用自旋锁作为一种有效的同步机制。例如,在应用程序打开一个设备后,有时需要开户多个线程去操作设备(例如,都调用ReadFile函数对设备进行

读取操作)。这时,IRP_MJ_READ的派遣函数也会并发执行。但是大部分设备没有能力响应并发的读请求,必须完成一个读请求后再完成一个读请求。这时需要进行同步处理,程序员可以选择采用前面介绍的事件、信号灯、互斥体等内核同步对象,但还有另外一种选择,也就是自旋锁。

对于要同步的代码,需要用同一把自旋锁进行同步。如果程序得到了自旋锁,其他程序希望获取自旋锁时,则不停地进入自旋状态。获得自旋锁的内核函数是KeAcquireSpinLock。直到自旋锁被释放后,另外的程序才能获取自旋锁,释放自旋锁的内核函数是KeReleaseSpinLock。

如果希望同步某段代码区域,需要在这段代码区域前获取自旋锁,在代码区域后释放自旋锁。在单CPU的系统中,获取自旋锁是通过提升IRQL实现的,而在多CPU系统中,

实现方法比较复杂,有兴趣的可以自己研究。

无法获得自旋锁的线程会不停地自旋,这会浪费很多CPU时间,因此需要同步的代码不能过长,换句话说就是占有自旋锁时间不能过长。

下面的代码模拟了应用程序创建一个设备后,同时开户多个线程对设备进行请求情况,这个例子采用的同步机制是使用自旋锁:



[cpp]view
plaincopy






#include<windows.h>
#include<process.h>
#include<stdio.h>
#include<winioctl.h>
#include"..\NT_Driver\Ioctls.h"

UITNWINAPIThread1(LPVOIDpCOntext)
{
BOOLbRet;
DWORDdwOutpt;
//发送IOCTL码
bRet=DeviceIoControl(*(PHANDLE)pContext,IOCTL_TEST1,NULL,0,NULL,0,&amp;dwOutput,NULL);
return0;
}


intmain()
{
//打开设备
HANDLEhDevice=Create("\\\\.\\HelloDDK",
GENERIC_READ|GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
//判断是否成功打开设备句柄
if(hDevice==INVALID_HANDLE_VALUE)
{
printf("Failedtoobtainfilehandletodevice\n");
return1;
}
HANDLEhThread[2];
//开启两个新线程,每个线程执行DeviceIoControl
//因此在IRP_MJ_DEVICE_CONTROL的派遣函数会并行进行
//为了让派遣函数不并行运行,而是串行运行,必须进行同步处理!
//本例在派遣函数中采用自旋锁进行同步处理
hThread[0]=(HANDLE)_beginthradex(NULL,0,Thread1,&amp;hDevice,0,NULL);
hTrhead[1]=(HANDLE)_beginthreaex(NULL,0,Thread2,&amp;hDevice,0,NULL);
//等待两个进程全部运行完毕
WaitForMultipleObjects(2,hThread,TRUE,INFINITE);
//关闭句柄
CloseHandle(hThread[0]);
CloseHandle(hThread[1]);
CloseHandle(hDevice);
return0;
}



驱动程序的派遣函数需要进行同步处理,下面是示例代码:



[cpp]view
plaincopy






NTSTATUSHelloDDKDeviceIOControl(INPDEVICE_OBJECTpDevObj,
INPIRPpIrp)
{
//为了避免多个派遣函数并行运行,所以进行同步处理
//此处采用自旋处理同步
//DeviceIoControl调用,来源自用户线程,因此处于PASSIVCE_LEVEL
ASSERT(KeGetCurrentIrql()==PASSIVE_LEVEL);

PDEVICE_EXTENSIONpdx=(PDEVICE_EXTENSION)pDevObj->DeviceExtension;
KIRQLoldirql;
KeAccequireSpinLock(&amp;pdx->My_SpinLock,&amp;oldirql);//获是自旋锁
//A点===========================================================
//从A点到B点为同步区域,不会被其他派遣函数
NTSTATUSstatus=STATUS_SUCCESS;
KdPrint(("EnterHeloDDKDeviceIOControl\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(("LeaveHelloDDKDeviceIOControl\n"));
//B点=========================================================
KeReleaseSpinLock(&amp;pdx->My_SpinLock,oldirql);//
returnstatus;
}





4、10 使用互锁操作进行同步

C语言中变量自增的语句,会被编译成一段汇编指令。例如,下面的代码在多线程环境中,就存在“条件竞争”问题,语句number++不是执行的最小单位,最小的执行单位

是汇编指令。每条汇编都有可能被打断。出现这个问题的原因是语句number++不是最小的执行单位。



[cpp]view
plaincopy






intnumber=0;
voidFoo()
{
number++;
//做一些事件....
number--;
}



为了让number++称为最小的执行单位,保证运行的原子性,可以采用很多种办法,例如,可以使用自旋锁,下面的代码是更改后的代码:



[cpp]view
plaincopy






intnumber=0;
voidFoo()
{
//获取自旋锁
KeAcquireSpinLock(..);
number++;
//释放自旋锁
KeReleaseSpinLock(..);

//做一些事件....
//获取自旋锁
KeAcquireSpinLock(..);
number--;
//释放自旋锁
KeReleaseSpinLock(..);
}



DDK提供了两类互锁操作来提供简单的同步处理,一类是InterLockedXX函数,另一类是ExInterLockedXX函数。

其中,InterLockedXX系列函数不需要程序员提供自旋锁,内部不会提升IRQL,因此InterLockedXX函数可以操作非分页的数据,也可以操作分页的数据。

而ExInterLockedXX需要程序员提供一个自旋锁,内部依靠这个自旋锁实现同步,所有ExInterLockedXX不能操作分页内存的数据。

下表列出了DDK提供的ExInterlockedXX系统互锁函数及功能。
点击按钮快速添加回复内容: 支持 高兴 激动 给力 加油 苦寻 生气 回帖 路过 感恩
您需要登录后才可以回帖 登录 | 注册账号

本版积分规则

小黑屋|手机版|Archiver|看流星社区 |网站地图

GMT+8, 2024-4-20 06:19

Powered by Kanliuxing X3.4

© 2010-2019 kanliuxing.com

快速回复 返回顶部 返回列表