Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6265
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

160 Часть II. Приступаем к работе

метре и адрес структуры JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION:

typedef struct JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION { JOBOBJECT_BASIC_ACCOUNTING_INFORMATION Basiclnfo; IO_COUNTERS IoInfo;

} JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION, *PJOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION;

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

typedef struct _I0_C0UNTERS { ULONGLONG ReadOperationCount; ULONGLONG WriteOperationCount; ULONGLONG OtherOperationCount; ULONGLONG ReadTransferCount; ULONGLONG WriteTransferCount; ULONGLONG OtherTransferCount;

} IO_COUNTERS, *PI0_C0UNTERS;

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

BOOL GetProcessIoCounters(

HANDLE hProcess,

PIO_COUNTERS ploCounters);

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

фикаторов и структура JOBOBJECT_BASIC_PROCESS_ID_LIST:

typedef struct _JOBOBJECT_BASIC_PROCESS_ID_LIST { DWORD NumberOfAssignedProcesses;

DWORD NumberOfProcessIdsInList; DWORD ProcessIdList[1];

}JOBOBJECT_BASIC_PROCESS_ID_LIST, *PJOBOBJECT_BASIC_PROCESS_ID_LIST;

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

void EnumProcessIdsInJob(HANDLE hjob) {

Глава 5. Задания.docx 161

//Я исхожу из того, что количество процессов

//в этом задании никогда не превысит 10.

#define MAX_PROCESS_IDS 10

//определяем размер блока памяти (в байтах)

DWORD cb = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST) + (MAX_PROCESS_IDS - 1) * sizeof(DWORD);

//для хранения идентификаторов и структуры

PJ0B0BJECT_BASIC_PR0CESS_ID_LIST pjobpil = (PJ0B0BJECT_BASIC_PR0CESS_ID_LIST)_alloca(cb);

//Сообщаем функции, на какое максимальное число процессов

//рассчитана выделенная нами память.

pjobpil->NumberOfAssignedProcesses = MAX_PROCESS_IDS;

//запрашиваем текущий список идентификаторов процессов

QueryInfоrmationJobObject(hjob, JobObjectBasicProcessIdList, pjobpil, cb, &cb);

//перечисляем идентификаторы процессов

for (DWORD x = 0; x < pjobpil->NumberOfProcessIdsInList; x++) { // используем pjobpil->ProcessIdList[x]…

}

//Так как для выделения памяти мы вызывали _alloca,

//освобождать память нам не потребуется.

}

Вот и все, что вам удастся получить через эти функции, хотя на самом деле операционная система знает о заданиях гораздо больше. Эту информацию, которая хранится в специальных счетчиках, можно извлечь с помощью функций из библиотеки Performance Data Helper (PDH.dll) или через модуль Performance Monitor, подключаемый к Microsoft Management Console (MMC). Для просмотра све-

дений о заданиях также используют инструмент Reliability and Performance Monitor (из категории Administrative Tools), но он отображает только объекты заданий с глобальными именами. Для работы с заданиями весьма удобна утилита Process Explorer от Sysinternals (http://www.microsoft.com/technet/sysinternals/ProcessesAnd Threads/ProcessExplorer.mspx). По умолчанию процессы, на которые наложены ограничения посредством заданий, выделяются в окне Process Explorer (см. рис. 5- 2).

162 Часть II. Приступаем к работе

Рис. 5-2. Сведения об ограничения процессов на вкладке Job в окне Process Explorer

Внимание! Значение параметра «User CPU Limit» по ошибке отображается в секундах, тогда как он измеряется в миллисекундах. Эта ошибка будет исправлена в следующей версии программы.

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

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

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

Глава 5. Задания.docx 163

«занято» (nonsignaled state), вызвав SetlnformationJobObject и выделив ему дополнительное процессорное время.

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

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

Создав порт завершения ввода-вывода, вы сопоставляете с ним задание, вызы-

вая SetInformationJobObject следующим образом:

J0B0BJECT_ASS0CIATE_C0MPLETI0N_P0RT joacp;

 

joacp.CompletionKey =1;

// Любое значение, уникально

 

// идентифицирующее это задание,

joacp.CompletionPort = hIOCP;

// Описатель порта завершения,

 

// принимающего уведомления.

SetInformationJobObject(hJob, JobObjectAssociateCompletionPortlnformation,

&joacp, sizeof(jaocp));

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

ния ввода-вывода, вызывая GetQueuedCompletionStatus:

BOOL GetQueuedCompletionStatus(

HANDLE hlOCP,

PDWORD pNumBytesTransferred,

PULONG_PTR pCompletionKey,

POVERLAPPED *pOverlapped,

DWORD dwMilliseconds);

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

164 Часть II. Приступаем к работе

ца 5-6). В зависимости от конкретного события в pOverlapped может возвращаться идентификатор процесса.

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

Событие

Описание

JOB_OBJECT_MSG_ACTIVE_

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

PROCESS_ZERO

 

JOB_OBJECT_MSG_END_OF_

Процессорное время, выделенное процессу, исчерпано;

PROCESS_TIME

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

JOB_OBJECT_MSG_ACTIVE_

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

PROCESS_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_

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

ABNORMAL_EXIT_PROCESS

сообщается идентификатор процесса

JOB_OBJECT_MSG_END_

Процессорное время, выделенное заданию, исчерпано;

OF_OB_TIME

процессы не завершаются, и вы можете либо возобновить

 

их работу, задав новый лимит по времени, либо самостоя-

 

тельно завершить процессы, вызвав TerminateJobObject

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

