Putting the Pieces Together
The remaining stuff needed to make an application out of the above code snippets is more or less boilerplate code. Once you have retrieved the SYSTEM_PROCESS_INFORMATION data, you can choose any output format that fits your needs. To keep things simple, I have written a simple Win32 console-mode application called w2k_pnt.exe that displays processes and threads in a console window. By the way, the suffix "pnt" stands for "Processes'n'Threads". Because the output lists can be rather lengthy, I have added a filter mechanism to match the process names against an optional pattern string that may contain an arbitrary number of the wildcard characters '*' and '?'. Note that the first process started by the systemthe so-called Idle processhas no name. The usName member of its process structure contains a NULL pointer. The Windows 2000 Task Manager displays it as System Idle Process, whereas my demo application simply says Idle.
In Example 1, I have issued the command w2k_pnt/p/treal*, which lists all processes whose names start with "real", as well as the threads these processes are hosting. Obviously, the output list comprises the RealPlayer and the RealJukebox applications by RealNetworks, Inc. As you can see, I just love to listen to MP3s during work!
Example 1: Sample Output of w2k_pnt.exe
D:\Program Files\DevStudio\MyProjects\w2k_pnt\Release>w2k_pnt /p/t real* // w2k_pnt.exe // Windows NT/2000 Processes'n'Threads V1.00 // 06-18-2001 Sven B. Schreiber // sbs@orgon.com Windows 2000 mode PROCESS INFO (Filter: 'real*') ============================== PID PPID KBytes PFaults Handles Threads Date & Time Name ------------------------------------------------------------------------------- 980 868 3724 2793 94 8 06-18 18:07 realplay.exe 308 868 13344 6724 166 15 06-18 19:25 realjbox.exe ------------------------------------------------------------------------------- 2 processes, 23 threads THREAD INFO (Filter: 'real*') ============================= PROCESS #980 (realplay.exe) TID Prio Base Switches Address State Date & Time Kernel(sec) User(sec) ------------------------------------------------------------------------------- 968 9 8 926597 77E878C1 5/13 06-18 18:07 8.191 3.595 1288 10 8 2 77E92C50 5/13 06-18 18:07 0.000 0.000 1300 15 15 100 77E92C50 5/06 06-18 18:07 0.000 0.000 1304 12 10 2 77E92C50 5/06 06-18 18:07 0.000 0.000 1308 10 8 239 77E92C50 5/13 06-18 18:07 0.000 0.000 1312 9 8 917053 77E92C50 5/13 06-18 18:07 0.320 0.270 1060 10 8 2 77E92C50 5/06 06-18 18:07 0.000 0.000 1320 9 8 917065 77E92C50 5/13 06-18 18:07 0.390 0.320 ------------------------------------------------------------------------------- 8 threads 8.901 4.185 PROCESS #308 (realjbox.exe) TID Prio Base Switches Address State Date & Time Kernel(sec) User(sec) ------------------------------------------------------------------------------- 280 9 8 1586897 77E878C1 5/13 06-18 19:25 61.598 47.117 716 8 8 1 77E92C50 5/06 06-18 19:25 0.000 0.000 1336 8 8 1 77E92C50 5/06 06-18 19:25 0.000 0.000 1296 15 15 123 77E92C50 5/06 06-18 19:25 0.000 0.000 524 12 10 132 77E92C50 5/13 06-18 19:25 0.030 0.010 404 8 8 1 77E92C50 5/13 06-18 19:25 0.000 0.000 768 10 10 1 77E92C50 5/06 06-18 19:25 0.000 0.000 1264 10 8 193 77E92C50 5/13 06-18 19:25 0.000 0.000 356 9 8 866495 77E92C50 5/13 06-18 19:25 19.938 180.709 1056 6 6 6872 77E92C50 5/06 06-18 19:25 0.040 0.000 860 8 8 2 77E92C50 5/06 06-18 19:25 0.010 0.000 848 10 8 143867 77E92C50 5/06 06-18 19:25 0.690 0.260 1020 8 8 1 77E92C50 5/13 06-18 19:25 0.000 0.000 780 10 8 114796 77E92C50 5/13 06-18 19:25 0.480 0.360 1332 8 8 2 77E92C50 5/06 06-18 19:25 0.000 0.000 ------------------------------------------------------------------------------- 15 threads 82.786 228.456 TOTAL: 2 processes, 23 threads
Listing 6 shows how w2k_pnt.exe walks down the list of processes, displaying some of the most useful members in a table. Please compare this data to the process view of the Windows 2000 Task Managerit should match very closely. Note how the psp pointer is moved forward by adding the value of the dNext member of the SYSTEM_PROCESS structure. The end of the list is reached if dNext is zero.
Listing 6 Enumerating Processes
VOID DisplayProcesses (PSYSTEM_PROCESS pspFirst, PWORD pwFilter) { DWORD dProcesses = 0; DWORD dThreads = 0; PSYSTEM_PROCESS psp = pspFirst; WORD awTime [] = L"mm-dd HH:MM"; PFILETIME pft; FILETIME ft; SYSTEMTIME st; PWORD pwName; printf (L"\r\n" L" PID PPID KBytes PFaults Handles Threads " L"Date & Time Name\r\n" L"----------------------------------------" L"---------------------------------------\r\n"); while (psp != NULL) { pwName = (psp->dUniqueProcessId ? psp->usName.Buffer : L"Idle"); if (PatternMatcher (pwFilter, pwName, TRUE)) { lstrcpy (awTime, L" ? ? "); pft = &psp->ftCreateTime; if ((pft->dwLowDateTime || pft->dwHighDateTime) && FileTimeToLocalFileTime (pft, &ft) && FileTimeToSystemTime (&ft, &st)) { sprintf (awTime, L"%02hu-%02hu %02hu:%02hu", st.wMonth, st.wDay, st.wHour, st.wMinute); } printf (L"%5lu %5lu %7lu %7lu %7lu %7lu " L"%-11s %s\r\n", psp->dUniqueProcessId, psp->dInheritedFromUniqueProcessId, psp->VmCounters.WorkingSetSize / 1024, psp->VmCounters.PageFaultCount, psp->dHandleCount, psp->dThreadCount, awTime, pwName); dProcesses += 1; dThreads += psp->dThreadCount; } psp = (psp->dNext ? (PVOID) ((PBYTE) psp + psp->dNext) : NULL); } printf (L"%s----------------------------------------" L"---------------------------------------\r\n" L"%5lu process%s, %lu thread%s\r\n", (dProcesses ? L"" : L"+++ No matching names +++\r\n"), dProcesses, (dProcesses == 1 ? L"" : L"es"), dThreads, (dThreads == 1 ? L"" : L"s" )); return; }
Once you have a pointer to the thread array of a process, walking down the list of threads is quite easy because the thread structures are of fixed size. Listing 7 shows the implementation used by w2k_pnt.exe. The only problem with the thread array is that it is located at a different offset within the process structures of Windows NT 4.0 and Windows 2000. Therefore, DisplayThreads() cannot be called safely unless you know which operating system your application is running on.
Listing 7 Enumerating Threads
VOID DisplayThreads (PSYSTEM_THREAD pstFirst, DWORD dThreads) { PSYSTEM_THREAD pst = pstFirst; WORD awTime [] = L"mm-dd HH:MM"; DWORDLONG dlTotalKernel = 0; DWORDLONG dlTotalUser = 0; DWORDLONG dlKernel, dlUser; PFILETIME pft; FILETIME ft; SYSTEMTIME st; DWORD i; printf (L"\r\n" L" TID Prio Base Switches Address State " L"Date & Time Kernel(sec) User(sec)\r\n" L"----------------------------------------" L"---------------------------------------\r\n"); for (i = 0; i < dThreads; i++) { lstrcpy (awTime, L" ? ? "); pft = &pst->ftCreateTime; if ((pft->dwLowDateTime || pft->dwHighDateTime) && FileTimeToLocalFileTime (pft, &ft) && FileTimeToSystemTime (&ft, &st)) { sprintf (awTime, L"%02hu-%02hu %02hu:%02hu", st.wMonth, st.wDay, st.wHour, st.wMinute); } dlKernel = pst->ftKernelTime.dwHighDateTime; dlKernel <<= 32; dlKernel += pst->ftKernelTime.dwLowDateTime; dlKernel /= 10000; dlUser = pst->ftUserTime.dwHighDateTime; dlUser <<= 32; dlUser += pst->ftUserTime.dwLowDateTime; dlUser /= 10000; dlTotalKernel += dlKernel; dlTotalUser += dlUser; printf (L"%5lu %4lu %4lu %10lu %08lX %2lu/%02lu " L"%-11s %8lu.%03lu %6lu.%03lu\r\n", pst->Cid.UniqueThread, pst->dPriority, pst->dBasePriority, pst->dContextSwitches, pst->pStartAddress, pst->dThreadState, pst->WaitReason, awTime, (DWORD) dlKernel / 1000, (DWORD) dlKernel % 1000, (DWORD) dlUser / 1000, (DWORD) dlUser % 1000); pst++; } printf (L"----------------------------------------" L"---------------------------------------\r\n" L"%5lu thread%s" L" " L"%8lu.%03lu %6lu.%03lu\r\n", dThreads, (dThreads == 1 ? L" " : L"s"), (DWORD) dlTotalKernel / 1000, (DWORD) dlTotalKernel % 1000, (DWORD) dlTotalUser / 1000, (DWORD) dlTotalUser % 1000); return; }
If you choose your application to be compatible to both Windows 2000 and NT 4.0, an operating system check is inevitable. The SystemVersion() function defined in Listing 8 does this job by calling GetVersionEx() and testing for platform VER_PLATFORM_WIN32_NT. SystemVersion() returns the major version number only; that is, 4 for Windows NT 4.0 or 5 for Windows 2000. The minor version number is irrelevant here.
Listing 8 Identifying the Operating System Version
DWORD SystemVersion (VOID) { OSVERSIONINFO ovi; DWORD dVersion = 0; ovi.dwOSVersionInfoSize = sizeof (OSVERSIONINFO); if (GetVersionEx (&ovi) && (ovi.dwPlatformId == VER_PLATFORM_WIN32_NT)) { dVersion = ovi.dwMajorVersion; } return dVersion; }