August 1996
Matt Pietrek is the author of Windows 95 System Programming Secrets (IDG Books, 1995). He works at NuMega Technologies Inc., and can be reached at 71774.362@compuserve.com. In my March and April 1996 columns, I described the Windows NT¨ performance data and how you can get information like process lists and memory statistics from it. As part of those columns, I devised a set of C++ classes that allow you to enumerate through the convoluted performance data structures. I just recently learned that there's a much easier way to get at some of that information; this month, I'll dig into some of the functionality in PSAPI.DLL, an SDK component that's finally getting its chance to shine with Windows NT 4.0. If you have the Windows NT 3.51 SDK, check out the PFMON sample in \win32sdk\mstools\samples\sdktools\image\winnt\pfmon. The PFMON program uses PSAPI.DLL to gather information about the working set of the process. What you don't see in the PFMON sample code is that PSAPI.DLL has functions for easily enumerating processes and modules within a process and for obtaining memory statistics for a process. In some ways, PSAPI.DLL is the Windows NT equivalent of the TOOLHELP32 functions from Windows¨ 95. Alas, the H file necessary to use all the functions in PSAPI.DLL wasn't included with the Windows NT 3.51 SDK. With Windows NT 4.0, PSAPI.DLL is used by other Win32¨ SDK components (such as APIMON.EXE) so PSAPI.DLL now has a proper H file (PSAPI.H) and is redistributable. Looking under the hood of PSAPI.DLL for a second, you'll find that it's a fairly small DLL. That's because it relies on the NtQuerySystemInformation, NtQueryInformationProcess, and NtQueryVirtualMemory functions from NTDLL.DLL to do the real work. While you could call these functions directly, they're subject to change. PSAPI.DLL At the topmost level, PSAPI has sets of functions for retrieving the following information: I'll focus on just the first four sets of functions. In a future column, I'll describe the last two. When working with processes, the first PSAPI function you're likely to use is EnumProcesses. This function fills in an array of DWORDs (supplied by you) with the IDs of all the processes in the system. Since you probably don't know ahead of time how many processes there are, it's a good idea to give EnumProcesses a large array of DWORDs. Afterwards, you can use the cbNeeded field to tell how many entries in the array were actually filled (for example, cbNeeded / sizeof(DWORD)). Frankly, a program that prints out process IDs ("2 4 7 11 16...") isn't going to make anybody swoon. Some effort on your part is needed to take those process IDs and make something useful out of them. Your secret weapon is the OpenProcess function, which takes a process ID and returns a process handle. With a process handle, you're ready to party! In fact, many PSAPI functions require process handles. One slightly messy part of getting process handles is deciding what sort of access you need. On my first attempt to use PSAPI, I tried to use PROCESS_ALL_ACCESS, which is every possible flag with every possible access right. While this worked on some processes, OpenProcess failed on some of the critical system processes, such as CSRSS. Trying the opposite approach, I tried passing a minimal PROCESS_QUERY_INFORMATION value to OpenProcess. While that allowed me to open additional process handles, some of the PSAPI functions were unable to work with just those access rights. What finally worked for the largest number of processes were PROCESS_QUERY_INFORMATION and PROCESS_VM_READ. This is because the Windows NT operating system allows a user-level program to use those values for almost every process on the system. Remember, call CloseHandle with your process handles when you're done with them. Once you get a process handle, what can PSAPI do for you? For starters, EnumProcessModules will give you a list of the HMODULEs in a specified process. Like the EnumProcesses function, EnumProcessModules fills an array for you and sets the cbNeeded output parameter to indicate how many HMODULEs it placed into the array. While a list of HMODULEs is a good start, you're still not out of the woods yet. Remember, these HMODULEs are most likely from some process other than you, so you can't use these HMODULEs with functions like GetModuleFileName. While a masochistic programmer could use ReadProcessMemory to find out about an out-of-process HMODULE, there's really no need to do this. GetModuleBaseName takes a process handle and an HMODULE and fills in a buffer with the base name of a module (like "KERNEL32.DLL"). A related function, GetModuleFileNameEx, takes the same parameters but returns the full path to the module (for example, "C:\WINNT\SYSTEM32\KERNEL32.DLL"). As with just about any Win32 function that works with strings, there are both ANSI and UNICODE versions of GetModuleBaseName and GetModuleFileNameEx. The absence or presence of #define UNICODE determines which version is used. The final module-related function in PSAPI.DLL is GetModuleInformation, which takes a process handle and HMODULE and fills in a MODULEINFO structure. The MODULEINFO structure contains the base load address of the module (lpBaseOfDll), the amount of linear address space it takes up (SizeOfImage), and the address of its entry point (EntryPoint). A module's entry point is the location called during process and thread startup and shutdown. While not exactly the same as the DllMain function, it's a close enough approximation for most people. GetModuleInformation doesn't do anything difficult; the load address of a module is the same as the HMODULE, and the SizeOfImage and EntryPoint information comes straight out of a module's Portable Executable (PE) header in memory. In some ways, device drivers and program modules (DLLs and EXEs) are similar in that they're both based on PE files. However, while each process has its own private list of loaded modules, device drivers are global. Thus, PSAPI.DLL has separate functions for grabbing the list of device drivers and obtaining the names of the drivers. EnumDeviceDrivers is similar to the EnumProcesses function except that it returns a list of load addresses for each device driver instead of filling in an array of process IDs. Also, as with EnumProcesses, you rely on the cbNeeded parameter to determine how many device drivers it reported on. The parallels between the process and device driver functions continue with the driver name functions. GetDeviceDriverBaseName takes a driver load address as input and fills in a buffer with the base name of the driver (for example, "WIN32K.SYS"). GetDeviceDriverFileName returns the full path to the device driver's file (for example,"C:\WINNT\SYSTEM32\WIN32K.SYS"). As you'd expect, there are both ANSI and UNICODE versions of these functions. The final function is GetProcessMemoryInfo, which takes a process handle as input and fills in a structure with information about the process's memory statistics. This structure, defined in PSAPI.H, is called PROCESS_MEMORY_COUNTERS. The first structure member (cb) is filled in with the size of the structure. The meaning of the second structure member (PageFaultCount) is fairly obvious. The remaining fields in the PROCESS_MEMORY_COUNTERS struct are the current and peak consumption of memory in the following categories: working set, paged pool, nonpaged pool, and pagefile. The working set is the amount of memory physically mapped into the process context at a given time. The paged and nonpaged pools are system memory areas. Memory in the paged pool can be paged to disk as necessary, while nonpaged memory will always be present. The pagefile usage represents how much memory is set aside in the system swapfile for the process; it essentially represents how much memory has been committed by the process. However, committing memory is not the same as making it physically present. I will cover the working set capabilities in detail in a future column. To show how to use some of the PSAPI functions, I wrote PSAPIDEM (see Figure 1). As you can see from Figure 2, the main window of PSAPIDEM is a dialog box with two tree controls and a couple of buttons. (I figured it was time to break down and learn how to use tree controls!) The upper tree control lists all of the processes in the system along with the process ID and name (if available). The lower tree view contains a list of the running device drivers. The Refresh button forces an update on both tree views, while the About and Exit buttons should be self-explanatory. Figure 2 PSAPIDEM In the process tree view, the UpdateProcessList function obtains a list of processes with the EnumProcesses function. For each process, the code calls AddProcessToList, passing the process ID. AddProcessToList in turn calls OpenProcess for the process ID; if there's a name next to the process ID, AddProcessToList obtained that value by calling EnumProcessModules to get just the first HMODULE for the process. PSAPIDEM takes this first HMODULE and passes it to GetModuleBaseName to get the process name. If OpenProcess failed, the tree view shows just the process ID. On my machine, the only two processes that I can't open are Idle and CSRSS. These processes have access restrictions so user-level code can't open them. If there's a + button next to a process name, you can click it to expand that node out into module, memory, and timing information for the selected process. The information under the modules node is obtained by calling EnumProcessModules (again), this time grabbing the entire list of HMODULEs. PSAPIDEM passes each of those modules in turn to GetModuleFileNameEx. The information under the memory node of a process is simply a listing of all the values in a PROCESS_MEMORY_COUNTERS structure. You get this information with a call to GetProcessMemoryInfo. The final piece of process-related information PSAPIDEM shows is the total time spent in user and kernel modes. To get this information I used GetProcessTimes, which is a KERNEL32 function, not a PSAPI.DLL function. I figured, what the heck, I've already got a process handle open so why not show everything I can. This code is complicated by the fact that the information is returned in a FILETIME structure, which is two DWORDs expressing time in units of a hundred nanoseconds. A bit of contorted code converts these values into something more reasonable, like milli-seconds. To populate the bottom tree-view control, the UpdateDriverList function first calls EnumDeviceDrivers to obtain an array of load addresses for all the running drivers. Next, the code iterates through each driver, calling GetDeviceDriverBaseName, and puts the results at the root of the tree view. For each device driver, the code also calls GetDeviceDriverFileName and puts the resulting string under the appropriate driver node in the tree. This wraps up my whirlwind tour of PSAPI functions. These functions provide a quick and easy way under Windows NT to get system-level information such as process, module, and device driver lists. There are other functions in PSAPI that I haven't touched upon, mainly the working set-related functions. In a future column I'll go over what these other functions can do for you. Have a question about programming in Windows? Send it to Matt at 71774.362@compuserve.com
is intended to be the standard way for getting at this information. Process Information
Device Driver Information
Process Memory Usage
The PSAPIDEM Program
Conclusion