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

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

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

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

Теперь, как я и обещал, обсудим элемент dwFlags. Он содержит набор флагов, позволяющих управлять созданием дочернего процесса. Большая часть флагов просто сообщает функции CreateProcess, содержат ли прочие элементы структуры STARTUPINFO полезную информацию или некоторые из них можно игнорировать. Список допустимых флагов приведен в следующей таблице.

Табл. 4-7. Флаги элемента dwFlags

Флаг

Описание

STARTF_USESIZE

Заставляет использовать элементы dwXSize и dwYSize

STARTF_USESHOWWINDOW

Заставляет использовать элемент wShowWindow

STARTF_USEPOSITION

Заставляет использовать элементы dwX и dwY

STARTF_USECOUNTCHARS

Заставляет использовать элементы dwXCountChars и

 

dwYCountChars

STARTF_USEFILLATTRIBUTE

Заставляет использовать элемент dwFillAttribute

STARTF_USESTDHANDLES

Заставляет использовать элементы hStdInput, hStdOut-

 

put и hStdError

STARTF_RUN_FULLSCREEN

Приводит к тому, что консольное приложение на ком-

 

пьютере с процессором типа x86 запускается в полно-

 

экранном режиме

Два дополнительных флага — STARTF_FORCEONFEEDBACK и STARTF_FORCEOFFFEEDBACK — позволяют контролировать форму указателя мыши в момент запуска нового процесса. Поскольку Windows поддерживает истинную вытесняющую многозадачность, можно запустить одно приложение и, пока оно инициализируется, поработать с другой программой. Для визуальной обратной связи с пользователем функция CreateProcess временно изменяет форму системного указателя мыши:

Указатель такой формы подсказывает: можно либо подождать чего-нибудь, что вот-вот случится, либо продолжить работу в системе. Если же вы укажете флаг STARTF_FORCEOFFFEEDBACK, CreateProcess не станет добавлять «пе-

сочные часы» к стандартной стрелке.

Флаг STARTF_FORCEONFEEDBACK заставляет CreateProcess отслеживать инициализацию нового процесса и в зависимости от результата проверки изменять форму указателя. Когда функция CreateProcess вызывается с этим флагом, указатель преобразуется в «песочные часы». Если спустя две секунды от нового процесса не поступает GUI-вызов, она восстанавливает исходную форму указателя.

Глава 4. Процессы.docx 113

Если же в течение двух секунд процесс все же делает GUI-вызов, CreateProcess ждет, когда приложение откроет свое окно. Это должно произойти в течение пяти секунд после GUI-вызова. Если окно не появилось, CreateProcess восстанавливает указатель, а появилось — сохраняет его в виде «песочных часов» еще на пять секунд. Как только приложение вызовет функцию GetMessage, сообщая тем самым, что оно закончило инициализацию, CreateProcess немедленно сменит указатель на стандартный и прекратит мониторинг нового процесса.

Теперь несколько слов об элементе wShowWindow структуры STARTUP-INFO. Этот элемент инициализируется значением, которое вы передаете в (w)WinMain через ее последний параметр, nCmdShow. Он позволяет указать, в каком виде должно появиться главное окно вашего приложения. В качестве значения используется один из идентификаторов, обычно передаваемых в ShowWindow (чаще все-

го SW_SHOWNORMAL или SW_SHOWMINNOACTIVE, но иногда и SW_SHOWDEFAULT).

После запуска программы из Explorer ее функция (w)WinMain вызывается с SW_SHOWNORMAL в параметре nCmdShow. Если же вы создаете для нее ярлык, то можете указать в его свойствах, в каком виде должно появляться ее главное окно. На рис. 4-3 показано окно свойств для ярлыка Notepad. Обратите внимание на список Run, в котором выбирается начальное состояние окна Notepad.

Когда вы активизируете этот ярлык из Explorer, последний создает и инициализирует структуру STARTUPINFO, а затем вызывает CreateProcess. Это приводит к запуску Notepad, а его функция (w)WinMain получает

SW_SHOWMINNOACTIVE в параметре nCmdShow.

Таким образом, пользователь может легко выбирать, в каком окне запускать программу — нормальном, свернутом или развернутом.

Чтобы получить копию структуры STARTUPINFO, инициализированной родительским процессом, приложение может вызвать:

VOID GetStartupInfo(LPSTARTUPINFO pStartupInfo);

Анализируя эту структуру, дочерний процесс может изменять свое поведение в зависимости от значений ее элементов.

Эта функция всегда заполняет структуру STARTUPINFO, даже в дочерних процессах, созданных вызовом CreateProcess с передачей STARTUPINFOEX. Дополнительные атрибуты (см. ниже) имеют смысл лишь в адресном пространстве родительского процесса, где для них выделена память. Следовательно, нужен другой способ передачи наследуемых описателей, например, через командную строку.

