window驱动开发-内核线程

在内核开发中,不可避免需要用到多线程技术,毕竟驱动是针对所有进程,而非某个特定进程,故驱动自然也是多线程的。在设计目标中,已经明确了驱动例程设计中,需要考虑的问题之一就是"重入"的概念,"重入"一方面因为中断,另外一方面也因为驱动例程可能在同一时间再次调用。

如果使用调试器调试过"重入"就知道,两个线程调用同一个函数,最大的区别就是栈空间不一样,并且下断点之后又是单线程,故看起来非常奇怪。

要保证可重入性的主要思路是:

1. 函数内部只引用函数栈空间的值(参数),任何函数之外局部或者全局值都不要引用;

2. 函数参数本身也要考虑同步访问的问题,最好是每个结构都有同步锁等机制保障;

不过话说回来,驱动确实很少用到线程,大部分情况下,功能驱动很少主动去发起什么I/O操作,它只是被动的处理,总线驱动和Filter驱动倒是有需要这么做,不过在那之前,先简单讨论一下定时器。

DPC和定时器

和用户层的定时器不一样,驱动的定时器优先级非常高,在正常情况下,线程代码一般都低于DPC级别,但是我们可以提升IRQL,只是驱动代码中,除了总线驱动级别,不会提高到DISPATCH_LEVEL以上。

由于 ISR 必须尽快执行,因此驱动程序通常必须将中断维护的完成推迟到 ISR 返回后。 因此,系统支持 延迟过程调用 (DPC) ,这些 DPC 可以从 ISR 排队,并在以后的时间以低于 ISR 的 IRQL 执行。

每个 DPC 都与系统定义的 DPC 对象相关联。 系统为每个设备对象提供一个 DPC 对象。 当驱动程序注册称为 DpcForIsr 例程的 DPC 例程时,系统会初始化此 DPC 对象。 如果需要多个 DPC,驱动程序可以创建其他 DPC 对象。 这些额外的 DPC 称为 CustomDpc 例程。

驱动程序不应直接引用 DPC 对象内容。 未记录对象的结构。 驱动程序无权访问分配给每个设备对象的系统提供的 DPC 对象。 驱动程序为额外的 DPC 分配存储,但这些 DPC 对象的内容只能由系统例程引用。

至于为什么讨论DPC要和定时器一起呢,其实原因就是定时器对象经常和DPC例程绑定,看下面的代码:

typedef void (__stdcall* PFUNC_DPC)(IN PKDPC Dpc, IN PVOID DeferredContext, IN PVOID SystemArgument1, IN PVOID SystemArgument2);

// 自定义DPC相关结构
typedef struct tagDPC_ROUTINE
{
	PFUNC_DPC pFunction;

	KDPC     pDpcRoutine;
	KTIMER   Timer;

	ULONG    ulFlags;
	ULONG    ulReset;

	PVOID   pParamter;
}DPC_ROUTINE, *PDPC_ROUTINE ;

// 初始化并启动定时器
void InitDpcRoutine(PDPC_ROUTINE pDpcInfo)
{
	LARGE_INTEGER llTime;
	if(NULL == pDpcInfo) return;
    
    // 初始化DPC结构
	KeInitializeDpc(&pDpcInfo->pDpcRoutine, pDpcInfo->pFunction, pDpcInfo);
    // 初始化定时器结构
	KeInitializeTimer(&pDpcInfo->Timer);

	llTime.QuadPart = _DPC_TIME;
    // 设置定时器
	KeSetTimer(&pDpcInfo->Timer, llTime, &pDpcInfo->pDpcRoutine);
}

// 设置定时器
void SetDpcTimer(LONG lTime, PDPC_ROUTINE pDpcInfo)
{
	LARGE_INTEGER llTime;
	if(NULL == pDpcInfo) return;
	
	// 单位是毫秒
	llTime.QuadPart = 300 * lTime;

	KeSetTimer(&pDpcInfo->Timer, llTime, &pDpcInfo->pDpcRoutine);
}

// 释放定时器
void FreeDpcRoutine(PDPC_ROUTINE pDpcInfo)
{
	if(NULL == pDpcInfo) return;
	KeCancelTimer(&pDpcInfo->Timer);
}

