Сообщений 12    Оценка 512        Оценить  
Система Orphus

Как получить список запущенных процессов?

Автор: Александр Федотов
Опубликовано: 23.10.2001
Исправлено: 13.03.2005
Версия текста: 1.1

Тестовое приложение Process Viewer

Введение

Win32 предоставляет несколько способов перечисления запущенных процессов. К сожалению, нет единого способа, который бы работал на всех Win32-платформах. Программистам приходится комбинировать несколько методов в одной программе, чтобы она работала на всех версиях Windows.

Мы рассмотрим следующие методы:

Каждый метод мы будем иллюстрировать с помощью функции, имеющей следующий прототип:

// эта функция вызывается для каждого процесса в перечислении
typedef BOOL (CALLBACK * PFNENUMPROC)(
	IN DWORD dwProcessId,	    // идентификатор процесса
	IN LPCTSTR pszProcessName,  // имя процесса; NULL, если недоступно
	IN LPARAM lParam	    // пользовательский параметр
	);

// функция перечисления процессов
BOOL WINAPI EnumProcesses_Method(
        IN LPCTSTR pszMachineName,  // имя компьютера
	IN PFNENUMPROC pfnEnumProc, // пользовательская функция
	IN LPARAM lParam	    // пользовательский параметр
	);

Функция перечисления вызывает для каждого процесса указанную пользовательскую функцию, передавая ей информацию об очередном процессе. Пользователькая функция распоряжается этими данными в соответствии с логикой программы, например, в тестовом приложении для этой статьи, она добавляет очередной элемент в список. Некоторые из рассматриваемых нами методов позволяют перечислить процессы на других компьютеров, поэтому прототип функции содержит параметр pszMachineName, позволяющий задать имя компьютера. Значение NULL для этого параметра означает перечисление процессов на локальном компьютере.

Способ 1. Использование библиотеки Process Status Helper

Библиотека Process Status Helper, известная также под названием PSAPI, предоставляет набор функций, позволяющих получить информацию о процессах и драйверах устройств. Библиотека поставляется в составе Windows 2000/XP и доступна в качестве устанавливаемой компоненты для Windows NT 4.0.

Для перечисления процессов библиотека предоставляет функцию EnumProcesses, которая возвращает массив идентификаторов запущенных процессов. Ниже приведен текст функции EnumProcesses_PsApi, реализующей перечисление процессов с помощью PSAPI.

#include <psapi.h>

BOOL
WINAPI
EnumProcesses_PsApi(
    IN LPCTSTR pszMachineName,
    IN PFNENUMPROC pfnEnumProc,
    IN LPARAM lParam
    )
{
    _ASSERTE(pfnEnumProc != NULL);

    if (pszMachineName != NULL)
	return SetLastError(ERROR_INVALID_PARAMETER), FALSE;

    HINSTANCE hPsApi;
    BOOL (WINAPI * _EnumProcesses)(DWORD *, DWORD, DWORD *);
    BOOL (WINAPI * _EnumProcessModules)(HANDLE, HMODULE *, DWORD, DWORD *);
    BOOL (WINAPI * _GetModuleFileNameEx)(HANDLE, HMODULE, LPTSTR, DWORD);
    
    // загружаем PSAPI.DLL
    hPsApi = LoadLibrary(_T("psapi.dll"));
    if (hPsApi == NULL)
        return FALSE;

    // находим нужные функции в PSAPI.DLL
    *(FARPROC *)&_EnumProcesses = 
        GetProcAddress(hPsApi, "EnumProcesses");
    *(FARPROC *)&_EnumProcessModules =
        GetProcAddress(hPsApi, "EnumProcessModules");
#ifdef UNICODE
    *(FARPROC *)&_GetModuleFileNameEx =
        GetProcAddress(hPsApi, "GetModuleFileNameExW");
#else
    *(FARPROC *)&_GetModuleFileNameEx =
        GetProcAddress(hPsApi, "GetModuleFileNameExA");
#endif

    if (_EnumProcesses == NULL ||
        _EnumProcessModules == NULL ||
        _GetModuleFileNameEx == NULL)
    {
        FreeLibrary(hPsApi);
        return SetLastError(ERROR_PROC_NOT_FOUND), FALSE;
    }

    // получаем описатель кучи процесса по умолчанию
    HANDLE hHeap = GetProcessHeap();

    DWORD dwError;
    DWORD cbReturned;
    DWORD cbAlloc = 128;
    DWORD * pdwIds = NULL;

    OSVERSIONINFO osvi;
    osvi.dwOSVersionInfoSize = sizeof(osvi);
    GetVersionEx(&osvi);

    DWORD dwSystemId = (osvi.dwMajorVersion < 5) ? 2 : 8;
    
    do
    {
        cbAlloc *= 2;

        if (pdwIds != NULL)
            HeapFree(hHeap, 0, pdwIds);

        // выделяем память для массива идентификаторов
        pdwIds = (DWORD *)HeapAlloc(hHeap, 0, cbAlloc);
        if (pdwIds == NULL)
        {
            FreeLibrary(hPsApi);
            return SetLastError(ERROR_NOT_ENOUGH_MEMORY), FALSE;
        }

        // получаем идентификаторы процессов
        if (!_EnumProcesses(pdwIds, cbAlloc, &cbReturned))
        {
            dwError = GetLastError();

            HeapFree(hHeap, 0, pdwIds);
            FreeLibrary(hPsApi);
            return SetLastError(dwError), FALSE;
        }
    }
    while (cbReturned == cbAlloc);

    // теперь перечисляем все идентификаторы в массиве и вызываем
    // пользовательскую функцию для каждого процесса
    for (DWORD i = 0; i < cbReturned / sizeof(DWORD); i++)
    {
        BOOL bContinue;
        DWORD dwProcessId = pdwIds[i];

        // обрабатываем два специальных случая: процесс Idle (0) и
	// процесс System (8)
        if (dwProcessId == 0)
        {
            bContinue = pfnEnumProc(0, _T("Idle"), lParam);
        }
        else if (dwProcessId == dwSystemId)
        {
            bContinue = pfnEnumProc(dwSystemId, _T("System"), lParam);
        }
        else
        {
            HANDLE hProcess;
            HMODULE hExeModule;
            DWORD cbNeeded;
            TCHAR szModulePath[MAX_PATH];
            LPTSTR pszProcessName = NULL;

            // открываем описатель процесса
            hProcess = OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_READ,
                                   FALSE, dwProcessId);
            if (hProcess != NULL)
            {
                if (_EnumProcessModules(hProcess, &hExeModule,
                                        sizeof(HMODULE), &cbNeeded))
                {
                    if (_GetModuleFileNameEx(hProcess, hExeModule,
                                             szModulePath, MAX_PATH))
                    {
                        pszProcessName = _tcsrchr(szModulePath, _T('\\'));
                        if (pszProcessName == NULL)
                            pszProcessName = szModulePath;
                        else
                            pszProcessName++;
                    }
                }
            }

            CloseHandle(hProcess);

            // вызываем пользовательскую функцию
            bContinue = pfnEnumProc(dwProcessId, pszProcessName, lParam);
        }

        if (!bContinue)
            break;
    }

    HeapFree(hHeap, 0, pdwIds);
    FreeLibrary(hPsApi);

    return TRUE;
}

