Strat
Strat

Reputation: 43

Kill a process if it reaches x% of CPU usage

I want to stop processes that are running higher than 14% CPU usage.

$process = get-process
    foreach ($proc in (Get-WmiObject  Win32_Processor)){
if($proc.numberofcores -eq $null){
    $cores++
}else{
    $cores = $cores + $proc.numberofcores
}
} 
foreach($name in $process){
    $processName = $name.processName
foreach($hog in $processName){
       $cpuusage = [Math]::round(((((Get-Counter "\Process($processName)\% 
Processor Time" -MaxSamples 2).Countersamples)[0].CookedValue)/$cores),2)

        if($cpuusage -gt 14){
            Stop-Process -Name $processName
        }
}
}

I am getting the following as an error, an nothing else. I expect the Idle(0) not to work, but nothing else is being killed.

Stop-Process : Cannot stop process "Idle (0)" because of the following error: Access is denied At line:14 char:17 + Stop-Process -Name $processName + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : CloseError: (System.Diagnostics.Process (Idle):Process) [Stop-Process], ProcessCommandException + FullyQualifiedErrorId : CouldNotStopProcess,Microsoft.PowerShell.Commands.StopProcessCommand

I have tried to replace the $processName variables within the second foreach loop to $hog and I still get the same error.

After reading @JosefZ answer I got something that satisfies what I require for my class. Posting it here for reference;

$process = get-process
foreach ($pro in $process){
    $name = $pro.ProcessName
    $CpuCores = (Get-WmiObject -Class Win32_Processor).NumberOfCores
    $CpuValue = ((Get-Counter "\Process($name)\% Processor Time").CounterSamples.CookedValue)/$CpuCores
    $percent = [Decimal]::Round($CpuValue, 3)
        if($percent -ge 15){
        Stop-Process -Name $name
        $wshell = New-Object -ComObject Wscript.Shell
        $wshell.Popup("Process $name was using more than $percent % CPU. We have eliminated it.",0,"Ok",0x1)
    }
}

Upvotes: 4

Views: 9954

Answers (2)

CodedBeard
CodedBeard

Reputation: 912

Couple of additional pointers for you. Firstly you should use Win32_ComputerSystem and NumberOfLogicalProcessors instead of Win32_Processor with NumberOfCores. The reason being, that the performance counters account for HyperThreading on systems that have it, so your calculation based off the physical cores will give you a processor time value twice the size of what it should be. For example, on my machine with 6 Physical Cores, I have 12 logical processors due to HyperThreading. Using your original calculation, a process using 8% CPU would be reported as 16% and thus be incorrectly stopped. The Win32_ComputerSystem version will return all logical processors across all physical CPU's, so multi socketed servers would also be calculated correctly.

C:\WINDOWS\system32> (Get-WmiObject win32_processor).NumberofCores
6
C:\WINDOWS\system32> (Get-WmiObject win32_computersystem).Numberoflogicalprocessors
12

Second, you should be stopping the process by it's ID not by Name, as this will have unintended consequences. A simple example is Chrome, which has a process per tab, all of which have the Name Chrome. So a single tab could be having issues causing high CPU, but your script calling Stop-Process -Name Chrome would close all instances of Chrome, including the ones that aren't doing anything.

The following example script resolves both of these issues:

#Get all cores, which includes virtual cores from hyperthreading
$cores = (Get-WmiObject  Win32_ComputerSystem).NumberOfLogicalProcessors
#Get all process with there ID's, excluding processes you can't stop.
$processes = ((Get-Counter "\Process(*)\ID Process").CounterSamples).where({$_.InstanceName -notin "idle","_total","system"})
#Get cpu time for all processes
$cputime = $processes.Path.Replace("id process", "% Processor Time") | get-counter | select -ExpandProperty CounterSamples
#Get the processes with above 14% utilisation.
$highUsage = $cputime.where({[Math]::round($_.CookedValue / $cores,2) -gt 14})
# For each high usage process, grab it's process ID from the processes list, by matching on the relevant part of the path
$highUsage |%{
    $path = $_.Path
    $id = $processes.where({$_.Path -like "*$($path.Split('(')[1].Split(')')[0])*"}) | select -ExpandProperty CookedValue
    Stop-Process -Id $id -Force -ErrorAction SilentlyContinue
}

Note that the usage of the .where( syntax requires PowerShell 5 or above. This script also has the added bonus of being much faster than calling Get-Counter in a foreach loop.

Upvotes: 2

JosefZ
JosefZ

Reputation: 30113

Note that Performance counters are often protected by access control lists (ACLs). To get all available performance counters, open Windows PowerShell with the "Run as administrator" option and Your ability to stop processes depends on your permissions.

The following script was used for debugging with CPU-time consuming wmic path cim_datafile and antivirus full scan:

Set-StrictMode -Version latest
$process = get-process
$cores = 0
foreach ($proc in (Get-WmiObject  Win32_Processor)){
    if($proc.numberofcores -eq $null){
        $cores++
    }else{
        $cores = $cores + $proc.numberofcores
    }
} 
### $cores
foreach($name in $process){
    $processName = $name.processName
    if ( $processName -notmatch "^Idle" ) {
        foreach($hog in $processName){
            $cpuusage = -1
            $cpuusage = [Math]::round(((((Get-Counter "\Process($processName)\% Processor Time" -MaxSamples 2).Countersamples)[0].CookedValue)/$cores),2)
            if ($cpuusage -gt 14) {
                Stop-Process -Name $processName -PassThru -ErrorAction Continue
                ### "{0} {1} {2} {3}" -f '+', $cpuusage, $name.Id, $processName
            } else {
                if($cpuusage -ne 0){
                    ### "{0} {1} {2} {3}" -f '-', $cpuusage, $name.Id, $processName
                }
            }
        }
    }
}

Dirty solution: Stop-Process -Name $processName -ErrorAction SilentlyContinue -PassThru

"Idle" means "inactive" (not operating or being used). When the "System Idle Process" is at 100 %, that means nothing is using your CPU resources.

Read Stopping Processes (Stop-Process) at MSDN:

Windows PowerShell gives you flexibility for listing processes, but what about stopping a process?

The Stop-Process cmdlet takes a Name or Id to specify a process you want to stop. Your ability to stop processes depends on your permissions. Some processes cannot be stopped. For example, if you try to stop the idle process, you get an error:

PS> Stop-Process -Name Idle
Stop-Process : Process 'Idle (0)' cannot be stopped due to the following error:
 Access is denied
At line:1 char:13
+ Stop-Process  <<<< -Name Idle

Read Get-Help 'Stop-Process' -Online

Outputs

None, System.Diagnostics.Process

This cmdlet returns a System.Diagnostics.Process object that represents the stopped process, if you specify the PassThru parameter. Otherwise, this cmdlet does not generate any output.

Upvotes: 3

Related Questions