首页
社区
课程
招聘
[原创]Windows 进程 CPU、内存、I/O 与调度指标采样
发表于: 2026-8-7 20:16 634

[原创]Windows 进程 CPU、内存、I/O 与调度指标采样

2026-8-7 20:16
634

开源项目实现:
1baK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6%4j5h3&6Y4N6$3g2A6i4K6u0V1j5$3#2Q4x3V1k6C8M7%4N6G2M7X3b7`.
图片描述

目标是取得指定 Windows 进程的 CPU 使用率、I/O 速率、内存快照和调度状态,并说明这些结果属于哪个进程实例和哪个时间窗口。要完成这件事,分为 4 步。

PID(Process Identifier,进程标识符)是 Windows 分配给运行中进程的数值编号。进程退出后,这个数值未来可以交给另一个进程使用。进程对象由内核维护,包含创建时间、线程集合、访问权限和退出状态。查询方用 OpenProcess 得到 HANDLE(句柄)。句柄是当前进程持有的受访问控制引用,指向一个具体进程对象,使用完必须关闭。

同一个进程对象连接着多个 Windows 组件。内核调度器把全部线程的用户态和内核态 CPU 时间累积到进程层。内存管理器提供工作集和提交量快照。I/O 管理器积累读、写与其它传输量。对象管理器维护句柄表项。调度器读取优先级类、动态优先级提升设置和处理器亲和范围。各来源的更新时刻并不完全相同,样本需要保存字段状态与采样时钟。

整个过程为:输入 PID -> 建立样本和进程句柄 -> 读取样本 A -> 等待实际时间窗口 -> 读取样本 B -> 核验实例与计时条件 -> 输出快照或差分结果。

一个样本分为三类数据。实例身份由 PID 和创建时间组成。累计计数包括 CPU 时间和 I/O 传输字节。瞬时属性包括工作集、私有提交量、句柄数、优先级类、优先级提升设置和亲和掩码。累计量只有放在两个有效样本之间才有速率含义。瞬时属性的单个数值只描述读取瞬间,报告中应附带采样时间和字段成功状态。

创建时间是识别 PID 复用的关键。监控服务常在每轮按 PID 重新打开进程,也可能在缓存恢复后继续使用旧基线。此时仅比较 PID 会把两个不同生命周期的累计 CPU 或 I/O 相减。创建时间来自进程对象的时间信息,它与 PID 一起形成实例键。键发生变化后,当前样本只可作为新的基线。

采样结构需要让读取失败可见。零工作集、零 I/O 增量和零句柄变化都可能是真实结果。把 API 失败也写成零会把“未知”伪装成“没有活动”。下面的结构把值、单位和字段状态分开。FieldStatus::error 只在 availablefalse 时解释为失败代码,成功字段的错误码没有业务含义。

SIZE_T 是与当前程序指针宽度相同的无符号整数类型,内存计数以字节为单位时用它保存最合适。ULONGLONG 是 64 位无符号整数,适合保存 100ns 累计时间和 I/O 字节。进程长期运行时累计值会变大,使用 32 位 DWORD 保存 CPU 时间或 I/O 字节会很快截断高位。

一个成熟的监控记录还应保存每项 API 的错误码、进程退出状态、查询权限、采样线程的计时器类型和程序自身位数。查看结果的人看到 available=false 时应显示“未取得”,同时保留错误码供权限与生命周期诊断。将失败字段从速率计算中剔除,能够避免除零、错误尖峰和误判闲置。

GetLastError 读取当前线程最近一次失败的 Win32 API 错误。这个值只保留当前线程的最近失败状态。后续 API、格式化库或诊断代码都有机会改写它,因此必须紧贴失败调用保存。它没有参数、缓冲区和资源所有权。

正确流程先把用户输入的 0 转成 GetCurrentProcessId 返回的 PID,再申请最低限度的权限。PROCESS_QUERY_LIMITED_INFORMATION 支持大多数只读属性查询。PROCESS_VM_READ 允许内存计数器读取相关进程信息。受保护进程、退出中的进程和安全描述符都可能让 OpenProcess 失败,失败后没有句柄可传给后续 API。

以下写法把失败句柄带入查询。空句柄没有指向任何进程对象,返回的错误属于错误调用本身,原始的拒绝访问或进程退出原因也会丢失。

完成第 1 步后,已有一条能区分“读取失败”和“真实零值”的样本结构,也已持有指向目标进程实例的有效句柄。接下来要从这个进程对象取得创建时间、累计 CPU 时间和两个时钟,才能让后续速率计算有身份依据和实际分母。

FILETIME 用两个 32 位字段表达一个 64 位计数,单位为 100ns(1 秒等于 10,000,000 个单位),基准点为 1601-01-01 UTC。GetProcessTimes 的创建时间、退出时间、内核 CPU 时间和用户 CPU 时间都采用这一单位。用户态 CPU 时间表示线程在应用代码和用户态库中获得处理器的累计时间。内核态 CPU 时间表示线程在系统服务、驱动和内核工作中获得处理器的累计时间。两者相加得到进程累计 CPU 时间。