// DPC例程
VOID DPCRoutine(IN PKDPC Dpc, IN PVOID DeferredContext, IN PVOID SystemArgument1, IN PVOID SystemArgument2)
{
	PDPC_ROUTINE pDpcInfo = (PDPC_ROUTINE )DeferredContext;
    // ..
    SetDpcTimer(_DPC_TIME, pDpcInfo);
}

从代码中可以看到,定时器会和一个DPC例程绑定,当调用了KeSetTimer之后,设定的时间到达时,DPC例程被调用了。

注意: DPC例程中,中断级为DISPATCH_LEVEL,故需要注意访问的内存需要时非分页内存池分配的内存!

APC

异步过程调用 (APC) 是异步执行的函数。 APC也是一个类似于DPC的过程调用 ,但与 DPC 不同,APC 在特定线程的上下文中执行。 除文件系统和文件系统FIlter驱动程序以外的驱动程序不直接使用 APC,但操作系统的其他部分使用 APC,在实际情况中,win7以后APC就没有直接导出接口了。

Windows 操作系统使用四种 APC:

特殊的用户模式 APC 严格在用户模式下运行,并且始终执行,即使目标线程不处于可警报等待状态;

常规用户模式 APC 严格在用户模式下运行,并且仅在目标线程处于可警报等待状态时运行, 重叠I/O技术就是这样实现的;

正常内核 APC 在 IRQL = PASSIVE_LEVEL 的内核模式下运行。 普通内核 APC 会抢占所有用户模式代码,包括用户 APC。 文件系统和文件系统FIlter驱动程序通常使用正常的内核 APC;

特殊内核 APC 在 IRQL = APC_LEVEL 的内核模式下运行。 特殊内核 APC 会抢占 IRQL = PASSIVE_LEVEL 执行的用户模式代码和内核模式代码,包括用户 APC 和普通内核 APC。 操作系统使用特殊的内核 APC 来处理 I/O 请求完成等操作;

系统本身也支持如何禁用APC,系统提供三种机制来禁用当前线程的 APC:

关键区域: 当线程位于关键区域中时,不会执行其用户 APC 和普通内核 APC,但仍会执行特殊的内核 APC;

受保护的区域: 当线程位于受保护的区域内时,不会执行其任何 APC;

IRQL提升: 将当前 IRQL 提高到 APC_LEVEL 或更高级。 在 IRQL >= APC_LEVEL 执行的线程在禁用所有 APC 的情况下执行;

注意,这些设置适用于当前线程,不会影响任何其他线程的行为。

某些驱动程序支持例程必须在禁用特定类型的 APC 的情况下调用。 

驱动程序可以通过调用适当的例程显式进入关键或受保护的区域。 驱动程序还可以通过调用 KeRaiseIrql 将当前 IRQL 显式提升为APC_LEVEL ,驱动程序随后必须通过调用 KeLowerIrql 将 IRQL 降低到其原始值。

在上面的描述中,关键区域和保护区域看上去很像临界区,但是临界区并不是使用二者实现的,二者的使用如下:

关键区域: 驱动程序可以进入和退出关键区域,如下所示:

调用 KeEnterCriticalRegion 以进入关键区域;

调用 KeLeaveCriticalRegion 退出关键区域;

每次调用 KeEnterCriticalRegion 都必须具有对 KeLeaveCriticalRegion 的匹配调用;

保护区域: 驱动程序可以进入和退出受保护的区域,如下所示:

调用 KeEnterGuardedRegion 以进入受保护的区域;

调用 KeLeaveGuardedRegion 以离开受保护的区域;

和关键区域一样,两个函数的调用需要配对。

工作项

驱动中用到线程的情况也分两种:

1. 因为业务需要一个长期稳定调用的任务,但不需要那么高的优先级,例如发送broadcast的情况,这种情况使用线程;

2. 临时有大量的数据/控制流需要处理,但又希望尽快恢复工作,这种情况使用工作项;

需要延迟处理的驱动程序也可以使用工作项,工作项包含指向执行实际处理的驱动程序回调例程的指针。 驱动程序将工作项排队, 系统工作线程从队列中删除工作项并运行驱动程序的回调例程。 系统维护这些系统工作线程的池,这些线程是系统线程,每个线程一次处理一个工作项。

驱动程序将 WorkItem 回调例程与工作项相关联。 当系统工作线程处理工作项时,它会调用关联的 WorkItem 例程。 在 Windows Vista 和更高版本的 Windows 中,驱动程序可以改为将 WorkItemEx 例程与工作项相关联。 WorkItemEx 采用的参数不同于 WorkItem 采用的参数。