// Создаем структуру J0B0BJECT_END_0F_J0B_TIME_INF0RMATI0N

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

J0B0BJECT_END_0F_J0B_TIME_INF0RMATI0N joeojti; joeojti.EndOfJobTimeAction = J0B_0BJECT_P0ST_AT_END_0F_J0B;

//сообщаем заданию, что ему нужно делать по истечении его времени

SetInformationJobObJect(hJob, JobObjectEndOfJobTimelnformation, &joeojti, &sizeof(joeojti));

Глава 5. Задания.docx 165

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

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

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

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

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

Изначально в задании нет процессов, и никаких ограничений для него не установлено. Поля в верхней части окна позволяют задавать базовые и расширенные ограничения. Все, что от вас требуется, — ввести в них допустимые значения и щелкнуть кнопку Apply Limits. Если вы оставляете

поле пустым, соответствующие

ограничения не вводятся.

Кроме базовых

и расширенных, вы можете

задавать ограничения по

пользовательско-

му интерфейсу. Обратите внимание: помечая флажок Preserve Job Time When Applying Limits, вы не устанавливаете ограничение, а просто полу-

166 Часть II. Приступаем к работе

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

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

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

Внимание! Велик соблазн использовать для получения полных путей по идентификаторам процессов функции GetModuleFileNameEx и GetProcessImageFileName из psapi.h. Однако использовать первую функцию в этих целях не удастся, так как задание получит уведомление о создании нового процесса с ограничениями этого задания, поскольку адресное пространство процесса еще не инициализировано полностью: в него не спроецированы нужные модули. Функция GetProcessImageFileName интересна тем, что способна и в этом случае получит полный путь, но его синтаксис будет похож на тот, что получается в режиме ядра, а не в пользовательском режиме, на-

пример \Device\Harddisk Volume 1\ Windows\System32\notepad.exe вместо

C:\Windows\System32\notepad. ехе. Поэтому следует использовать новую функцию QueryFullProcessImageName, которая всегда возвращает стандартный полный путь.

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

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

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

Оглавление

 

Г Л А В А 6 Базовые сведения о потоках .........................................................................

167

В каких случаях потоки создаются..................................................................................

168

И в каких случаях потоки не создаются .........................................................................

170

Ваша первая функция потока............................................................................................

171

Функция CreateThread ..........................................................................................................

172

Параметр psa ......................................................................................................................

173

Параметр cbStackSize.......................................................................................................

173

Параметры pfhStartAddr и pvParam .............................................................................

174

Параметр dwCreateFlags..................................................................................................

175

Параметр pdwThreadID.....................................................................................................

175

Завершение потока ...............................................................................................................

176

Возврат управления функцией потока .......................................................................

176

Функция ExitThread ...........................................................................................................

176

Функция TermlnateThread................................................................................................

177

Если завершается процесс.............................................................................................

177

Что происходит при завершении потока ...................................................................

178

Кое-что о внутреннем устройстве потока......................................................................

179

Некоторые соображения по библиотеке С/С++ ...........................................................

181

Ой, вместо _beginthreadex я по ошибке вызвал CreateThread ............................

192

Библиотечные функции, которые лучше не вызывать ........................................

192

Как узнать о себе ...................................................................................................................

193

Преобразование псевдоописателя в настоящий описатель ...............................

194

Г Л А В А 6

Базовые сведения о потоках

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

Вглаве 4 я говорил, что процесс фактически состоит из двух компонентов: объекта ядра «процесс» и адресного пространства. Так вот, любой поток тоже состоит из двух компонентов:

объекта ядра, через который операционная система управляет потоком. Там же хранится статистическая информация о потоке;

стека потока, который содержит параметры всех функций и локальные переменные, необходимые потоку для выполнения кода. (О том, как система управляет стеком потока, я расскажу в главе 16.)

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

168 Часть II. Приступаем к работе

Как видите, процессы используют куда больше системных ресурсов, чем потоки. Причина кроется в адресном пространстве. Создание виртуального адресного пространства для процесса требует значительных системных ресурсов. При этом ведется масса всяческой статистики, на что уходит немало памяти. В адресное пространство загружаются EXE- и DLL-файлы, а значит, нужны файловые ресурсы. С другой стороны, потоку требуются лишь соответствующий объект ядра и стек; объем статистических сведений о потоке невелик и много памяти не занимает.

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

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

В каких случаях потоки создаются

Поток (thread) определяет последовательность исполнения кода в процессе. При инициализации процесса система всегда создает первичный поток. Начинаясь со стартового кода из библиотеки С/С++, который в свою очередь вызывает входную функцию (__tmain, _tWinMain) из вашей программы, он живет до того момента, когда входная функция возвращает управление стартовому коду и тот вызывает функцию ExitProcess. Большинство приложений обходится единственным, первичным потоком. Однако процессы могут создавать дополнительные потоки, что позволяет им эффективнее выполнять свою работу.

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

Вы активизируете службы индексации Windows Indexing Services. Она создает поток с низким приоритетом, который, периодически пробуждаясь, индексирует содержимое файлов на дисковых устройствах вашего компьютера. Чтобы найти какой-либо файл, вы открываете окно Search Results (щелкнув кнопку Start и выбрав из меню Search команду For Files Оr Folders) и вводите в поле Containing Text нужные критерии поиска. После этого начинается поиск по индексу, и на экране появляется список файлов, удовлетворяющих этим критериям. Служба индексации данных значительно увеличивает скорость поиска, так как при ее использовании больше не требуется открывать, сканировать и закрывать каждый файл на диске.

Соседние файлы в предмете Программирование на C++