首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 嵌入开发 > 驱动开发 >

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

2012-09-02 
Window XP驱动开发(二十二) 驱动程序的同步处理转载请标明是引用于 http://blog.csdn.net/chenyujing1234

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

转载请标明是引用于 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个中断。

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

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

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级别,

并且运行中断处理函数。

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

 

用户模式的代码是运行在最低优先级的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。

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

线程运行在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_LEVELASSERT(KeGetCurrentIrql() <= DIPATCH_LEVEL);// 提升IRQL到DISPATCH_LEVEL,并将先前的IRQL保存起来KeRaiseIrql(DISPATCH_LEVEL, &oldirql);//...// 恢复到先前的IRQLKeLowerIrql(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、用户模式下的同上对象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函数。

NTKERNELAPINTSTATUSKeWaitForSingleObject (    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负责在内核模式下等待多个同步对象。

NTKERNELAPINTSTATUSKeWaitForMultipleObjects (    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,我们可以通过任务管理器查看进程。

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

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

NTKERNELAPINTSTATUSPsCreateSystemThread(    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查看这个结构。

方法可以参考我的文章:<<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对事件进行初始化。

NTKERNELAPIVOIDKeInitializeEvent (    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 PAGEDCODEVOID 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 noneNULL,// no securityOPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL );// no templateif (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, dwOutput0, &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  内核模式下的信号灯 4、7 内核模式下的互斥体4、8 快速互斥体

 

 

热点排行