Windows 驱动开发 新手入门(三)
引言
本系列所有文章
Windows 驱动开发 新手入门(一)
Windows 驱动开发 新手入门(二)
Windows 驱动开发 新手入门(三)
Windows 驱动开发 新手入门(四)
在之前我们大概知道驱动是什么,应用层如何和内核层通信后,我打算再补充一些知识。
驱动所属进程
在第一篇文章中,为了好理解,我写过一句话,DLL
是用户层模块
,驱动
就是内核层模块
,在驱动中入口函数中打印PsCurrentProcessId()
,可以看见所属PID为4
,也就是system进程
。
system
进程实际上是系统虚拟出的一个进程,代表着系统内核,它并不是主动运行的一个PE文件(不要认为它是system.exe),在win10中,还有一个进程与它相似,那就是Registry
,如果你通过任务管理器右键打开文件位置,可以发现定位到了一个ntoskrnl.exe(NT Operating System Kernel缩写)
,专栏中的win10 ssdt表就是靠这个文件分析的。
IRQL (Interrupt Request Level) 中断请求级别
我们知道在应用层开发时,通过Windows公开的API,我们可以在应用层去设置线程,进程的优先级,优先级越高,那么下次它获得CPU调度的机会就越大,此时我们可以通过SwitchToThread允许执行优先级较低的线程,或Sleep(0)立即重新调度主调线程,即使低优先级线程还处于饥饿状态。
中断请求同样有级别,不过我们并不能通过API去让低优先级的中断请求先执行。
IRQL有很多,但是在Windows驱动开发中,我们用到是这3个
IRQL | 值 | 描述 |
---|---|---|
PASSIVE_LEVEL | 0 | IRQL最低级别,没有被屏蔽的中断,在这个级别上,线程执行用户模式,可以访问分页内存。 |
APC_LEVEL | 1 | 在这个级别上,只有APC级别的中断被屏蔽,可以访问分页内存。当有APC发生时,处理器提升到APC级别,这样,就屏蔽掉其它APC,为了和APC执行一些同步,驱动程序可以手动提升到这个级别。比如,如果提升到这个级别,APC就不能调用。在这个级别,APC被禁止了,导致禁止一些I/O完成APC,所以有一些API不能调用。 |
DISPATCH_LEVEL | 2 | 这个级别,DPC 和更低的中断被屏蔽,不能访问分页内存,所有的被访问的内存不能分页。因为只能处理非分页内存,所以在这个级别,能够访问的Api大大减少。 |
DPC
正常情况下,驱动的入口函数,卸载函数,派遣函数的IRQL
是PASSIVE_LEVEL
,而各种回调函数一般是DISPATCH_LEVEL
。
我们可以通过KeGetCurrentIrql()
获取当前的中断请求级别
。
无论是DPC还是APC,都有一个队列,当CPU处理完高于DPC中断级的任务后就来找这个队列,然后依次执行这个队列,我们也可以不用插队列的形式短暂的把我们当前任务请求级别提高到DPC。
为什么DPC中不允许使用分页内存
DPC的中断级很高,DPC中不允许使用分页内存,当我们在一个DPC的函数里面访问一个换页内存的时候,如果这个换页内存刚好换进了物理内存,那么还不会有什么问题,但是当页面被换到磁盘上的pagefile.sys 会引发缺页中断,缺页中断一般打不断DPC的过程,这时DPC访问的是一个无效的内存时会蓝屏。所以DPC编程的时候切记不要使用换页内存。
使用DPC
KeRaiseIrqlToDpcLevel()
可以将当前IRQL暂时提升到DPC级别,并且会返回一个KIRQL(之前的IRQL)
,但是需要注意的是,我们不能长时间的在DPC级别下,时间过久系统会蓝屏。
KeLowerIrql(oldirql)
可以将之前提升到DPC的IRQL降回之前的IRQL。
在DPC级别下的例程对于操作系统来说相当于上了一把锁,因为此时CPU不接收任何比DPC级别低的中断。
下面举一些简单的例子,关于 链表使用锁这里不写了。
使用自旋锁
我们还可以使用自旋锁的方式,它会自动将IRQL提升至DPC。
KSPIN_LOCK spinlock = { 0 };//自旋锁全局变量
KeInitializeSpinLock(&spinlock);//初始化自旋锁全局变量
KIRQL oldirql = 0;
KeAcquireSpinLock(&spinlock,&oldirql); //上锁
//
//do something
//
KeReleaseSpinLock(&spinlock, oldirql); // 解锁
插入DPC队列
KDPC dpcObject = { 0 };//存放插入DPC优先级队列函数的一个全局变量
VOID DpcRoutine(PVOID conetxt)
{
DbgPrint("DpcRoutine DPC=%d\n", KeGetCurrentIrql());
return;
}
KeInitializeDpc(&dpcObject, DpcRoutine,NULL);//初始化插入DPC优先级队列的函数
KeInsertQueueDpc(&dpcObject, NULL, NULL);//往DPC优先级队列插入函数
DbgPrint("Current DPC=%d\n", KeGetCurrentIrql());
使用定时器
这和应用层SetWaitableTimer
有些类似,区别在于应用层的SetWaitableTimer
使用的是APC。
-
KeSetTimer 开启定时器(只执行一次)
-
KeSetTimerEx 开启定时器(可以设置循环)
-
KeCancelTimer 取消定时器
-
KeSetTimerEx 参数中的 Period
指定计时器的可选定期间隔(以毫秒为单位)。 必须是大于或等于零的值。 如果此参数的值为零,计时器是一个非重复计时器,不会自动重新排队本身。
VOID TimerFunc(IN struct _KDPC *Dpc,IN PVOID DeferredContext,IN PVOID SystemArgument1,IN PVOID SystemArgument2
)
{
KdPrint(("TimerFunc\n"));
};
KTIMER DPC_TIMER;
KDPC TimerDPCObject ;
LARGE_INTEGER duetime = { };
int sec = 5;
duetime.QuadPart = -(sec * (1000000000 / 100));//1000000000纳秒 = 1秒,以100纳秒为时间单位所以/100
KeInitializeTimerEx(&DPC_TIMER, NotificationTimer);
KeInitializeDpc(&TimerDPCObject , (PKDEFERRED_ROUTINE)TimerFunc, NULL);
KeSetTimerEx(&DPC_TIMER, duetime, 0, &TimerDPCObject);//第一次启动时间为5秒后,由于我们Period写的是0,所以它并不会重复执行TimerFunc。