ГЛАВА 5 Задания

Гpynny процессов зачастую нужно рассматривать как единую сущность. Например, когда Вы командуете Microsoft Developer Studio собрать проект, он порождает про цесс Ct.exe, а тот в свою очередь может создать другие процессы (скажем, для допол нительных проходов компилятора) Но, если Вы пожелаете прервать сборку, Developer Studio должен каким-то образом завершить C1.exe и все его дочерние процессы. Ре шение этой простой (и распространенной) проблемы в Windows было весьма затруд нительно, поскольку она не отслеживает родственные связи между процессами. В ча стности, выполнение дочерних процессов продолжается даже после завершения ро дительского

При разработке сервера тоже бывает полезно группировать процессы. Допустим, клиентская программа просит сервер выполнить приложение (которое создает ряд дочерних процессов) и сообщить результаты Поскольку к серверу может обратиться сразу несколько клиентов, было бы неплохо, если бы он умел как-то ограничивать ресурсы, выделяемые каждому клиенту, и тем самым не давал бы одному клиенту мо нопольно использовать все серверные ресурсы. Под ограничения могли бы подпадать такие ресурсы, как процессорное время, выделяемое на обработку клиентского зап роса, и размеры рабочего набира (working set). Кроме того, у клиентской программы не должно быть возможности завершить работу сервера и т д.

В Wmdows 2000 введен новый объект ядра — задание job). Он позволяет группи ровать процессы и помещать их в нечто вроде песочницы, которая определенным образом ограничивает их действия. Относитесь к этому объекту как к контейнеру процессов Кстати, очень полезно создавать задание и с одним процессом — это по зволяет налагать на процесс- ограничения, которые иначе указать нельзя

Взгляните на мою функцию StartRestrictedProcess (рис. 5-1). Она включает процесс в задание, которое ограничивает возможность выполнения определенных операций

WININDOWS 98
Windows 98 не поддерживает задания.

void StartRestictedProcess() {
// создаем объект ядра "задание" HANDLE hjob = CreateJobObject(NULL, NULL);

// вводим oграничения для процессов в задании
// сначала определяем некоторые базовые ограничения
JOBOBJECT_BASIC_LIMIT_INFORMATION jobli = { 0 };

// процесс всегда выполняется с классом приоритета idle
jobli.PriontyClass = IDLE_PRIORITY_CLASS;

// задание не может использовать более одной секунды процессорного времени
jobli.PerJobUserTimeLimit.QuadPart = 10000000;

// 1 секунда, выраженная в 100-наносекундных интервалах

// два ограничения, которые я налагаю на задание (процесс)
jobli.LimitFlags = JOB_OBJECT_LIMIT_PRIORITY_CLASS | JOB_OBJECT_LIMIT_JOB_TIME;
SetInforrnationJobObject(hjob, JobOb]ectBasicLimitInformation, &jobli, sizeof(jobli));

// теперь вводим некоторые ограничения по пользовательскому интерфейсу

JOBOBJECT_BASIC_UI_RESTRICTIONS jobuir;

jobuir.UTRestrictionsClass = JOB_OBJECT_UILIMIT_NONE;
// "замысловатый" нуль

// процесс не имеет права останавливать систему
jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_EXITWINDOWS;

// процесс не имеет права обращаться к USER-объектам в системе (например, к другим окнам)

jobuir.UIRestrictionsClass |= JOB_OBJECT_UILIMIT_HANDLES;

SetInrormationJobObject(hjob, JobObjectBasicUIRestrictions, &jobuir, sizeof(jobuir));

// Порождаем процесс, который будет размещен в задании.

// ПРИМЕЧАНИЕ: процесс нужно сначала создать и только потом поместить
// в задание А это значит, что поток процесса должен быть создан
// и тут же приостановлен, чтобы он не смог выполнить какой-нибудь код
// еще до введения ограничений,

STARTUPTNFO si = { sizeof(si) };

PROCESS_INFORMATION pi;

CreatePiocess(NULL, "CMD", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
// Включаем процесс в задание

// ПРИМЕЧАНИЕ, дочерние процессы, порождаемые этим процессом,
// автоматически становятся частью того же задания.
AssignProcessToJobObject(hjob, pi hProcess);

// теперь потоки дочерних процессов могут выполнять код

ResumeThread(pi.hThread);

CloseHandle(pi.hThread);

// ждем, когда процесс завершится или будет исчерпан
// лимит процессорного времени, указанный для задания
HANDLE h[2];
h[0] = pi.hProcess;
h[1] = hjob;

DWORD dw = WaitForMultipleObjects(2, h, FALSE, INFINITE);

switch (dw - WAIT_OBJECT_0){

case 0: // процесс завершился.,
break;

case 1:
// лимит процессорного времени исчерпан
break;

}

// проводим очистку
CloseHandle(pi hProcess), CloseHandle(hjob);

}

Рис. 5-1. Функция StartRestrictedProcess

А теперь я объясню, как работает StartRestrictedProcess. Сначала я создаю новый объект ядра «задание», вызывая:

HANDLE CreateJobObject( PSECURITY_ATTRIBUTES psa, PCTSTR pszName);

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

HANDLE OpenJobObject( DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName);

Закончив работу с объектом-заданием, закройте сго описатель, вызвав, как всегда, CloseHandle Именно так я и делаю в конце своей функции StartRestrictedProcess Имейте в виду, что закрытие объекта-задания не приводит к автоматическому завершению всех его процессов. На самом деле этот объект просто помечается как подлежащий разрушению, и система уничтожает его только после завершения всех включенных в него процессов

