首页 > 代码库 > 驱动程序的同步处理
驱动程序的同步处理
驱动程序的同步处理
Windows是个多任务的操作系统,每个任务对应一个运行的进程。每个运行的进程中可以包含多个线程。如果没有同步机制的控制,所有的线程会任意运行。然而,多个线程可能会要求操作同一个资源,这时就需要同步处理。
1、基本概念
1.1、问题的引出
在支持多线程的操作系统下,有些函数会出现不可重入现象。所谓“可重入”,是指函数的执行结果和执行顺序无关。反之,如果执行结果和执行顺序有关,则称这个函数是“不可重入”的。
“不可重入”的函数会对多线程操作系统下的程序带来错误,“不可重入”的根本原因就是线程之间的切换导致的。
1.2、同步与异步
多线程运行的基本原理:如果PC中只有一个CPU,CPU将时间分成一个个时间片段,然后CPU将这些时间片段分配给各个线程,当前的线程消耗完这个时间片段后,CPU会转而执行其他的线程。由于CPU运行速度非常快,每个线程仿佛是在同时运行一样。
这时候,各个线程之间的关系成为异步的。每个线程的运行不受其他线程的影响。
2、中断请求级
在设计windows的时候,设计者将中断请求划分为软件中断和硬件中断,并将这些中断都映射成不同级别的中断请求级(IRQL)。同步处理机制很大程度上依赖于中断请求级。
2.1、中断请求(IRQ)与可编程中断控制器(PIC)
中断请求(IRQ)一般有两种,一种是外部中断,也就是硬件产生的中断,另一种由软件指令int n产生的中断。
在传统PC中,一般可以接收16个中断信号,每个中断信号对应一个中断号,外部中断分为不可屏蔽(NMI)和可屏蔽中断,分别由CPU的两根引脚NMI和INTR来接收。
2.2、高级可编程控制器(APIC)
2.3、中断请求级(IRQL)
在APIC中,IRQ的数量被增加到24个,每个IRQ有各自的优先级别,正在运行的线程随时可以被中断打断,进入到中断处理程序,当优先级高的中断来临时,处在优先级低的中断处理程序,也会被打断,进入到更高级别的中断处理函数。
Windows将中断的概念进行了扩展,,提出一个中断请求级(IRQL)的概念。其中规定了32个中断请求级别,分别是0~2级别为软件中断,3~31级为硬件中断,数字从0~31,优先级递增
Windows将24个IRQ映射到了从DISPATCH_LEVEL到PROFILE_LEVEL之间,不同硬件的中断处理程序运行在不同的IRQL级别中。硬件的IRQL称为设备中断请求级别,简称DIRQL。Windows大部分时间运行在软件中断级别中,当设备中断来临时,操作系统提升IRQL至DIRQL级别,并且运行中断处理函数。当中断处理函数结束后,操作系统把IRQL降到原来的级别
用户模式的代码是运行在最低优先级的PASSIVE_LEVEL级别。驱动程序的DriverEntry函数、派遣函数、AddDevice等函数一般都运行在PASSIVE_LEVEL级别,它们在必要时可以申请进入DISPATCH_LEVEL级别。
Windows负责线程调度的组件是运行在DISPATCH_LEVEL级别,当前的线程运行时间片后,系统自动从PASSIVE_LEVEL级别提升到DISPATCH_LEVEL级别。当线程切换完毕后,操作系统又从DISPATCH_LEVEL级别降到PASSIVE_LEVEL级别。
在内核模式下,可以通过调用KeGetCurrentIrql内核函数来得到当前IRQL级别。
8.2.4线程调度与线程优先级
线程优先级和IRQL是两个容易混淆的概念。所有应用程序都运行在PASSIVE_LEVEL级别上,它的优先级别最低,可以被其他IRQL级别的程序打断。线程优先级只针对应用程序而言,只有程序运行在PASSIVE_LEVEL级别才有意义。
线程的优先级别是指某线程是否有更多的机会运行在CPU上,线程优先级高的线程有更多的机会被内核调度。负责调度线程的内核组件运行在DISPATCH_LEVEL级别的IRQL上,这时候所有应用程序的线程都停止,等待着被调度。
ReadFile内部创建IRP_MJ_READ,然后这个IRP被传递到驱动程序的派遣函数中。这时候派遣函数运行于ReadFile所在的线程中,或者说ReadFile和派遣函数位于同一个线程上下文中。
2.4 IRQL的变化
以下描述一个线程的运行过程:
① 一个普通线程A正在运行
② 这个时刻有一个中断发生,它的IRQL为0xD。CPU中断当前运行的线程A,将IRQL提升至0xD级别。
③ 这个时候有一个更高优先级的中断发生,它的IRQL是0x1A。这时候CPU将IRQL提升至0x1A级别
④ 这个时候又有一个中断发生,但它的IRQL为0x18,低于上一个中断优先级。CPU不会理睬这个中断
⑤ 这时候IRQL为0x1A的中断结束,操作系统进入IRQL为0x18的中断服务。
⑥ 这时候IRQL为0x18的中断结束,于是进入IRQL为0xD的中断服务
⑦ 最后IRQL为0xD的终端结束,操作系统恢复线程A
线程运行在PASSIVE_LEVEL级别,这个时候操作系统随时可能将当前切换到别的线程。但是如果提升IRQL到DISPATCH_LEVEL级别,这时候,这时候不会出现线程的切换。这是一种很常用的同步处理机制,但这种方法只能使用于单CPU的系统。对于CPU的系统,需要采用别的同步处理机制。
2.6 IRQL与内存分页
在使用分页内存时,可能后导致页故障。因为分页内存随时可能从物理内存交换到磁盘文件。读取不在物理内存中的分页内存时,会引发一个页故障,从而执行这个异常的处理函数。异常处理函数会重新将磁盘文件的内容交换到物理内存中。
页故障允许出现在PASSIVE_LEVEL级别的程序中,但如果在DISPATCH_LEVEL或更高级别IRQL的程序中会带来系统崩溃。
2.7 控制IRQL提升与降低
驱动程序使用内核函数KeRaiseIrql将IRQL提高
VOID
KeRaiseIrql(
IN KIRQL NewIrql,//提升后的IRQL级别
OUT PKIRQL OldIrql//保存提升前的IRQL级别
);
驱动程序使用内核函数KeLowerIrql将IRQL恢复到以前IRQL级别
VOID
KeLowerIrql(
IN KIRQL NewIrql
);
3、自旋锁
自旋锁也是一种同步处理机制。他能保证某个资源只能被一个线程所拥有。这种保护被形象称为“上锁”。
3.1、原理
在Windows内核中,有一种被称为自旋锁(Spin Lock)的锁,它可以用于驱动程序中的同步处理。初始化自旋锁时,处于解锁状态,这时它可以被程序“获取”。“获取”后的自旋锁处于锁住状态,不能被再次“获取”。锁住的自旋锁必须被“释放”后才能被再次“获取”。
如果自旋锁已经被锁住,这时有程序申请“获取”这个自旋锁,程序则处于“自旋”状态,所谓自旋状态,就是不停地询问是否可以“获取”自旋锁,自旋锁也因此得名。
在单个CPU的系统中,“获取”自旋锁仅仅是将当前的IRQL从PASSIVE_LEVEL级别提升到DISPATCH_LEVEL级别,但是在多CPU系统中,自旋锁的实现方法会复杂的多。驱动程序必须在低于或者等于DISPATCH_LEVEL的IRQL级别中使用自旋锁。
3.2 使用方法
自旋锁的作用一般是为使各派遣函数之间同步。尽量不要将自旋锁放在全局变量中,而应该将自旋锁放在设备扩展里。自旋锁用KSPIN_LOCK数据结构表示:
Type struct _DEVICE_EXTENSION{
.....
KSPIN_LOCK My_SpinLock;//在设备扩展中定义自旋锁
}DEVICE_EXTENSION,*PDEVICE_EXTENSION;
使用自旋锁前,需要先对其进行初始化,可以使用KeInitializeSpinLock内核函数,一般在驱动程序的DriverEntry或者AddDevice函数中初始化自旋锁。
申请自旋锁可以使用内核函数KeAcquireSpinLock
释放自旋锁使用内核函数KeReleaseSpinLock
4、用户模式下的同步对象
在内核模式下可以使用很多种内核同步对象,这些内核同步对象和用户模式下的同步对象非常类似。同步对象包括事件(Event)、互斥体(Mutex)、信号灯(Semaphore)等。用户模式下的同步对象其实是内核模式下同步对象的再次封装。
4.1 用户模式的等待
在应用程序中,可以使用WaitForSingleObject(用于等待一个同步对象)和WaitForMultipleObjects(用于等待多个同步对象)等待同步对象。
DWORD WaitForSingleObject{
HANDLE hHandle, //同步对象句柄
DWORD dwMilliseconds //等待时间ms,值为INFINITE表示无限等待,值为0表示强迫操作系统将当前线程切换到其他线程
};
WaitForMultipleObjects函数声明:
DWORD WaitForMultipleObjects{
DWORD nCount, //同步对象数组元素个数
CONST HANDLE *lpHandles, //同步对象数组
BOOL bWaitAll, //是否等待全部同步对象
DWORD dwMillseconds //等待时间
};
4.2 用户模式开启多线程
等待同步对象一般出现在多线程的编程中,因此这里介绍一下应用程序如何创建新线程。Win32 API CreateThread函数负责创建新线程。
HANDLE CreateThread{
LPSECURITY_ATTRIBUTES lpThreadAttributes, //安全属性
SIZE_T dwStackSize, //初始化堆栈大小
LPTHREAD_START_ROUTINE lpStartAddress, //线程运行的函数指针
LPVOID lpParameter, //传入函数中的参数
DWORD dwCreationFlags, //开启线程时的状态
LPDWORD lpThreadId //返回线程ID
}
_beginthreadex函数对CreateThread函数进行了封装,其参数与CreateThread完全一致。
4.3、用户模式的事件
事件是一种典型的同步对象。用户模式下的事件和内核模式的事件对象紧密相连。在使用事件之前,需要对事件进行初始化,使用CreateEvent API函数。
主线程开启新的辅助线程,主线程把一个事件的句柄传递给子线程。同时,主线程等待该事件激发,辅助线程所做的事情就是现实一些信息,并设置该事件。如果主线程不等待事件,也是以异步的方式共同的和辅线程执行,这时很有可能主线程都退出来了,辅助线程还在继续运行。
4.4 用户模式的信号灯
信号灯也是一种常用的同步对象,信号灯也有两种状态,一种是激发状态,另一种是未激发状态。信号灯内部有个计数器,可以理解信号灯内部有N个灯泡。如果一个灯泡亮着,就代表信号处于激发状态,如果全部熄灭,则代表信号灯处于未激发状态。使用信号灯钱需要先创建信号灯。CreateSemaphore函数负责创建信号灯。
4.5 用户模式的互斥体
互斥体也是一种常用的同步对象。互斥体可以避免多个线程争夺同一个资源。例如:多线程环境中,只能有一个线程占有互斥体,获得互斥体的线程如果不释放互斥体,其他线程永远不会获得这个互斥体。互斥体的概念类似于同步事件,所不同的是同一个线程可以递归获得互斥体:即得到互斥体的线程还可以再次获得这个互斥体,或者说互斥体对于已经获得互斥体的线程不产生“互斥”关系。而同步事件不能递归获取。
互斥体也有两种状态:激发态和未激发态。如果线程获得互斥体时,此时的状态时未激发态,当释放互斥体时,互斥体的状态为激发态。初始化互斥体的函数是CreateMutex。
4.6 等待线程完成
还有一种同步对象,就是线程对象,每个线程同样有两个状态,激发状态和未激发状态。当线程处于运行之中的时候,是未激发状态。当线程终止后,线程处于激发状态。
以上内容参考自张帆 史彩成等编著的《Windows 驱动开发技术详解》第8章