В завершение этого раздела я расскажу о структуре STARTUPINFOEX. Сигнатура функции CreateProcess не менялась со времени появления Win32. В Майкрософт решили расширить ее возможности, но не стали ни изменять сигнатуру функции, ни создавать ее новые версии (CreateProcessEx, CreateProcess2 и т. д.). В результате структура STARTUPINFOEX в дополне-

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

ние к полю StartupInfo получила поле lpAttributeList, в котором передаются дополнительные параметры, называемые атрибутами:

typedef struct _STARTUPINFOEXA { STARTUPINFOA StartupInfo;

struct _PROC_THREAD_ATTRIBUTE_LIST *lpAttributeList;

}STARTUPINFOEXA, *LPSTARTUPINFOEXA; typedef struct _STARTUPINFOEXW {

STARTUPINFOW StartupInfo;

struct _PROC_THREAD_ATTRIBUTE_LIST *iPAttributeList;

}STARTUPINFOEXW, *LPSTARTUPINFOEXW;

Рис. 4-3. Окно свойств для ярлыка Notepad

Список атрибутов состоит из пар «ключ - значение», представляющих атрибуты. В настоящее время задокументированы два ключа-атрибута:

атрибут PROC_THREAD_ATTRIBUTE_HANDLE_LIST сообщает функции

CreateProcess, какие именно описатели объектов ядра должен унаследовать дочерний процесс. Эти описатели должны быть созданы как наследуемые, при этом в их структуре SECURITY_ATTRIBUTES поле bInheritHandle должно быть установлено в TRUE (см. выше). Однако передавать TRUE в параметре bInheritHandles функции CreateProcess не обязательно. Использование этого

атрибута позволяет выборочно передавать дочернему процессу наследуемые описатели. Это особенно важно для процессов, порождающих множество дочерних процессов в разных-

Глава 4. Процессы.docx 115

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

в атрибуте PROC_THREAD_ATTRIBUTE_PARENT_PROCESS передается значение описателя процесса. Заданный процесс (вместе с его наследуемыми описателями, настройками привязки к процессорам, классом приоритета, квотами, маркером пользователя и связанным заданием) будет использован при вызове CreateProcess в качестве родительского процесса вместо текущего процесса. Обратите внимание, что такая «смена» родительского процесса не касается процесса отладчика, который по-прежнему будет получать контролировать созданный им отлаживаемый процесс и получать от него все отладочные уведомления. Однако показанная ранее программа ToolHelp API будет отображать процесс, указанный этим атрибутом, как родительский для процесса, созданного с передачей данного атрибута.

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

BOOL InitializeProcThreadAttributeList(

PPROC_THREAD_ATTRIBUTE_LIST pAttributeList,

DWORD dwAttributeCount,

DWORD dwFlags,

PSIZE_T pSize);

Заметьте, что параметр dwFlags зарезервирован, в нем всегда следует передавать 0. Первый вызов необходим, чтобы узнать размер блока памяти, необходимого Windows для хранения атрибутов:

SIZE_T cbAttributeListSize = 0;

BOOL bReturn = InitializeProcThreadAttributeList( NULL, 1, 0, &cbAttributeListSize);

// bReturn = FALSE, но GetLastError() возвращает ERROR_INSUFFICIENT_BUFFER

Размер блока будет рассчитан на основе числа передаваемых атрибутов, заданного dwAttributeCount, и записан в переменную SIZE_T, на которую указывает pSize:

pAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST) HeapAlloc(GetProcessHeap(), 0, cbAttributeListSize);

После выделения памяти для списка атрибутов функция InitializeProcThradAttributeList вызывается снова для инициализации его содержимого:

bReturn = InitializeProcThreadAttributeList(

pAttributeList, 1, 0, AcbAttributeListSize);

После выделения памяти и инициализации атрибутов следует добавить необходимые пары «ключ — значение» с помощью этой функции:

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

BOOL UpdateProcThreadAttribute(

PPROC_THREAD_ATTRIBUTE_LIST pAttributeList,

DWORD dwFlags,

DWORD_PTR Attribute,

PVOID pValue,

SIZE_T cbSize,

PVOID pPreviousValue,

PSIZE_T pReturnSize);

Параметр pAttributeList содержит инициализированный список атрибутов, для которого ранее была выделена память; к этому списку функция будет добавлять заданные пары «ключ - значение». Параметр Attribute представляет ключ в соста-

ве пары и принимает значение PROC_THREAD_ATTRIBUTE_PARENT_ PROCESS либо PROC_THREAD_ATTRIBUTE_HANDLE_LIST. В первом случае параметр pValue должен указывать на переменную, содержащую описатель нового родительского процесса, а параметр cbSize должен быть равен sizeof (HANDLE). Во втором случае pValue должен указывать на начало массива описателей наследуемых объектов ядра, которые необходимо передать дочернему процессу, a cbSize должен быть равен произведению sizeof (HANDLE) и числа описателей. Параметры dwFlags, pPreviousValue и pReturnSize зарезервированы и должны быть установлены в 0, NULL и NULL, соответственно.

