Как принудительно завершить процесс?

Автор: Александр Федотов
Опубликовано: 24.10.2001
Версия текста: 1.0

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

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

Функция TerminateProcess

Для принудительного и безоговорочного завершения процессов в Win32 служит функция TerminateProcess:

BOOL TerminateProcess(
    IN HANDLE hProcess,	    // описатель процесса
    IN DWORD dwExitCode	    // код завершения процесса
    );

Сейчас я обязан сделать

Функцию TerminateProcess следует использовать только в исключительных случаях, когда исчерпаны все другие способы воздействия на процесс, поскольку она не позволяет потокам процесса выполнить очистку или сохранить данные, а также не оповещает загруженные DLL о завершении процесса.

Я уверен, что это предупреждение вас не остановит. Для того, чтобы воспользоваться функцией TerminateProcess, необходимо получить описатель (handle) процесса. Зная идентификатор процесса, это несложно сделать c помощью функции OpenProcess, в итоге функция принудительного завершения процесса может выглядеть, например, так:

BOOL KillProcess(
    IN DWORD dwProcessId
    )
{
    // получаем описатель процесса
    HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, dwProcessId);
    if (hProcess == NULL)
        return FALSE;

    DWORD dwError = ERROR_SUCCESS;

    // пытаемся завершить процесс
    if (!TerminateProcess(hProcess, (DWORD)-1))
        dwError = GetLastError();

    // закрываем описатель процесса
    CloseHandle(hProcess);

    SetLastError(dwError);
    return dwError == ERROR_SUCCESS;
}

Завершение процессов и система безопасности Windows NT

Если вы воспользуетесь функцией KillProcess, то обнаружите, что хотя она замечательно работает в Windows 95/98, в Windows NT она не может завершить некоторые процессы, потому что OpenProcess возвращает ошибку ERROR_ACCESS_DENIED. В частности, это происходит при попытке остановить процессы системных служб и некоторых DCOM-серверов. Как вы наверное уже догадываетесь, все дело в правах доступа.

Как и любой объект ядра, объект процесса защищен дескриптором безопасности, который проверяется всякий раз, когда открывается описатель процесса. Для разных видов процессов дескриптор безопасности инициализируется по-разному. Вы можете воспользоваться тестовой программой Process Viewer для изучения дескрипторов безопасности процессов. Так, вы можете обнаружить, что дескриптор безопасности для интерактивных процессов выглядит так:

Интерактивный пользовательПолный доступ
СИСТЕМАПолный доступ

в то время как для системных служб, работающих в контексте системной логон-сессии, дескриптор безопасности выглядит иначе:

АдминистраторыЧтение
СИСТЕМАПолный доступ

Теперь ясно, почему OpenProcess не позволяет открыть описатель процесса системной службы с правами PROCESS_TERMINATE, даже если вызывающий пользователь является администратором компьютера. Это может показаться странным, почему администратор компьютера не может остановить любой процесс в системе. На самом деле, такая возможность у него есть, и заключается она в наличии привилегии SE_DEBUG_NAME.

Все привилегии, которые имеет пользователь, могут находиться в двух состояниях: выключенном, неактивном, и включенном, активном состоянии. Администраторы компьютера имеют привилегию SE_DEBUG_NAME, но по умолчанию она находится в выключенном состоянии и не оказывает влияния на работу OpenProcess. Когда эта привилегия включена, вызывающий поток может открывать описатели процессов с любыми правами доступа, вне зависимости от того, какой дескриптор безопасности назначен объекту процесса. Это как раз то, что нам нужно.

Ниже приведен модифицированный код функции KillProcess, которая при необходимости включает привилегию SE_DEBUG_NAME, чтобы получить описатель процесса.