Обратите внимание, что мы связываемся с PSAPI.DLL динамически, загружая библиотеку с помощью LoadLibrary и получая адреса необходимых функций посредством GetProcAddress. Это позволит нам в дальнейшем включить эту функцию в программу, которая должна выполняться в том числе и на Windows 9x/Me, где PSAPI.DLL отстутствует. При реализации других способов перечисления процессов мы поступаем таким же образом.

Функция EnumProcesses не позволяет узнать, сколько места нужно для того, чтобы принять весь массив идентификаторов. Поэтому мы вызываем ее в цикле, увеличивая размер буфера, до тех пор, пока размер возвращенного массива не будет меньше размера буфера.

Поскольку мы хотим помимо идентификаторов процессов получить и имена процессов, мы должны проделать дополнительную работу. Для каждого процесса мы сначала получаем его описатель (handle) c помощью функции OpenProcess и затем используем функцию ЕnumProcessModules, которая возвращает список модулей, загруженных в адресное пространство процесса. Первым модулем в списке всегда является модуль, соответсвующий EXE-файлу программы. Наконец, мы вызываем функцию GetModuleFileNameEx (которая также является частью PSAPI), чтобы получить путь к EXE-файлу по описателю модуля. Мы используем имя EXE-файла без пути в качестве имени процесса.

Oбратите внимание на специальную обработку для двух процессов. Мы вынуждены обрабатывать отдельно процесс бездействия системы (Idle) с идентификатором 0, и системный процесс (System), который имеет идентификатор 2 на Windows NT 4 и 8 - на Windows 2000/XP, потому что OpenProcess не позволяет открыть описатель для этих процессов, возвращая код ошибки ERROR_ACCESS_DENIED.

Если вы запустите тестовое приложение, прилагающееся к этой статье, то обнаружите, что для одного из процессов в качестве имени возвращается "(name unavailable)". Запустив Task Manager нетрудно определить, что этим процессом является CSRSS.EXE. Оказывается, система устанавливает такой дескиптор безопасности этому процессу, который не позволяет открыть его с необходимыми нам правами доступа.

С одной стороны, мы могли бы поступить с этим процессом так же, как с процессами Idle и System, однако нет уверенности, что идентификатор этого процесса является постоянным. С другой стороны, проблему можно решить, включив (enable) привилегию SE_DEBUG_NAME. Когда включена эта привилегия, вызывающий поток может открывать процессы с любыми флагами доступа, независимо от того, какой дескриптор безопасности назначен процессу. Поскольку эта привилегия открывет возможности по преодолению системы безопасности Windows NT, ее имеют лишь администраторы компьютера, и мы не стали добавлять код для включения привилегии непосредственно в функцию EnumProcesses_PsApi. Если необходимо, то привилегия может быть включена перед вызовом EnumProcesses_PsApi, и тогда функция вернет имя и для процесса CSRSS.EXE.

Способ 2. Использование ToolHelp32 API

