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被称为延迟过程调用的原因。

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/4361.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

重生之我是Nginx服务专家

nginx服务访问页面白色 问题描述 访问一个域名服务返回页面空白,非响应404。报错如下图。 排查问题 域名解析正常,网络通讯正常,绕过解析地址访问源站IP地址端口访问正常,nginx无异常报错。 在打开文件时,发现无法…

R可视化:ggplot2绘制双y轴图

介绍 ggplot2绘制双y轴图加载R包 knitr::opts_chunk$set(message = FALSE, warning = FALSE) library(tidyverse) library(readxl)# rm(list = ls()) options(stringsAsFactors = F) options(future.globals.maxSize = 10000 * 1024^2)Importing data 下载Underdetection of c…

IDEA实现Springboot项目自动热部署

每当我们在修改代码时,往往需要重新启动项目,这样不仅浪费时间而且很麻烦,我们可以通过IDEA的热部署来提高效率 1、首先点file >> settings >> Build Excution >> Compire,选择Build project auto matically 2.…

CMakeLists.txt中如何添加编译选项?

1. 引子 编译器有多种可供选择,如g、c、clang等,如下以c作为示例。 2. 使用CMAKE_CXX_FLAGS添加编译选项 在Makefile中可能用类似如下的指令来添加编译选项: /usr/bin/c -Wall -Wextra -Wno-sign-compare -Wno-unused-variable -Wno-unuse…

flutter笔记-主要控件及布局

文章目录 1. 富文本实例2. Image2.1 本地图片2.2 网络图片 笔记3. 布局4. 滑动相关view4.1 GridView类似九宫格view4.2 ListView 关于widget的生命周期的相关知识这里就不做介绍,和很多语言类似; 1. 富文本实例 Dart中使用richtext,示例如下…

int和byte数组相互转换详解

转换之前我们需要了解各种进制之间的关系,不太了解的可以先看下计算机组成原理和体系 这篇文章 byte byte是字节的意思,一个字节等于8位,范围是 0000 0000 ~ 1111 1111(十六进制:0x0~0xff),总共包含256个数。 有符号的byte表示的…

java 和 php 的AES 128位 256位 加解密 【java解密php的AES加密方案】

项目需要 需要java可以解密 php加密的 字符串 , 使用的方法是 AES128位加解密 坑一踩完 ,还是直接上代码 package com.xxx.init.utils;import com.xxx.init.utils.BaseDataUtil; import com.xxx.init.exception.xxxRuntimeException; import com.xxx.i…

ARCGIS PRO SDK POINT层唯一值渲染按角度旋转

c 代码: 按Direction字段旋转&#xff0c;旋转样式为数学 protected override async void OnClick(){var featLayer MapView.Active.Map.GetLayersAsFlattenedList().OfType<FeatureLayer>().First();await QueuedTask.Run(() >{var render featLayer.GetRenderer()…

Python异步编程详解:asyncio和多线程

Python 的异步编程是一种通过协程、事件循环和异步I/O操作来实现并发的技术。在 Python 中&#xff0c;asyncio 是用于编写单线程并发代码的库&#xff0c;而多线程则涉及使用 Python 的 threading 模块。下面我们将详细探讨这两种技术的使用和它们的适用场景。 ### 1. asynci…

6 Zookeeper 配置说明

Zookeeper 的三种工作模式 单机模式:存在单点故障。集群模式:在多台机器上部署 Zookeeper 集群,适合线上环境使用。伪集群模式:在一台机器同时运行多个 Zookeeper 实例,仍然有单点故障问题,当然其中配置的端口号要错开的,适合实验环境模拟集群使用。Zookeeper 的三种端口…

C 练习实例36 - 求100之内的素数

C 练习实例36 - 求100之内的素数 题目&#xff1a; 求100之内的素数。 程序分析&#xff1a; 质数&#xff08;prime number&#xff09;又称素数&#xff0c;有无限个。一个大于1的自然数&#xff0c;除了1和它本身外&#xff0c;不能被其他自然数整除。 程序源代码&#x…

Vue3+Vite开发的项目进行加密打包

本文主要介绍Vue3+Vite开发的项目如何进行加密打包。 目录 一、vite简介二、混淆工具三、使用方法1. 安装插件:2. 配置插件:3. 运行构建:4. 自定义混淆选项:5. 排除文件:下面是Vue 3+Vite开发的项目进行加密打包的方法。 一、vite简介 Vite 是一个由 Evan You 创造的现代…

XBoot:基于Spring Boot 2.x的一站式前后端分离快速开发平台

XBoot&#xff1a;基于Spring Boot 2.x的一站式前后端分离快速开发平台 摘要 随着信息技术的迅速发展&#xff0c;快速构建高质量、高可靠性的企业级应用成为了迫切需求。XBoot&#xff0c;作为一个基于Spring Boot 2.x的一站式前后端分离快速开发平台&#xff0c;通过整合微信…

python_AI库 matplotlib在AI程序中的应用介绍

本文默认读者具备以下技能&#xff1a; 熟悉Python基础知识&#xff0c;能自行阅读并理解代码含义 对AI有基础了解 基础高等数学知识 前文对matplotlib在日常生活的基础应用作了介绍&#xff0c;那么matplotlib与我们的AI又有什么联系呢&#xff1f; 在 AI 程序中&#xff0c…

针对icon报错

针对上篇文章生成图标链接中图标报错 C# winfrom应用程序添加图标-CSDN博客 问题&#xff1a;参数“picture”必须是可用作Icon的参数 原因&#xff1a;生成的ico图标类型不匹配 解决方法&#xff1a; 更改导出的ico类型

iOS - 多线程-读写安全

文章目录 iOS - 多线程-读写安全1. 多读单写1.1 场景1.2 实现方案1.2.1 pthread_rwlock&#xff1a;读写锁1.2.1.1 示例 1.2.2 dispatch_barrier_async&#xff1a;异步栅栏调用1.2.2.1 示例 iOS - 多线程-读写安全 假设有一个文件&#xff0c;A线程进行读取操作&#xff0c;B…

数智时代的AI人才粮仓模型解读白皮书(2024版)

来源&#xff1a;极客邦科技 自 2023 年上半年起&#xff0c;ChatGPT 等大模型技术蓬勃发展&#xff0c;AI 技术不断突破边界&#xff0c;展现 出惊人的潜力和发展速度。从早期的逻辑推理、专家系统&#xff0c;到如今的深度学习、神经网络&#xff0c; AI 技术显著缩小了科学…

ASP.NET企业投资价值分析系统

摘 要 本文将影响股票投资价值的宏观因素、行业因素、企业内部等诸多因素予以量化分析&#xff0c;对钢铁板块和汽车板块各上市公司进行综合评估&#xff0c;为广大股民的投资方向和资金安全提供了有力的支持。本文还阐述了企业投资价值分析的必要性&#xff0c;说明了企业投…

K8s: 持久化存储之卷, NFS卷

卷 Volume 1 ) 概述 容器中的文件在磁盘上是临时存放的&#xff0c;这给容器中运行的特殊应用程序带来一些问题 首先&#xff0c;当容器崩溃时&#xff0c;kubelet 将重新启动容器&#xff0c;容器中的文件将会丢失——因为容器会以干净的状态重建其次&#xff0c;当在一个 Po…

分类算法——模型评估(八)

1混淆矩阵 在分类任务下&#xff0c;预测结果与正确标记之间存在四种不同的组合&#xff0c;构成混淆矩阵&#xff08;适用于多分类&#xff09; TP True Possitive FN False Negative 2精确率&#xff08;Precision&#xff09;与召回率&#xff08;Recall&#xff09; 精…