Заметьте, что после закрытия описателя объект-задание становится недоступным для процессов, даже несмотря на то что объект все еще существует Этот факт иллю стрирует следующий код:

// создаем именованный объект-задание
HANDlF hjob = CreateJobObject(NULL, TEXT("Jeff"));

// включаем в него наш процесс
AssignProcessToJobObject(hjob, GetCurrentProcess());

// закрытие обьекта-задания не убивает ни наш процесс, ни само задание,
// но присвоенное ему имя ('Jeff') моментально удаляется

CloseHandle(hjob);

// пробуем открыть существующее задание

hjob = OpenJobObject(JOB_OBJECT_ALL_ACCESS, FALSE, TEXT("Jeff"));

// OpenJobOb]ect терпит неудачу и возвращает NULL, поскольку имя ('Jeff")

// уже не указывает на объект-задание после вызова CloseHandle; // получить описатель этого объекта больше нельзя

Определение ограничений, налагаемых на процессы в задании

Создав задание, Вы обычно строите "песочницу" (набор ограничений) для включае мых в него процессов. Ограничения бывают нескольких видов:

Ограничения на задание вводятся вызовом:

BOOL SetInformationJobObject( HANDLE hJob, JOBOBJECTINFOCLASS JobObjectInformationClass, PVOID pJobObjectTnformation, DWORD cbJobObjectInformationLength);

Первый параметр определяет нужное Вам задание, второй параметр (перечисли мого типа) — вид ограничений, третий — адрес структуры данных, содержащей под робную информацию о задаваемых ограничениях, а четвертый — размер этой струк туры (используется для указания версии). Следующая таблица показывает, как уста навливаются ограничения.

Вид ограничений

Значение второго параметра

Структура, указываемая в третьем параметре

Базовые ограничения

JobObjectBasicLimitInformation

JOBOBJECT_BASIC_ LIMIT_INFORMATION

Расширенные базовые ограничения

JobObjectExtendedLimitInformation

JOBOBJECT_EXTENDED_ LIMIT_INFORMATION

Базовые ограничения по ользовательскому интерфейсу

JobObjectBasicUIRestrictions

JOBOBJECT_BASIC UI_RESTRICTIONS

Ограничения, связанные с защитой

JobObjectSecurityLimitInformation

JOBOBJECT_SECURITY_ LIMIT_INFORMATION

В функции StartRestrictedProcess я устанавливаю для задания лишь несколько базовых ограничений. Для этого я создаю структуру JOB_OBJECT_BASIC_LIMIT_INFOR MATION, инициализирую ее и вызываю функцию SetInformationJobObject. Данная струк тура выглядит так:

typedef struct _JOBOBJECT_BASIC_LIMIT_INFORMATION
{
LARGE_INTEGER PerProcessUserTimeLimit;
LARGE_INTEGER PorJobUserTimeLimit;
DWORD LimitFlags;
DWORD MinimumWorkingSetSize;

DWORD MaximumWorkingSetSize;
DWORD ActiveProcessLimit;
DWORD^PTR Affinity;
DWORD PriorityClass;
DWORD SchedulingClass;
} JOBOBJECT_BASIC_LIMIT_INFORMATION, *PJOBOBJECT_BASIC_LIMIT_INFORMATION;

Все элементы этой структуры кратко описаны в таблице 5-1.

Элементы

Описание

Примечание

PerProcessUserTtmeLimit

Максимальное время в пользова тельском режиме, выделяемое каждому процессу (в порциях по 100 нс)

Система автоматически завершает любой процесс, который пытается использовать больше обведенного времени. Это ограничение вводится флагом JOB OBJECT LIMIT PROCESS_TIME в LimitFlags

PerJobUserTimeLimit

Максимальное время в пользова тельском режиме для всех процессов в данном задании (в порциях по 100 нс)

По умолчанию система автомати чески завершает все процессы, когда заканчивается это время Данное значение можно изменять в процес ее выполнения задания. Это ограничение вводится флагом JOB_OBJFCT_LIMIT_JOB_TIME в LimitFlags

LimitFlags

Виды ограничений для задания

См раздел после таблицы.

MinimumWorkingSetSize и MaximumWorkingSetSize

Верхний и нижний предел рабочего набора для каждого процесса (а не для всех процессов в задании)

Обычно рабочий набор процесса может расширяться за стандартный предел; указав MaximumWorkingSetSize, Вы введете жесткое ограничение. Когда размер рабочего набора какого-либо процесса достигнет заданного предела, процесс начнет сбрасывать свои страницы на диск.
Вызовы функции SetProcessWorkingSetSize этим процессом будут игнорироваться, если только он не обра щается к ней для того, чтобы очистить свой рабочий набор. Это ограничение вводится флагом JOB_OBJECT_LIMIT_WORKINGSET в LimitFlags.

ActiveProcessLimit

Максимальное количество процессов, одновременно выполняемых в задании

Любая попьпка обойти такое ограничение приведет к завершению нового процесса с ошибкой "not enough quota" ("превышение квоты") Это ограничение вводится флагом JOB_OBJECT_LIMIT_ACTIVE_ PROCESS в LimitFlags.

Affinity

Подмножество процессоров, на которых можно выполнять процессы этого задания