Корпорация Microsoft добавила набор функций под названием ToolHelp API в Windows 3.1, чтобы позволить сторонним разработчикам получить доступ к системной информации, которая ранее была доступна только программистам Microsoft. При создании Windows 95, эти функции перекочевали в новую систему под названием ToolHelp32 API. Операционная система Windows NT c cамого создания содержала средства для получения подобной информации под названием "данные производительности". Интерфейс для доступа к данным производительности был крайне запутанным и неудобным (справедливости ради надо отметить, что начиная с Windows NT 4.0, Microsoft предоставляет библиотеку Performance Data Helper, значительно облегчающую получение данных производительности; мы воспользуемся этой библиотекой при реализации соответствующего метода перечисления процессов). Говорят, команда Windows NT долгое время сопротивлялась включению ToolHelp32 API в систему, тем не менее, начиная с Windows 2000, ToolHelp32 API присутствует и в этой операционной системе.

Используя ToolHelp32 API, мы сначала создаем моментальный снимок (snapshot) списка процессов с помощью функции CreateToolhelp32Snapshot, а затем проходим по списку используя функции Process32First и Process32Next. Структура PROCESSENTRY32, заполняемая этими функциями, содержит всю необходимую информацию. Ниже приведен код функции EnumProcesses_ToolHelp, реализующей перечисление процессов с помощью ToolHelp32.

#include <tlhelp32.h>

BOOL
WINAPI
EnumProcesses_ToolHelp(
    IN LPCTSTR pszMachineName,
    IN PFNENUMPROC pfnEnumProc,
    IN LPARAM lParam
    )
{
    _ASSERTE(pfnEnumProc != NULL);

    if (pszMachineName != NULL)
	return SetLastError(ERROR_INVALID_PARAMETER), FALSE;

    HINSTANCE hKernel;
    HANDLE (WINAPI * _CreateToolhelp32Snapshot)(DWORD, DWORD);
    BOOL (WINAPI * _Process32First)(HANDLE, PROCESSENTRY32 *);
    BOOL (WINAPI * _Process32Next)(HANDLE, PROCESSENTRY32 *);

    // получаем описатель KERNEL32.DLL
    hKernel = GetModuleHandle(_T("kernel32.dll"));
    _ASSERTE(hKernel != NULL);

    // находим необходимые функции в KERNEL32.DLL
    *(FARPROC *)&_CreateToolhelp32Snapshot =
        GetProcAddress(hKernel, "CreateToolhelp32Snapshot");
#ifdef UNICODE
    *(FARPROC *)&_Process32First =
        GetProcAddress(hKernel, "Process32FirstW");
    *(FARPROC *)&_Process32Next =
        GetProcAddress(hKernel, "Process32NextW");
#else
    *(FARPROC *)&_Process32First =
        GetProcAddress(hKernel, "Process32First");
    *(FARPROC *)&_Process32Next =
        GetProcAddress(hKernel, "Process32Next");
#endif

    if (_CreateToolhelp32Snapshot == NULL ||
        _Process32First == NULL ||
        _Process32Next == NULL)
        return SetLastError(ERROR_PROC_NOT_FOUND), FALSE;

    HANDLE hSnapshot;
    PROCESSENTRY32 Entry;

    // создаем моментальный снимок
    hSnapshot = _CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnapshot == INVALID_HANDLE_VALUE)
        return FALSE;

    // получаем информацию о первом процессе
    Entry.dwSize = sizeof(Entry);
    if (!_Process32First(hSnapshot, &Entry))
        return FALSE;

    // перечисляем остальные процессы
    do
    {
        LPTSTR pszProcessName = NULL;

        if (Entry.dwSize > offsetof(PROCESSENTRY32, szExeFile))
        {
            pszProcessName = _tcsrchr(Entry.szExeFile, _T('\\'));
            if (pszProcessName == NULL)
                pszProcessName = Entry.szExeFile;
        }

        if (!pfnEnumProc(Entry.th32ProcessID, pszProcessName, lParam))
            break;

        Entry.dwSize = sizeof(Entry);
    }
    while (_Process32Next(hSnapshot, &Entry));

    CloseHandle(hSnapshot);
    return TRUE;
}

Перед вызовом функций Process32First и Process32Next мы должны инициализировать поле dwSize структуры PROCESSENTRY32 таким образом, чтобы оно содержало размер структуры. Функции в свою очередь заносят в это поле количество байтов, записанных в структуру. Мы сравниваем это значение со смещением поля szExeFile в структуре, чтобы определить, было ли заполнено имя процесса.

Способ 3. Использование функции ZwQuerySystemInformation

Несмотря на наличие документированного способа получения списка процессов в Windows NT с помощью данных производительности, Windows NT Task Manager никогда не использовал этот интерфейс. Вместо этого он использовал недокументированную функцию ZwQuerySystemInformation, экспортируемую из NTDLL.DLL, которая позволяет получить доступ к самой разнообразной системной информации и списку процессов в том числе.

Функция ZwQuerySystemInformation имеет следующий прототип [2]:

