Контроль дочерних процессов с помощью C++ и WinAPI
#C++
#WinAPI
#Windows
#контроль
#процессы
Человек лучше всего следит за собой тогда, когда другие следят за ним тоже.
(с) Джордж Сэвил Галифакс
Небольшой пример контроля дочерних процессов в Windows с помощью C++ и WinAPI.
Содержание
Начало
Задача запуска дочерних процессов из приложений - дело не редкое. В программных продуктах такое встречается довольно часто. Напрмер, различные IDE запускают процессы компилятора, анализа исходных кодов программы, приложения для отладки и многое другое.
Запустить процесс обычно дело нехитрое. Языки программирования и различного рода платформы для разработки обычно имеют удобные средства для решения этих задач. Однако, чем чаще приходится запускать дочерние процессы, тем актуальней становится вопрос их контроля. Ведь могут быть случаи, когда дочерний процесс запустился, а родительский процесс после этого "упал". Получается, что дочерние процессы будут "висеть" вечно. А бывают и более сложные случаи, когда дочерние процессы зависли по каким-либо причинам и этот момент нужно учитывать для надежной работы приложения.
Этой задачи мы уже подробно касались в контексте платформы .NET и языка C# в частности. В статье Берем процессы под контроль в .NET мы уже подробно рассматривали эту тему с помощью простых и сложных примеров реализации различного рода контролей.
Сегодня же мы рассмотрим этот вопрос немного с другой стороны. Мы будем запускать процессы в операционной системе Windows из приложения на C++, здесь же добавим контроль дочернего процесса с помощью возможностей WinAPI. Но все по порядку. Приступим!
Начнем с простого
Самый простой способ запуска дочерних процессов средствами C++ является функция **system()** из стандартной библиотеки.
#include <iostream>using namespace std;int main(){setlocale(LC_ALL, "");system("notepad.exe");return 0;}
Мы запускаем приложение **notepad.exe** (Блокнот) и продолжаем выполнение программы. Конечно, приложение запущено, но ни прочитать результат его роботы, ни ожидать его завершения в этом случае мы не можем. Поэтому обратимся перейдем к следующему варианту.
Сложнее, выше, быстрее!
Следующим шагом для запуска приложения будет метод WinAPI - ShellExecuteEx. Этот метод позволяет выполнить произвольную команду, что также позволит решить нашу задачу с запуском дочернего процесса.
Запустим также блокнот, но в этот раз будем ожидать его закрытия 10 секунд. Если процесс "Блокнота" не завершится за указанное время, то завершим его принудительно.
#include <iostream>#include <windows.h>using namespace std;int main(){setlocale(LC_ALL, "");// Инициализируем и запускаем процесс// В примере мы явно используем вариант функции "ShellExecuteExW",// который поддерживает работу с Unicode. Сути примера это не меняет.CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);SHELLEXECUTEINFOW shellExecuteInfo = {};shellExecuteInfo.cbSize = sizeof(SHELLEXECUTEINFO);shellExecuteInfo.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;shellExecuteInfo.lpFile = L"notepad.exe";shellExecuteInfo.nShow = SW_SHOWNORMAL;BOOL success = ShellExecuteExW(&shellExecuteInfo);assert(success);// Ждем завершение дочернего процесса 10 секундauto wait_result = WaitForSingleObject(shellExecuteInfo.hProcess, 10000);if (wait_result == WAIT_TIMEOUT){// Завершаем процесс, если он еще не завершился за отведенное времяTerminateProcess(shellExecuteInfo.hProcess, 1);}// Закрываем дескрипторыCloseHandle(shellExecuteInfo.hProcess);return 0;}
Пример уже содержит контроль выполнения и это просто отлично! Но от совершенства мы все еще далеко. Ведь могут быть случаи, когда дочерний процесс завершится до момента завершения ожидания. Это приведет к тому, что родительский процесс будет завершен, а дочерний останется жить, чего не хотелось бы.
Значит пришло время пойти к следующему примеру!
Я знаю кто ты
Теперь мы будем использовать метод WinAPI - CreateProcess. Данный метод создаем новый процесс по указанным параметрам.
Для наглядности сначала запустим процесс "Блокнота" как мы делали выше с помощью метода ShellExecuteEx. То есть с контролем времени выполнения и автозавершением процесса по таймауту.
#include <iostream>#include <windows.h>using namespace std;int main(){setlocale(LC_ALL, "");// Команда для запуска приложенияWCHAR lpCommandLine[] = L"notepad.exe";// Подготовка объектов для запуска процессовSTARTUPINFOW startupInfo;ZeroMemory(&startupInfo, sizeof(startupInfo));startupInfo.cb = sizeof(startupInfo);PROCESS_INFORMATION processInfo = {};// Инициализация процесса в спящем режимеBOOL bSuccess = CreateProcessW(NULL, // lpApplicationName <- !!!lpCommandLine, // lpCommandLine <- !!!NULL, // lpProcessAttributesNULL, // lpThreadAttributesFALSE, // bInheritHandles0, // dwCreationFlagsNULL, // lpEnvironmentNULL, // lpCurrentDirectory&startupInfo,&processInfo);assert(bSuccess);// Ждем завершение дочернего процесса 10 секундauto wait_result = WaitForSingleObject(processInfo.hProcess, 10000);if (wait_result == WAIT_TIMEOUT){// Завершаем процесс, если он еще не завершился за отведенное времяTerminateProcess(processInfo.hProcess, 1);}// Закрываем дескрипторыCloseHandle(processInfo.hThread);CloseHandle(processInfo.hProcess);return 0;}
Пока мы только перешли на использование метода CreateProcess, вместо ShellExecuteEx. Новых механизмов контроля здесь нет, только таймаут ожидания и автозавершение по окончании ожидания, если процесс еще не завершен. Но если нет разницы, то зачем все это?
Все дело в том, что при использовании CreateProcess мы можем также задействовать так называемые объекты заданий CreateJobObject.
Объект задания - это объект ядра, который позволяет группировать процессы и помещать их в некоторую песочницу, которая позволяет ограничивать работу процессов различными способами. Настроек и возможностей для управления процессами открывается при этом великое множество!
Конечно, все возможности мы здесь рассмотреть не сможем. Однако, мы модифицируем код выше таким образом, чтобы добавить ограничение для дочернего процесса - при завершении родительского процесса дочерний должен быть завершен принудительно. И не важно как завершился родительский процесс - дочерний должен завершиться вместе с ним.
Ниже приведен пример работы с объектом задания для достижения такого поведения.
#include <iostream>#include <windows.h>using namespace std;int main(){setlocale(LC_ALL, "");// Команда для запуска приложенияWCHAR lpCommandLine[] = L"notepad.exe";BOOL bSuccess;// Создаем объект заданияHANDLE hJob;hJob = CreateJobObjectW(NULL, NULL);// Определяем ограничение на автозавершение дочерних процессов// после завершения родительского процессаJOBOBJECT_EXTENDED_LIMIT_INFORMATION info = { 0 };info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;bSuccess = SetInformationJobObject(hJob, JobObjectExtendedLimitInformation, &info, sizeof(info));if (!bSuccess){wcout << L"Не удалось настроить ограничения для процесса:" << GetLastError() << endl;}// Подготовка объектов для запуска процессовSTARTUPINFOW startupInfo;ZeroMemory(&startupInfo, sizeof(startupInfo));startupInfo.cb = sizeof(startupInfo);PROCESS_INFORMATION processInfo = {};// Инициализация процесса в спящем режимеbSuccess = CreateProcessW(NULL, // lpApplicationName <- !!!lpCommandLine, // lpCommandLine <- !!!NULL, // lpProcessAttributesNULL, // lpThreadAttributesFALSE, // bInheritHandles// !!! Процесс создается НЕактивным !!!CREATE_SUSPENDED, // dwCreationFlagsNULL, // lpEnvironmentNULL, // lpCurrentDirectory&startupInfo,&processInfo);if (!bSuccess){wcout << L"Не удалось инициализировать процесс:" << GetLastError() << endl;}// Присваиваем новый процесс созданному заданиюbSuccess = AssignProcessToJobObject(hJob, processInfo.hProcess);if (!bSuccess){wcout << L"Не удалось применить настройки ограничений процессов:" << GetLastError() << endl;}// Разрешаем прилоежнию начать работуbSuccess = ResumeThread(processInfo.hThread);if (!bSuccess){wcout << L"Не удалось запустить работу в дочернем процессе:" << GetLastError() << endl;}assert(bSuccess);// Ждем завершение дочернего процесса 10 секундauto wait_result = WaitForSingleObject(processInfo.hProcess, 10000);if (wait_result == WAIT_TIMEOUT){// Завершаем процесс, если он еще не завершился за отведенное времяTerminateProcess(processInfo.hProcess, 1);}// Закрываем дескрипторыCloseHandle(processInfo.hThread);CloseHandle(processInfo.hProcess);return 0;}
Мы добавили инициализацию объекта задания и настроили ограничение работы процессов в нем. С помощью флага JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE мы указываем, что все связанные с объектом задания процессы должны быть завершены, если главный процесс завершился.
Особым моментом здесь является передача в метод CreateProcess флага создания процесса CREATE_SUSPENDED, который позволяет создать объект процесса в режиме ожидания. То есть процесс создан, но фактически в режиме ожидания. Если бы мы этого не сделали, то процесс начал работу сразу после создания и все последующие настройки ограничений, которые бы мы применяли, он просто бы игнорировал.
Ну и дальше остается созданный процесс связать с объектом задания через метод AssignProcessToJobObject, а после непосредственный запуск ранее созданного процесса через метод ResumeThread.
Далее уже будет знакомый Вам код с ожиданием завершения процесса и его принудительным "убийством", если за указанное время дочерний процесс не выполнил свою работу.
Но в чем отличие от предыдущих примеров? Все очень просто! Если дочерний процесс завершится по любой причине, то и все дочерние процессы также будут принудительно завершены. Об этом позаботится операционная система. Вот как это выглядит.
Теперь родительский и дочерний процесс крепко связаны! Если завершить родительский процесс - то завершится и дочерний. Это нам и было нужно!
Конец игры
Конечно, мы рассмотрели лишь базовый функционал в части контроля процессов. WinAPI позволяет намного больше!
Например. с помощью объектов задания можно ограничить потребление ресурсов центрального процессора, задать приоритет работы процесса и многое другое. Все это можно посмотреть в официальной документации, ссылки на которую были выше для каждого из методов WinAPI.
В статье Берем процессы под контроль в .NET мы рассматривали возможности контроля процессов при работе из платформы .NET (C#), но упор делали на штатные возможности .NET. Но в целом мы можем и в управляемой среде использовать контроль процессов через объекты задания.
Например, есть .NET-библиотека ChildProcess, которая позволяет реализовать такой же контроль дочерних процессов, что и мы делали в примере с объектами заданий. На C# это будет выглядеть примерно так:
var si = new ChildProcessStartInfo("notepad.exe"){StdOutputRedirection = OutputRedirection.OutputPipe,// Works like 2>&1StdErrorRedirection = OutputRedirection.OutputPipe,// DisableArgumentQuoting: See ChildProcessExamplesWindows.cs for detailsFlags = ChildProcessFlags.DisableArgumentQuoting,};using (var p = ChildProcess.Start(si)){using (var sr = new StreamReader(p.StandardOutput)){// "foo"Console.Write(await sr.ReadToEndAsync());}await p.WaitForExitAsync();// ExitCode: 0Console.WriteLine("ExitCode: {0}", p.ExitCode);}
В части реализации для Windows (а сама библиотека позволяет работать и в других операционных системах) для контроля дочерних процессов как-раз и используется объекты заданий, которые мы "щупали" выше. Вот этот самый модуль.
Итог - что в C++, что в C# для эффективного контроля дочерних процессов задействованы одни и те же объекты операционной системы и методы API, просто разные уровни абстракции.
В любом случае, теперь у вас есть пример контроля процессов под C++ и знания для применения этих механизмов и в других языках и платформах.
Спасибо за интерес к теме и удачи в делах!
Полезные ссылки
- WinAPI CreateProcess - метод для создания процессов WinAPI.
- WinAPI CreateJobObject - метод для объектов заданий WinAPI для контроля процессов.
- ChildProcess - библиотека .NET для контроля дочерних процессов.
- system vs CreateProcess vs ShellExecute - сравнение методов WinAPI для запуска процессов и команд операционной системы.
- Ways to Print and Capture Text Output of a Process - способы получения вывода на консоль от запущенных дочерних процессов.
- Destroying all child processes (and grandchildren) when the parent exits - краткий пример по уничтожению дочерних процессов любой вложеннсоит при завершении родительского процесса.