WorkItem 和 WorkItemEx 例程在系统线程上下文中运行。 如果驱动程序调度例程可以在用户模式线程上下文中运行,该例程可以调用 WorkItem 或 WorkItemEx 例程来执行需要系统线程上下文的任何操作。

若要使用工作项,驱动程序会执行以下步骤:

1. 分配和初始化新的工作项。

系统使用 IO_WORKITEM 结构来保存工作项。 若要分配新的 IO_WORKITEM 结构并将其初始化为工作项,驱动程序可以调用 IoAllocateWorkItem。 在 Windows Vista 和更高版本的 Windows 中,驱动程序也可以分配自己的 IO_WORKITEM 结构,并调用 IoInitializeWorkItem 将结构初始化为工作项。 驱动程序应调用 IoSizeofWorkItem 以确定保存工作项所需的字节数。

2. 将回调例程与工作项相关联,并将工作项排队,以便由系统工作线程处理。

若要将 WorkItem 例程与工作项相关联并将工作项排队,驱动程序应调用 IoQueueWorkItem。 若要改为将 WorkItemEx 例程与工作项相关联并将工作项排队,驱动程序应调用 IoQueueWorkItemEx。

3. 不再需要工作项后,请将其释放。

IoAllocateWorkItem 分配的工作项应由 IoFreeWorkItem 释放。 IoInitializeWorkItem 初始化的工作项必须先由 IoUninitializeWorkItem 取消初始化,然后才能将其释放。

仅当工作项当前未排队时,才能取消初始化或释放工作项。 系统在调用工作项的回调例程之前将工作项取消排队,因此可以从回调中调用 IoFreeWorkItem 和 IoUninitializeWorkItem 。

需要启动长时间处理的处理任务或发出阻止调用的 DPC 应将该任务的处理委托给一个或多个工作项。 当 DPC 运行时,会阻止所有线程运行。 此外,在 IRQL = DISPATCH_LEVEL 运行的 DPC 不得进行阻止调用,但是处理工作项的系统工作线程在 IRQL = PASSIVE_LEVEL 运行,因此,工作项可以包含阻止调用。

由于系统工作线程池是有限的资源, WorkItem 和 WorkItemEx 例程只能用于需要短时间的操作。 如果其中一个例程运行时间过长 (如果它包含无限循环或等待太长),则系统可能会死锁。 因此,如果驱动程序需要长时间延迟处理,则应改为调用 PsCreateSystemThread 来创建自己的系统线程。

请勿调用 IoQueueWorkItem 或 IoQueueWorkItemEx 将已加入队列的工作项排入队列。 这样做可能会导致系统数据结构损坏。 如果驱动程序在每次运行特定驱动程序例程时对同一工作项进行排队,则可以使用以下技术来避免第二次排队工作项(如果工作项已在队列中):

驱动程序维护辅助角色例程的任务列表;
此任务列表在提供给辅助角色例程的上下文中可用。 辅助角色例程和修改任务列表的任何驱动程序例程将同步其对列表的访问权限;’
每次运行辅助角色例程时,它都会执行列表中的所有任务,并在任务完成时从列表中删除每个任务;
当新任务到达时,驱动程序会将此任务添加到列表中。 仅当任务列表以前为空时,驱动程序才会将工作项排队;

系统工作线程在调用工作线程之前从队列中删除工作项。 因此,一旦工作线程开始运行,驱动程序线程就可以安全地再次将工作项排队。

内核线程

下面是创建内核线程的函数:

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
);

创建内核线程的驱动程序在初始化或 I/O 请求开始传入此类驱动程序的 Dispatch 例程时调用此例程。 

PsCreateSystemThread 创建内核模式线程,该线程在系统中开始单独的执行线程。 此类系统线程没有 TEB 或用户模式上下文,并且仅在内核模式下运行。

如果输入 ProcessHandle 为 NULL,则创建的线程与系统进程相关联。 此类线程将继续运行,直到系统关闭或线程通过调用 PsTerminateSystemThread 终止自身。

在非系统进程上下文中运行的驱动程序例程必须为 PsCreateSystemThread 的 ObjectAttributes 参数设置 OBJ_KERNEL_HANDLE 属性。 这会将 PsCreateSystemThread 返回的句柄的使用限制为在内核模式下运行的进程。 否则,线程句柄可由运行驱动程序的上下文的进程访问。