NTSYSAPI
NTSTATUS
NTAPI
ZwQuerySystemInformation(
    IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
    IN OUT PVOID SystemInformation,
    IN ULONG SystemInformationLength,
    OUT PULONG ReturnLength
    );

где

SystemInformationClass задает тип получаемой информации, нас интересует SystemProcessesAndThreadsInformation (5)
SystemInformation указатель на буфер, принимающий запрошенную информацию
SystemInformationLength задает длину приемного буфера в байтах
ReturnLength указатель на переменную, в которую заносится количество байтов, записанных в выходной буфер

Формат информации о процессах и потоках описывается структурой SYSTEM_PROCESSES, которая содержит почти всю информацию, отображаемую Task Manager. Ниже привeден код функции EnumProcesses_NtApi, реализующей перечисление процессов с использованием функции ZwQuerySystemInformation.

typedef LONG    NTSTATUS;
typedef LONG    KPRIORITY;

#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0)

#define STATUS_INFO_LENGTH_MISMATCH      ((NTSTATUS)0xC0000004L)

#define SystemProcessesAndThreadsInformation    5

typedef struct _CLIENT_ID {
    DWORD         UniqueProcess;
    DWORD         UniqueThread;
} CLIENT_ID;

typedef struct _UNICODE_STRING {
    USHORT        Length;
    USHORT        MaximumLength;
    PWSTR         Buffer;
} UNICODE_STRING;

typedef struct _VM_COUNTERS {
    SIZE_T        PeakVirtualSize;
    SIZE_T        VirtualSize;
    ULONG         PageFaultCount;
    SIZE_T        PeakWorkingSetSize;
    SIZE_T        WorkingSetSize;
    SIZE_T        QuotaPeakPagedPoolUsage;
    SIZE_T        QuotaPagedPoolUsage;
    SIZE_T        QuotaPeakNonPagedPoolUsage;
    SIZE_T        QuotaNonPagedPoolUsage;
    SIZE_T        PagefileUsage;
    SIZE_T        PeakPagefileUsage;
} VM_COUNTERS;

typedef struct _SYSTEM_THREADS {
    LARGE_INTEGER KernelTime;
    LARGE_INTEGER UserTime;
    LARGE_INTEGER CreateTime;
    ULONG         WaitTime;
    PVOID         StartAddress;
    CLIENT_ID     ClientId;
    KPRIORITY     Priority;
    KPRIORITY     BasePriority;
    ULONG         ContextSwitchCount;
    LONG          State;
    LONG          WaitReason;
} SYSTEM_THREADS, * PSYSTEM_THREADS;

typedef struct _SYSTEM_PROCESSES {
    ULONG             NextEntryDelta;
    ULONG             ThreadCount;
    ULONG             Reserved1[6];
    LARGE_INTEGER     CreateTime;
    LARGE_INTEGER     UserTime;
    LARGE_INTEGER     KernelTime;
    UNICODE_STRING    ProcessName;
    KPRIORITY         BasePriority;
    ULONG             ProcessId;
    ULONG             InheritedFromProcessId;
    ULONG             HandleCount;
    ULONG             Reserved2[2];
    VM_COUNTERS       VmCounters;
#if _WIN32_WINNT >= 0x500
    IO_COUNTERS       IoCounters;
#endif
    SYSTEM_THREADS    Threads[1];
} SYSTEM_PROCESSES, * PSYSTEM_PROCESSES;

BOOL
WINAPI
EnumProcesses_NtApi(
    IN LPCTSTR pszMachineName,
    IN PFNENUMPROC pfnEnumProc,
    IN LPARAM lParam
    )
{
    _ASSERTE(pfnEnumProc != NULL);

    if (pszMachineName != NULL)
	return SetLastError(ERROR_INVALID_PARAMETER), FALSE;

    HINSTANCE hNtDll;
    NTSTATUS (WINAPI * _ZwQuerySystemInformation)(UINT, PVOID, ULONG, PULONG);

    // получаем описатель NTDLL.DLL
    hNtDll = GetModuleHandle(_T("ntdll.dll"));
    _ASSERTE(hNtDll != NULL);

    // находим адрес ZwQuerySystemInformation
    *(FARPROC *)&_ZwQuerySystemInformation =
        GetProcAddress(hNtDll, "ZwQuerySystemInformation");
    if (_ZwQuerySystemInformation == NULL)
        return SetLastError(ERROR_PROC_NOT_FOUND), FALSE;

    // получаем описатель кучи процесса по умолчанию
    HANDLE hHeap = GetProcessHeap();
    
    NTSTATUS Status;
    ULONG cbBuffer = 0x8000;
    PVOID pBuffer = NULL;

    // трудно заранее определить, какой размер выходного
    // буфера будет достаточным, поэтому мы начинам с буфера
    // размером 32K и увеличиваем его по необходимости
    do
    {
        pBuffer = HeapAlloc(hHeap, 0, cbBuffer);
        if (pBuffer == NULL)
            return SetLastError(ERROR_NOT_ENOUGH_MEMORY), FALSE;

        Status = _ZwQuerySystemInformation(
                    SystemProcessesAndThreadsInformation,
                    pBuffer, cbBuffer, NULL);

        if (Status == STATUS_INFO_LENGTH_MISMATCH)
        {
            HeapFree(hHeap, 0, pBuffer);
            cbBuffer *= 2;
        }
        else if (!NT_SUCCESS(Status))
        {
            HeapFree(hHeap, 0, pBuffer);
            return SetLastError(Status), FALSE;
        }
    }
    while (Status == STATUS_INFO_LENGTH_MISMATCH);

    PSYSTEM_PROCESSES pProcesses = (PSYSTEM_PROCESSES)pBuffer;

    for (;;)
    {
        PCWSTR pszProcessName = pProcesses->ProcessName.Buffer;
        if (pszProcessName == NULL)
            pszProcessName = L"Idle";

#ifdef UNICODE

        if (!pfnEnumProc(pProcesses->ProcessId, pszProcessName, lParam))
            break;

#else

        CHAR szProcessName[MAX_PATH];
        WideCharToMultiByte(CP_ACP, 0, pszProcessName, -1,
                            szProcessName, MAX_PATH, NULL, NULL);

        if (!pfnEnumProc(pProcesses->ProcessId, szProcessName, lParam))
            break;

#endif
        if (pProcesses->NextEntryDelta == 0)
            break;

        // find the address of the next process structure
        pProcesses = (PSYSTEM_PROCESSES)(((LPBYTE)pProcesses)
                        + pProcesses->NextEntryDelta);
    }

    HeapFree(hHeap, 0, pBuffer);
    return TRUE;
}