BOOL KillProcess(
    IN DWORD dwProcessId
    )
{
    HANDLE hProcess;
    DWORD dwError;

    // сначала попробуем получить описатель процесса без 
    // использования дополнительных привилегий
    hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, dwProcessId);
    if (hProcess == NULL)
    {
        if (GetLastError() != ERROR_ACCESS_DENIED)
            return FALSE;

        OSVERSIONINFO osvi;

        // определяем версию операционной системы
        osvi.dwOSVersionInfoSize = sizeof(osvi);
        GetVersionEx(&osvi);

        // мы больше ничего не можем сделать, если это не Windows NT
        if (osvi.dwPlatformId != VER_PLATFORM_WIN32_NT)
            return SetLastError(ERROR_ACCESS_DENIED), FALSE;

        // включим привилегию SE_DEBUG_NAME и попробуем еще раз

        TOKEN_PRIVILEGES Priv, PrivOld;
        DWORD cbPriv = sizeof(PrivOld);
        HANDLE hToken;

        // получаем токен текущего потока 
        if (!OpenThreadToken(GetCurrentThread(), 
                             TOKEN_QUERY|TOKEN_ADJUST_PRIVILEGES,
                             FALSE, &hToken))
        {
            if (GetLastError() != ERROR_NO_TOKEN)
                return FALSE;

            // используем токен процесса, если потоку не назначено
	    // никакого токена
            if (!OpenProcessToken(GetCurrentProcess(),
                                  TOKEN_QUERY|TOKEN_ADJUST_PRIVILEGES,
                                  &hToken))
                return FALSE;
        }

        _ASSERTE(ANYSIZE_ARRAY > 0);

        Priv.PrivilegeCount = 1;
        Priv.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
        LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &Priv.Privileges[0].Luid);

        // попробуем включить привилегию
        if (!AdjustTokenPrivileges(hToken, FALSE, &Priv, sizeof(Priv),
                                   &PrivOld, &cbPriv))
        {
            dwError = GetLastError();
            CloseHandle(hToken);
            return SetLastError(dwError), FALSE;
        }

        if (GetLastError() == ERROR_NOT_ALL_ASSIGNED)
        {
            // привилегия SE_DEBUG_NAME отсутствует в токене
            // вызывающего
            CloseHandle(hToken);
            return SetLastError(ERROR_ACCESS_DENIED), FALSE;
        }

        // попробуем открыть описатель процесса еще раз
        hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, dwProcessId);
        dwError = GetLastError();
        
        // восстанавливаем исходное состояние привилегии
        AdjustTokenPrivileges(hToken, FALSE, &PrivOld, sizeof(PrivOld),
                              NULL, NULL);
        CloseHandle(hToken);

        if (hProcess == NULL)
            return SetLastError(FALSE), NULL;
    }

    // пытаемся завершить процесс
    if (!TerminateProcess(hProcess, (UINT)-1))
    {
        dwError = GetLastError();
        CloseHandle(hProcess);
        return SetLastError(dwError), FALSE;
    }

    CloseHandle(hProcess);

    // успешное завершение
    return TRUE;
}

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

Теперь с помощью функции KillProcess можно убить практически любой процесс в системе, если, конечно, вы являетесь администратором компьютера. Чтобы проверить новый вариант функции в деле, вы можете воспользоваться тестовым приложением Process Viewer, которое теперь умеет завершать процессы. Например, можно остановить процесс Winlogon и через несколько секунд увидеть синий экран с надписью STOP 0xC000021A (не забудьте сохранить все открытые документы, прежде чем это сделать).

Завершение дерева процессов

Некоторое время назад мой коллега по работе попросил меня написать утилиту, аналогичную утилите kill в Unix. Он занимался переносом каких-то скриптов из Unix на Windows NT и ему нужна была такая утилита. Использовать утилиту, поставляемую в составе Windows NT Resource Kit, он не мог по лицензионным (а может, и религиозным) соображениям.

Я быстро оформил функцию KillProcess в виде небольшого консольного приложения и отправил ему. Коллега был приятно удивлен скорости, с которой я выполнил его просьбу, однако он отметил, что моя версия kill работает не так, как аналог из Unix. В Unix, kill завершает не только сам процесс, но и все дочерние процессы, которые были запущены из него прямо или косвенно (так называемое дерево процессов). Моя же версия работала как kill -9, когда завершается только указанный процесс. Таким образом, встала задача повторить поведение Unix kill на Windows NT.

Чтобы завершить все процессы в дереве, необходимо каким-либо образом отследить отношения родитель - потомок между процессами. В Windows 9x/Me и Windows 2000/XP это позволяют сделать функции перечисления процессов из ToolHelp32 API, в частности поле th32ParentProcessID структуры PROCESSENTRY32 содержит идентификатор родительского процесса. В Windows NT 4.0 эту информацию можно получить с помощью официально недокументированной функции ZwQuerySystemInformation. (Подробнее о функциях перечисления процессов рассказано в статье Как получить список запущенных процессов?)

Ниже приведен код функции KillProcessEx, которая позволяет завершать как отдельный процесс, так и целое дерево процессов. Если вы читали статью про перечисление процессов, то найдете много знакомых кусков кода в этой функции.

