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

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

.pdf
Скачиваний:
6823
Добавлен:
13.08.2013
Размер:
31 Мб
Скачать
☆

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++