При вызове ZwQuerySystemInformation трудно заранее определить, какой размер выходного буфера будет достаточным, поэтому мы начинам с буфера размером 32K и увеличиваем его по необходимости.

Структура SYSTEM_PROCESSES имеет переменный размер, поле NextEntryDelta указывает смещение к следующей струтктуре в массиве. Наличие этого поля позволяет нам игнорировать различия в размере фиксированной части структуры между Windows NT 4.0 и Windows 2000, где в состав структуры было добавлено поле IoCounters.

Способ 4. Использование cчетчиков производительности

Как уже отмечалось, операционная система Windows NT с самого создания содержала интерфейс для получения разнообразной информации о системе в виде счетчиков производительности. Этот интерфейс является, скажем, далеко не интуитивным. Для получения той или иной информации нужно прочитать из ключа реестра HKEY_PERFORMANCE_DATA значение со специально сформированным именем. В результате возвращается набор глубоко вложенных структур, многие из которых переменного размера, и разбор этих данных требует определенной усидчивости.

Ситуация изменилась в лучшую сторону с появлением в Windows NT 4.0 библиотеки Performance Data Helper (PDH), которая предоставляет более удобный интерфейс к данным производительности. Эта библиотека, однако, не входила в комплект поставки Windows NT 4.0, она распространялась в составе Microsoft Platform SDK. В Windows 2000 PDH.DLL присутствует по умолчанию.

Подробное рассмотрение данных производительности выходит за рамки данной статьи, отметим лишь, что система подсчета производительности в Windows NT определяет понятие объекта, для которого осуществляется подсчет производительности. Примерами объектов являются процессор и жесткий диск. Каждый объект может иметь один или более экземпляров, и для каждого объекта существует свой набор счетчиков производительности. Наша задача состоит в перечислении всех экземпляров объекта с именем "Process" и получении для каждого из них значения счетчика с именем "ID Process". Ниже приведен код функции EnumProcesses_PerfData, которая использует PDH для перечисления процессов.

#include <pdh.h>
#include <pdhmsg.h>