Внимание! Если требуется одновременно передать оба атрибута, помните,

что описатели, связанные с PROC_THREAD_ATTRIBUTE_HANDLELIST,

должны быть действительными в новом родительском процессе, представ-

ленном PROC_THREAD_ATTRIBUTE_PARENT_PROCESS (поскольку они наследуются от него), а не в текущем процессе, вызывающем CreateProcess.

Перед вызовом CreateProcess с установленным в dwCreationFlags флагом EXTENDED_STARTUPINFO_PRESENT необходимо определить переменную STARTUPINFOEX (с только что инициализированным списком атрибутов в поле pAttributeList), которая будет передана как параметр pStartupInfo:

STARTUPINFOEX esi = { sizeof(STARTUPINFOEX) }; esi.lpAttributeList = pAttributeList;

bReturn = CreateProcess(

…, EXTENDED_STARTUPINFO_PRESENT, … &esi.StartupInfo, …);

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

VOID DeleteProcTh readAttributeList(

PPROC_THREAD_ATTRIBUTE_LIST pAttributeList);

Глава 4. Процессы.docx 117

Параметр ppiProclnfo

Параметр ppiProcInfo указывает на структуру PROCESS_INFORMATION, которую вы должны предварительно создать; ее элементы инициализируются самой функцией CreateProcess. Структура представляет собой следующее:

typedef struct _PROCESS_INFORMATION { HANDLE hProcess;

HANDLE hThread; DWORD dwProcessId; DWORD dwThreadId;

} PROCESS_INFORMATION;

Как я уже говорил, создание нового процесса влечет за собой создание объектов ядра «процесс» и «поток». В момент создания система присваивает счетчику каждого объекта начальное значение — единицу. Далее функция CreateProcess (перед самым возвратом управления) открывает объекты «процесс» и «поток» и заносит их описатели, специфичные для данного процесса, в элементы hProcess и hThread структуры PROCESS_INFORMATION. Когда CreateProcess открывает эти объекты, счетчики каждого из них увеличиваются до 2.

Это означает, что, перед тем как система сможет высвободить из памяти объект «процесс», процесс должен быть завершен (счетчик уменьшен до 1), а родительский процесс обязан вызвать функцию CloseHandle (и тем самым обнулить счетчик). То же самое относится и к объекту «поток»: поток должен быть завершен, а родительский процесс должен закрыть описатель объекта «поток». Подробнее об освобождении объектов «поток» см. раздел «Дочерние процессы» в этой главе.

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

Созданному объекту ядра «процесс» присваивается уникальный идентификатор; ни у каких других объектов этого типа в системе не может быть одинаковых идентификаторов. Это же касается и объектов ядра «поток». Причем идентификаторы процесса и потока тоже разные, и их значения никогда не бывают нулевыми.

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

Завершая свою работу, CreateProcess заносит значения идентификаторов в элементы dwProcessId и dwThreadId структуры PROCESS_INFORMATION. Эти идентификаторы просто облегчают определение процессов и потоков в системе; их используют, как правило, лишь специализированные утилиты вроде Task Manager. Обратите внимание, что Task Manager присваивает идентификатор «О» процессу «System Idle» («Бездействие системы», см. ниже). Однако на самом деле такого процесса нет, просто в Task Manager так обозначается поток, исполняемый в то время, когда не исполняется никакой другой код. Число потоков «процесса» System Idle всегда равно числу установленных в системе процессоров. Этот «процесс» отображает процентную долю времени процессора, не использованную настоящими процессами.

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

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

Узнать идентификатор текущего процесса можно вызовом GetCurrentProcessId, а идентификатор текущего потока — вызовом функции GetCurrentThreadId. Кроме того, идентификатор процесса можно узнать по его описателю, вызвав GetProcessId (а в случае потока — GetThreadId). И последнее (по порядку, но не по важности): обладая описателем потока, можно узнать идентификатор процесса, которому этот поток принадлежит, для этого достаточно вызвать GetProcessIdOfThread.

Программе иногда приходится определять свой родительский процесс. Однако родственные связи между процессами существуют лишь на стадии создания дочернего процесса. Непосредственно перед началом исполнения кода в дочернем процессе Windows перестает учитывать его родственные

Глава 4. Процессы.docx 119