Для индивидуальных процессов это ограничение можно еще больше детализировать. Вводится флагом JOB_OBJECT_LIMIT AFFINITY в LimitFlags.

PriorityClass

Класс приоритета для всех процессов в задании

Вызванная процессом функция SetPriorityClass сообщает об успехе даже в том случае, если на самом деле она не выполнила свою задачу, a GetPriorityClass возвращает класс приоритета, каковой и пытался уста новить процесс, хотя в реальности его класс может быть совсем другим. Кроме того, SetThreadPriority не может поднять приоритет потоков выше normal, но позволяет понижать его. Это ограничение вводится флагом JOB_OBJECT_LIMIT_ PRIORITY_CLASS в LimitFlags.

SchedulingClass

Относительная продолжительность кванта времени, выделяемого всем потокам в задании

Этот элемент может принимать значения от 0 до 9; по умолчанию устанавливается 5. Подробнее о его назначении см. ниже. Это ограниче ние вводится флагом JOB_OBJECT_ LIMIT_SCHEDULING_CLASS в LimitFlags.

Таблица 5-1. Элементы структуры JOBOBJECT_BASIC_LIMIT_INFORMATION

Хочу пояснить некоторые вещи, связанные с этой структурой, которые, по-моему довольно туманно изложены в документации Platform SDK, Указывая ограничения для задания, Вы устанавливаете те или иные биты в элементе LimitFlags. Например, в StartRestrictedProcess я использовал флаги JOB_OBJECT_LIMIT_PRIORITY_CLASS и JOB_ OBJECT_LIMIT_JOB_TIME, т. e. определил всего два ограничения.

При выполнении задание ведет учет по нескольким показателям — например, сколько процессорного времени уже использовали его процессы. Всякий раз, когда Вы устанавливаете базовые ограничения с помощью флага JOB_OBJECT_LIMIT_JOB_ TIME, из общего процессорного времени, израсходованного всеми процессами, вы читается то, которое использовали завершившиеся процессы. Этот показатель сооб щает, сколько процессорного времени израсходовали активные на данный момент процессы, А что если Вам понадобится изменить ограничения на доступ к подмно жеству процессоров, не сбрасывая при этом учетную информацию по процессорно му времени? Для этого Вы должны ввести новое базовое ограничение флагом JOB_OB JECT_LIMIT_AFFINITY и отказаться от флага JOB_OBJECT_LIMIT_JOB_TIME. Но тогда получится, что Вы снимаете ограничения на процессорное время.

Вы хотели другого: ограничить доступ к подмножеству процессоров, сохранив существующее ограничение на процессорное время, и не вычитать время, израсхо дованное завершенными процессами, из общего времени. Чтобы решить эту пробле му, используйте специальный флаг JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME. Этот флaг и JOB_OBJECT_LIMIT_JOB_TIME являются взаимоисключающими. Флаг JOB_OB JECT_LIMIT_PRESERVE_JOB_TIME указывает системе изменить ограничения, не вычи тая процессорное время, использованное уже завершенными процессами.

Обсудим также элемент SchedulingCtoss структуры JOBOBJECT_BASIC_LIMIT_INFOR MATION. Представьте, что для двух заданий определен класс приоритета NORMAL_ PRIORITY_CLASS, а Вы хотите, чтобы процессы одного задания получали больше про цессорного времени, чем процессы другого. Так вот, элемент SchedulingClass позволя ет изменять распределение процессорного времени между заданиями с одинаковым

классом приоритета. Вы можете присвоить ему любое значение в пределах 0-9 (по умолчанию он равен 5). Увеличивая сго значение, Вы заставляете Windows 2000 вы делять потокам в процессах конкретного задания более длительный квант времени, а снижая — напротив, уменьшаете этот квант,

Допустим, у меня есть два задания с обычным (normal) классом приоритета: в каждом задании — по одному процессу, а в каждом процессе — по одному потоку (тоже с обычным приоритетом). В нормальной ситуации эти два потока обрабатыва лись бы процессором по принципу каруссли и получали бы равные кванты процес сорного времени. Но если я запишу в элемент SchedulingClass для первого задания значение 3, система будет выделять его потокам более короткий квант процессорно го времени, чсм потокам второго задания.

Используя SchedulingClass, избегайте слишком больших его значений, иначе Вы замедлите общую реакцию других заданий, процессов и потоков на ка-кие-либо со бытия в системе. Кроме того, учтите, что все сказанное здесь относится только к Windows 2000. В будущих версиях Windows планировщик потоков предполагается существенно изменить, чтобы операционная система могла более гибко планировать потоки в заданиях и процессах.

И последнее ограничение, которое заслуживает отдельного упоминания, связано с флагом JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION. Он отключает для всех процессов в задании вывод диалогового окна с сообщением о необработанном исключении. Система реагирует на этот флаг вызовом SetErrorMode с флагом SEM_NOG PFAULTERRORBOX для каждого из процессов в задании Процесс, в котором возник нет необрабатываемое им исключение, немедленно завершается бсз уведомления пользователя. Этот флаг полезен в сервисных и других пакетных заданиях. В его от сутствие один из процессов в задании мог бы вызвать исключение и не завершиться, впустую расходуя системные ресурсы.

Помимо базовых ограничений, Вы можстс устанавливать расширенные, для чего применяется структура JOBOBJECT_EXTENDED_LIMIT_INFORMATION:

