Windows驱动开发技术详解第八章(驱动程序的同步处理)

中断请求(IRQ)

中断请求分为两种:

  1. 外部中断(硬件产生的中断)
  2. 由软件指令int n产生的中断
外部中断

传统PC共可接受16个中断信号,分别对应一个中断号
外部中断分为:不可屏蔽中断(CPU的NMI引脚)和可屏蔽中断(CPU的INTR引脚)
传统CP使用Intel 8259A中断控制器向CPU发出可屏蔽中断

在这里插入图片描述

现代x86机器使用高级可编程中断控制器(APIC)
APIC兼容传统的16个中断,共24个中断(IRQ)
每个IRQ有各自的优先级别,正在运行的线程可被IRQ打断,进入IRQ的中断处理程序
当优先级高IRQ来临时,即使正在进入优先级低的IRQ中断处理程序也会被打断,先处理优先级高的IRQ

Windows下的中断(IRQL)

windows对中断进行了扩展,提出IRQL
IRQL规定了32个中断请求级别(优先级依次递增)
0 ~ 2 级别为软件中断
3 ~ 31 级别为硬件中断(包括APIC中的24个中断)

不同硬件的中断处理程序运行在不同的IRQL级别中,硬件的IRQL称为DIRQL
windows大部分时间运行在软件中断级别中,当硬件中断来临时,操作系统提示IRQL至DIRQL级别,运行中断处理函数,结束后将IRQL降低为原本的级别
在这里插入图片描述

  1. 用户模式代码运行在最低等级0级的PASSIVE_LEVEL级别中
  2. 驱动程序的DriverEntry派遣函数AddDevice等函数一般运行在PASSIVE_LEVEL级别,但在必要时可申请进入DISPATCH_LEVEL级别
  3. Windows负责线程调度的组件运行在DISPATCH_LEVEL级别。当前线程运行完时间片之后,系统从PASSIVE_LEVEL级别提升至DISPATCH_LEVEL,进行线程切换,切换完毕后,系统从DISPATCH_LEVEL级别降低至PASSIVE_LEVEL,继续运行其他线程
  4. 驱动程序的StartIO函数和DPC函数也运行在DISPATCH_LEVEL级别

内核模式下,可通过调用KeGetCurrentIrql内核函数得到当前的IRQL级别

区分线程优先级和IRQL
  1. 所有应用程序都运行在PASSIVE_LEVEL的IRQL级别上,可别其他任意级别程序打断
  2. 线程优先级指某线程是否有更多机会被运行在CPU上,即更多的机会被内核调度
  3. 负责线程调度的内核组件运行在DISPATCH_LEVEL级别,此时所有应用程序的线程都停止,等待被调度

ReadFile内部创建IRP_MJ_READ,然后这个IRP被传递到驱动程序的派遣函数中,此时派遣函数位于运行在ReadFile所在的线程中,或者说ReadFile和派遣函数运行在同一线程的上下文

现场运行时IRQL的变化

在这里插入图片描述
线程运行在PASSIVE_LEVEL级别,此时操作系统随时可将当前线程切换到别的线程继续运行,但是如果线程的IRQL提升到DISPATCH_LEVEL级别,由于IRQL级别比较高,不会出现线程的切换。

IRQL和分页内存
  1. 使用分页内存时,由于分页内存允许被交换到硬盘空间内,此时读取分页内存由于分页内存不在内存中,所以会引发一个页故障,从而执行这个异常的处理函数,异常处理函数会重新将磁盘文件内容交换到物理内存中
  2. 页故障运行出现在PASSIVE_LEVEL级别的程序中,但如果在DISPATCH_LEVEL或者更高级别的IRQL中会带来系统崩溃
  3. 对于等于或高于DISPATCH_LEVEL级别的程序不能使用分页内存,必须使用非分页内存。如驱动的StartIO例程、DPC例程、中断服务例程都运行在DISPATCH_LEVEL或者更高的IRQL中,这些例程都不能使用分页内存,必须使用非分页内存
控制IRQL的提升与降低

驱动程序可以手动控制IRQL的提升和降低(一般为了满足同步需要)
一般用于防止线程切换
获取当前IRQL级别:KeGetCurrentIrql函数
提升IRQL级别:KeRaiseIrql函数
降低IRQL级别:KeLowerIrql函数
在这里插入图片描述

自旋锁(Spin Lock)

自旋锁能够保证某一资源只能被一个线程所拥有

实现原理
  1. 初始化自旋锁时,处于解锁状态,此时可以被程序获取
  2. 被获取后的自旋锁处于锁住状态,不能被再次获取
  3. 锁住的自旋锁必须被释放以后,才能再次被获取

如果自旋锁被锁住,线程尝试获取时,将会处于自旋状态,即不停的循环询问是否能够获取自旋锁

自旋锁与等待事件的不同

等待事件在等待时,该线程处于休眠状态,不占用CPU运行时间
自旋锁在锁住时,尝试获取的线程会处于一个不停循环状态,占用CPU时间

