Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009
.pdf160 Часть 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 нужные критерии поиска. После этого начинается поиск по индексу, и на экране появляется список файлов, удовлетворяющих этим критериям. Служба индексации данных значительно увеличивает скорость поиска, так как при ее использовании больше не требуется открывать, сканировать и закрывать каждый файл на диске.