typedef struct _JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
IO_COUNTERS Iolnfo;
SIZE_T Proces&MemoryLimit;
SIZE_T JobMemoryLimit;
SIZE_T PeakProcessMemoryUsed;
SIZE_T PeakJobMemoryUsed;
} JOBOBJECT_EXTENDED_LIMIT_INFORHATION, *PJOBOBJECT_EXTENDED LIMIT_INFORMATION;

Как видите, она включает структуру JOBOBJECT_BASIC_LIMIT_INFORMATION, яв ляясь фактически ее надстройкой, Это несколько странная структура, потому что в ней есть элементы, не имеющие никакого отношения к определению ограничений для задания. Во-первых, элемент IoInfo зарезервирован, и Вы ни в коем случае не дол жны обращаться к нему. О том, как узнать значение счетчика ввода-вывода, я расска жу позже Кроме того, элементы PeakProcessMemoryUsed и PeakJobMemoryUsed пред назначены только для чтения и сообщают о максимальном объеме памяти, передан ной соответственно одному из процессов или всем процессам в задании.

Остальные два элемента, ProcessMemoryLimit и JobMemoryLimit, ограничивают со ответственно объем переданной памяти, который может быть использован одним из процессов или всеми процессами в задании Чтобы задать любое из этих ограниче ний, укажите в элементе LimitFlags флаг JOB_OBJECT_LIMIT_JOB_MEMORY или JOB_OB JECT_LIMIT_PROCESS_MEMORY.

А теперь вернемся к прочим ограничениям, которые можно налагять на задания. Структура JOBOBJECT_BASIC_UI_RESTRICTIONS выглядит так:

typedef struct _JOBOBJECT_BASIC_UI_RESTRICTIONS
{
DWORD UIRestrictionsClass;
} JOBOBJECT_BASIC_UI_RESTRICTIONS, *PJOBOBJECT_BASIC_UI_RESTRICTIONS;

В этой структуре всего один элемент, UIRestrictionsClass, который содержит набор битовых флагов, кратко описанных н таблице 5-2.

Флаг

Описание

JOB_OBJECT_UILIMIT_EXITWINDOWS

Запрещает выдачу команд из процессов на выход из системы, завершение ее работы, перезагрузку или выключение компьютера через функцию ExitWindowsEx

JOB_OBJECT_UILIMIT_READCLIPBOARD

Запрещает процессам чтение из буфера обмена

JOB_OBJECT_UILIMIT_WRITECLIPBOARD

Запрещает процессам стирание буфера обмена

JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS

Запрещает процессам изменение системных параметров через SystemParametersInfo

JOB_OBJECT_UILIMIT DISPLAYSETTINGS

Запрещает процессам изменение параметров экрана через ChangeDisplaySettings

JOB_OBJECT_UILIMIT_GLOBALATOMS

Предоставляет заданию отдельную глобаль ную таблицу атомарного доступа (global atom table) и разрешает его процессам пользоваться только этой таблицей

JOB_OBJECT_UILIM1T_DESKTOP

Запрещает процессам создание новых рабочих столов или переключение между ними через функции CreateDesktop или SwitchDesktop

JOB_OBJECT_UILIMIT HANDLES

Запрещает процессам в задании использо вать USER-объекты (например, HWND), созданные внешними по отношению к этому заданию процессами

Таблица 5-2. Битовые флаги базовых ограничений по пользовательскому интерфейсу дпя объекта-задания

Последний флaг, JOB_OBJECT_UILIMIT_HANDLES, представляет особый интерес: он запрещает процессам в задании обращаться к USER-объектам, созданным внешними по отношению к этому заданию процессами. Так, запустив утилиту Microsoft Spy++ из задания, Вы не обнаружите никаких окон, кроме тех, которые создаст сама Spy++. Ha рис. 5-2 показано окно Microsoft Spy++ с двумя открытыми дочерними MDI-окнами. Заметьте, что в левой секции (Threads 1) содержится список потоков в системе. Ка жется, что лишь у одного из них, 000006АС SPYXX, есть дочерние окна. А все дело в том, что я запустил Microsoft Spy++ из задания и ограничил ему права па использова ние описателей USER-объектов. В том же окне сообщается о потоках MSDEV и EXPLO RER, но никаких упоминаний о созданных ими окнах нет. Уверяю Вас, эти потоки наверняка создали какие-нибудь окна — просто Spy++ лишена возможности их ви деть. В правой секции (Windows 3) утилита Spy++ должна показывать иерархию окон на рабочем столе, но там нет ничего, кроме одного элемента — 00000000. (Это не настоящий элемент, но Spy++ была обязана поместить сюда хоть что-нибудь.)

Обратите внимание, что такие oграничения односторонни, т e. внешние процес сы все равно видят USER-объекты, которые созданы процессами, включенными в за дание. Например, если запустить Notepad в задании, a Spy++ — внс сго, последняя увидит окно Notepad, даже если для задания указан флаг JOB_OBJECT_UILIMIT_HAND LES Кроме того, Spy++, запущенная в отдельном задании, все равно увидинт окно Notepad, если только для ее задания не установлен флаг JOB_OBJECT_UILIMIT_HAN DLES.

h5-1.jpg

Рис. 5-2. Microsoft Spy++ работает в задании, которому ограничен доступ к описателям USER-объектов

