Prevent CPU-hogging processes from slowing you down
Every month, I run a customized Perl script that reads in a bunch of Web server log files and tallies up Web service statistics. The script works well, with one exception: It's a processor hog. The script has to read in hundreds of megabytes of data, so it takes more than 1 hour to finish. And while I run the script, the perl.exe process consumes approximately 80 percent of my CPU processing time, making other work at the same machine agonizingly slow. I thought, "There has to be a better way to do this!" As it turns out, there is: I modified the script to run at a lower priority. Now, the OS gives other running applications more processing time than it gives the Perl script. The script takes a bit longer to run—but using the machine during that time is no longer painful.
Application Priority Classes
Today's multitasking OSs simultaneously run multiple applications by granting each process a certain amount of time to run, then cycling through the processes. The OS uses a process's priority class to determine the amount of CPU time that each processor receives. Windows 2000 and later recognizes six process priority classes:
- Idle—Processes in the Idle class (aka the Low priority class) run only when no other processes are consuming processor time. A process in this class, which grants the lowest possible priority, will surrender processing time whenever another process requires it. Screen savers run in the Idle class.
- BelowNormal—Processes in the BelowNormal class get more time than those in the Idle class but less time than those in the Normal class.
- Normal—The default class for most processes is the Normal class.
- AboveNormal—Processes in the AboveNormal class get more time than those in the Normal class but less time than processes in the High class.
- High—Processes in the High class are expected to respond very quickly, even when the system is under high load. The OS allocates considerable amounts of processor time to an application running processes in this class, even at the expense of other applications.
- Realtime—Using the Realtime class requires extreme caution because this class is designed to run only for very brief periods. The OS dedicates so much CPU time to any application running in this priority class that other processes become starved for CPU time. Therefore, components such as device drivers (e.g., mouse, keyboard, NIC) can be negatively affected. This class typically is used only by device drivers that need intensive processor time for a very short duration (e.g., when reading data coming in over a USB port).
To discover the priority class that an application or service is running under, open Task Manager and select the Processes tab, which displays a list of running processes. Right-click the process you're interested in and select Set Priority from the context menu. In the submenu that appears, you'll see a mark next to the process's current class. You can change the process's priority class by selecting a different class from the list—if you have the appropriate permissions.
A Matter of Permission
Any user can change the process priority of an application that he or she started. For example, when you open Microsoft Excel, you can change its priority class. However, you can only change the priority class of a process that you own (i.e., that's running under your user account), so you can't change the priority class of a copy of Excel that someone else started (e.g., in another terminal session, as a hot-switched logon session, by running runas.exe).
Furthermore, you might be limited regarding which priority class you can switch a process to. In particular, most users can't change a process's priority class to Realtime; only users who have the Increase Scheduling Priority user right can make this change. By default, only Administrators and Power Users have that right enabled. If you're an administrator who needs to assign this right to yourself or another user, you can use the Local Security Policy console (available under Administrative Tools) to do so. Keep in mind, however, that you should avoid assigning this class to applications because of the potential negative performance effect on other programs and services.
Adding Priority Support to Perl Scripts
Changing the priority class that an application runs under can help you manage your machine's processing resources. An even more useful tactic is to put code into a script to modify the script's priority. For example, you might want a scheduled Perl script to run every day but to run in the background so it doesn't interfere with other work on the same system.
You can easily add the code that Listing 1 shows to a Perl script to manage the priority of the process that's running the script. The nice feature about this setup is that any script that contains this code will always have the appropriate permissions to modify its own priority because the process it needs to modify—the process that's running the script—will always be running under the same user account as the script.
First, the code at callout A in Listing 1 loads the Win32::API::Prototype module. Win32 API comes standard with ActiveState's ActivePerl, but the module doesn't. To use the Perl Package Manager (PPM) to install the module, use the following command:
The code at callout B begins by defining the priority class that the script will use. When you run the script, you can pass in any valid priority class name that's listed in the %PRIORITIES hash, which defines the priority classes and their numeric values. (I'll explain how to pass in the priority later, when I give an example of a script that uses the code in Listing 1.) If you don't specify a priority class, the script will use the Low class. The code then defines the %PRIORITIES hash.
The code at callout C is the task engine. This code first does all the necessary work to load the various libraries and to import required functions, then calls the GetCurrentProcess() function. This Win32 function returns a process handle to the current process (i.e., the process that's running the script). Typically, this process will be perl.exe, but it could be Apache, Microsoft IIS, a custom application that embeds the Perl engine, or an application that uses the Windows Script Host (WSH) Perl engine.
The call to the GetCurrentProcess() function doesn't return a true process handle; it returns a pseudo handle. This pseudo handle functions as a real handle would, but its value is predictable and it doesn't need to be closed. The code in Listing 1 treats the pseudo handle as if it were real, passing it into the Win32 SetPriorityClass() function, along with the priority class value that the script is setting. Then, even though doing so isn't technically necessary, the script uses the CloseHandle() function to close the pseudo handle. (Closing handles is simply a good habit to get into.)
The code at callout D loops until you terminate the script. This code, which prints out an increasing numeric value, provides a way for you to see how different priority classes affect the speed of the script. It also gives you time to open Task Manager and verify that the priority class has changed. To end the script, you can either press Ctrl+C or Ctrl+Break, or you can use Task Manager to terminate the process that's running the script.
Changing Process Priorities
Listing 2 shows an excerpt of the priority.p1 script, which does effectively the same thing as the code in Listing 1, except that priority.pl changes the priority class of processes other than its own. This capability is extremely handy for taming running processes that are taking too much CPU time. This task is a bit more complicated than changing the priority class for the current process. (Readers who have a UNIX background might notice that the code in priority.pl is similar to the Nice and Renice commands.) You can use this script either to set the priority class for any number of processes or simply to query processes for their current priority class. To identify the processes that you want the script to set or query, you'll need to specify each process's process identifier (PID). You can use Task Manager (or a tool such as the modules.pl script at http://www.roth.net/perl/scripts) to find this PID. I'll show you how to pass in PIDs and how to specify a priority class for those processes after I walk you through the script.
The code at callout A in Listing 2 starts as you might expect, loading the Win32::API::Prototype module and defining the %PRIORITIES hash. However, the code's next step is to define the %PRIORITY_CLASS hash, which is simply a reverse of the %PRIORITIES hash. Whereas %PRIORITIES uses a priority name as a key to look up that priority's value, the %PRIORITY_CLASS hash uses a priority value as a key to look up the corresponding priority name. This hash is useful later, when the script needs to display the name of a class according to a value.
The last two lines of code at callout A define the $PROCESS_SET_INFORMATION and $PROCESS_QUERY_INFORMATION variables, which the script uses later when it attempts to open a specified process. Unlike the Listing 1 code, which receives a process handle to itself, this script must attempt to open another process. The pseudo handle that the script in Listing 1 uses as a return value from the GetCurrentProcess() function provides all the rights and privileges necessary to change the priority class of the process that's running the script. But when a script attempts to change the priority of another process, as priority.pl does, it must set information rights and request the query information.
The code at callout B in Listing 2 loads the required DLLs and imports the functions that the script will call. The code at callout C iterates through each PID that the user has passed to the script. For each PID, the script attempts to query or set that process's priority class. If the user specified a priority class for the process, the code tries to set that priority. To do so, the script uses the %PRIORITIES hash to look up the value of the specified priority class, then passes that value, along with the PID, into the SetPriority() subroutine.
The script then calls the GetPriority() function to display the current priority class for the process so that the user can verify that the priority change occurred. If the user didn't specify a priority class for a PID, the script still calls the GetPriority function and displays the current priority class.
The code at callout D defines the SetPriority() subroutine. First, the script calls the OpenProcess() function, passing in the $PROCESS_SET_INFORMATION flag and the PID. Passing this flag into the OpenProcess() function attempts to open the process in such a way that the script can alter process information (e.g., priority class). When the function call is successful, the returned value is an opened process handle, which the script then passes to the SetPriorityClass() function. The script then closes the process handle.
The code at callout E is much like the code at callout D. However, the code at callout E calls the GetPriorityClass() function instead of the SetPriorityClass() function and passes the $PROCESS_QUERY_INFORMATION flag (rather than the $PROCESS_SET_INFORMATION flag) into the OpenProcess() function.
Passing In PIDs and Priorities
If you want to use priority.pl to discover a process's current priority class, run the following command on the local system:
perl priority.pl PID
where PID is the process's PID (e.g., 532). To query more than one process, pass in multiple PIDS separated by a space, as in
perl priority.pl 532 1112 8832
To set a new priority for the specified PID, add the -s option and the priority class:
perl priority.pl -s low PID
To set the priority for more than one process, pass in multiple PIDS, separated by a space. For example, the command
perl priority.pl -s low 532 1112 8832
sets the Low priority class for the processes that have PIDs 532, 1112, and 8832. You can specify only one class, which will go into effect for all the specified PIDs.
Rule Your Scripts
While writing this column, I used priority.pl to tame some mischievous processes. I was using Microsoft Windows Media Encoder to encode a video into Microsoft Windows Media Format, and the process was taking up too much CPU time. After I ran priority.pl and passed in the -s low parameter and the offending application's PID, the encoder slid into the background, where it no longer affected my work.
This type of coding is one of the most powerful and useful techniques you can use to manage your machines, especially in any script you write to work as a background task or Win32 service. Setting the right priority class makes a script behave and can prevent a world of frustration for you and other users. You can download the code in Listing 1 and priority.pl (in its entirety) from the Windows Scripting Solutions Web site. Go to http://www.winnetmag.com/windowsscripting, enter InstantDoc ID 42375 in the InstantDoc ID box, then click the 42375.zip hotlink.