驱动程序的同步处理

驱动程序的同步处理

Windows是个多任务的操作系统,每个任务对应一个运行的进程。每个运行的进程中可以包含多个线程。如果没有同步机制的控制,所有的线程会任意运行。然而,多个线程可能会要求操作同一个资源,这时就需要同步处理。

1、基本概念

1.1、问题的引出

在支持多线程的操作系统下,有些函数会出现不可重入现象。所谓“可重入”,是指函数的执行结果和执行顺序无关。反之,如果执行结果和执行顺序有关,则称这个函数是“不可重入”的。

“不可重入”的函数会对多线程操作系统下的程序带来错误,“不可重入”的根本原因就是线程之间的切换导致的。

1.2、同步与异步

多线程运行的基本原理:如果PC中只有一个CPUCPU将时间分成一个个时间片段,然后CPU将这些时间片段分配给各个线程,当前的线程消耗完这个时间片段后,CPU会转而执行其他的线程。由于CPU运行速度非常快,每个线程仿佛是在同时运行一样。

这时候,各个线程之间的关系成为异步的。每个线程的运行不受其他线程的影响。

2、中断请求级

在设计windows的时候,设计者将中断请求划分为软件中断和硬件中断,并将这些中断都映射成不同级别的中断请求级(IRQL)。同步处理机制很大程度上依赖于中断请求级。

2.1、中断请求(IRQ)与可编程中断控制器(PIC

中断请求(IRQ)一般有两种,一种是外部中断,也就是硬件产生的中断,另一种由软件指令int n产生的中断。

在传统PC中,一般可以接收16个中断信号,每个中断信号对应一个中断号,外部中断分为不可屏蔽(NMI)和可屏蔽中断,分别由CPU的两根引脚NMIINTR来接收。

2.2、高级可编程控制器(APIC

2.3、中断请求级(IRQL

APIC中,IRQ的数量被增加到24个,每个IRQ有各自的优先级别,正在运行的线程随时可以被中断打断,进入到中断处理程序,当优先级高的中断来临时,处在优先级低的中断处理程序,也会被打断,进入到更高级别的中断处理函数。

Windows将中断的概念进行了扩展,,提出一个中断请求级(IRQL)的概念。其中规定了32个中断请求级别,分别是0~2级别为软件中断,3~31级为硬件中断,数字从0~31,优先级递增

Windows24IRQ映射到了从DISPATCH_LEVELPROFILE_LEVEL之间,不同硬件的中断处理程序运行在不同的IRQL级别中。硬件的IRQL称为设备中断请求级别,简称DIRQLWindows大部分时间运行在软件中断级别中,当设备中断来临时,操作系统提升IRQLDIRQL级别,并且运行中断处理函数。当中断处理函数结束后,操作系统把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级别,这个时候操作系统随时可能将当前切换到别的线程。但是如果提升IRQLDISPATCH_LEVEL级别,这时候,这时候不会出现线程的切换。这是一种很常用的同步处理机制,但这种方法只能使用于单CPU的系统。对于CPU的系统,需要采用别的同步处理机制。

2.6 IRQL与内存分页

在使用分页内存时,可能后导致页故障。因为分页内存随时可能从物理内存交换到磁盘文件。读取不在物理内存中的分页内存时,会引发一个页故障,从而执行这个异常的处理函数。异常处理函数会重新将磁盘文件的内容交换到物理内存中。

页故障允许出现在PASSIVE_LEVEL级别的程序中,但如果在DISPATCH_LEVEL或更高级别IRQL的程序中会带来系统崩溃。

2.7 控制IRQL提升与降低

驱动程序使用内核函数KeRaiseIrqlIRQL提高

VOID 
  KeRaiseIrql(
    IN KIRQL  NewIrql,//提升后的IRQL级别
    OUT PKIRQL  OldIrql//保存提升前的IRQL级别
    );

驱动程序使用内核函数KeLowerIrqlIRQL恢复到以前IRQL级别

VOID 
  KeLowerIrql(
    IN KIRQL  NewIrql
    );

 

3、自旋锁

自旋锁也是一种同步处理机制。他能保证某个资源只能被一个线程所拥有。这种保护被形象称为“上锁”。

3.1、原理

Windows内核中,有一种被称为自旋锁(Spin Lock)的锁,它可以用于驱动程序中的同步处理。初始化自旋锁时,处于解锁状态,这时它可以被程序“获取”。“获取”后的自旋锁处于锁住状态,不能被再次“获取”。锁住的自旋锁必须被“释放”后才能被再次“获取”。

如果自旋锁已经被锁住,这时有程序申请“获取”这个自旋锁,程序则处于“自旋”状态,所谓自旋状态,就是不停地询问是否可以“获取”自旋锁,自旋锁也因此得名。

在单个CPU的系统中,“获取”自旋锁仅仅是将当前的IRQLPASSIVE_LEVEL级别提升到DISPATCH_LEVEL级别,但是在多CPU系统中,自旋锁的实现方法会复杂的多。驱动程序必须在低于或者等于DISPATCH_LEVELIRQL级别中使用自旋锁。

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章

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

疯的世界

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值