本篇内容对应<竹林蹊径>3.4.7对象同步一节。作者对WDF同步域及运行级别的解释停留在纸面,然而,这两个概念却涉及到WDF设备\WDF队列的初始化,因此,不得不探究其背后的原理。

1.同步范围

书上P77提到可以指定2种同步范围:设备同步范围及队列同步范围。首先需要确定什么时候设置同步范围?分别以WdfSynchronizationScopeDevice和WdfSynchronizationScopeQueue为关键字,搜索整个ddk sample代码,在以下文件中找到相关设置:

设备同步范围:

//serial\serial\pnp.c
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE (&attributes,
SERIAL_DEVICE_EXTENSION);

attributes.EvtCleanupCallback = SerialEvtDeviceContextCleanup;
attributes.SynchronizationScope = WdfSynchronizationScopeDevice;

status = WdfDeviceCreate(&DeviceInit, &attributes, &device);

队列同步范围:

//general\echo\kmdf\autosync\queue.c
WDF_OBJECT_ATTRIBUTES_INIT_CONTEXT_TYPE(&queueAttributes, QUEUE_CONTEXT);

queueAttributes.SynchronizationScope = WdfSynchronizationScopeQueue;
queueAttributes.EvtDestroyCallback = EchoEvtIoQueueContextDestroy;
status = WdfIoQueueCreate(
Device,
&queueConfig,
&queueAttributes,
&queue
);

从上面的代码片可知:在创建device或queue时可以设置同步范围。新的问题出现了:为什么都在创建WDF对象前设置?让我们顺着WdfDeviceCreate源码中层层推进

> WdfDeviceCreate()
> FxDevice::_Create();
> pDevice->Initialize();
> FxDevice::Initialize();
> status = ConfigureConstraints(DeviceAttributes);

最终框架在FxDeviceBase::ConfigureConstraints中对同步范围进行了设置(同时也设置了运行级别):

NTSTATUS
FxDeviceBase::ConfigureConstraints(
__in_opt PWDF_OBJECT_ATTRIBUTES ObjectAttributes
)
{
NTSTATUS status;
WDF_EXECUTION_LEVEL driverLevel;
WDF_SYNCHRONIZATION_SCOPE driverScope;

if (ObjectAttributes != NULL) {
m_ExecutionLevel = ObjectAttributes->ExecutionLevel;
m_SynchronizationScope = ObjectAttributes->SynchronizationScope;
}
//取出父对象的同步范围和运行级别
m_Driver->GetConstraints(&driverLevel, &driverScope);
//如果Device继承父对象(Driver)的属性,则依葫芦画瓢
if (m_ExecutionLevel == WdfExecutionLevelInheritFromParent) {
m_ExecutionLevel = driverLevel;
}

if (m_SynchronizationScope == WdfSynchronizationScopeInheritFromParent) {
m_SynchronizationScope = driverScope;
}
//如果是运行级别是PASSIVE_LEVEL,为驱动同步创建MUTEX(因为MUTEX对象会造成线程等待失去CPU)
if (m_ExecutionLevel == WdfExecutionLevelPassive) {
m_CallbackLockPtr = new(GetDriverGlobals())
FxCallbackMutexLock(GetDriverGlobals());
}
else { //如果运行级别是DISPATCH_LEVEL,为驱动同步创建spin_lock
m_CallbackLockPtr = new(GetDriverGlobals())
FxCallbackSpinLock(GetDriverGlobals());
}

if (NULL == m_CallbackLockPtr) {
...
}
//晦涩的代码
m_CallbackLockPtr->Initialize(this);
m_CallbackLockObjectPtr = this; //this对于这个函数而言,是指FxDevice

status = STATUS_SUCCESS;

Done:
return status;
}

在ConfigureConstraints中,框架首先根据WDF_OBJECT_ATTRIBUTE相关域设置即将被创建的Device的同步范围/运行级别属性。驱动同步往往通过MUTEX/SPIN_LOCK来实现,因此,框架根据coder设置的不同运行级别,创建适合运行在不同LEVEL上的同步对象。

代码中有两行容易被忽视的代码:

m_CallbackLockPtr->Initialize(this);
m_CallbackLockObjectPtr = this;

为什么说它重要?后面第二节会用到[注1]。查看Initialize的定义,发现它是一个纯虚函数:

class FxCallbackLock : public FxGlobalsStump {
...
virtual
void
Initialize(
FxObject* ParentObject
) = 0;
...
};

要找到运行时执行的Initialize函数需要费点功夫:

Step1.m_CallbackLockPtr是定义在class FxDeviceBase中基类指针:

FxCallbackLock* m_CallbackLockPtr;

Step2.在FxDeviceBase::ConfigureConstraints中,基类指针指向了不同派生类对象:

if (m_ExecutionLevel == WdfExecutionLevelPassive) {
m_CallbackLockPtr = new(GetDriverGlobals())
FxCallbackMutexLock(GetDriverGlobals());
}
else { //如果运行级别是DISPATCH_LEVEL,为驱动同步创建spin_lock
m_CallbackLockPtr = new(GetDriverGlobals())
FxCallbackSpinLock(GetDriverGlobals());
}