связи. В предыдущих версиях Windows не было функций, которые позволяли бы программе обращаться с запросом к ее родительскому процессу. Но ToolHelpфункции, появившиеся в современных версиях Windows, сделали это возможным. С этой целью вы должны использовать структуру PROCESSENTRY32: ее элемент th32ParentProcessID возвращает идентификатор «родителя» данного процесса. Тем не менее, если вашей программе нужно взаимодействовать с родительским процессом, от идентификаторов лучше отказаться. Почему — я уже говорил. Для определения родительского процесса существуют более надежные механизмы: объекты ядра, описатели окон и т. д.

Единственный способ добиться того, чтобы идентификатор процесса или потока не использовался повторно, — не допускать разрушения объекта ядра «процесс» или «поток». Если вы только что создали новый процесс или поток, то можете просто не закрывать описатели на эти объекты — вот и все. А по окончании операций с идентификатором, вызовите функцию Close-Handle и освободите соответствующие объекты ядра. Однако для дочернего процесса этот способ не годится, если только он не унаследовал описатели объектов ядра от родительского процесса.

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

Процесс можно завершить четырьмя способами:

входная функция первичного потока возвращает управление (рекомендуемый способ);

один из потоков процесса вызывает функцию ExitProcess (нежелательный способ);

поток другого процесса вызывает функцию TerminateProcess (тоже нежелательно);

все потоки процесса умирают по своей воле (большая редкость).

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

Возврат управления входной функцией первичного потока

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

любые С++-объекты, созданные данным потоком, уничтожаются соответствующими деструкторами;

система освобождает память, которую занимал стек потока;

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

L система устанавливает код завершения процесса (поддерживаемый объектом ядра «процесс») — его и возвращает ваша входная функция;

счетчик пользователей данного объекта ядра «процесс» уменьшается на 1.

Функция ExitProcess

Процесс завершается, когда один из его потоков вызывает ExitProcess:

VOID ExitProcess(UINT fuExitCode);

Эта функция завершает процесс и заносит в параметр fuExitCode код завершения процесса. Возвращаемого значения у ExitProcess нет, так как результат ее действия - завершение процесса. Если за вызовом этой функции в программе присутствует какой-нибудь код, он никогда не исполняется.

Когда входная функция (WinMain, wWinMain, main или wmain) в вашей программе возвращает управление, оно передается стартовому коду из библиотеки C/C++, и тот проводит очистку всех ресурсов, выделенных им процессу, а затем обращается к ExitProcess, передавая ей значение, возвращенное входной функцией. Вот почему возврат управления входной функцией первичного потока приводит к завершению всего процесса. Обратите внимание, что при завершении процесса прекращается выполнение и всех других его потоков.

Кстати, в документации из Platform SDK утверждается, что процесс не завершается до тех пор, пока не завершится выполнение всех его потоков. Это, конечно, верно, но тут есть одна тонкость. Стартовый код из библиотеки C/C++ обеспечивает завершение процесса, вызывая ExitProcess после того, как первичный поток вашего приложения возвращается из входной функции. Однако, вызвав из нее функцию ExitThread (вместо того чтобы вызвать ExitProcess или просто вернуть управление), вы завершите первичный поток, но не сам процесс — если в нем еще выполняется какой-то другой поток (или потоки).

Заметьте, что такой вызов ExitProcess или ExitThread приводит к уничтожению процесса или потока, когда выполнение функции еще не завершилось. Что касается операционной системы, то здесь все в порядке: она корректно очистит все ресурсы, выделенные процессу или потоку. Но в приложении, написанном на C/C++, следует избегать вызова этих функций, так как библиотеке C/C++ скорее всего не удастся провести должную очистку. Взгляните на этот код:

«include <windows.h> «include <stdio.h>

class CSomeObj { public:

CSomeObj() { printf(“Constructor\r\n”); } ~CSomeObJ() { printf(“CDestructor\r\n”); }

};

 

Глава 4. Процессы.docx 121

CSomeObj g.GlobalObj;

 

void main () {

 

CSomeObj LocalObj;

 

ExitProcess(0);

// этого здесь не должно быть

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

//для вызова деструктора LocalObj, но ExitProcess не дает его выполнить

}

При исполнении эта программа выводит следующие строки;

Constructor

Constructor

Код конструирует два объекта: глобальный и локальный. Но вы никогда не увидите строку Destructor. С++-объекты не разрушаются должным образом из-за того, что ExitProcess форсирует уничтожение процесса и библиотека C/C++ не получает шанса на очистку.

Как я уже говорил, никогда не вызывайте ExitProcess в явном виде. Если я уберу из предыдущего примера вызов ExitProcess, программа выведет такие строки:

Constructor

Constructor

Destructor

Destructor

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

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

Функция TerminateProcess

Вызов функции TerminateProcess тоже завершает процесс:

BOOL TerminateProcess(

HANDLE hProcess,

UINT fuExitCode);

Главное отличие этой функции от ExitProcess в том, что ее может вызвать любой поток и завершить любой процесс. Параметр hProcess идентифициру-

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