CPU 百分比需要真实的采样窗口。单逻辑处理器基准使用 ΔCPU / ΔActiveTime × 100,其中 ΔCPU 是内核与用户累计时间的差,ΔActiveTime 来自 QueryUnbiasedInterruptTime。多线程进程可以同时在多个逻辑处理器上运行,因此单逻辑处理器基准能够超过 100%。全机份额需要额外记录可用逻辑处理器数、处理器组、作业对象限制和 CPU 集合范围,再选择相应分母。

墙钟与非休眠时间各自回答不同问题。GetSystemTimeAsFileTime 便于记录“何时采到”,也会受人工校时、时间服务校正和睡眠恢复影响。QueryUnbiasedInterruptTime 适合描述系统处于运行状态的持续时间,并排除睡眠与休眠时间。连续样本同时保存两者:两者的增量明显不一致时,当前窗口跨越了休眠、恢复或时钟改变,应废弃旧基线。

进程退出状态需要独立读取。退出能发生在任何两个 API 调用之间,进程句柄仍可能保持有效并指向已经结束的对象。GetExitCodeProcess 返回查询瞬间的退出状态。STILL_ACTIVE 表示进程尚未报告退出。该常量值为 259,程序本身也可能以 259 退出,采集器只把它作为状态提示,并结合采样时间处理。

错误写法把 Sleep(1000) 当作精确一秒,并把墙钟直接当作可靠分母。调度延迟会延长等待,睡眠恢复和校时会改变墙钟增量,速率会被放大或压低。两次实际计时结果才是计算依据。

完成第 2 步后,每份样本已有实例键、累计 CPU 时间、墙钟和非休眠时间。接下来读取内存、I/O 和调度字段,使结果同时说明进程此刻占用的资源与可运行范围。

工作集(Working Set)是当前驻留在物理内存中的页面集合,其大小由内存管理器回收、预取、共享页活动和进程访问模式共同影响。私有提交量(Private Usage)是只归该进程负责的已提交虚拟内存数量。工作集下降可以来自工作集修剪,私有提交量下降反映提交状态变化。两个字段不能互相替换。

I/O 计数器中的 ReadTransferCountWriteTransferCountOtherTransferCount 是累计传输字节。其它传输包括控制操作等非普通读写请求。API 成功返回时可把三者相加作为总 I/O 累计量。相加前应检查 64 位无符号溢出。报告速率时也可分别计算读、写、其它速率,以保留活动性质。

PROCESS_MEMORY_COUNTERS_EX 继承基础内存计数器并增加 PrivateUsage。调用前必须把 cb 写为整个结构的字节数,API 根据它判断调用方可接收的版本。IO_COUNTERS 由调用方在栈或其它本地存储中分配,API 填写结构。两个 API 都不转移任何缓冲区所有权。

以下错误没有设置 cb,系统无法知道调用方缓冲区的有效布局。即使某个系统版本恰好返回成功,后续版本或结构扩展也会使结果失去可靠性。

进程句柄数是对象管理器为该进程句柄表维护的可见表项数量。它会随着文件、线程、事件、注册表键和其它对象引用的打开关闭而变化,适合以时间序列观察趋势。句柄数本身不能指出对象类型,也不能单独证明泄漏。持续增长、对象分类和调用栈需要结合调查。

优先级类是进程级调度策略输入,例如 NORMAL_PRIORITY_CLASSHIGH_PRIORITY_CLASS。线程还有自己的相对优先级和动态提升,调度器会在运行中调整实际调度优先级。优先级提升禁用标志说明系统能否为该进程的线程应用动态提升。亲和掩码说明该进程可在哪些逻辑处理器上运行。超过一个处理器组的机器还涉及组范围,单个 DWORD_PTR 掩码只描述当前 API 适用的处理器集合。

读取这些属性时,每一个输出变量都归调用方所有,没有堆内存或句柄需要额外释放。优先级类返回零才表示失败,GetProcessPriorityBoostGetProcessAffinityMaskBOOL 返回值也必须检查。权限不足时保留失败字段,依然可以输出同一轮成功的 CPU、I/O 或内存信息。

完成第 3 步后,单份样本已经包含累计量、瞬时资源量和调度属性,并且每个字段都保留可用状态。接下来需要在等待后取得第二份样本,确认两份记录仍属于同一进程且计时窗口连续,才能把累计量换算为速率。

Sleep 用于在演示中形成最短等待请求。它并不提供精确计时保证:当前线程至少在指定毫秒后才重新具备运行机会,调度压力、调试暂停、系统睡眠和恢复都会拉长实际间隔。采样器因此使用两次实测计时值计算窗口长度。

有效基线要求所有参与差分的字段都可用,且两个样本描述同一实例。具体条件包括:PID 与创建时间相等。两次退出状态都显示活动。墙钟和非休眠时钟都向前。CPU 与 I/O 累计值未倒退。墙钟增量和非休眠时钟增量的差没有跨过设定阈值。任一条件失败时,当前快照替换旧基线,下一轮从它开始计量。


冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

上传的附件:
收藏
免费 1
打赏
分享
最新回复 (1)
雪    币: 915
活跃值: (129)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2026-8-8 11:49
0
游客
登录 | 注册 方可回帖
返回