因此自旋锁占用的时间不宜过长,否则会导致申请自旋锁的其他宣传处于不停循环状态,占用CPU时间

使用方法和注意事项

注意:驱动程序必须在低于或者等于DISPATCH_LEVEL的IRQL级别中使用自旋锁

自旋锁常用于使各个派遣函数同步,尽量不要将自旋锁放在全局变量中,而是将自旋锁放在设备扩展里

自旋锁数据结构:KSPIN_LOCK
初始化自旋锁:KeInitializeSpinLock内核函数
申请自旋锁:KeAcquireSpinLock内核函数(参数1自旋锁指针,参数2记录获取自旋锁之前的IRQL级别)
释放自旋锁:KeRealease内核函数(参数1自旋锁指针,参数2释放自旋锁之后应该恢复的IRQL级别)

如果在DISPATCH_LEVEL级别申请和释放自旋锁,不会改变IRQL级别,申请和释放函数可以简化为
DISPATCH_LEVEL级别申请自旋锁:KeAcquireSpinLockAtDpcLevel
DISPATCH_LEVEL级别释放自旋锁:KeReleaseSpinLockFromDpcLevel

同步对象

同步对象包括:事件(Event)、互斥体(Mutex)、信号灯(Semaphore)等
内核模式下,每种同步对象都对应一种数据结构,程序员可自由操作这些对象

注意:所有形如CreateXXX的win32API,如果第一个参数是LPSECURITY_ATTRIBUTES类型,则一定会创建一个相应的内核对象。
这种API返回一个句柄,系统通过这个句柄查找到相应的内核对象

参考:内核模式下的多线程

内核函数:PsCreateSystemThread
该函数可以创建两种线程:

  1. 用户线程:当前进程中的线程(当前IO操作发起者的线程)
    如果在IRP_MJ_READ的派遣函数中调用PsCreateSystemThread创建用户线程,新线程就是属于调用ReadFile的线程
  2. 系统线程:属于系统进程(操作系统中PID为4,名叫System的特殊进程)
    驱动程序的DriverEntry和AddDevice等函数都是被系统线程调用的
NTSTATUS PsCreateSystemThread(
  [out]           PHANDLE            ThreadHandle,
  [in]            ULONG              DesiredAccess,
  [in, optional]  POBJECT_ATTRIBUTES ObjectAttributes,
  [in, optional]  HANDLE             ProcessHandle,
  [out, optional] PCLIENT_ID         ClientId,
  [in]            PKSTART_ROUTINE    StartRoutine,
  [in, optional]  PVOID              StartContext
);

ThreadHandle:用于输出,新创建的线程的句柄
DesiredAccess:创建的权限
ObjectAttributes:该线程的属性,一般设为NULL
ProcessHandle:创建用户线程还是系统线程,NULL表示系统线程,如果该值为一个进程句柄,则新创建的线程属于这个指定的进程(NtCurrentProcess宏可得到当前进程句柄)
StartRoutine:新线程的运行地址
StartContext:新线程接收的参数

内核模式下创建的线程必须使用PsTerminateSystemThread强制结束线程,否则该线程无法自动退出

tip:内核线程查看当前属于哪个进程
首先使用IoGetCurrentProcess函数得到当前进程,IoGetCurrentProcess会得到一个PERPROCESS数据结构,记录着进程的相关信息,包括进程名。但PERPROCESS数据结构微软并未公开,可通过windbg查看该结构

同步对象的等待

应用层:WaitForSingleObject和WaitForMultipleObjects
内核层:KeWaitForSingleObject和KeWaitForMultipleObjects

NTSTATUS
KeWaitForSingleObject (
    PVOID Object,	
    KWAIT_REASON WaitReason,
    KPROCESSOR_MODE WaitMode,
    BOOLEAN Alertable,
    PLARGE_INTEGER Timeout
    );

Object:同步对象的指针
WaitReason:等待原因,一般设为Executeive
WaitMode:等待模式,用户模式下等待还是内核模式下等待,一般设为KernelMode
Alertable:等待是否是警惕的,一般设为FALSE
Timeout:等待的时间,一个64位的整数,设为NULL代表无限期等待,直到同步对象变为激发态。数字为正数代表未来的某一个时刻,距离1601年1月1日所经历的时间。数字为负数代表从现在时刻开始算起经历的时间,100ns为单位

KeWaitForSingleObject如果等待的同步对象变为激发态,这个函数会退出睡眠状态,并返回STATUS_SUCCESS
如果这个函数因为超时而退出,则返回STATUS_TIMEOUT

NTSTATUS
KeWaitForMultipleObjects (
    ULONG Count,
    PVOID Object[],
    WAIT_TYPE WaitType,
    KWAIT_REASON WaitReason,
    KPROCESSOR_MODE WaitMode,
    BOOLEAN Alertable,
    PLARGE_INTEGER Timeout,
    PKWAIT_BLOCK WaitBlockArray
    );