Step3.定位到派生类FxCallbackMutexLock/FxCallbackSpinLock,查看它们如何实现Initialize函数:

class FxCallbackMutexLock : public FxCallbackLock {
...
virtual
void
Initialize(
FxObject* ParentObject
)
{
PFX_DRIVER_GLOBALS fxDriverGlobals;

m_Verifier = NULL;
fxDriverGlobals = GetDriverGlobals();

if (fxDriverGlobals->FxVerifierLock) {

//
// VerifierLock CreateAndInitialize failure is not fatal,
// we just won't track anything
//
(void) FxVerifierLock::CreateAndInitialize(&m_Verifier,
fxDriverGlobals,
ParentObject,
TRUE);
}
}
...
};

fxDriverGlobals->FxVerifierLock好像只有当开启Driver Verifier时才会非空,所以可以简单的认为这段if分支不会执行。

2.同步范围的应用

讲到这,还是看不出同步范围对驱动运行的影响,看官莫急,马上见分晓。我们来看看Queue对象的创建。它的调用流程和WdfDevice类似,也经历这样的过程:

FxIoQueue::_Create();
FxIoQueue->Initialize();
FxIoQueue::Initialize();
status = ConfigureConstraints(DeviceAttributes);

只是在FxIoQueue::ConfigureConstraints中略有不同:

if (m_SynchronizationScope == WdfSynchronizationScopeDevice) {
NTSTATUS status;
...
AutomaticLockingRequired = TRUE;
m_CallbackLockPtr = m_Device->GetCallbackLockPtr(&m_CallbackLockObjectPtr); //<-------重要代码,和[注1]呼应!!!
}
else if (m_SynchronizationScope == WdfSynchronizationScopeQueue) {
AutomaticLockingRequired = TRUE;
}

if (AutomaticLockingRequired) {
//
// If automatic locking has been configured, set the lock
// on the FxCallback object delegates
//
m_IoDefault.SetCallbackLockPtr(m_CallbackLockPtr);
m_IoStop.SetCallbackLockPtr(m_CallbackLockPtr);
m_IoResume.SetCallbackLockPtr(m_CallbackLockPtr);
...

m_IoCancelCallbackLockPtr = m_CallbackLockPtr;
}
else {
//
// No automatic locking specified
//
m_IoDefault.SetCallbackLockPtr(NULL);
m_IoStop.SetCallbackLockPtr(NULL);
...

m_IoCancelCallbackLockPtr = NULL;
}

FxIoQueue从父对象中获得同步范围和运行级别,FxIoQueue的父对象是谁?是前面说过FxDevice。还记得在前面[注1]中写到过两行容易被忽视的代码?第一行代码已经解释过,来看下第二行:

m_CallbackLockPtr->Initialize(this);
m_CallbackLockObjectPtr = this;

m_CallbackLockObjectPtr = this中this指针指向谁?应该是调用者FxDevice,即m_CallbackLockObjectPtr指向了FxDevice。

对于框架这样的设置,如果不把眼光放到其他函数中,可能会不知所以然;但是,如果把眼光投到FxIoQueue::ConfigureConstraints中,可能会理解框架的用意。在此之前看看GetCallbackLockPtr实现:

FxCallbackLock*
FxDeviceBase::GetCallbackLockPtr(
__out_opt FxObject** LockObject
)
{
if (LockObject != NULL) {
*LockObject = m_CallbackLockObjectPtr;
}

return m_CallbackLockPtr;
}

FxIoQueue::ConfigureConstraints中执行这句后

m_CallbackLockPtr = m_Device->GetCallbackLockPtr(&m_CallbackLockObjectPtr); //<-------重要代码,和[注1]呼应!!!

进入FxDeviceBase::GetCallbackLockPtr函数时,将用到两个变量:m_CallbackLockObjectPtr/m_CallbackLockPtr。对于FxDevice类和FxIoQueue类他们都有这两个成员变量,那么,请不假思索的告诉我在FxDeviceBase::GetCallbackLockPtr用到的m_CallbackLockObjectPtr/m_CallbackLockPtr变量是哪个类的?显然,调用者是FxDevice m_Device----框架用FxDevice!m_CallbackLockObjectPtr和FxDevice!m_CallbackLockPtr成员变量赋值给FxIoQueue对应成员变量。

为什么要这样做?这句语句是在if (m_SynchronizationScope == WdfSynchronizationScopeDevice)分支中执行的,换言之,当FxIoQueue被设置为设备同步范围时,它使用父对象FxDevice的同步锁/互斥量。当驱动程序中有2个以上的FxIoQueue对象,只有获得FxDevice创建的同步锁/互斥量的FxIoQueue对象才能进一步操作。所以,可以认为设备同步范围的锁颗粒度较大

至于设置为队列同步范围的FxIoQueue对象,在操作前不需要争抢FxDeviceFxDevice创建的同步锁/互斥量。相对于设备同步范围,它的锁颗粒度较小。