简介:面向Windows平台系统管理员、开发者和硬件爱好者的CPU负载模拟工具,核心用途是在浏览器中按需设定占用率与持续时间,快速制造可控的高负载环境,解决压力测试、代码性能评估与散热验证等场景中缺少轻量压测手段的问题。压缩包共2个文件,分别为HTML操作界面和JavaScript逻辑脚本,整体仅34KB,结构精简,无需安装依赖即可直接运行。已有718人学习下载。使用者可自定义CPU占用百分比与运行时长,执行过程中能配合任务管理器直观观察负载变化,关闭页面后负载随即释放,兼顾灵活性与安全性。对于需要快速评估系统散热能力、电源策略或定位性能瓶颈的技术人员,这是一份即开即用的便捷选择;但需留意长时间高负载可能引发过热,建议在良好散热条件下短时使用。
1. 这个“刷CPU使用率”工具,到底解决什么问题
不少人在验证脚本、压测散热、给云主机保活时会撞上同一个尴尬:任务管理器里CPU占用率怎么都上不去,死循环一跑又直接撞到100%,想刷个“半载”反而最难。这份资源不是那种常驻后台的插件,也不是改注册表的小把戏,而是一个多线程、可绑核、能用命令行精确指定占用率的Windows压载软件包。它解决的核心问题是:目标核数、目标占用率、持续时间三个参数给定后,让系统稳定呈现你想要的负载曲线。适合四类人:做云主机规格验证的运维、调散热方案的硬件爱好者、跑性能测试的开发者,以及需要演示系统满载效果的同学。
2. 凭什么能刷出指定占用率:线程亲和性与忙闲时间片
2.1 占用率的本质不是“算力”,而是“忙/总时间比”
CPU占用率在Windows任务管理器里并不是“当前算得多快”,而是采样间隔内“忙时间 / 总时间”的百分比。一个线程如果一直占着逻辑处理器执行,这个核的占用率就是100%;想让占用率变成50%,就得让它忙50ms、闲50ms,让出半个周期给别的负载。常见的错误做法是写一个不带延时的空转循环,那样只能得到100%,没法得到50%。反过来,有人用Sleep(50)来实现空闲,又发现占用率变成0或者个位数,因为Sleep会让线程彻底离开就绪队列,调度器不会把它当成“正在忙的线程”。正确做法是在忙闲循环里用短时间片轮转:忙一段,用Sleep(1)让出极小间隙再进入下一轮忙。这里的核心是用时间片比例逼近目标占用率,而不是靠计算量碰运气。
2.2 多核负载要靠绑核,不能靠开线程碰运气
Windows默认会把线程扔给任意一个空闲逻辑处理器,如果你只创建一个线程,表现就是某个核一会儿100%、一会儿0%,总占用率看着像“单核/物理核数”,而不是你想要的平滑数值。要刷出“整机70%”这种总量目标,通常做法是:按目标占用率计算需要压的线程数,然后把每个线程用SetThreadAffinityMask绑定到指定的逻辑处理器索引上,再用忙闲循环控制单个核的占用率。这一步必须显式做,否则线程会在核之间漂移,任务管理器里的“逻辑处理器”视图就是几根抖成乱麻的线。
另外要注意超线程拓扑。在同一物理核的两个逻辑处理器上绑两个线程,两者共享执行单元,刷出来的总占用率往往达不到200%。所以资源包里的参数扫描工具会先跑一遍GetLogicalProcessorInformation读取逻辑处理器拓扑,自动跳过相邻逻辑核,尽量避免把两个线程塞进同一个物理核。
2.3 这份资源包里是什么:主程序、控制脚本与参数表
| 文件/模块 | 作用 | 使用场景 |
|---|---|---|
loadgen.cpp | 多线程压载主程序源码,编译出loadgen.exe | 手动单次压测,最常用 |
loadgen.exe(预编译版) | 直接运行,不需要本机装编译环境 | 应急验证、临时拉高负载 |
loadctl.ps1 | 批量启动/停止脚本,支持参数透传 | 多核压测、定时巡航、批量清理 |
loadcheck.ps1 | 抽样统计脚本,对比设定值与实际值 | 校准负载、压测后出数据 |
params.md | 完整参数表与周期换算公式 | 调参时对照,省去翻代码 |
资源包的结构很朴素:一个核心程序加两个管理脚本加一份参数文档。loadgen.exe负责真正干活,loadctl.ps1负责把“单核能力”组织成“整机负载”并用进程对象管理生命周期,loadcheck.ps1负责事后验证。实际用的时候,我一般只碰前两个文件,参数文档放在手边查。
3. 从编译到复现:跑一个单核50%压测
3.1 最小可运行:loadgen.cpp 编译与首跑
先看核心源码,loadgen.cpp的骨架长这样:
#include <windows.h> #include <stdio.h> #include <stdlib.h> #include <vector> struct WorkerArg { int coreIndex; // 要绑定的逻辑处理器索引 int targetPercent; // 目标占用率 0-100 int durationSec; // 持续时间,秒 }; DWORD WINAPI Worker(LPVOID param) { WorkerArg* arg = (WorkerArg*)param; // 关键:把当前线程绑定到指定逻辑核心 SetThreadAffinityMask(GetCurrentThread(), 1ULL << arg->coreIndex); ULONGLONG startMs = GetTickCount64(); while (GetTickCount64() - startMs < (ULONGLONG)arg->durationSec * 1000) { int busyMs = arg->targetPercent / 10; // 忙时间片 int idleMs = 10 - busyMs; // 闲时间片 if (arg->targetPercent < 10) busyMs = 1; // 低负载保护 ULONGLONG busyUntil = GetTickCount64() + busyMs; while (GetTickCount64() < busyUntil) { // 忙循环,什么都不做,就是占用 CPU 时间片 } Sleep(idleMs); } return 0; } int main(int argc, char* argv[]) { // 简化:固定绑定逻辑核 0,目标 50%,跑 60 秒 WorkerArg arg = { 0, 50, 60 }; CreateThread(NULL, 0, Worker, &arg, 0, NULL); Sleep(60000); return 0; }这段的逻辑是:线程启动后先用SetThreadAffinityMask把自己锁到逻辑处理器0,这样任务管理器的“逻辑处理器”视图里能看到单一核心的负载变化。随后进入一个按秒为单位计数的循环,内部用“忙busyMs毫秒→睡idleMs毫秒→再忙busyMs毫秒”的组合逼近目标占用率。50%目标对应busyMs=5、idleMs=5,一个周期10ms,视觉效果比100ms周期平滑得多。GetTickCount64()返回的是毫秒时间戳,用来切分忙闲区间和判断总时长。低于10%的占用率把busyMs下限置为1,避免出现targetPercent=0时完全没忙循环的边界问题。
编译命令在MinGW环境下是:
g++ loadgen.cpp -o loadgen.exe -O2 -D_WIN32_WINNT=0x0601参数解释:-O2保证忙循环的代码不会被过度优化成无操作;_WIN32_WINNT=0x0601把目标系统版本标到Windows 7及以上,让SetThreadAffinityMask这些接口的原型完整展开。编译完执行loadgen.exe,默认就是“逻辑核0刷50%跑60秒”。如果你拿到的是预编译版本,直接跳到下一步。
3.2 参数表:-c / -t / -d / -p / -y 都管什么
实际用的时候不可能每次改源码重新编译,所以loadgen.cpp没有把参数写死,而是走命令行。资源包里的params.md把参数整理成了下表:
| 参数 | 含义 | 示例 | 备注 |
|---|---|---|---|
-c <idx> | 绑定逻辑处理器索引 | -c 3 | 从0开始,不能超过CPU逻辑核数-1 |
-t <percent> | 目标占用率 | -t 50 | 范围1-100,小于10会走低负载保护 |
-d <sec> | 持续时间 | -d 120 | 到点自动退出,0表示一直跑 |
-p <priority> | 线程优先级值 | -p 1 | 1=空闲,2=低于正常,3=正常,4=高于正常 |
-y | 自检模式 | -y -t 70 -d 30 | 结束后打印实际均值与误差 |
实际命令行形态是:
loadgen.exe -c 0 -t 50 -d 120 -p 3这条命令的意思是:只刷逻辑核0,占用率目标50%,持续120秒,线程优先级正常。优先级参数通常不用动,但如果你在刷数同时还要跑真正的工作负载,建议把压载线程降到低于正常,避免把业务线程挤到遥不可及。自检模式-y会额外做一件事:结束时用QueryProcessCycleTime读取进程累计CPU周期,换算成实际占用率,打印一行“实际均值/设定值/误差百分比”,方便脚本捕获结果。
3.3 批量刷多核:PowerShell 启动与停止
单核压测只是验证工具是否正常,真正压测一个8核系统需要启动多个实例。手动开8个黑窗口不现实,资源包里的loadctl.ps1解决的是批量生命周期管理:
param( [int]$CoreCount = 8, # 要压的逻辑核数量 [int]$Target = 70, # 每个核的目标占用率 [int]$Seconds = 60 # 持续时间 ) $procs = @() for ($i = 0; $i -lt $CoreCount; $i++) { $argList = "-c $i -t $Target -d $Seconds" $p = Start-Process -FilePath ".\loadgen.exe" ` -ArgumentList $argList ` -PassThru -WindowStyle Hidden $procs += $p Write-Output "$(Get-Date -Format 'HH:mm:ss') started pid=$($p.Id) core=$i" } $procs | Select-Object Id, ProcessName | Format-Table # 等目标时长 + 3 秒,确保所有进程都自行退出 Start-Sleep -Seconds ($Seconds + 3) # 只清理还没退出的实例,避免杀掉其它场景的 loadgen $alive = $procs | Where-Object { -not $_.HasExited } if ($alive) { Write-Output "killing stale processes: $($alive.Id -join ', ')" $alive | Stop-Process -Force }这段脚本的关键点是Start-Process -PassThru会返回进程对象,后续可以用$p.Id记录PID、用$_.HasExited判断是否还在;-WindowStyle Hidden让黑窗口不弹满整个屏幕。循环里把逻辑核索引依次传给-c参数,从0到CoreCount-1。停止逻辑做了一层双重保护:先等目标时长+3秒,让进程正常退出,再用.HasExited过滤出真正卡住的实例强杀。这里不要用Stop-Process -Name loadgen这种写法,它会误杀你自己手动开的其它压载进程,PID级收尾更精确。启动日志和时间戳一并打印到控制台,方便事后回溯。
3.4 别急着看总占用率:任务管理器的校验方式
启动8个实例后,第一反应是切到“性能”页看总占用率。实际上更严谨的做法是:打开任务管理器→“性能”页→右键CPU曲线图→“更改图形为”→“逻辑处理器”。这一步不夸张地说是所有验证里最重要的一步,因为总占用率是所有核的平均值,绑核不均时它也能凑出一个“看似合理”的数字。逻辑处理器视图能直接看到第0到第7号核各自是不是真的都稳定在70%附近。如果某些核是0、某些核是99,说明参数解析或绑核逻辑出了问题。
观察的时候注意任务管理器的刷新周期大约1秒,瞬时波动会被平滑掉,不要看到某个核跳了5%就紧张。如果只关心稳定后的均值,等启动10秒后再读数据——忙闲循环刚启动的头几秒可能还没进入稳态。
4. 从“跑得动”到“刷得准”:优先级与周期的微调
4.1 优先级设错,数值会漂
线程优先级会直接影响忙闲时间片的兑现程度。-p 1(空闲优先级)在系统空闲时拿得到CPU,但一旦后台有别的负载,可能整个忙循环被挤到只剩一半运行时间;-p 4(高于正常)能让压载线程占据绝对主动,但如果这台机器上同时有数据库实例或消息队列,它们的响应延迟会明显变差。这里有个常见的翻车现场:有人为了让占用率稳定,把所有压载线程都提到高优先级,结果目标值是刷出来了,业务请求超时一大片,最后复盘发现不是业务代码有问题,是压载工具把自己的优先级抬得太高。我的默认习惯是-p 3正常优先级,需要压得更硬时顶多到4。优先级不是越高越好,它是用来约束“压载线程和真实负载之间资源分配关系”的旋钮,和占用率数值没有直接映射关系,调它时盯着任务管理器里业务线程的CPU时间看,而不是盯着曲线看。
4.2 忙闲周期选10ms还是100ms,实际差多少
忙闲周期是这套工具里最值得调的一个参数,但源码里没有直接暴露,params.md里给出了换算公式:单周期T = busyMs + idleMs,期望占用率R = busyMs / T × 100%。默认实现固定T=10ms,对于50%目标就是忙5ms闲5ms。这个配置优点是曲线平滑、反应快,缺点是在低占用率目标下误差大,因为Sleep的精度在10ms量级,闲5ms其实经常睡成10ms以上。T=100ms的模式则更适合低目标值场景。
| 周期T | busyMs(目标70%) | idleMs(目标70%) | 曲线表现 | 适合场景 |
|---|---|---|---|---|
| 10ms | 7ms | 3ms | 锯齿小,肉眼平滑 | 常规压测、演示 |
| 100ms | 70ms | 30ms | 阶梯感明显 | 低占用率目标、长时间巡航 |
| 200ms | 140ms | 60ms | 大波浪 | 散热测试观察温度爬升 |
反过来,T太短也有问题:忙闲切换本身要消耗调度时间,如果T小于5ms,调度器的切换开销占比增大,实际占用率会比设定值偏高。调周期时的经验原则是:目标低于30%时用100ms周期减少Sleep误差,目标高于60%时用10ms周期保证视觉平滑,折中方案是维持默认10ms不做改动,大多数场景已经够用。
4.3 平滑启动与退出自清理,省去手动收尾
直接扔一个满载循环会有一个副作用:启动瞬间占用率从0直接跳到100%,在长时间压测刚开始的几十秒内CPU温度会急剧爬升。源码里因此在Worker循环开头加了一个“渐进启动”逻辑,前5秒从目标占用率的30%(原始版本是10%)逐步移到目标值,5秒后进入稳态。这样做的原因很简单——散热验证时最怕“接通电源先看到温度瞬间顶到警戒线”,渐进启动能区分出“散热系统吃得住的满载”和“瞬间冲击造成的假警报”。退出侧同样有讲究:压测线程退出后,资源包里的loadctl.ps1会再等3秒统一清理残留进程。主程序内部也会在main返回前调用TerminateThread?不会,为了优雅,Worker循环靠时间判断自然退出,但有一个防御性设计:如果调用方在-d到期前向进程发送了CTRL_BREAK信号,程序会捕获事件,把当前实际占用率写入日志再退出,避免下次启动时读不到上一轮数据。自清理环节是所有压载工具最容易做砸的地方,最忌讳的就是“启动一时爽、进程忘杀光”,一份合格的资源包必须把收尾步骤做到脚本层而不是依赖使用者的记性。
5. 避坑:五次把数值刷飞之后我学到的排查顺序
5.1 现象:任务管理器曲线一条直线,占用率纹丝不动
最常见的新手翻车现场是编译完跑起来,任务管理器里占用率是0%,曲线平得像心电监护仪没接电。原因通常出在SetThreadAffinityMask的返回值没判断,绑核掩码写错导致线程压根没在目标核上运行;另一个更隐蔽的原因是线程刚创建出来就直接阻塞在等待事件上,忙循环根本没进去。解决顺序是:先看任务管理器的“逻辑处理器”视图,确认是哪个核在动;再回源码检查1ULL << coreIndex在coreIndex大于31时是否溢出,Windows的处理器掩码在64位系统上是64位,但编译器默认整数宽度可能导致移位异常;最后在main里临时加一行printf打印SetThreadAffinityMask返回值和当前线程ID,确认线程真的创建成功且绑定成功。注意绑定失败时SetThreadAffinityMask返回0,这个返回值很多人不看,一看一个准。
5.2 现象:占用率在15%和90%之间来回过山车
当你看到目标70%,任务管理器曲线却在15%到90%之间大幅摆动时,先说结论:大概率不是参数算错,是忙闲周期太短加上Sleep精度问题。目标70%对应的10ms周期是“忙7ms闲3ms”,但Windows的Sleep(3)实际可能睡8ms到15ms,因为系统时钟粒度约15.6ms,于是实际忙闲比例被破坏。解决方式有两层:第一层把Sleep(idleMs)换成Sleep(1)后紧接一个SwitchToThread()调用,强行让出剩余时间片,能改善小时间片下的抖动;第二层直接改用100ms周期,把-t 70换算成忙70ms闲30ms,Sleep误差占比从30%降到0.3%以下,曲线立刻稳定。
5.3 现象:目标70%,实际只有50%,总数凑不对
设定值和实际均值系统性偏差几成,最常见原因是拓扑感知没做。多线程各自绑定到并行逻辑核后,如果两个线程恰好绑在同一个物理核的两个逻辑处理器上(例如逻辑核0和逻辑核1在同一物理核内),它们共享ALU执行资源,两个线程的忙循环互相争抢,单个线程账面是70%,实际物理执行能力只能跑出35%到40%。这就是为什么资源包里的loadgen.cpp会调用GetLogicalProcessorInformationEx读取处理器拓扑,跳过同一物理核的相邻逻辑核,保证每个物理核只放一个压载线程。用脚本批量压测时也不要盲目把CoreCount设成逻辑核总数,先看msinfo32里的“物理核心数”,把CoreCount字段设成物理核数更稳。
5.4 现象:压测结束、进程退干净了,系统还是卡
进程列表里loadgen.exe已经消失,但整机响应仍然迟钝,这种现象的解释往往在“别的进程被带偏”。常见场景是压载线程的优先级被设得过高,曾经占满了CPU的线程就算退出,进程的DLL工作集也不会立刻释放,底层硬件中断和系统管理中断短时间内还在满负荷处理后台积压。解决方法是:压测结束后不要只看loadgen是否退出,再用typeperf "\Processor(_Total)\% Processor Time" -si 1 -sc 5观察5秒,如果总CPU还停在90%以上,去任务管理器里按CPU占用排序,找真正在跑的其它进程。另外脚本里Stop-Process -Force强制结束进程,可能导致程序来不及做自清理,所以正常流程是等待自然退出后再用PID确认,不要一结束压测就无脑强杀。
5.5 现象:把同一台物理机上的其它业务进程打挂了
这个现象在云主机环境下特别明显:你只是刷自己租户的CPU占用率,但宿主机上其它租户的业务延迟集体飙升。根因是压载线程把所在物理核的L2/L3缓存踩满了,导致同一物理核上其它虚拟机的指令流转慢。解决方向不是“把占用率调低一点再跑”,而是从策略上限定资源边界:用-c只绑定到指定的几个逻辑核,不要全核全开;用-d设置硬性时间上限;控制脚本里额外增加一个负载阈值检查——连续5次采样CPU均值超过设定值8%以上就自动终止本轮压测。在压测某台云主机前,先看一眼同一物理机上的未休眠虚拟核数量,不是逼不得已不要真把全部核心刷满。
6. 进阶:让负载值可校准——抽样统计与误差检查
一套压载工具如果只有“刷”没有“验”,那只能算半成品。资源包里的loadcheck.ps1用Windows自带的性能计数器做抽样统计,不依赖第三方工具:
param([int]$Target = 70, [int]$Seconds = 30) $samples = 1..$Seconds | ForEach-Object { (Get-Counter -Counter "\Processor(_Total)\% Processor Time" -SampleInterval 1).CounterSamples[0].CookedValue } $avg = ($samples | Measure-Object -Average).Average $min = ($samples | Measure-Object -Minimum).Minimum $max = ($samples | Measure-Object -Maximum).Maximum $diff = [math]::Round(($avg - $Target) / $Target * 100, 2) Write-Output "avg=$([math]::Round($avg,2)) min=$([math]::Round($min,2)) max=$([math]::Round($max,2)) err=$diff%"这段脚本的逻辑是:每秒取一次% Processor Time,连续30秒得到30个样本,分别计算平均值、最小最大值和相对误差。SampleInterval 1表示采样间隔1秒,CookedValue拿到的是已经换算好的百分比数值。如果误差超过5%,我会按固定顺序排查:先看逻辑处理器视图里每个核的负载是否均匀;再查msinfo32确认线程有没有绑到同一物理核;最后把忙闲周期从10ms切到100ms再试一轮。这套脚本适合长时间压测后的总结,比肉眼看任务管理器靠谱得多。
长效巡航场景下有个技巧:把loadctl.ps1和loadcheck.ps1配合定时任务,做成“刷30分钟→停10分钟→自动抽查结果”的循环,既能模拟真实业务波峰,又不会把机器压到失去响应。某次压测结束时我从任务管理器看到曲线标准得像教科书,但loadcheck.ps1跑出偏差12%,一查是超线程拓扑导致两个压载线程挤在同一物理核上。从那以后我每次刷数完都会强制走一遍抽样校准,误差超5%绝不收工。希望帮到你。
本文还有配套的精品资源,点击获取