Count:等待同步对象的个数
Object:等待同步对象数组
WaitType:等待任意一个同步对象激发还是所有同步对象都激发
其余参数同KeWaitForSingleObject

KeWaitForMultipleObjects:如果这个函数因为超时而退出,返回STATUS_TIMEOUT
如果数组中其中一个同步对象变为激发态,这个函数返回的状态码减去STATUS_WAIT_0就是激发的同步对象在数组中的索引号

事件对象

应用层:CreateEvent
内核层:KeInitializeEvent

void KeInitializeEvent(
  [out] PRKEVENT   Event,
  [in]  EVENT_TYPE Type,
  [in]  BOOLEAN    State
);

Event:要初始化的事件对象的指针
Type:事件的类型,两种类型,通知事件(NotificationEvent)和同步事件(SynchornizationEvent)
State:初始化状态,TRUE为激发状态,FALSE为未激发状态

注意:
如果创建的事件对象是通知事件,当事件对象变为激发态时,程序员需手动将其改为未激发态。
如果创建的事件对象是同步事件,当事件对象变为激发态时(遇到KeWaitForXXX等内核函数),事件对象自动变回未激发态

示例代码:
在这里插入图片描述

参考:应用程序和驱动如何使用同一个事件对象

事件对象在用户模式使用句柄代表,在驱动下使用KEVENT数据结构代表

  1. 在用户模式创建一个同步事件
  2. 然后使用DeviceIoControl将同步事件的句柄传递给驱动
  3. 驱动程序在通过DeviceIoContol的IRP派遣函数获得同步事件的句柄
  4. 驱动程序调用ObReferenceObjectByHandle将句柄转化为实际的事件对象指针(此时该事件对象内部维护的一个引用计数将加1)
  5. 驱动程序使用此事件对象指针进行同步等自定义操作
  6. 使用完毕后,调用ObDereferenceObject函数(将这个事件对象内部维护的引用计数减1)

示例代码:
在这里插入图片描述

参考:驱动程序和驱动程序使用同一个事件对象

场景:驱动程序A的派遣函数需要和驱动程序B的派遣函数进行同步
问题:驱动A如何获取驱动B所创建的事件对象
解决:驱动B创建一个带有名字的事件对象,这样驱动A即可通过名字寻找到事件对象的指针

创建有名事件对象:IoCreateNotificatiionEvent(创建通知事件对象)和IoCreateSynchronizationEvent(创建同步事件对象)

这两个函数得到的是事件对象的句柄,要想进一步得到事件对象指针,还需调用ObReferenceObjectByHandle
内核中不存在指定名称的事件对象,这两个函数会创建;如果已经存在指定名称的事件对象,则会打开

信号灯

应用层:CreateSemaphore
内核层:KeInitializeSemaphore对信号对象初始化

void KeInitializeSemaphore(
  [out] PRKSEMAPHORE Semaphore,
  [in]  LONG         Count,
  [in]  LONG         Limit
);

Semaphore:需要初始化的内核对象信号灯的指针
Count:初始化时信号灯的计数
Limit:指明信号灯计数的上限值

读取信号灯当前的计数值:KeReadStateSemaphore
释放信号灯,增加计数值:KeReleaseSemaphore(可指定增加计数值的数目)
获得信号灯,减少计数值:KeWaitXXX系列函数(如果能获得信号灯,则计数减1,否则陷入等待)

示例代码:
在这里插入图片描述

互斥体

应用层:CreateMutex
内核层:KeInitializeMutex

void KeInitializeMutex(
  [out] PRKMUTEX Mutex,
  [in]  ULONG    Level
);

Mutex:需要初始化的互斥体
Level:保留值,一般为0

获得互斥体:KeWaitXXX系列函数
释放互斥体:KeReleaseMutex函数

代码示例:
在这里插入图片描述

快速互斥体

快速互斥体类似于互斥体,作用完全一样,但是快速互斥体的获取和释放速度比互斥体快
缺点:线程不能递归获取快速互斥体对象(已经获取互斥体的线程不能再次获取这个互斥体)

互斥体只互斥两个不相同的线程,对于同一个线程不互斥,但是快速互斥体对于同一个线程也进行互斥

初始化快速互斥体:ExInitializeFastMutex
获取快速互斥体:ExAcquireFastMutex
释放快速互斥体:ExReleaseFastMutex

代码示例:
在这里插入图片描述

其他同步方法

使用自旋锁进行同步

驱动程序常使用自旋锁作为一种有效的同步机制
举例:应用程序打开一个设备后,使用多线程对设备进行读取,此时IRP_MJ_READ的派遣函数会并发进行,然而绝大部分设备没有响应并发请求的能力,此时需要使用自旋锁进行同步

使用互锁操作进行同步(原子操作函数)

共两类:

  1. InterlockedXX函数(不通过自旋锁实现,内部不会提升IRQL,分页和非分页内存都可操作)
    在这里插入图片描述
  2. ExInterlockedXX函数(通过自旋锁实现,需要程序员提供一个自旋锁,不能操作分页内存)
    在这里插入图片描述
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值