
简介面向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%目标对应busyMs5、idleMs5一个周期10ms视觉效果比100ms周期平滑得多。GetTickCount64()返回的是毫秒时间戳用来切分忙闲区间和判断总时长。低于10%的占用率把busyMs下限置为1避免出现targetPercent0时完全没忙循环的边界问题。编译命令在MinGW环境下是g loadgen.cpp -o loadgen.exe -O2 -D_WIN32_WINNT0x0601参数解释-O2保证忙循环的代码不会被过度优化成无操作_WIN32_WINNT0x0601把目标系统版本标到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 11空闲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%。默认实现固定T10ms对于50%目标就是忙5ms闲5ms。这个配置优点是曲线平滑、反应快缺点是在低占用率目标下误差大因为Sleep的精度在10ms量级闲5ms其实经常睡成10ms以上。T100ms的模式则更适合低目标值场景。周期TbusyMs(目标70%)idleMs(目标70%)曲线表现适合场景10ms7ms3ms锯齿小肉眼平滑常规压测、演示100ms70ms30ms阶梯感明显低占用率目标、长时间巡航200ms140ms60ms大波浪散热测试观察温度爬升反过来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闲30msSleep误差占比从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%绝不收工。希望帮到你。本文还有配套的精品资源点击获取