自定义startio为什么要提升irql not less equal

1226人阅读
window xp 驱动(USB)/FireFox插件/汇编(63)
转载自:&&
对设备的任何操作都会最终转化为IRP请求,而IRP一般都是由操作系统异步发送的。异步处理IRP有助于提高效率,但是有时异步处理会带来逻辑上的错误,这时需要将异步的IRP同步化。将IRP同步化的方法有StartIO例程,使用中断服务例程等。
二、应用程序对设备的同步异步操作
大部分IRP都是由应用程序的Win32 API函数发起的。这些Win32 API本身就支持同步和异步的操作。例如:ReadFile,WriteFile和DeviceIoControl等,这些都有两种操作方式。一种同步,一种异步。
1.同步操作和异步操作的原理
操作设备的Win32 API主要是三个函数,即ReadFile函数,WriteFile函数,DeviceIOControl函数。以DeviceIOControl函数为例,当应用程序调用DeviceIoControl函数时,它的内部会创建一个IRP_MJ_DEVICE_CONTROL类型的IRP,并将这个IRP传送到驱动程序的派遣函数中。处理该IRP需要一段时间,直到IRP处理完毕后,DeviceIOControl函数才会返回。
同步操作时,在DeviceIOControl的内部,会调用WaitForSingleObject函数去等待一个事件。这个事件直到IRP被结束时,才会被触发。如果通过反汇编IoCompleteRequest内核函数,就会发现IoComplpeteRequest内部设置了该事件。DeviceIOControl会暂时进入睡眠状态,直到IRP被结束。
而对于异步操作的情况下,当DeviceIOControl被调用时,其内部会产生IRP,并将IRP传递给驱动程序的内部派遣函数。但此时DeviceIOControl不会等待该IRP的结束,而是直接返回。当IRP经过一段时间被结束时,操作系统会触发一个IRP相关事件。这个事件可以通知应用程序IRP请求被执行完毕。
2.同步操作设备
如果需要同步操作设备,在打开设备的时候就要制定以“同步”的方式打开设备。打开设备用CreateFile函数,其函数声明如下:
CreateFile(
LPCSTR lpFileName,
DWORD dwDesiredAccess,
//访问权限
DWORD dwShareMode,
//共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, //安全属性
DWORD dwCreationDisposition,
//如何创建
DWORD dwFlagsAndAttributes,
//设备属性
HANDLE hTemplateFile
//文件模板
其中第六个参数dwFlagsAndAttributes是同步异步操作的关键。如果这个参数没有设置FILE_FLAG_OVERLAPPED,则以后对该设备的操作都是同步操作,否则都是异步操作。
对设备的操作Win32 API,例如ReadFile,WriteFile和DeviceIOControl函数,都会提供一个OVERLAP参数,如ReadFile函数:
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
在同步操作设备时,其lpOverlapped参数设置为NULL。
3.异步操作设备(方式一)
异步操作设备时主要需要OVERLAP参数,Windows中用一种数据结构OVERLAPPED表示。
typedef struct _OVERLAPPED {
ULONG_PTR I
ULONG_PTR InternalH
DWORD OffsetH
} OVERLAPPED, *LPOVERLAPPED;
第三个参数Offset:操作设备会指定一个偏移量,从设备的偏移量进行读取。该偏移量用一个64位整型表示,Offset就是偏移量的低32位。
第四个参数OffsetHigh是偏移量的高32位。
第五个参数hEvent:这个事件用于该操作完成后通知应用程序。程序员可以初始化该事件为未激发,当操作设备结束后,即在驱动程序中调用IoCompleteRequest后,设置该事件为激发态。
在使用OVERLAPPED结构前,要先对其内部清零,并为其创建事件。
下面代码演示如何在应用程序中使用异步操作:
#include &windows.h&
#include &stdio.h&
#define BUFFER_SIZE 512//假设该文件大于或等于BUFFER_SIZE
#define DEVICE_NAME &test.dat&
int main()
HANDLE hDevice = CreateFile(
&test.dat&,
GENERIC_READ | GENERIC_WRITE,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,//此处设置FILE_FLAG_OVERLAPPED
if (hDevice == INVALID_HANDLE_VALUE)
printf(&Read Error\n&);
UCHAR buffer[BUFFER_SIZE];
DWORD dwR //初始化overlap使其内部全部为零
OVERLAPPED overlap={0}; //创建overlap事件
overlap.hEvent = CreateEvent(NULL,FALSE,FALSE,NULL); //这里没有设置OVERLAP参数,因此是异步操作
ReadFile(hDevice,buffer,BUFFER_SIZE,&dwRead,&overlap); //做一些其他操作,这些操作会与读设备并行执行 //等待读设备结束
WaitForSingleObject(overlap.hEvent,INFINITE);
CloseHandle(hDevice);
4.异步操作设备(方式二)
除了ReadFile和WriteFile函数外,还有两个API也可以实现异步读写,这就是ReadFileEx和WriteFileEx函数。ReadFile和WriteFile既可以支持同步读写操作,又可以支持异步读写操作。而ReadFileEx和WriteFileEx函数时专门用于异步操作的,不能进行同步读写。ReadFileEx的声明如下:
WINBASEAPI
ReadFileEx(
HANDLE hFile,
__out_bcount_opt(nNumberOfBytesToRead) __out_data_source(FILE) LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPOVERLAPPED lpOverlapped,
__in_opt LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
第一个参数hFile:要操作的设备句柄
第二个参数lpBuffer:读入数据的缓冲区
第三个参数nNumberOfBytesToRead:需要读取的字节数
第四个参数lpOverlapped:一个OVERLAPPED指针
第五个参数lpComletionRoutine:完成例程
需要注意的是,这里提供的OVERLAPPED不需要提供事件句柄。ReadFileEx将读请求传递到驱动程序后立刻返回。驱动程序在结束读操作后,会通过调用ReadFileEx提供的回调历程(CALL BACK FUNCTION)。这类似一个软中断,也就是当读操作结束后,系统立刻回调ReadFileEx提供的回调历程。Windows将这种机制称为异步过程调用(APC AsynchronousProcedureCall)
然后,APC的回调函数被调用是有条件的。只有线程处于警惕状态(Alert)时,回调函数才有可能被调用。有多个API可以使系统进入警惕状态,如
SleepEx,WaitForSingleObjectEx,WaitForMultipleObjectEx函数等。
这些Win32 API都会有一个BOOL型的参数bAlertable,当设置TRUE时,就进入警惕模式。
当系统进入警惕模式后,操作系统会枚举当前线程的APC队列。驱动程序一旦结束读取操作,就会把ReadFileEx提供的完成历程插入到APC队列。
回调历程会报告本次操作的完成状况,比如是成功或是失败。同时会报告本次读取操作实际读取字节数等。下面是一般回调历程的声明:
VOID CALLBACK FileIOCompletionRoutine( DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped );
第一个参数dwErrorCode:如果读取错误,会返回错误码;
第二个参数dwNumberOfBytesTransfered:返回实际读取操作的字节数;
第三个参数lpOverlapped:OVERLAP参数,指定读取的偏移量等信息;
下面代码演示如何在应用程序中使用ReadFileEx进行异步读操作:
#include &windows.h&
#include &stdio.h&
#define DEVICE_NAME &test.dat&
#define BUFFER_SIZE 512//假设该文件大于或等于BUFFER_SIZEVOID
CALLBACK MyFileIOCompletionRoutine(
DWORD dwErrorCode,
// 对于此次操作返回的状态
DWORD dwNumberOfBytesTransfered,
// 告诉已经操作了多少字节,也就是在IRP里的Infomation
LPOVERLAPPED lpOverlapped
// 这个数据结构)
printf(&IO operation end!\n&);
int main()
HANDLE hDevice = CreateFile(&test.dat&,
GENERIC_READ | GENERIC_WRITE,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED,//此处设置FILE_FLAG_OVERLAPPED
if (hDevice == INVALID_HANDLE_VALUE)
printf(&Read Error\n&);
UCHAR buffer[BUFFER_SIZE]; //初始化overlap使其内部全部为零 //不用初始化事件!!
OVERLAPPED overlap={0}; //这里没有设置OVERLAP参数,因此是异步操作
ReadFileEx(hDevice, buffer, BUFFER_SIZE,&overlap,MyFileIOCompletionRoutine); //做一些其他操作,这些操作会与读设备并行执行
//进入alterable SleepEx(0,TRUE);
CloseHandle(hDevice);
三、IRP的同步和异步完成
1.IRP的同步完成
下面介绍Win32 API函数是如何一层层通过调用进入到派遣函数的。
(1)在应用程序中调用CreateFile Win32 API函数,这个函数用于打开设备
(2)CreateFile Win32 API函调用ntdll.dll中的NtCreateFile函数
(3)ntdll.dll中的NtCreateFile函数进入内核模式,然后调用ntoskrnl.exe中的NtCreateFile函数
(4)内核模式中ntoskrnl.exe的NtCreateFile函数创建IRP_MJ_CREATE类型的IRP,然后调用相应驱动程序的派遣函数,并将IRP的指针传递给该派遣函数
(5)派遣函数调用IoCompleteRequest,将IRP结束
(6)操作系统按原路返回,一直退到CreateFile Win32 API函数。至此CreateFile函数返回
(7)如果需要读取设备,应用程序调用ReadFile Win32 API函数
(8)ReadFile Win32 API调用ntdll.dll中的NtReadFile函数
(9)NtReadFile函数进入内核模式,调用ntoskenl.exe中的NtReadFile函数
(10)ntoskrnl.exe中的NtReadFile函数创建IRP_MJ_READ类型的IRP,并将其传入相应的派遣函数中
对设备进行读取可以有三种方法,第一种方式是用ReadFile函数进行同步读取,第二种是通过ReadFile方式进行异步读取,第三种方法是用ReadFileEx函数进行异步读取。
如果是用ReadFile进行同步读取时:
(1)ReadFile函数内部会创建一个事件,这个事件连同IRP一起被传到派遣函数中(这个事件是IRP的UserEvent子域)
(2)派遣函数用IoCompleteRequest时,IoCompleteRequest内部会设置IRP的UserEvent事件
(3)操作系统按照原路一只返回到ReadFile函数,ReadFile等待这个事件,因为该事件已经被设置,所以无需等待
(4)如果在派遣函数中没有调用IoCompleteRequest函数,该事件就没有被设置,ReadFile会一直等IRP结束
如果使用ReadFile进行异步读取:
(1)这时,ReadFile内部不会创建事件,但ReadFile函数会接受overlap参数,overlap参数中会提供一个事件,这个事件被用作同步处理
(2)IoCompleteRequest内不会设置overlap提供的事件
(3)在ReadFile函数退出前后,它不会检测该事件是否被设置,因此可以不等待操作是否被完成
(4)当IRP操作被结束后,overlap提供的事件被设置,这个事件会通知应用程序IRP请求被完成
如果使用ReadFileEx函数进行异步读取:
(1)ReadFileEx不提供事件,但是提供一个回调函数,这个回调函数的地址会作为IRP的参数传递给驱动程序
(2)IoCompleteRequest会将这个函数插入APC队列
(3)应用程序只要进入警惕模式,APC队列会自动出队列,完成函数会被执行,这相当于通知应用程序操作已完成
2.IRP的异步完成
IRP被“异步完成”指的是不在派遣函数中调用IoCompleteRequest内核函数。调用IoCompleteRequest意味着IRP请求的结束,也标志着本次对设备操作的结束。
IRP是被异步完成,而发起IRP的应用程序会有三种发起IRP的形式,分别是ReadFile同步读取,ReadFile异步读取,ReadFileEx异步读取。
(1)IRP是由ReadFile的同步操作引起的:当派遣函数退出时,由于IoCompleteRequest没有被调用,IRP请求没有被结束,ReadFile会一直等待,知道操作结束。
(2)IRP是由ReadFile的异步操作引起的:当派遣函数退出时,由于IoCompleteRequest没有被调用,IRP请求没有被结束,但是ReadFile会立刻返回,返回值为失败,但代表操作没有完成。通过调用GetLastError函数,可以得到这时的错误代码是ERROR_IO_PENDING。这不是真正的错误,而是意味着ReadFile并没有真正完成操作,ReadFile只是异步返回。当IRP请求被真正的结束,即调用了IoCompleteRequest,ReadFile提供的overlap的事件才会被设置。这个事件可以通知了应用程序ReadFile的请求真正被执行完毕。
(3)IRP是由ReadFileEx操作异步引起的:ReadFileEx会立刻返回,但是返回值是FALSE,说明操作没有成功。调用GetLastError会发现错误码是ERROR_IO_PENDING,表明当前操作被“挂起”。当IRP结束后,ReadFileEx提供的回调历程会被插入到APC队列中。一旦操作系统进入警惕状态,线程的APC队列会自动出列。
如果派遣函数不调用IoCompleteRequest函数,则需要告诉操作系统此IRP处于“挂起”状态。这需要调用内核函数IoMarkIrpPending。同时,派遣函数应该返回STATUS_PENDING。下面代码演示了派遣函数异步处理IRP。
NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
KdPrint((&Enter HelloDDKRead\n&));
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj-&DeviceE
PMY_IRP_ENTRY pIrp_Entry = (PMY_IRP_ENTRY)ExAllocatePool(PagedPool,sizeof(MY_IRP_ENTRY));
pIrp_Entry-&pIRP = pI
//插入队列
InsertHeadList(pDevExt-&pIRPLinkListHead,&pIrp_Entry-&ListEntry);
//将IRP设置为挂起
IoMarkIrpPending(pIrp);
KdPrint((&Leave HelloDDKRead\n&));
//返回pending状态
return STATUS_PENDING;
为了能存储哪些IRP_MJ_READ被挂起,这里使用一个队列,也就是把每个挂起的IRP_MJ_READ的指针都插入队列,最后IRP_MJ_CLEANUP的派遣函数将一个个IRP出队列,并且调用IoCompleteRequest函数将他们结束。
首先要定义队列的数据结构,(关于驱动中链表的操作能参考文章&&&&)
typedef struct _MY_IRP_ENTRY
PIRP pIRP;
LIST_ENTRY ListE
} MY_IRP_ENTRY, *PMY_IRP_ENTRY;
在关闭设备的时候,会产生IRP_MJ_CLEANUP类型的IRP,其派遣函数抽取队列中每一个“挂起”的IRP,并调用IoCompleteRequest设置完成。
NTSTATUS HelloDDKCleanUp(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
KdPrint((&Enter HelloDDKCleanUp\n&));
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj-&DeviceE
//将存在队列中的IRP逐个出队列,并处理
PMY_IRP_ENTRY my_irp_
while(!IsListEmpty(pDevExt-&pIRPLinkListHead))
PLIST_ENTRY pEntry = RemoveHeadList(pDevExt-&pIRPLinkListHead);
my_irp_entry = CONTAINING_RECORD(pEntry, MY_IRP_ENTRY, LIST_ENTRY);
my_irp_entry-&pIRP-&IoStatus.Status = STATUS_SUCCESS;
my_irp_entry-&pIRP-&rmation = 0;
IoCompleteRequest(my_irp_entry-&pIRP,IO_NO_INCREMENT);
ExFreePool(my_irp_entry);
//处理IRP_MJ_CLEANUP的IRP
NTSTATUS status = STATUS_SUCCESS;
pIrp-&IoStatus.Status =
pIrp-&rmation = 0;
IoCompleteRequest(pIrp,IO_NO_INCREMENT);
KdPrint((&Leave HelloDDKCleanUp\n&));
return STATUS_SUCCESS;
除了将“挂起”的IRP插入队列,并在关闭设备时,将“挂起”的IRP结束,还有另外一个办法可以将“挂起”的IRP逐个结束,这就是取消IRP请求。内核函数IoSetCancelRoutine可以设置取消IRP请求的回调函数,其声明如下:
PDRIVER_CANCEL
IoSetCancelRoutine(PIRP Irp,IN PDRIVER_CANCEL CancelRoutine );
第一个参数Irp:这个参数是需要取消的IRP
第二个参数CancelRoutine:这个是取消函数的函数指针。一旦IRP取消的时候,操作系统会调用这个取消函数。
返回值:标志是否操作成功
下面代码演示如何编写取消例程:
VOIDCancelReadIRP( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
KdPrint((&Enter CancelReadIRP\n&));
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)DeviceObject-&DeviceE
//设置完成状态为STATUS_CANCELLED
Irp-&IoStatus.Status = STATUS_CANCELLED;
Irp-&rmation = 0;
IoCompleteRequest(Irp,IO_NO_INCREMENT);
//释放Cancel自旋锁
IoReleaseCancelSpinLock(Irp-&CancelIrql);
KdPrint((&Leave CancelReadIRP\n&));
NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
KdPrint((&Enter HelloDDKRead\n&));
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj-&DeviceE
IoSetCancelRoutine(pIrp,CancelReadIRP);
//将IRP设置为挂起
IoMarkIrpPending(pIrp);
KdPrint((&Leave HelloDDKRead\n&));
//返回pending状态
return STATUS_PENDING;
四、StrartIO历程
StartIO历程能够保证各个并行的IRP顺序执行,即串行化。
1.并行执行与串行执行
在很多情况下,对设备的操作必须是串行执行而不是并行执行。因此,驱动程序有必要讲并行的请求变换成串行请求。这需要用到队列,如果想依次处理每个IRP,必须采用队列将处理串行化。采用原则是“先来先服务”。
当一个新的IRP请求到来时,首先检查设备是否处于“忙”状态。设备在初始化的时候设置为“空闲”状态。当设备处于“空闲”状态时,可以处理一个IRP得请求,并改变当前设备为“忙”状态。如果设备处于“忙”状态,则将新来的IRP插入队列,并立刻返回,IRP留在以后处理。
当设备由“忙”转向“空闲”状态时,则从队列取出一个IRP进行处理,并重新将状态变为“忙”。
DDK为程序员提供了一个内部队列,并将IRP用StartIO例程串行处理。
2.StartIO历程
操作系统为程序员提供了一个IRP队列来实现串行,这个队列用KDEVICE_QUEUE数据结构表示:
// Device Queue object and entry
typedef struct _KDEVICE_QUEUE {
LIST_ENTRY DeviceListH
KSPIN_LOCK L
} KDEVICE_QUEUE, *PKDEVICE_QUEUE, *RESTRICTED_POINTER PRKDEVICE_QUEUE;
这个队列的列头保存在设备对象Device_Object-&DeviceQueue子域中。插入和删除队列中的元素都是由操作系统负责的。在使用这个队列的时候,需要向系统提供一个StartI例程,并将这个函数的函数名传递给操作系统,代码如下:
#pragma INITCODE
extern &C& NTSTATUS DriverEntry (
IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath )
//设置StartIO例程
pDriverObject-&DriverStartIo = HelloDDKStartIO;
这个StartIO历程运行在DISPATCH_LEVEL级别,要在声明时加上#pragram LOCKEDCODE,因此这个例程是不会被线程所打断的。
#pragma LOCKEDCODE
HelloDDKStartIO(
IN PDEVICE_OBJECT
DeviceObject,
KdPrint((&Enter HelloDDKStartIO\n&));
//获取cancel自旋锁
IoAcquireCancelSpinLock(&oldirql);
if (Irp!=DeviceObject-&CurrentIrp||Irp-&Cancel)
//如果当前有正在处理的IRP,则简单的入队列,并直接返回
//入队列的工作由系统完成,在StartIO中不用负责
IoReleaseCancelSpinLock(oldirql);
KdPrint((&Leave HelloDDKStartIO\n&));
//由于正在处理该IRP,所以不允许调用取消例程
//因此将此IRP的取消例程设置为NULL
IoSetCancelRoutine(Irp,NULL);
IoReleaseCancelSpinLock(oldirql);
KeInitializeEvent(&event,NotificationEvent,FALSE);
LARGE_INTEGER
timeout.QuadPart = -3*;
//定义一个3秒的延时,主要是为了模拟该IRP操作需要大概3秒左右时间
KeWaitForSingleObject(&event,Executive,KernelMode,FALSE,&timeout);
Irp-&IoStatus.Status = STATUS_SUCCESS;
Irp-&rmation = 0; // no bytes xfered
IoCompleteRequest(Irp,IO_NO_INCREMENT);
//在队列中读取一个IRP,并进行StartIo
IoStartNextPacket(DeviceObject,TRUE);
KdPrint((&Leave HelloDDKStartIO\n&));
派遣函数如果想把IRP串行化,只需要加入IoStartPacket函数,就可以将IRP插入队列。并且IoStartPacket函数还可以让程序员指定其取消例程。
IoSatrtPacket首先判断当前设备处于“忙”还是“空闲”状态。如果设备“空闲”,则提升当前IRQL到DISPATCH_LEVEL级别,并进入StartIO例程“串行”处理该IRP。如果设备“忙”,则将IRP插入后返回。
在StartIO例程结束前,应该调用IoStartNextPacket函数,其作用是从队列中抽取下一个IRP,并将这个IRP作为参数调用StartIO例程。
NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,
IN PIRP pIrp)
KdPrint((&Enter HelloDDKRead\n&));
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
pDevObj-&DeviceE
//将IRP设置为挂起
IoMarkIrpPending(pIrp);
//将IRP插入系统的队列
1IoStartPacket(pDevObj,pIrp,0,OnCancelIRP);
KdPrint((&Leave HelloDDKRead\n&));
//返回pending状态
return STATUS_PENDING;
在派遣函数中调用IoStartPacket内核函数指定取消例程。下面代码演示了如何编写取消例程
OnCancelIRP(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
KdPrint((&Enter CancelReadIRP\n&));
if (Irp==DeviceObject-&CurrentIrp)
//表明当前正在改由StartIo处理
//但StartIo并没有获取cancel自旋锁之前
//这时候需要
KIRQL oldirql = Irp-&CancelI
//释放Cancel自旋锁
IoReleaseCancelSpinLock(Irp-&CancelIrql);
IoStartNextPacket(DeviceObject,TRUE);
KeLowerIrql(oldirql);
//从设备队列中将该IRP抽取出来
KeRemoveEntryDeviceQueue(&DeviceObject-&DeviceQueue,&Irp-&Tail.Overlay.DeviceQueueEntry);
//释放Cancel自旋锁
IoReleaseCancelSpinLock(Irp-&CancelIrql);
//设置完成状态为STATUS_CANCELLED
Irp-&IoStatus.Status = STATUS_CANCELLED;
Irp-&rmation = 0; // bytes xfered
IoCompleteRequest( Irp, IO_NO_INCREMENT );
KdPrint((&Leave CancelReadIRP\n&));
4.自定义的StartIO
系统定义的StartIO例程只能使用一个队列,这个队列会将所有的IRP进行处理化。例如,读,写操作都会混在一起进行串行处理。然而,在有些情况下,需要将读,写分别进行串行处理。这时候就需要自定义StartIO例程。
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:2004622次
积分:21556
积分:21556
排名:第265名
原创:243篇
转载:265篇
评论:447条
(1)(2)(5)(1)(1)(1)(2)(5)(4)(1)(1)(4)(3)(2)(6)(5)(14)(6)(4)(29)(20)(33)(25)(45)(49)(61)(55)(56)(69)1636人阅读
驱动内核(44)
Windows NT为每个硬件中断和少数软件事件赋予了一个优先级,即中断请求级(interrupt request level - IRQL)。IRQL为单CPU上的活动提供了同步方法,它基于下面规则:
一旦某CPU执行在高于PASSIVE_LEVEL的IRQL上时,该CPU上的活动仅能被拥有更高IRQL的活动抢先。
图4-1显示了x86平台上的IRQL值范围。(通常,这个IRQL数值要取决于你所面对的平台) 用户模式程序执行在PASSIVE_LEVEL上,可以被任何执行在高于该IRQL上的活动抢先。许多设备驱动程序例程也执行在 PASSIVE_LEVEL上。第二章中讨论的DriverEntry 和AddDevice 例程就属于这类,大部分IRP派遣例程也属于这类。
某些公共驱动程序例程执行在DISPATCH_LEVEL上,而DISPATCH_LEVEL级要比PASSIVE_LEVEL级高。这些公共例程包括StartIo 例程,DPC(推迟过程调用)例程,和其它一些例程。这些例程的共同特点是,它们都需要访问设备对象和设备扩展中的某些域,它们都不受派遣例程的干扰或互相干扰。当任何一个这样的例程运行时,上面陈述的规则可以保证它们不被任何驱动程序的派遣例程抢先,因为派遣例程本身执行在更低级的IRQL上。另外,它们也不会被同类例程抢先,因为那些例程运行的IRQL与它们自己的相同。只有拥有更高IRQL的活动才能抢先它们。
派遣例程(Dispatch routine)和DISPATCH_LEVEL级名称类似。之所以称做派遣例程是因为I/O管理器向这些函数派遣I/O请求。而存在派遣级 (DISPATCH_LEVEL)这个名称是因为内核线程派遣器运行在这个IRQL上,它决定下一次该执行哪个线程。(现在,线程调度程序通常运行在 SYNCH_LEVEL级上)
图4-1. 中断请求级
在DISPATCH_LEVEL级和PROFILE_LEVEL级之间是各种硬件中断级。通常,每个有中断能力的设备都有一个IRQL,它定义了该设备的中断优先级别。WDM驱动程序只有在收到一个副功能码为IRP_MN_START_DEVICE的IRP_MJ_PNP请求后,才能确定其设备的 IRQL。设备的配置信息作为参数传递给该请求,而设备的IRQL就包含在这个配置信息中。我们通常把设备的中断级称为设备IRQL,或DIRQL。
其它IRQL级的含义有时需要依靠具体的CPU结构。这些IRQL通常仅被Windows NT内核内部使用,因此它们的含义与设备驱动程序的编写不是特别密切相关。例如,我将要在本章后面详细讨论的APC_LEVEL,当系统在该级上为某线程调度APC(异步过程调用)例程时不会被同一CPU上的其它线程所干扰。在HIGH_LEVEL级上系统可以执行一些特殊操作,如系统休眠前的内存快照、处理bug check、处理假中断,等等。
IRQL的变化
为了演示IRQL的重要性,参见图4-2,该图显示了发生在单CPU上的一系列事件。在时间序列的开始处,CPU执行在PASSIVE_LEVEL 级上。在t1时刻,一个中断到达,它的服务例程执行在DIRQL1上,该级是在DISPATCH_LEVEL和PROFILE_LEVEL之间的某个 DIRQL。在t2时刻,另一个中断到达,它的服务例程执行在DIRQL2上,比DIRQL1低一级。我们讨论过抢先规则,所以CPU将继续服务于第一个中断。当第一个中断服务例程在t3时刻完成时,该中断服务程序可能会请求一个DPC。而DPC例程是执行在DISPATCH_LEVEL上。所以当前存在的未执行的最高优先级的活动就是第二个中断的服务例程,所以系统接着执行第二个中断的服务例程。这个例程在t4时刻结束,假设这之后再没有其它中断发生,CPU将降到DISPATCH_LEVEL级上执行第一个中断的DPC例程。当DPC例程在t5时刻完成后,IRQL又落回到原来的
PASSIVE_LEVEL级。
图4-2. 变化中的中断优先级
基本同步规则
遵循下面规则,你可以利用IRQL的同步效果:
所有对共享数据的访问都应该在同一(提升的)IRQL上进行。
换句话说,不论何时何地,如果你的代码访问的数据对象被其它代码共享,那么你应该使你的代码执行在高于PASSIVE_LEVEL的级上。一旦越过 PASSIVE_LEVEL级,操作系统将不允许同IRQL的活动相互抢先,从而防止了潜在的冲突。然而这个规则不足以保护多处理器机器上的数据,在多处理器机器中你还需要另外的防护措施——自旋锁(spin lock)。如果你仅关心单CPU上的操作,那么使用IRQL就可以解决所有同步问题。但事实上,所有WDM驱动程序都必须设计成能够运行在多处理器的系统上。
IRQL与线程优先级
线程优先级是与IRQL非常不同的概念。线程优先级控制着线程调度器的调度动作,决定何时抢先运行线程以及下一次运行什么线程。然而,当IRQL级高于或等于DISPATCH_LEVEL级时线程切换停止,无论当前活动的是什么线程都将保持活动状态直到IRQL降到DISPATCH_LEVEL级之下。而此时的“优先级”仅指IRQL本身,由它控制到底哪个活动该执行,而不是该切换到哪个线程的上下文。
IRQL和分页
执行在提升的IRQL级上的一个后果是,系统将不能处理页故障(系统在APC级处理页故障)。这意味着:
执行在高于或等于DISPATCH_LEVEL级上的代码绝对不能造成页故障。
这也意味着执行在高于或等于DISPATCH_LEVEL级上的代码必须存在于非分页内存中。此外,所有这些代码要访问的数据也必须存在于非分页内存中。最后,随着IRQL的提升,你能使用的内核模式支持例程将会越来越少。
DDK文档中明确指出支持例程的IRQL限定。例如,KeWaitForSingleObject 例程有两个限定:
调用者必须运行在低于或等于DISPATCH_LEVEL级上。 如果调用中指定了非0的超时,那么调用者必须严格地运行在低于DISPATCH_LEVEL的IRQL上。
上面这两行想要说明的是:如果KeWaitForSingleObject真的被阻塞了指定长的时间(你指定的非0超时),那么你必定运行在低于 DISPATCH_LEVEL的IRQL上,因为只有在这样的IRQL上线程阻塞才是允许的。如果你所做的一切就是为了检测事件是否进入信号态,则可以执行在DISPATCH_LEVEL级上。但你不能在ISR或其它运行在高于DISPATCH_LEVEL级上的例程中调用 KeWaitForSingleObject例程。
IRQL的隐含控制
在大部分时间里,系统都是在正确的IRQL上调用驱动程序中的例程。虽然我们还没有详细地讨论过这些例程,但我希望举一个例子来表达这句话的含义。你首先遇到的I/O请求就是I/O管理器调用你的某个派遣例程来处理一个IRP。这个调用发生在PASSIVE_LEVEL级上,因为你需要阻塞调用者线程,还需要调用其它支持例程。当然,你不能在更高的IRQL级上阻塞一个线程,而PASSIVE_LEVEL也是唯一能让你无限制地调用任何支持例程的 IRQL级。
如果你的派遣例程通过调用IoStartPacket 来排队IRP,那么你第一个遇到的请求将发生在I/O管理器调用你的StartIo例程时。这个调用发生在DISPATCH_LEVEL级,因为系统需要在没有其它例程(这些例程能在队列中插入或删除IRP)干扰的情况下访问I/O队列。回想一下前面提到的规则:所有对共享数据的访问都应该在同一(提升的)IRQL级上进行。因为每个能访问IRP队列的例程都执行在DISPATCH_LEVEL级上,所以任何例程在操作队列期间都不可能被打断(仅指在单CPU系统)。
之后,设备可能生成一个中断,而该中断的服务例程(ISR)将在DIRQL级上被调用。设备上的某些寄存器也许不能被安全地共享。但是,如果你仅在 DIRQL上访问那些寄存器,可以保证在单CPU计算机上没人能妨碍你的ISR执行。如果驱动程序的其它代码需要访问这些关键的硬件寄存器,你应该让这些代码仅执行在DIRQL级上。KeSynchronizeExecution 服务函数可以帮助你强制执行这个规则,我将在第七章的“与中断处理连接”段中讨论这个函数。
再往后,你应该安排一个DPC调用。DPC例程执行在DISPATCH_LEVEL级上,它们需要访问你的IRP队列,并取出队列中的下一个请求,然后把这个请求发送给StartIo例程。你可以调用IoStartNextPacket 服务函数从队列中提取下一个请求,但必须在DISPATCH_LEVEL级上调用。该函数在返回前将调用你的StartIo例程。注意,这里的IRQL吻合得相当巧妙:队列访问,调用IoStartNextPacket,和调用StartIo都需要发生在DISPATCH_LEVEL级上,并且系统也是在这个IRQL级上调用DPC
尽管明确地控制IRQL也是可能的,但几乎没有理由这样做,因为你需要的IRQL和系统调用你时使用的IRQL总是相应的。所以不必不时地提高 IRQL,例程希望的IRQL和系统使用的IRQL几乎总是正确对应的。
IRQL的明确控制
如果必要,你还可以在当前处理器上临时提升IRQL,然后再降回到原来的IRQL,使用KeRaiseIrql 和KeLowerIrql 函数。下面代码运行在PASSIVE_LEVEL级上:
ASSERT(KeGetCurrentIrql() &= DISPATCH_LEVEL);
KeRaiseIrql(DISPATCH_LEVEL, &oldirql);
KeLowerIrql(oldirql);
KIRQL定义了用于保存IRQL值的数据类型。我们需要一个变量来保存当前IRQL。 这个ASSERT断定了调用KeRaiseIrql的必要条件:新IRQL必须大于或等于当前IRQL。如果这个关系不成立,KeRaiseIrql将导致bug check。(即用死亡蓝屏报告一个致命错误)
KeRaiseIrql把当前的IRQL提升到第一个参数指定的IRQL级上。它同时还把当前的IRQL值保存到第二个参数指定的变量中。在这个例子中,我们把IRQL提升到DISPATCH_LEVEL级,并把原来的IRQL级保存到oldirql 变量中。
执行完任何需要在提升的IRQL上执行的代码后,我们调用KeLowerIrql把IRQL降低到调用KeRaiseIrql时的级别。
DDK文档中提到,你必须用与你最近的KeRaiseIrql调用所返回的值调用KeLowerIrql。这在大的方面是对的,因为你提升了 IRQL就必须再降低它。然而,由于你调用的代码或者调用你的代码所做的各种假设会使后面的决定变得不正确。所以,文档中的这句话从严格意义上讲是不正确的。应用到KeLowerIrql函数的唯一的规则就是新IRQL必须低于或等于当前IRQL。
当系统调用你的驱动程序例程时,你降低了IRQL(系统调用你的例程时使用的IRQL,或你的例程希望执行的IRQL),这是一个错误,而且是严重错误,尽管你在例程返回前又提升了IRQL。这种打破同步的结果是,某些活动可以抢先你的例程,并能访问你的调用者认为不能被共享的数据对象。
有一个函数专用于把IRQL提升到DISPATCH_LEVEL级:
KIRQL oldirql = KeRaiseIrqlToDpcLevel();
KeLowerIrql(oldirql)
注意:该函数仅在NTDDK.H中声明,WDM.H中并没有声明该函数,因此WDM驱动程序不应该使用该函数。
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:282739次
积分:4815
积分:4815
排名:第4741名
原创:189篇
转载:47篇
评论:55条
(1)(3)(1)(2)(3)(2)(4)(2)(2)(1)(2)(4)(2)(6)(6)(2)(4)(1)(4)(2)(1)(1)(1)(3)(3)(2)(4)(9)(5)(1)(1)(1)(2)(1)(3)(1)(2)(1)(1)(1)(3)(6)(6)(4)(6)(1)(5)(11)(7)(4)(4)(5)(5)(13)(57)(2)}

我要回帖

更多关于 win10蓝屏driver irql 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信