BOOL
WINAPI
EnumProcesses_PerfData(
    IN LPCTSTR pszMachineName,
    IN PFNENUMPROC pfnEnumProc,
    IN LPARAM lParam
    )
{
    _ASSERTE(pfnEnumProc != NULL);

    HINSTANCE hPdh;

    PDH_STATUS (WINAPI * _PdhOpenQuery)(LPCWSTR, DWORD_PTR, HQUERY *);
    PDH_STATUS (WINAPI * _PdhAddCounter)(HQUERY, LPCWSTR, DWORD_PTR, 
        HCOUNTER *);
    PDH_STATUS (WINAPI * _PdhCollectQueryData)(HQUERY);
    PDH_STATUS (WINAPI * _PdhGetRawCounterArray)(HCOUNTER,
        LPDWORD, LPDWORD, PDH_RAW_COUNTER_ITEM_W *);
    PDH_STATUS (WINAPI * _PdhCloseQuery)(HQUERY);
    
    // загружаем PDH.DLL
    hPdh = LoadLibrary(_T("pdh.dll"));
    if (hPdh == NULL)
        return FALSE;

    // находим необходимые функции в PDH.DLL
    *(FARPROC *)&_PdhOpenQuery =
        GetProcAddress(hPdh, "PdhOpenQueryW");
    *(FARPROC *)&_PdhAddCounter =
        GetProcAddress(hPdh, "PdhAddCounterW");
    *(FARPROC *)&_PdhGetRawCounterArray =
        GetProcAddress(hPdh, "PdhGetRawCounterArrayW");
    *(FARPROC *)&_PdhCollectQueryData =
        GetProcAddress(hPdh, "PdhCollectQueryData");
    *(FARPROC *)&_PdhCloseQuery =
        GetProcAddress(hPdh, "PdhCloseQuery");

    // проверяем, что были найдены все функции
    if (_PdhOpenQuery == NULL ||
        _PdhAddCounter == NULL ||
        _PdhCollectQueryData == NULL ||
        _PdhGetRawCounterArray == NULL ||
        _PdhCloseQuery == NULL)
    {
        FreeLibrary(hPdh);
        return SetLastError(ERROR_PROC_NOT_FOUND), FALSE;
    }

    PDH_STATUS Status;
    WCHAR szCounterPath[1024];

    // формируем путь к счетчику производительности
    Status = PerfFormatCounterPath(hPdh, pszMachineName, szCounterPath, 1024);
    if (Status != ERROR_SUCCESS)
    {
        FreeLibrary(hPdh);
        return SetLastError(Status), FALSE;
    }

    HQUERY hQuery;
    HCOUNTER hCounter;

    // открываем запрос
    Status = _PdhOpenQuery(NULL, 0, &hQuery);
    if (Status != ERROR_SUCCESS)
    {
        FreeLibrary(hPdh);
        return SetLastError(Status), FALSE;
    }

    // добавляем счетчик к запросу
    Status = _PdhAddCounter(hQuery, szCounterPath, 0, &hCounter);
    if (Status != ERROR_SUCCESS)
    {
        _PdhCloseQuery(hQuery);
        FreeLibrary(hPdh);
        return SetLastError(Status), FALSE;
    }

    // получаем текущие значения счетчика
    Status = _PdhCollectQueryData(hQuery);
    if (Status != ERROR_SUCCESS)
    {
        _PdhCloseQuery(hQuery);
        FreeLibrary(hPdh);
        return SetLastError(Status), FALSE;
    }

    DWORD cbSize = 0;
    DWORD cItems = 0;
    HANDLE hHeap = GetProcessHeap();

    // определяем необходимый размер буфера
    Status = _PdhGetRawCounterArray(hCounter, &cbSize, &cItems, NULL);
    if (Status != ERROR_SUCCESS)
    {
        _PdhCloseQuery(hQuery);
        FreeLibrary(hPdh);
        return SetLastError(Status), FALSE;
    }

    // выделяем память для буфера
    PDH_RAW_COUNTER_ITEM_W * pRaw =
        (PDH_RAW_COUNTER_ITEM_W *)HeapAlloc(hHeap, 0, cbSize);
    if (pRaw == NULL)
    {
        _PdhCloseQuery(hQuery);
        FreeLibrary(hPdh);
        return SetLastError(ERROR_NOT_ENOUGH_MEMORY), FALSE;
    }

    // получаем значения счетчика
    Status = _PdhGetRawCounterArray(hCounter, &cbSize, &cItems, pRaw);

    // закрываем запрос
    _PdhCloseQuery(hQuery);

    if (Status != ERROR_SUCCESS)
    {
        HeapFree(hHeap, 0, pRaw);
        FreeLibrary(hPdh);
        return SetLastError(Status), FALSE;
    }

    // перечисляем все экземпляры
    for (DWORD i = 0; i < cItems; i++)
    {
        DWORD dwProcessId = (DWORD)pRaw[i].RawValue.FirstValue;
        LPCTSTR pszProcessName;

#ifdef UNICODE
        pszProcessName = pRaw[i].szName;
#else
        CHAR szProcessName[MAX_PATH];
        WideCharToMultiByte(CP_ACP, 0, pRaw[i].szName, -1,
                            szProcessName, MAX_PATH, NULL, NULL);
        pszProcessName = szProcessName;
#endif

        // исключаем экземпляр с именем _Total
        if (dwProcessId == 0 && lstrcmp(pszProcessName, _T("_Total")) == 0)
            continue;

        if (!pfnEnumProc(dwProcessId, pszProcessName, lParam))
            break;
    }

    HeapFree(hHeap, 0, pRaw);
    FreeLibrary(hPdh);

    return TRUE;
}

Мы возвращаем имя экземпляра объекта в качестве имени процесса. Если вы запустите тестовую программу, то увидите, что имена экземпляров соответсвуют именам исполняемых файлов процессов без расширения.

Названия объектов и счетчиков производительности являются локализуемыми. Это значит, что, например, на русской версии Windows NT необходимо использовать "Процесс" и "Идентификатор процесса" вместо "Process" и "ID Process". Для получения локализованных имен объектов и формирования полного пути к интересующему нас счетчику производительности служит вспомогательная функция PerfFormatCounterPath, исходный код которой приведен ниже.