Ограничение доступа к описателям USER-объектов — вещь изумительная, если Вы хотите создать по-настоящему безопасную песочницу, в которой будут «копаться" процессы Вашего задания. Однако часто бывает нужно, чтобы процесс в задании взаимодействовал с внешними процессами. Одно из самых простых решений здесь — использовать оконные сообщения, но, ссли процессам в задании доступ к описате лям пользовательского интерфейса запрещен, ни один из них не сможет послать со общение (синхронно или асинхронно) окну, созданному внешним процессом К счастью,теперь есть функция, которая поможет решить эту проблему:

BOOL UserHandleGrantAccess( HANOIF hUserObj, HANDLE hjob, BOOL fGrant);

Параметр hUserObj идентифицирует конкретный USER-объект, доступ к которому Вы хотите предоставить или запретить процессам в задании. Это почти всегда опи сатель окна, но USER объектом может быть, например, рабочий стол, программная ловушка, ярлык или меню Последние два параметра, hjob и fGrant, указывают на зада ние и вид ограничения. Обратите внимание, что функция не сработает, если ее выз вать из процесса в том задании, на которое указывает hjob, — процесс нс имеет права сам себе предоставлять доступ к объекту.

И последний вид ограничений, устанавливаемых для задания, относится к защи те. (Введя в действие такие ограничения, Вы не сможете их отменить) Структура JOBOBJECT_SECURITY_LIMIT_INFORMATION выглядит так.

typedef struct _JOBOBJECT_SECURITY_LIMIT_INFORMATION
{
DWORD SecurityLimitFlags;
HANDLE JobToken;
PTOKEN GROUPS SidsToDisable;
PTOKEN_PRIVILEGES PrivilegesToDelete;
PTOKEN_GROUPS RestrictedSids;
} JOBOBJECT_SECURITY LIMIT_INFORMATION, *PJOBOBJECT_SECURITY_LIMIT_INFORMATION;

Ее элементы описаны в следующей таблице

Элемент

Описание

SecurityLimitFlags

Набор флагов, которые закрывают доступ администратору, запре щают маркер неограниченного доступа, принудительно назначают заданный маркер доступа, блокируют доступ по каким-либо иден тификаторам защиты (security ID, SID) и отменяют указанные при вилегии

JobToken

Маркер доступа, связываемый со всеми процессами в задании

SidsToDisable

Указывает, по каким SID не разрешается доступ

PrivilegesToDelete

Определяет привилегии, которые снимаются с маркера доступа

RestrictedSids

Задает набор SID, по которым запрещается доступ к любому защи щенному объекту (deny-only SIDs); этот набор добавляется к марке ру доступа

Естественно, если Вы налагаете ограничения, то потом Вам, наверное, понадобится информация о них. Для этого вызовите:

BOOL QueryInformationJobObject( HANDLE hJob, JOBOBJECTINFOCLASS JobObjectInformationClass. PVOID pvJobObjectInformation, DWORD cbJobObjectInformationLength, PDWORD pdwReturnLength);

В эту функцию, как и в SetInformationJobObject, передается описатель задания, пе ременная перечислимого типа JOBOJECTINFOCLASS. Она сообщает информацию об ограничениях, адрес и размер структуры данных, инициализируемой функцией. Пос ледний параметр, pdwReturnLength, заполняется самой функцией и указывает, сколь ко байтов помещено в буфер Если эти сведения Вас не интересуют (что обычно и бывает), передавайте в этом параметре NULL.

NOTE:
Процесс может получить информацию о своем задании, передав при вызове QuerylnformationJobObject вместо описателя задания значение NULL, Это позво лит ему выяснить установленные для него ограничения Однако аналогичный вызов SetInformationJobOtject даст ошибку, так как процесс не имеет права са мостоятельно изменять заданные для него ограничения

Включение процесса в задание

О'кэй, с ограничениями па этом закончим Вернемся к StartRestrictedProcess. Устано вив ограничения для задания, я вызываю CreateProcess и создаю процесс, который помещаю в это задание. Я использую здесь флаг CREATE_SUSPENDED, и он приводит к тому, что процесс порождается, но код пока не выполняет. Поскольку StartRestricted Process вызывается из процесса, внешнего по отношению к заданию, его дочерний

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

BOOL AssignProcessToJobObject( HANDLE hJob, HANDLE hProcess);

Эта функция заставляет систему рассматривать процесс, идентифицируемый па раметром hProcess, как часть существующего задания, на которое указывает hJob. Об ратите внимание, что AssignProcessToJobObject позволяет включить в задание только тот процесс, который еще не относится ни к одному заданию. Как только процесс стал частью какого-нибудь задания, его нельзя переместить в другое задание или отпус тить на волю. Кроме того, когда процесс, включенный в задание, порождает новый процесс, последний автоматически помещается в то же задание. Однако этот поря док можно изменить.

Что касается StartRestrictedProcess, то после вызова AssignProcessToJobObject новый процесс становится частью задания. Далее я вызываю ResumeThread, чтобы поток нового процесса начал выполняться в рамках ограничений, установлепных для зада ния. В этот момент я также закрываю описатель потока, поскольку он мне больше не нужен.

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

Уверен, именно это Вы и будете делать чаще всего. В начале главы я упомянул о том, как непросто остановить сборку в Developer Studio, потому что для этого ему должны быть известны все процессы, которые успел создать его самый первый процесс. (Это очень каверзная задача. Как Developer Studio справляется с ней, я объяснял в своей колонке «Вопросы и ответы по Win32» в июньском выпуске Microsoft Systems Journal за 1998 год.) Подозреваю, что следующие версии Developer Studio будут использовать механизм заданий, и решать задачу, о которой мы с Вами говорили, станет гораздо легче.