// вспомогательная функция, которая рекурсивно обходит дерево процессов
// в Windows NT и завершает все процессы в дереве
static
BOOL KillProcessTreeNtHelper(
    IN PSYSTEM_PROCESSES pInfo,
    IN DWORD dwProcessId
    )
{
    _ASSERTE(pInfo != NULL);

    PSYSTEM_PROCESSES p = pInfo;

    // сначала завершаем все дочерние процессы
    for (;;)
    {
        if (p->InheritedFromProcessId == dwProcessId)
            KillProcessTreeNtHelper(pInfo, p->ProcessId);

        if (p->NextEntryDelta == 0)
            break;

        // находим адрес следующей структуры
        p = (PSYSTEM_PROCESSES)(((LPBYTE)p) + p->NextEntryDelta);
    }

    // завершаем исходный процесс
    if (!KillProcess(dwProcessId))
        return GetLastError();

    return ERROR_SUCCESS;
}

// вспомогательная функция, которая рекурсивно обходит дерево процессов
// в Windows 9x и завершает все процессы в дереве
static
BOOL KillProcessTreeWinHelper(
    IN DWORD dwProcessId
    )
{
    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");
    *(FARPROC *)&_Process32First =
        GetProcAddress(hKernel, "Process32First");
    *(FARPROC *)&_Process32Next =
        GetProcAddress(hKernel, "Process32Next");

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

    HANDLE hSnapshot;
    PROCESSENTRY32 Entry;

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

    Entry.dwSize = sizeof(Entry);
    if (!_Process32First(hSnapshot, &Entry))
    {
        DWORD dwError = GetLastError();
        CloseHandle(hSnapshot);
        return dwError;
    }

    // завершаем сначала дочерние процессы
    do
    {
        if (Entry.th32ParentProcessID == dwProcessId)
            KillProcessTreeWinHelper(Entry.th32ProcessID);

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

    CloseHandle(hSnapshot);

    // завершаем исходный процесс
    if (!KillProcess(dwProcessId))
        return GetLastError();

    return ERROR_SUCCESS;
}

BOOL KillProcessEx(
    IN DWORD dwProcessId,   // идентификатор процесса
    IN BOOL bTree           // признак завершения всего дерева
    )
{
    if (!bTree)
        return KillProcess(dwProcessId);

    OSVERSIONINFO osvi;
    DWORD dwError;

    // определяем версию операционной системы
    osvi.dwOSVersionInfoSize = sizeof(osvi);
    GetVersionEx(&osvi);

    if (osvi.dwPlatformId == VER_PLATFORM_WIN32_NT)
    {
        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), NULL;

        // получаем описатель кучи процесса по умолчанию
        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), NULL;
            }
        }
        while (Status == STATUS_INFO_LENGTH_MISMATCH);

        // вызываем вспомогательную функцию
        dwError = KillProcessTreeNtHelper((PSYSTEM_PROCESSES)pBuffer, 
                                          dwProcessId);
        
        HeapFree(hHeap, 0, pBuffer);
    }
    else
    {
        // вызываем вспомогательную функцию
        dwError = KillProcessTreeWinHelper(dwProcessId);
    }

    SetLastError(dwError);
    return dwError == ERROR_SUCCESS;
}

Используя функцию KillProcessEx, я смог написать утилиту PKILL, которая более точно соответствует своему Unix-аналогу. Эта утилита прилагается в качестве тестового приложения к данной статье.

Завершение 16-битных задач в Windows NT

Рассмотрение вопроса о завершении процессов не будет полным, если мы не затронем тему 16-битных задач на Windows NT. Для завершения 16-битных задач VDMDBG.DLL предоставляет функцию VDMTerminateTaskWOW:

BOOL VDMTerminateTaskWOW(
    IN DWORD dwProcessId,   // идентификатор виртуальной DOS-машины
    IN WORD hTask16	    // идентификатор 16-битной задачи
    );

Параметры этой функции говорят сами за себя. Тестовое приложение Process Viewer использует ее для завершения 16-битных задач на Windows NT.

Заключение

Таким образом, теперь вы во всеоружии готовы завершить любой процесс. Но, как и всякое оружие, используйте возможность принудительного завершения процессов с осторожностью.

Cсылки

  1. Q131065 HOWTO: How to Obtain a Handle to Any Process with SeDebugPrivilege, Microsoft Knowledge Base.
  2. Q182559 HOWTO: Use VDMDBG Functions on Windows NT, Microsoft Knowledge Base.

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