static
PDH_STATUS WINAPI PerfFormatCounterPath(
    IN HINSTANCE hPdh,
    IN LPCTSTR pszMachineName,
    IN LPWSTR pszPath,
    IN ULONG cchPath
    )
{
    _ASSERTE(hPdh != NULL);
    _ASSERTE(_CrtIsValidPointer(pszPath, cchPath * sizeof(WCHAR), 1));

    PDH_STATUS (WINAPI * _PdhMakeCounterPath)(
        PDH_COUNTER_PATH_ELEMENTS_W *, LPWSTR, LPDWORD, DWORD);
    PDH_STATUS (WINAPI * _PdhLookupPerfNameByIndex)(
        LPCWSTR, DWORD, LPWSTR, LPDWORD);

    *(FARPROC *)&_PdhMakeCounterPath =
        GetProcAddress(hPdh, "PdhMakeCounterPathW");
    *(FARPROC *)&_PdhLookupPerfNameByIndex =
        GetProcAddress(hPdh, "PdhLookupPerfNameByIndexW");

    if (_PdhMakeCounterPath == NULL ||
        _PdhLookupPerfNameByIndex == NULL)
        return ERROR_PROC_NOT_FOUND;

    PDH_STATUS Status;
    DWORD cbName;
    WCHAR szObjectName[80];
    WCHAR szCounterName[80];

    LPWSTR pszMachineW = NULL;

#ifndef _UNICODE
    WCHAR szMachineName[256];
    if (pszMachineName != NULL)
    {
        MultiByteToWideChar(CP_ACP, 0, pszMachineName, -1, szMachineName, 256);
        pszMachineW = szMachineName;
    }
#else
    pszMachineW = (LPWSTR)pszMachineName;
#endif

    // находим имя объекта Process
    cbName = sizeof(szObjectName);
    Status = _PdhLookupPerfNameByIndex(pszMachineW, 230, 
                                       szObjectName, &cbName);
    if (Status != ERROR_SUCCESS)
        return Status;

    // находим имя счетчика ID Process
    cbName = sizeof(szCounterName);
    Status = _PdhLookupPerfNameByIndex(pszMachineW, 784,
                                       szCounterName, &cbName);
    if (Status != ERROR_SUCCESS)
        return Status;

    PDH_COUNTER_PATH_ELEMENTS_W pcpe;
    pcpe.szMachineName = pszMachineW;
    pcpe.szObjectName = szObjectName;
    pcpe.szInstanceName = L"*";
    pcpe.szParentInstance = NULL;
    pcpe.dwInstanceIndex = 0;
    pcpe.szCounterName = szCounterName;

    // формируем полный путь к счетчику
    return _PdhMakeCounterPath(&pcpe, pszPath, &cchPath, 0);
}

Мы используем функцию PdhLookupPerfNameByIndex, чтобы получить локализованные имена по их индексам. Для стандартных объектов и счетчиков производительности индексы являются постоянными и не зависят от версии операционной системы, поэтому вполне безопасно задать значения индексов непосредственно в коде.

Отметим, что из перечисленных выше методов, этот метод - первый, который позволяет перечислить процессы на другом компьютере. Все что нужно сделать - это указать имя компьютера в поле szMachineName структуры PDH_COUNTER_PATH_ELEMENTS.

Способ 5. Использование Windows Management Instrumentation

Windows Management Instrumentation (WMI) является реализацией Mircrosoft для так называемой технологии Web-Based Enterprise Management (WBEM). WBEM определяет унифицированную архитектуру, которая позволяет получать данные от различных источников, построенных с помощью различных технологий и платформ, и единообразно представлять эти данные. WBEM основана на схеме общей информационной модели (Common Information Model, CIM), которая является индустриальным стандартом, управляемым Distributed Management Task Force (DMTF). WMI поставляется в составе Windows 2000, но также может быть установлен на Windows 95/98/Me и Windows NT 4.0.

Сколько-либо подробное рассмотрение WMI выходит за рамки этой статьи. Ограничимся лишь кодом, который перечисляет процессы с использованием интерфейсов, предоставляемых WMI.

#include <comdef.h>
#include <SshWbemHelpers.h>

BOOL
WINAPI
EnumProcesses_Wmi(
    IN LPCTSTR pszMachineName,
    IN PFNENUMPROC pfnEnumProc,
    IN LPARAM lParam
    )
{
    _ASSERTE(pfnEnumProc != NULL);

    try
    {
        HRESULT hRes;
        TCHAR szServer[256];
        _bstr_t bstrServer;
	IWbemLocatorPtr spLocator;
        IWbemServicesPtr spServices;
        IEnumWbemClassObjectPtr spEnum;
        IWbemClassObjectPtr spObject;
        ULONG ulCount;

        // создаем объект WBEM locator
        hRes = CoCreateInstance(__uuidof(WbemLocator), NULL,
                            CLSCTX_INPROC_SERVER, __uuidof(IWbemLocator),
                            (PVOID *)&spLocator);
        if (FAILED(hRes))
            _com_issue_error(hRes);

        if (pszMachineName == NULL)
            lstrcpy(szServer, _T("root\\cimv2"));
        else
	    wsprintf(szServer, _T("\\\\%s\\root\\cimv2"), pszMachineName);
	    
	// соединяемся с указанной машиной
        hRes = spLocator->ConnectServer(_bstr_t(L"root\\cimv2"), NULL,
                                        NULL, NULL, 0, NULL, NULL, 
                                        &spServices);
        if (FAILED(hRes))
            _com_issue_error(hRes);

        // создаем перечислитель процессов
        hRes = spServices->CreateInstanceEnum(_bstr_t(L"Win32_Process"),
                                    WBEM_FLAG_SHALLOW|WBEM_FLAG_FORWARD_ONLY,
                                    NULL, &spEnum);
        if (FAILED(hRes))
            _com_issue_error(hRes);

        // перечисляем процессы
        while (spEnum->Next(WBEM_INFINITE, 1, &spObject, &ulCount) == S_OK)
        {
            _variant_t valId;
            _variant_t valName;

            // получаем идентификатор процесса
            hRes = spObject->Get(L"ProcessId", 0, &valId, NULL, NULL);
            if (FAILED(hRes))
            {
                spObject = NULL;
                continue;
            }

            // получаем имя процесса
            hRes = spObject->Get(L"Name", 0, &valName, NULL, NULL);
            if (FAILED(hRes))
            {
                spObject = NULL;
                continue;
            }

            _ASSERTE(valId.vt == VT_I4);
            _ASSERTE(valName.vt == VT_BSTR);

            if (!pfnEnumProc(valId.lVal, (LPCTSTR)_bstr_t(valName), lParam))
                break;

            spObject = NULL;
        }
    }
    catch (_com_error& err)
    {
        return SetLastError(err.Error()), FALSE;
    }

    return TRUE;
}

