同事在看设备驱动同步时,问了我一个事:如果驱动程序创建了一个设备,在应用层是否允许多个进程同时打开这个设备;如果允许,这种方式应用层和驱动的通信方式是否会相互影响?我不是很确定,写了个测试代码并把结果记录下来。

1.我们在DriverEntry/AddDevice中调用IoCreateDevice创建设备对象。这是IoCreateDevice的接口,倒数第二个参数用于设置设备是否支持独占式访问。一般情况下我们都会传入FALSE(MSDN也建议使用这种方式)。

NTSTATUS IoCreateDevice(
_In_ PDRIVER_OBJECT DriverObject,
_In_ ULONG DeviceExtensionSize,
_In_opt_ PUNICODE_STRING DeviceName,
_In_ DEVICE_TYPE DeviceType,
_In_ ULONG DeviceCharacteristics,
_In_ BOOLEAN Exclusive,
_Out_ PDEVICE_OBJECT *DeviceObject
);

对于FALSE的情况,设备支持多个进程访问:


这部分是驱动创建设备的片段,驱动在响应IRP_MJ_CREATE的派遣函数中设置了一个定时器。

NTSTATUS SampleCharAddDevice(PDRIVER_OBJECT drvObj, PDEVICE_OBJECT pdo)
{
NTSTATUS status = STATUS_SUCCESS;

RtlInitUnicodeString(&fdoName, L"\\Device\\SampleChar");
RtlInitUnicodeString(&fdoSymName, L"\\DosDevices\\SampleChar");

status = IoCreateDevice(drvObj, sizeof(SampleCharDevContext),
&fdoName,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE, &fdo);
if (!NT_SUCCESS(status))
{
return status;
}
...
}

这部分是应用层打开设备的片段:

int main()
{
GetDeviceInterface(interfaceBuff);

hDev = CreateFileA(interfaceBuff,
GENERIC_ALL,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
if (hDev == INVALID_HANDLE_VALUE)
{
lstErr = GetLastError();
return 0;
}

while (1)
{
Sleep(1000);
}
}

这是同时运行两个测试程序打开设备的截图。从截图中可以看出,两个进程在打开设备时都没有从失败的分支退出。

多进程(线程)访问设备的一些疑惑_局部变量


2.修改代码,试图以独占方式访问设备。如果单独在调用IoCreateDevice时把Exclusive改为TRUE,并不能实现这个功能,还需要修改inf文件设备安装节AddReg部分:

多进程(线程)访问设备的一些疑惑_初始化_02

AddReg中"HKR,,Exclusive,0x10001,1"的行为将会为设备的注册表硬件键(HKLM\SYSTEM\CurrentControl\Set\Enum\设备总线\设备号)添加一个值为1的Exclusive键。

多进程(线程)访问设备的一些疑惑_初始化_03

之后再用两个进程去打开设备,后调用CreateFile的进程会调用失败,并且驱动程序的IRP_MJ_CREATE派遣函数也没有得到执行(换句话说是文件系统让CreateFile调用失败的,而不是驱动程序在派遣函数中做了特殊处理)。

多进程(线程)访问设备的一些疑惑_初始化_04

3.进程间相互影响.个人认为,因为派遣函数运行在线程上下文中,必然用了不同线程的内核栈。因此,在不同线程的内核栈中定义的局部变量,如:buf/spinlock/Kevent等,不会相互影响;如果派遣函数中要用到全局变量,则这些全局变量的初始化应该尽早在DriverEntry/AddDevice中做初始化,避免在派遣函数中重复初始化。至于R0和R3之间通信的IRP及IRP的参数,这是IO管理器为每次IO请求新创建的,因此也不会有影响。

1.
kd> ?? devCtx->timer
struct _KTIMER
+0x000 Header : _DISPATCHER_HEADER
+0x010 DueTime : _ULARGE_INTEGER 0x0
+0x018 TimerListEntry : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x020 Dpc : (null)
+0x024 Period : 0
2.
kd> ?? devCtx->timer
struct _KTIMER
+0x000 Header : _DISPATCHER_HEADER
+0x010 DueTime : _ULARGE_INTEGER 0x00000001`5ecb0982
+0x018 TimerListEntry : _LIST_ENTRY [ 0x86e86f80 - 0x82b34f94 ]
+0x020 Dpc : 0x85381564 _KDPC
+0x024 Period : 0
3.
kd> ?? devCtx->timer
struct _KTIMER
+0x000 Header : _DISPATCHER_HEADER
+0x010 DueTime : _ULARGE_INTEGER 0x00000001`65ffd3f7
+0x018 TimerListEntry : _LIST_ENTRY [ 0x851544c0 - 0x82b3503c ]
+0x020 Dpc : 0x85381564 _KDPC
+0x024 Period
4.
kd> ?? devCtx->timer
struct _KTIMER
+0x000 Header : _DISPATCHER_HEADER
+0x010 DueTime : _ULARGE_INTEGER 0x00000001`760536ef
+0x018 TimerListEntry : _LIST_ENTRY [ 0x863e2e90 - 0x82b349c4 ]
+0x020 Dpc : 0x85381564 _KDPC
+0x024 Period : 0

5.
kd> ?? &timeEvt
struct _KEVENT * 0x8a8eba48
+0x000 Header : _DISPATCHER_HEADER

6.
kd> ?? &timeEvt
struct _KEVENT * 0x8cec6a48
+0x000 Header : _DISPATCHER_HEADER

解释一下:1-4是进入SampleCharCreateClose时,调用KeSetTimer设置定时器的windbg的输出,这是一个全局变量,存放在设备扩展区;5-6是局部变量KEVENT timeEvt;的变量地址。

1处 进入SampleCharCreateClose时定时器对象时,KTIMER仅做过初始化,各个域值都是0;调用KeSetTimer后再次进入SampleCharCreateClose,可以看到每次定时器的定时间隔都非0,且每次改动都会被保存到下一次进入SampleCharCreateClose前。这可以说明多线程环境下,设备对象扩展区中的变量会相互影响;

5.6处 进入SampleCharCreateClose操作的KEVENT对象都是栈变量,且每次生成的栈变量的地址都是不同的。因此,在一个线程上下文中的局部变量不会影响其他线程上下文中的局部变量。换言之,如果要做线程同步,绝对不能在派遣函数中新分配一个spinlock/KEVENT,这根本起不到同步的作用


参考:

MSDN:​​Setting Device Object Properties in the Registry​

MSDN:​​INF AddReg Directive​

MSDN:​​Specifying Exclusive Access to Device Objects​