Чтобы уничтожить все процессы в задании, Вы просто вызываете

BOOL TerminateJobOb]ect( HANDLE hJob, UINT uExitCode);

Вызов этой функции похож на вызов TerminateProcessw для каждого процесса в за дании и присвоение всем кодам завершения одного значения — uExitCode.

Получение статистической информации о задании

Мы уже обсудили, как с помощью QueryInformationJobObject получить информацию о текущих ограничениях, установленных для задания. Этой функцией можно пользо ваться и для получения статистической информации. Например, чтобы выяснить ба зовые учетные сведения, вызовите ее, передав JobObjeсtBasicAccountingInformation во втором параметре и адрес структуры JOBOBJECT_BASIC_ACCOUNTING_INFORMATION:

typedef struct _JOBOBJECT_BASIC_ACCOUNTING_INFORMATION
{
LARGE_INTEGER TotalUserTime;
LARGE_INTEGER TotalKernelTime;
LARGE_INTEGER ThisPeriodTotalUserTime;
LARGE_INTEGER ThisPeriodTotalKernelTime;
DWORD TotalPageFaultCount;
DWORD TotalProcesses;
DWORD ActiveProcesses;
DWORD TotalTerminatedProcesses;
} JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, *PJOBOBJECT_BASIC_ACCOUNTING_INFORMATION;

Элементы этой структуры кратко описаны в таблице 5-3

Элемент

Описание

TotalUserTtme

Процессорное время, израсходованное процессами задания в пользовательском режиме

TotalKernelTime

Процессорное время, израсходованное процессами задания в режиме ядра

ThisPeriodTotalUserTime

То же, что TotalUserTime, но обнуляется, когда базовые oгpa ничения изменяются вызовом SetIniformationJobObject, а флаг JOB OBJECT_LIMIT_PRESERVE_JOB_TIME не используется

ThisPeriodTotalKernelTime

То же, что ThisPeriodTotalUserTime, но относится к процессор ному времени, израсходованному в режиме ядра

TotalPageFaultCount

Общее количество ошибок страниц, вызванных процессами задания

TotalProcesses

Общее число процессов, когда-либо выполнявшихся в зтом задании

ActiveProcesses

Текущее количество процессов в задании

TotalTermtnatedProcesses

Количество процессов, завершенных из-за превышения ими отведенного лимита процессорного времени

Таблица 5-3. Элементы структуры JOBOBJECT_BASIC_ACCOUNTING_INFORMATION

Вы можете извлечь те же сведения вместе с учетной информацией по вводу-выво ду, передав JobObjectBasicAndIoAccountingInformation во втором параметре и адрес структуры JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION:

typedef struct JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION
{
JOBOBJECT_BASIC_ACCOUNTING_TNFORMATION Basiclnto;
IO_COUNTERS IoInfo;
} JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION;

Как видите, она просто возвращает JOBOBJECT_BASIC_ACCOUNTlNG_INFORMA TION и IO_COUNTERS. Последняя структура показана на следующей странице

typedef struct _IO_COUNTERS
{
ULONGlONG ReadOperationCount;
ULONGLONG WriteOperationCount;
ULONGLONG OtherOperationCount;
ULONGLONG ReadTransferCount;
ULONGLONG WriteTransferCount;
ULONGLONG OtheiTransferCount;
} IO_COUNTERS;

Она сообщает о числе операций чтения, записи и перемещения (а также о коли честве байтов, переданных при выполнении этих операций) Данные относятся ко всем процессам в задании Кстати, новая функция GetProcessIoCounters позволяет по лучить ту же информацию о процессах, не входящих ни в какие задания

BOOL GetProcessIoCounters( HANDLE hProcess, PIO_GOUNTERS pToCounters);

QueryInformationJobObject такжe возвращает набор идентификаторов текущих про цессов в задании Но перед этим Вы должны прикинуть, сколько их там может быть, и выделить соответствующий блок памяти, где поместятся массив идентификаторов и структура JOBOBJECT_BASIC_PROCESS_ID_LIST

typedef struct _JOBOBJECT_BASIC_PROCESS_ID_LIST
{
DWORD NumberOfAssigncdProcessps;
DWORD NurrberOfProcessIdsInList;
DWORD ProcessIdList[1];
} JOBOBJECT_BASIC_PROCESS_ID_LIST, *PJOBOBJECT_BASIC_PROCESS_ID_LIST ;

В итоге, чтобы получить набор идентификаторов текущих процессов в задании, нужно написать примерно такой код

void EnumProcessIdsInJob(HANDLE hjob)
{

// я исхожу из того, что количество процессов
// в этом задании никогда не превысит 10
#define MAX_PROCESS_TDS 10

// определяем размер блока памяти (в байтах)
// для хранения идентификаторов и структуры
DWORD cb = sizeof(JOBOBJECT_BASlC_PROCESS_ID LIST) + (MAX_PROCESS_IDS - 1) * sizeof(DWORD);

// выделяем этот блок памяти
PJOBOBJECT_BASIC_PROCESS_ID_LIST pjobpil = _alloca(cb);

// сообщаем функции, на какое максимальное число процессов
// рассчитана выделенная нами память pjobpil->NumberOfAssignedProcesseb = MAX_PROCESS_IDS;

// запрашиваем текущий список идентификаторов процессов
QuerylnformationJobObject(hjob, JobObjectBasicProcessIdList pjobpil, cb &cb);

// перечисляем идентификаторы процессов
for (int x =- 0; x < pjobpil->NumberOfProcessIdsInList; x++)
{

// используем
pjobpil->ProcessIdList[x]
}

// так как для выделения памяти мы вызывали _alloca,
// освобождать память нам не потребуется
}