Поскольку WMI основана на технологии COM, это избавило нас от необходимости явно загружать требуемые библиотеки, как это мы делали в предыдущих примерах. Этот же факт требует инициализации библиотеки COM прежде чем будет вызвана функция перечисления процессов. В приложении, созданном с использованием MFC, это можно сделать с помощью функции AfxOleInit, в остальных случаях следует пользоваться функциями CoInitialize или CoInitializeEx.

Кроме того, использование WMI требует инициализации безопасности COM с помощью функции CoInitializeSecurity:

    
    CoInitializeSecurity(
          NULL,
          -1,
          NULL,
          NULL,
          RPC_C_AUTHN_LEVEL_CONNECT,
          RPC_C_IMP_LEVEL_IMPERSONATE,
          NULL,
          EOAC_NONE,
          0);

Заметим, что как и метод с использованием счетчиков производительности, этот метод позволяет перечислить процессы на другом компьютере, для чего нужно указать имя компьютера в вызове IWbemLocator::ConnectServer.

Перечисление процессов на другом компьютере в том виде, в котором оно представлено в последних двух примерах, будет работать только если текущий пользователь локального компьютера включен в группу администраторов удаленного компьютера. Приведенные выше примеры не позволяют изменить имя пользователя и пароль, с которыми производится подключение. Чтобы указать альтернативные имя пользователя и пароль, для PDH следует использовать функцию NetUseAdd или WNetAddConnection2 чтобы установить аутентифицированное соединение с ресурсом IPC$ на удаленном компьютере. Для WMI следует использовать стандартные средства COM. Подробное рассмотрение этих механизмов выходит за рамки данной статьи.

Заключение

Таким образом, мы рассмотрели пять различных способов получения списка процессов. Ни один из них не является универсальным, так как существует версия Windows, где этот метод не работает. Приведенная ниже таблица показывает применимость различных методов.

Windows 9x/Me Windows NT 4.0 Windows 2000/XP
Способ 1 Нет Да* Да
Способ 2 Да Нет Да
Способ 3 Нет Да Да
Способ 4 Нет Да* Да
Способ 5 Да* Да* Да
* Требует установки дополнительных компонент.

Используя эту таблицу и приведенные выше функции, несложно написать функцию перечисления процессов, которая будет работать на всех версиях Windows. Например, если мы хотим использовать PSAPI при работе на Windows NT/2000/XP и ToolHelp32 API - на Windows 9x/Me, то функция будет выглядеть следующим образом:

BOOL
WINAPI
MyEnumProcesses(
    IN PFNENUMPROC pfnEnumProc,
    IN LPARAM lParam
    )
{
    OSVERSIONINFO osvi;
    osvi.dwOSVersionInfoSize = sizeof(osvi);

    if (osvi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS)
        return EnumProcesses_ToolHelp(pfnEnumProc, lParam);
    else if (osvi.dwPlatformId == VER_PLATFORM_WIN32_NT)
        return EnumProcesses_PsApi(pfnEnumProc, lParam);
    else
        return SetLastError(ERROR_CALL_NOT_IMPLEMENTED), FALSE;
}

Эту статью сопровождает небольшое тестовое приложение с амбициозным названием Process Viewer, которое демонстрирует все перечисленные выше способы перечисления процессов. Реализация всех способов находится в файлах enumproc.h и enumproc.cpp. Мы постарались оформить эти файлы таким образом, чтобы максимально облегчить их включение в другие проекты. Для сборки приложения необходимо иметь установленным Microsoft Platform SDK.

Cсылки

  1. Q175030 HOWTO: Enumerate Applications in Win32, Microsoft Knowledge Base.
  2. Windows NT/2000 Native API Reference by Gary Nebbett, New Riders Publishing, 2000.
  3. Q287159 INFO: Using PDH APIs Correctly in a Localized Language, Microsoft Knowledge Base.

Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 12    Оценка 512        Оценить