驱动可以调用 KeSetBasePriorityThread 来设置线程的基本优先级。 驱动程序应指定一个优先级值,以避免 SMP 计算机中的 运行时优先级反转 。 也就是说,将驱动程序创建的线程的基本优先级设置得太高可能会造成低优先级线程的执行延迟,这些线程为该驱动程序提交 I/O 请求。

由于线程对象本身是调度程序对象的一种类型,因此线程可以等待另一个线程完成。 若要获取与线程关联的线程对象指针,驱动程序可以调用 ObReferenceObjectByHandle,并传入从 PsCreateSystemThread 接收的线程句柄。

线程可以调用 KeDelayExecutionThread 来等待可能是全时间切片或更长的间隔。 KeDelayExecutionThread 间隔的粒度约为 10 毫秒。 由于 KeDelayExecutionThread 是计时器驱动的例程,因此其间隔的粒度略快或慢于 10 毫秒,具体取决于平台。

下面是一些内核线程的使用要点:

1. 最好是长周期的任务,再使用内核线程,因为内核线程是挂载到System进程下的,故如果我们不停止,它可能直到系统关闭才会终止!

2. 要考虑驱动会随时中断、随时移除,内核线程的稳定性和鲁棒性必须很强;

3. 内核线程可能既和DPC例程存在前后调用的关系,又和应用层调用有关,故需要小心平衡两者,不要有无限制等待的代码片段;

线程调度的例子

在某种意义上讲,内核线程和我们平时使用CreateThread创建的线程并没什么区别,故适用于正常线程的优先级规则也适用于内核线程。

在window系统中,每个CPU的数据结构中,都有一个线程队列,里面按照优先级排列着大量处于就绪状态的线程;当时钟中断例程被调用时,系统代码会将当前运行的线程中断,并保存线程上下文,根据一定规则将它插入到线程队列中,将队列中最开始处于就绪状态的线程取出,设置线程上下文,将代码控制权交回线程,如此周而复始,让系统看起来有许多线程在同时运行,这里也解释了为什么线程池的容量最佳是处理器核心的2倍,因为这种情况下,线程池能够保证在每个CPU队列中至少有两个相同的线程排队。

上面的描述看起来很合理,但遗憾的是,线程经常被打断,上面的情况是被时钟中断打断,其它中断也能打断它们。

线程调度器会根据实际情况调整每个CPU上的线程运行情况,在这里,线程调度器仍然是一个理论概念而不是实际概念,处于DISPATCH_LEVEL优先级以上的代码都被认为是线程调度器的一部分,原因在于它们都会打断当前线程的运行;APC_LEVEL优先级则不会打断线程,它是在线程被切换之后,开始运行的,故APC特定于线程,也不会改变线程的切换。

不会讨论系统具体的线程调度,这部分留到内核分析的时候,仅讨论在驱动编程中,什么情况下干涉线程调度以及如何避免线程调度出问题。

DPC例程前面已经描述了,但它不仅仅只作为定时器的例程,相反,那是一个非常典型的理论上的中断处理流程(这么说是因为,具体的内核代码并不是这样运行的,但是我们可以这样来理解内核代码)。

时钟中断是优先级非常高的中断,它会以一个恒定的频率被触发,Windows系统中例如线程调度、sleep等时间行为都会有一个最小粒度,就是因为时钟中断并不是以100纳秒为单位,而是以某一个常数,例如15ms为单位运行(我怀疑这是因为处理器切换的最小时间应该和中断的最短时间之间有一定关系),在中断中,不会做太多的事情,故一个中断会被分为两个部分,在这种语境下,时钟中断是上半部分中断,定时器绑设定的DPC例程是下半部分中断,这样既保持了良好的响应速度,又能让性能不那么糟糕,这也是DPC被称为延迟过程调用的原因。

故我们也能总结出线程如何应对调度的特点:

线程调度是不完全考虑线程代码运行情况的,故线程代码一定要避免和更高优先级的代码同时访问内存;

线程调度最好只访问线程参数指向的内存,并且尽量不要做一块内存两个线程用的事情,最好的办法是,使用多个队列来管理数据,并且在使用内存块的时候把项从队列中摘除

  • 20
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值