Вот и все, что Вам удастся получить через эти функции, хотя на самом деле опе рационная система знает о заданиях гораздо больше. Эту информацию, которая хра нится в специальных счетчиках, можно извлечь с помощью функций из библиотеки Performance Data Helper (PDH dIl) или через модуль Performance Monitor, подключае мый к Microsoft Management Console (MMC) Рис 5-3 иллюстрирует некоторые из доступных в системе счетчиков заданий (job object counters), а рис. 5-4 — счетчики, относящиеся к отдельным параметрам заданий (job object details counters) Заметьте, что в чадании Jeff содержится четыре процесса calc, cmd, notepad и wordpad.

h5-2.jpg

Рис. 5-3. MMC Performance Monitor счетчики задания

h5-3.jpg

Рис. 5-4. MMC Performance Monitor счетчики, относящиеся к отдельным параметрам задания

Извлечь сведения из этих счетчиков Вы сможете только для тех заданий, которым были присвоены имена при вызове CreateJobObject. По этой причине, наверное, луч ше всегда именовать задания, даже если Вы и не собираетесь ссылаться на них по именам из других процессов.

Уведомления заданий

Итак, базовые сведения об объектах-заданиях я изложил. Единственное, что осталось рассмотреть, — уведомления Допустим, Вам нужно знать, когда завершаются все про цессы в задании или заканчивается все отпущенное им процессорное время. Либо выяснить, когда в задании порождается или уничтожается очередной процесс Если такие уведомления Вас не интересуют (а во многих приложениях они и не нужны), работать с заданиями будет очень легко — не сложнее, чем я уже рассказывал. Но если они все же понадобятся, Вам придется копнуть чуть поглубже

Информацию о том, все ли выделенное процессорное время исчерпано, получить нетрудно. Объекты-задания не переходят в свободное состояние до тех пор, пока их процессы нс израсходуют отведенное процессорное время Как только оно заканчи вается, система уничтожает всс процессы в задании и переводит его объект в свобод ное состояние (signaled scate). Это событие легко перехватить с помощью WaitFor SingleObject (или похожей функции). Кстати, потом Вы можете вернуть объект-зада ние в состояние «занято" (nonsignaled state), вызвав SetInformationJobObject и выделив емудополншельное процессорное время.

Когда я только начинал разбираться с заданиями, мне казалось, что объект-зада ние должен переходить в свободное состояние после завершения всех его процес сов. В конце концов, прекращая свою работу, объекты процессов и потоков освобож даются, то же самое вроде бы должно происходить и с заданиями. Нo Microsoft пред почла сделать по-другому объект-задание переходит в свободное состояние после того, как исчерпает выделенное ему время Поскольку большинство заданий начина ет свою работу с одним процессом, который существует, пока не завершатся все eго дочерние процессы, Вам нужно просто следить за описателем родительского процес са — он освободится, как только завершится все задание. Моя функция StartRestricted Зrocess как раз и демонстрирует данный прием

Но это были лишь простейшие уведомления — более «продвинутые", например о создании или разрушении процесса, получать гораздо сложнее. В частности, Вам придется создать объект ядра «порт завершения ввода-вывода" и связать с ним объект или объекты «задание". После этого нужно будет перевести один или больше пото ков в режим ожидания порта завершения.

Создав порт завершения ввода-вывода. Вы сопоставляете с ним задание, вызывая SetInformationJobObject следующим образом:

JOBOBJECT_ASSOCIATE_COMPLETION_PORT joacp;

joacp.CompletionKey = 1;
// любое значение, уникально идентифицирующее это задание

joacp.CompletionPort = hIOCP;
// описатель порта завершения, принимающего уведомления

SetInformationJobObject(hJob, JobObjectAssociateCompletionPortInforrration, &jоаср, sizeof(joacp))

После выполнения этого кода система начнет отслеживать задание и при возник новении событий передавать их порту завершения. (Кстати, Вы можете вызывать QueryInformationJobQbjectw получать ключ завершения и описатель порта, по врядли

это Вам когда-нибудь понадобится ) Потоки следят за портом завершения ввода-вы вода, вызывая GetQueuedCompletionStatus.

BOOL GetQueuedCompletionStatus( HANDLE hIOCP, PDWORD pNumBytesTransferred, PULONG_PTR pCorripletionKey, POVERLAPPED *pOverlapped, DWORD dwMilliseconds);

Когда эта функция возвращает уведомление о событии задания, *pCompletionKey содержит значение ключа завершения, заданное при вызове SetInformationJobObjett для связывания задания с портом завершения По нему Вы узнаете, в каком из заданий возникло событие Значение в *pNumBytesTransferred указывет какое именно собы тие произошло (таблица 5-4). В зависимости от конкретного события в *pOverlapped может возвращаться идентификатор процесса.

Событие

Описание

JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO

В задании нет работающих процессов

JOB_OBJECT_MSG_END_OF_PROCESS_TIME

Процессорное время, выделенное процессу, исчерпано, процесс завершается, и сообщается его идентификатор

JOB_OBJECT_ MSG_ACTIVE_ROCESS_LIMIT

Была попытка превысить ограничение на число активных процессов в задании

JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT

Была попытки превысить ограничение на объем памяти, которая может быть передана процессу, сообщается идентификатор процесса

JOB_OBJECT_MSG_JOB_ MEMORY_LIMIT

Была попытка превысить ограничение на объем памяти, которая может быть передана заданию; сообщается идентификатор процесса

JOB_OBJECT_MSG_NEW_ PROCESS

В задание добавлен процесс; сообщается идентификатор процесса

JOB_OBJECT_MSG_EXIT_ PROCESS

Процесс завершен, сообщается идентификатор процесса

JOB_OBJECT_MSG_ABNOKMAL._EXIT_PROCESS

Процесс завершен из за необработанного им исключения; сообщается идентификатор процесса

JOB_OBJECT_MSG_END_ OFJOR_TIME

Процессорное время, выделенное заданию, исчерпано, процессы не завершаются, и Вы можете либо возобновить их работу, задав новый лимит по времени, либо самостоя тельно завершить процессы, вызвав TerminateJobObject

Таблица 5-4. Уведомления о событиях задания, посылаемые системой связанному с этим заданием порту завершения

И последнее замечание: по умолчанию объект-задание настраивается системой на автоматическое завершение всех его процессов по истечении выделенного ему про цессорного времени, а уведомление JOB_OBJECT_MSG_END_OF_JOB_TIME не посы лается. Если Вы хотите, чтобы объект-задание не уничтожал свои процессы, а просто сообщал о превышении лимита на процессорное время, Вам придется написать при мерно такой код:

// создаем структуру JOBOBJECT_END_OF_JOB_TIME_JNFORMATION
// и инициализируем ее единственный элемент

JOBOBJECT_END_OF_JOB_TIME_INFORMATION joeojti;
joeojti.EndOfJobTimeAction = J0B_OBJECT_POST_AT_END_OF_JOB;

// сообщаем заданию, что ену нужно делать по истечении его времени
SetInformationJobObject(hJob, JobObjectEndOfJobTimeInformation, &joeojti, sizeof(joeojti));

Вы можете указать и другое значение, JOB__OBJECT_TERMINATE_AT_END_OF_JOB, но оно задается по умолчанию, еще при создании задания

Программа-пример JobLab

Этапрограмма, "05КJobLab.ехе»(см листингнарис 5-6),позволяет легко эксперимен тировать с заданиями Ее файлы исходного кода и ресурсов находятся в каталоге 05-JobLab на компакт-диске, прилагаемом к книге После запуска JobLab открывается окно, показанное на рис 5-5

h5-4.jpg

Рис. 5-5. Программа-пример JobLab

Когда процесс инициализируется, он создает объект «задание» Я присваиваю ему имя JobLab, чтобы Вы могли наблюдать за ним с помощью MMC Performance Monitor Моя программа также создает порт завершения ввода-вывода и связывает с ним объ ект-задание Это позволяет отслеживать уведомления от задания и отображать их в списке в нижней части окна

Изначально в задании нет процессов, и никаких ограничений для него не уста новлено. Поля в верхней части окна позволяют задавать базовые и расширенные ог раничения Все, что от Вас требуется, — ввести в них допустимые значения и щелк нуть кнопкуАрр1у Limits Если Вы оставляете поле пустым, соответствующие ограни чения не вводятся Кроме базовых и расширенных, Вы можете задавать ограничения по пользовательскому интерфейсу Обратите внимание помечая флажок PreserveJob Time When Applymg Limits, Вы не устанавливаете ограничение, а просто получаете возможность изменять ограничения, не сбрасывая значения элементов ThisPeriod-

TotalUserTime и ThisPeriodTotalKemelTime при запросе базовой учетной информации. Этот флажок становится недоступен при наложении ограничений на процессорное время для отдельных заданий.

Остальные кнопки позволяют управлять заданием по-другому. Кнопка Terminate Processes уничтожает все процессы в задании. Кнопка Spawn CMD In Job запускает командный процессор, сопоставляемый с заданием Из этого процесса можно запус кать дочерние процессы и наблюдать, как они ведут себя, став частью задания И пос ледняя кнопка, Put PID In Job, позволяет связать существующий свободный процесс с заданием (т. e. включить его в задание).

Список в нижней части окна отображает обновляемую каждые 10 секунд инфор мацию о статусе задания, базовые и расширенные сведения, статистику ввода-выво да, а также пиковые объемы памяти, занимаемые процессом и заданием.

Кроме этой информации, в списке показываются уведомления, поступающие от задания в порт завершения ввода-вывода. (Кстати, вся информация обновляется и при приеме уведомления.)

И еще одно: если Вы измените исходный код и будете создавать безымянный объект ядра «задание», то сможете запускать несколько копий этой программы, со здавая тем самым два и более объектов-заданий на одной машине. Это расширит Ваши возможности в экспериментах с заданиями.

Что касается исходного кода, то специально обсуждать его нет смысла — в нем и так достаточно комментариев. Замечу лишь, что в файле Job.h я определил С++-класс CJob, инкапсулирующий объект "задание» операционной системы. Эти избавило меня от необходимости передавать туда-сюда описатель задания и позволило уменьшить число операций приведения типов, которые обычно приходится выполнять при вы зове функций QuerylnformationJobObject и SetInformationJobObject.

JobLab