Новые книги

MDK, «Ты не поверишь!», «Корпорация зла» – эти сообщества известны каждому активному пользователю социальной сети в «ВКонтакте». У них миллионы пользователей и они приносят внушительные доходы своим создателям.

Как добиться такого же успеха, как продвигать свое интернет-сообщество, не повторяя чужих ошибок, как привлечь и удержать подписчиков, став гуру SMM-продвижения? Об этом рассказывает создатель легендарного сообщества «Литорг» Артем А. Сенаторов.
В 1998 году вышла книга автора, посвященная инструментальным средствам системного анализа и проектирования информационных систем -BPwin и ERwin. (Маклаков С. BPwin и ERwin. CASE-средства разработки информационных систем. М: Диалог-МИФИ). Книга выдержала два издания и пользовалась популярностью среди специалистов в области информационных технологий. BPwin является средством, которое позволяет облегчить проведение обследования предприятия, построить функциональные модели и в дальнейшем с их помощью проанализировать и улучшить бизнес-процессы. Этот инструмент используют в основном системные аналитики и специалисты по внедрению информационных систем. ERwin предназначен для другого круга задач и для специалистов другого профиля - это система проектирования баз данных.

Многочисленные пожелания читателей и выход новой версии продукта фирмы Computer Associater - BPwin 4.0 побудили автора написать книгу, целиком посвященную BPwin и предназначенную для специалистов, задачей которых является создание функциональных моделей и реинжиниринг бизнес-процессов.

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

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

ЧАСТЬ II НАЧИНАЕМ РАБОТАТЬ

ГЛАВА 4 Процессы

Эта глава о том, как система управляет выполняемыми приложениями. Сначала я определю понятие "процесс" и объясню, как система создает объект ядра "процесс" Затем я покажу, как управлять процессом, используя сопоставленный с ним объект ядра Далее мы обсудим атрибуты (или свойства) процесса и поюворим о несколь ких функциях, позволяющих обращаться к этим свойствам и изменять их. Я расскажу также о функциях, которые создают (порождают) в системе дополнительные процес сы Ну и, конечно, описание процессов было бы неполным, если бы я не рассмотрел механизм их завершения. О'кэй, приступим

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

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

h4-1.jpg

Рис. 4-1. Операционная система выделяет потокам кванты времени по принципу карусели

Процессы инертны Чтобы процесс что-нибудь выполнил, в нем нужно создать поток Именно потоки отвечаю за исполнение кодя, содержащегося в адресном про странстве процесса В принципе, один процесс может владеть несколькими потока ми, и тогда они "одновременно" исполняют код в адресном пространстве процесса.

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

Чтобы всс эти потоки работали, операционная система отводит каждому из них определенное процессорное время. Выделяя потокам отрезки времени (называемые квантами) по принципу карусели, она создает тем самым иллюзию одновременного выполненияпотоков Рис 4-1 иллюстрирует распределение процессорноговремени между потоками па машине с одним процессором. Если в машине установлено более одного процессора, алгоритм работы операционной системы значительно усложняет ся (в этом случае система стремится сбалансировать нагрузку между процессорами).

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

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

WINDOWS 98
Windows 98 работает только с одним процессором. Даже если у компьютера несколько процессоров, под управлением Windows 98 действует лишь один из них — остальные простаивают.

Ваше первое Windows-приложение

Windows поддерживает два типа приложений: основанные на графическом интерфей се (graphical user interface, GUI) и консольные (console user interface, CUI) V приложе ний первого типа внешний интерфейс чисто графический GUI-приложения создают окна, имеют меню, взаимодействуют с пользователем через диалоговые окна и вооб ще пользуются всей стандартной "Windows'oвской" начинкой. Почти все стандартные программы Windows — Notepad, Calculator, Wordpad и др — являются GUI-приложе ниями. Приложения консольного типа работают в текстовом режиме: они не форми руют окна, не обрабатывают сообщения и не требуют GUI. И хотя консольные при ложения на экране тоже размещаются в окне, в нем содержится только текст. Коман дные процессоры вроде Cmd.exe (в Windows 2000) или Command.com (в Windows 98) — типичные образцы подобных приложений.

Вместе с тем граница между двумя типами приложений весьма условна. Можно, например, создать консольное приложение, способное отображать диалоговые окна. Скажем, в командном процессоре вполне может быть специальная команда, открыва ющая графическое диалоговое окно со списком команд, вроде мелочь — а избавляет от запоминания лишней информации В то же время можно создать и GUI-приложе ние, выводящее текстовые строки в консольное окно. Я сам часто писал такие пpo

граммы: создав консольное окно, я пересылал в него отладочную информацию, свя занную с исполняемым приложением. Но, конечно, графический интерфейс предпоч тительнее, чем старомодный текстовый Как показывает опыт, приложения на основе GUI "дружественнее" к пользователю, а значит и более популярны

Когда Вы создаете проект приложения, Microsoft Visual C++ устанавливает такие ключи для компоновщика, чтобы в исполняемом файле был указан соответствующий тип подсистемы Для CUI-программ используется ключ /SUBSYSTEM:CONSOLE, а для GUI-приложений — /SUBSYSTEM:WINDOWS Когда пользователь запускает приложе ние, загрузчик операционной системы проверяет помер подсистемы, хранящийся в заголовке образа исполняемого файла, и определяет, что это за программа — GUI или СUI Если номер указывает на приложение последнего типа, загрузчик автоматичес ки создает текстовое консольное окно, а если номер свидетельствует о противопо ложном — просто загружает программу в память После того как приложение начи нает работать, операционная система больше не интересуется, к какому типу оно относится.

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

int WINAPI WinMain( HINSTANCE hinstExe, HINSTANCE, PSTR pszCmdLine, int nCmdShow);

int WINAPT wWinMain( HINSTANCE hinstExe, HINSTANCE, PWSTR pszCmdLine, int nCmdShow);

int __cdecl main( int argc, char *argv[], char *envp[]);

int _cdecl wmain( int argc, wchar_t *argv[], wchar_t *envp[]);

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

Тип приложения

Входная функция

Стартовая функция, встраиваемая в Ваш исполняемый файл

GUI-приложение, работающее с ANSI-символами и строками

WinMain

WinMainCRTStartup

GUI-приложение, работающее с Unicode-символами и строками

wWinMain

wWinMainCRTStartup

GUI-приложение, работающее

с ANSI-символами и строками

main

mainCRTStartup

GUI-приложение, работающее с Unicode-символами и строками

wmain

wmainCRTStartup

Нужную стартовую функцию в библиотеке С/С++ выбирает компоновщик при сборке исполняемого файла. Если указан ключ /SUBSYSTEM:WINDOWS, компоновщик ищет в Вашем коде функцию WinMain или wWinMain, Если ни одной из них нет, он сообщает об ошибке "unresolved external symbol" ("неразрешенный внешний символ"); в ином случае — выбирает WtnMainCRTStartup или wWinMainCRTStartup соответственно.

Аналогичным образом, если ладан ключ /SUBSYSTEM:CONSOLE, компоновщик ищет в коде функцию main или wmain и выбирает соответственно mainCRTStartup или wmainCRTStartup; если в коде нет ни main, ни wmain, сообщается о той же ошибке — "unresolved external symbol"

Но не многие знают, что в проекте можно вообще не указывать ключ /SUBSYSTEM компоновщика. Если Вы так и сделаете, компоновщик будет сам определять подсис темудля Вашего приложения. При компоновке он проверит, какая из четырех функ ций (WinMain, wWinMain, main или wmain) присутствует в Вашем коде, и на основа нии этого выберет подсистему и стартовую функцию из библиотеки С/С++.

Одна из частых ошибок, допускаемых теми, кто лишь начинает работать с Vi sual С++, — выбор неверного типа проекта. Например, разработчик хочет создать проект Win32 Application, а сам включает в код функцию main При его сборке он получает сообщение об ошибке, так как для проекта Win32 Application в командной строке компоновщика автоматически указывается ключ /SUBSYSTEM:WlNDOWS, ко торый требует присутствия в коде функции WinMain или wWinMatn В этот момент раз работчик может выбрать один из четырех вариантов дальнейших действий:

  • заменить main на WinMain Как правило, это не лучший вариант, поскольку разработчик скорее всего и хотел создать консольное приложение,
  • открыть новый проект, на этот раз — Win32 Console Application, и перенести в него все модули кода. Этот вариант весьма утомителен, и возникает ощущение, будто начинаешь все заново,
  • открыть вкладку Link в диалоговом окне Project Settings и заменить ключ /SUBSYSTEM:WINDOWS на /SUBSYSTEM:CONSOLK. Некоторые думают, что это единственный вариант,
  • открыть вкладку Link в диалоговом окне Project Settings и вообще убрать ключ /SUBSYSTEM:WINDOWS. Я предпочитаю именно этот способ, потому что он самый гибкий. Компоновщик сам сделает все, что надо, в зависимости от вход ной функции, которую Вы реализуете в своем коде, Никак не пойму, почему это не предлагается по умолчанию при создании нового проекта Win32 Appli cation или Win32 Console Application.

Все стартовые функции из библиотеки С/С++ делают практически одно и то же. Разница лишь в том, какие строки они обрабатывают (в ANSI или Unicode) и какую входную функцию вызывают после инициализации библиотеки. Кстати, с Visual C++ поставляется исходный код этой библиотеки, и стартовые функции находятся в фай ле CRt0.c. А теперь рассмотрим, какие операции они выполняют:

  • считывают указатель на полную командную строку нового процесса;
  • считывают указатель на переменные окружения нового процесса;
  • инициализируют глобальные переменные из библиотеки С/С++, доступ к ко торым из Вашего кода обеспечивается включением файла StdLib.h. Список этих переменных приведен в таблице 4-1;
  • инициализируют кучу (динамически распределяемую область памяти), исполь зуемую С-функциями выделения памяти (т. с. malloc и calloc} и другими про цедурами низкоуровневого ввода-вывода;
  • вызывают конструкторы всех глобальных и статических объектов С++-классов.

Закончив эти операции, стартовая функция обращается к входной функции в Вашей программе. Если Вы написали ее в виде wWinMain, то она вызывается так:

GetStartupInfo(&StartupInfo);

int nMainRetVal = wWinMain(GetMjduleHandle(NULL), NULL, pszCommandLineUnicode, (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? StartupInfo.wShowWindow , SW_SHOWDEFAULT);

А если Вы предпочли WinMain, то:

GetStartupInfo(&StartupInfo);

int nMainReLVal = WinMain(GetModuleHandle(NULL), NULL, pszCommandLineANSI, (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? Startupinfo.wShowWindow , SW_SHOWDEFAULT);

И, наконец, то же самое для функций wmain и main.

int nMainRetVal = wmain(__argc, __wargv, _wenviron}; int nMainRetVal = main(_argc, __argv, _environ);

Когда Ваша входняя функция возвращает управление, стартовая обращается к функции exit библиотеки С/С++ и передает ей значение nMainRetVal. Функция exit выполняет следующие операции:

  • вызывает всс функции, зарегистрированные вызовами функции _onexit;
  • вызывает деструкторы всех глобальных и статических объектов С++-классов;
  • вызывает Windows-функцию ExifProcess, передавая ей значение nMainRetVal. Это заставляет операционную систему уничтожить Ваш процесс и установить код его завершения.

Имя переменной

Тип

Описание

_osver

unsigned int

Версия сборки операционной системы. Например, у Windows 2000 Beta 3 этот номер был 2031, соответственно _osver равна 2031.

_winmajor

unsigned int

Основной номер версии Windows в шестнадцатерич ной форме. Для Windows 2000 это значение равно 5.

Таблица 4-1. Глобальные переменные из библиотеки С/С++, доступные Вашим программам

Имя переменной

Тип

Описание

_winminor

unsigned int

Дополнительный номер версии Windows в шестнадца теричной форме Для Windows 2000 это значение равно 0

_winver

unsigned int

Вычисляется как ( winmajor << 8) + _winminor.

__argc

unsigned int

Количество аргументов, переданных в командной строке

__argv _ _wargv

char ** wchar_t **

Массив размером __argc с указателями на ANSI- или Unicode-строки. Каждый элемент массива указывает на один из аргументов командной строки.

_environ _wenviron

char ** wchar_t **

Массив указателей на ANSI- или Unicode-строки. Каждый элемент массива указывает на строку — переменную окружения.

_pgmptr _wpgmptr

char ** wchar_t**

Полный путь и имя (в ANSI или Unicode) запускаемой программы.

Описатель экземпляра процесса

Любому EXE- или DLL-модулю, загружаемому в адресное пространство процесса, при сваивается уникальный описатель экземпляра. Описатель экземпляра Вашего EXE файла передается как первый параметр функции (w)WinMain - hinstExe. Это значе ние обычно требуется при вызовах функций, загружающих те или иные ресурсы. На пример, чтобы загрузить из образа ЕХЕ-файла такой ресурс, как значок, надо вызвать:

HICON LoadIcon( HINSTANCE hinst, PCTSTR pszIcori);

Первый параметр в LoadIcon указывает, в каком файле (EXE или DLL) содержится интересующий Вас ресурс. Многие приложения сохраняют параметр hinstExe функ ции (w)WinMain в глобальной переменной, благодаря чему он доступен из любой части кода ЕХЕ-файла.

В документации Platform SDK утверждается, что некоторые Windows-функции требуют параметр типа HMODULE. Пример — функция GetModuleFileName

DWORD GetModuleFileName( HMODULE hinstModule, PTSTR pszPath, DWORD cchPath);

NOTE:
Как оказалось, HMODULE и HINSTANCE — это идно и то же. Встретив в доку ментации указание передать какой-то функции HMODULE, смело передавайте HINSTANCE, и наоборот. Они существуют в таком виде лишь потому, что в l6 разрядпой Windows идентифицировали совершенно разные вещи.

Истинное значение параметра hinstExe функции (w)WinMain — базовый адрес в памяти, определяющий ту область в адресном пространстве процесса, куда был заг ружен образ данного ЕХЕ-файла, Например, если система открывает исполняемый файл и загружает его содержимое по адресу 0x00400000, то hinstExe функции (w)Win Main получает значение 0x00400000.

Базовый адрес, но которому загружается приложение, определяется компоновщи ком. Разные компоновщики выбирают и разные (no умолчанию) базовые адреса. Ком поновщик Visual С++ использует по умолчанию базовый адрес 0x00400000 — самый нижний в Windows 98, начиная с которого в ней допускается загрузка образа испол няемого файла. Указав параметр /BASE: адрес (в случае компоновщика от Microsoft), можно изменить базовый адрес, по которому будет загружаться приложение.

При попытке загрузить исполняемый файл в Windows 98 по базовому адресу ниже 0x00400000 загрузчик переместит его на другой адрес. Это увеличит время загрузки приложения, но оно по крайней мере будет выполнено. Если Вы разрабатываете про граммы и для Windows 98, и для Windows 2000, сделайте так, чтобы приложение заг ружалось по базовому адресу не ниже 0x00400000. Функция GetModuleHandle.

HMODULE GetModuleHandle( PCTSTR pszModule);

возвращает описатель/базовый адрес, указывающий, куда именно (в адресном про странстве процесса) загружается EXE- или DLL-файл. При вызове этой функции имя нужного EXE- или DLL-файла передается как строка с нулевым символом в конце. Если система находит указанный файл, GetModuleHandle возвращает базовый адрес, по которому располагается образ данного файла. Если же файл системой не найден, функция возвращает NULL. Кроме того, можно вызвать эту функцию, передав ей NULL вместо параметра pszModule, — тогда Вы узнаете базовый адрес EXE-файла. Именно это и делает стартовый код из библиотеки С/С++ при вызове (w)WinMain из Вашей программы.

Есть еще две важные вещи, касающиеся GetModuleHandle. Во-первых, она прове ряет адресное пространство только того процесса, который ее вызвал. Если этот про цесс не использует никаких функций, связанных со стандартными диалоговыми ок нами, то, вызвав GetModuleHandle и передав ей аргумент "ComDlg32", Вы получите NULL - пусть даже модуль ComDlg32.dll и загружен в адресное пространство какого нибудь другого процесса. Во-вторых, вызов этой функции и передача ей NULL дает в результате базовый адрес ЕХЕ-фяйла в адресном пространстве процесса. Так что, вы зывая функцию в виде GetModuleHandle(NULL — даже из кода в DLL, — Вы получаете базовый адрес EXE-, а не DLL-файла.

Описатель предыдущего экземпляра процесса

Я уже говорил, что стартовый код из библиотеки С/С++ всегда передает в функцию (w)WinMain параметр binstExePrev как NULL. Этот параметр предусмотрен исключи тельно для совместимости с 16-разрядными версиями Windows и не имеет никакого смысла для Windows-приложений. Поэтому я всегда пишу заголовок (w)WinMain так:

int WINAPI WinMain(
HINSTANCE hinstExe,
HINSTANCE, PSTR pszCmdLine, int nCmdShow);

Поскольку у второго параметра нет имени, компилятор не выдает предупрежде ние "parameter not referenced" ("нет ссылки на параметр"),

Командная строка процесса

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

Параметр pszCmdLine всегда указывает на ANSI-строку. Но, заменив WinMain на wWinMain, Вы получите доступ к Unicode-версии командной строки для своего про цесса

Программа может анализировать и интерпретировать командную строку как угод но. Поскольку pszCrndLine относится к типу PSTR, а не PCSTR, не стесняйтесь и запи сывайте строку прямо в буфер, на который указывает этот параметр, но ни при каких условиях не переступайте границу буфера. Лично я всегда рассматриваю этот буфер как "только для чтений". Если в командную строку нужно внести изменения, я снача ла копирую буфер, содержащий командную строку, в локальный буфер (в своей про грамме), который затем и модифицирую.

Указатель на полную командную строку процесса можно.получить и вызовом функции GetCommandLine.

PTSTR GetCommandLine();

Она возвращает указатель на буфер, содержащий полную командную строку, вклю чая полное имя (вместе с путем) исполняемого файла.

Во многих приложениях безусловно удобнее использовать командную строку, предварительно разбитую на отдельные компоненты, доступ к которым приложение может получить через глобальные переменные _argc и _argv (или _wargu). Функ ция CommandLineToArgvW расщепляет Unicode-строку на отдельные компоненты:

PWSTR CommandLineToArgvW( PWSTR pszCmdLine, int pNumArgs);

Буква W в конце имени этой функции намекает на "широкие" (wide) символы и подсказывает, что функция существует только в Unicode-версии. Параметр pszCmdLine указывает на командную строку Его обычно получают предварительным вызовом GetCommandLineW Параметр pNumArgs — это адрес целочисленной переменной, в которой задается количество аргументов в командной строке. Функция Command LineToArgvW возвращает адрес массива указателей на Unicode-строки

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

int pNumArgs;
PWSTR *ppArgv = CommandLineToArgvW(GetCommandLineW(), &pNumArgs);

// используйте эти аргументы if (*ppArgv[1] == L x ) {
// освободите блок памяти HeapFree(GetProcessHeap() 0 ppArgv);

Переменные окружения

С любым процессом связан блок переменных окружения — область памяти, выделен ная в адресном пространстве процесса Каждый блок содержит группу строк такого вида

VarName1-VarValue1\0 VarName2-VarValue2\0 VarName3=VarValue3\0
...
VarNameX=VarValueX\0

\0

Первая часть каждой строки — имя переменной окружения. Зa ним следует знак равенства и значение, присваиваемое переменной Строки в блоке переменных ок ружения должны бьпь отсортированы в алфавитном порядке по именам переменных

Знак равенства разделяет имя переменной и ее значение, так что его нельзя ис пользовать как символ в имени переменной Важную роль играют и пробелы Например, объявив две переменные

XYZ= Windows (обратите внимание на пробел за знаком равенства) ABC=Windows

и сравнив значения переменных ХУZ и АВС, Вы увидите, что система их различает, — она учитывает любой пробел, поставленный перед знаком равенства или после него Вот что будет, если записать, скажем, так

XYZ =Home (обратите внимание на пробел перед знаком равенства) XYZ=Work

Вы получите первую переменную с именем "XYZ", содержащую строку "Home", и вторую переменную "XYZ", содержащую строку "Work"

Конец блока переменных окружения помечается дополнительным нулевым сим волом

WINDOWS 98
Чтобы создать исходный набор переменных окружения для Windows 98, надо модифицировать файл Autoexec bat, поместив в него группу строк SET в виде

SET VarName=VarValue

При перезагрузке система учтет новое содержимое файла Autoexecbat, и тогда любые заданные Вами переменные окружения станут доступны всем процессам, инициируемым в сеансе работы с Windows 98

WINDOWS 2000
При регистрации пользователя на входе в Windows 2000 система создает npo цесс-оболочку, связывая с ним группу строк — переменных окружения. Систе ма получает начальные значения этих строк, анализируя два раздела в рссст pe. В первом:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ SessionManager\Environment

содержится список переменных окружения, относящихся к системе, а во втором:

HKEY_CURRENT_USER\Environment

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

Пользователь может добавлять, удалять или изменять любые переменные через апплет System из Control Panel B этом апплете надо открыть вкладку Advanced и щелкнуть кнопку Environment Variables — тогда на экране появит ся следующее диалоговое окно.

h4-2.jpg

Модифицировать переменные из списка System Variables разрешается толь ко пользователю с правами администратора.

Кроме того, для модификации записей в реестре Ваша программа может обращаться к Windows-функциям, позволяющим манипулировать с реестром. Однако, чтобы изменения вступили в силу, пользователь должен выйти из си стемы и вновь войти в нее. Некоторые приложения типа Explorer, Task Manager или Control Pancl могут обновлять свои блоки переменных окружения на базе новых значений в реестре, когда их главные окна получают сообщение WM_SET TINGCHANGE, Например, если Вы, изменив реестр, хотите, чтобы какие-то приложения соответственно обновили свои блоки переменных окружения, вызовите

SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE 0, (LPARAM) TEXT("Envnuntnent"))

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

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

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

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

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

DWORD GetEnvironmentVariable( PCTSTR pszName, PTSTR pszValue, DWORD cchValue);

При вызове GetEnvironmentVariable параметр pszName должен указывать на имя интересующей Вас переменной, pszValue — на буфер, в который будет помещено зна чение переменной, а в cchValue следует сообщить размер буфера в символах. Функ ция возвращает либо количество символов, скопированных в буфер, либо 0, если ей не удалось обнаружить переменную окружения с таким именем.

Кстати, в реестре многие строки содержат подставляемые части, например.

%USERPROFILE%\My Documents

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

C:\Documents and Settings\Administrator

После подстановки переменной в строку реестра получим

C:\Documents and Settings\Admimstrator\My Documents

Поскольку такие подстановки делаются очень часто, в Windows есть функция ExpandEnvironmentStrings.

DWORD ExpandEnvironmentStrings( PCTSTR pszSrc, PTSTR pszDst, DWORD nSize);

Параметр pszSrc принимает адрес строки, содержащей подставляемые части, а пареметр pszDsf — адрес буфера, в который записывается развернутая строка Пара метр nSize определяет максимальный размер буфера в символах.

Наконец, функция SetEnvironmentVariable позволяет добавлять, удалять и модифи цировать значение переменной

DWORD SetEnvironmentVariable(
PCTSTR pszName,
PCTSTR pszValue);

Она устанавливает ту переменную, на чье имя указывает параметр pszName, и присваивает ей значение, заданное параметром pszValue. Если такая переменная уже существует, функция модифицирует ее значение. Если же spszValue содержится NULL, переменная удаляется из блока

Для манипуляций с блоком переменных окружения всегда используйте именно эти функции. Как я уже говорил, строки в блоке переменных нужно отсортировать в ал фавитном порядке по именам псрсмснных (тогда GetEnvironmentVariable быстрее находит нужные переменные), a SetEnvironmentVariable как раз и следит за порядком расположения переменных.

Привязка к процессорам

Обычно потоки внутри процесса могут выполняться на любом процессоре компью тера. Однако их можно закрепить за определенным подмножеством процессоров из числа имеющихся на компьютере Это свойство называется привязкой к процессорам (processor affinity) и подробно обсуждается в главе 7. Дочерние процессы наследуют привязку к процессорам от родительских.

Режим обработки ошибок

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

UINT SetErrorMode(UINT fuErrorMode) ;

Параметр fuErrorMode — это набор флагов, комбинируемых побитовой операцией OR

Флаг

Описание

SEM FAILCRITICALERRORS

Система не выводит окно с сообщением от обра ботчика критических ошибок и возвращает ошибку в вызывающий процесс

SEM_NOGPFAULTERRORBOX

Система не выводит окно с сообщением о наруше нии общей защиты; этим флагом манипулируют только отладчики, самостоятельно обрабатывающие нарушения общей защиты с помощью обработчика исключений

SEM_NOOPENFILEERRORBOX

Система не выводит окно с сообщением об отсут ствии искомого файла

SEM_NOALIGNMENTFAULTEXCEPT

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

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

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

Текущие диск и каталог для процесса

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

Поток может получать и устанавливать текущие каталог и диск для процесса с помощью двух функций:

DWORD GetCurrentDirectory( DWORD cchCurDir, PTSTR pszCurDir);

BOOL SetCurrentDirectory(PCTSTR pszCurDir);

Текущие каталоги для процесса

Система отслеживает текущие диск и каталог для процесса, но не текущие каталоги на каждом диске. Однако в операционной системе предусмотрен кое-какой сервис для манипуляций с текущими каталогами на разных дисках. Он реализуется через пере менные окружения конкретного процесса. Например:

=C:=C-\Utility\Bin =D:=D:\Program Files

Эти переменные указывают, что текущим каталогом на диске С является \Utllity\ Bin, а на диске D — Program Files.

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

Скажем, если текущий каталог для процесса — C:\Uiiltty\Bin и Вы вызываете фун кцию CreateFile, чтобы открыть файл D:\ReadMe.txt, система ищет переменную =D:. Поскольку переменная =D: существует, система пытается открыть файл ReadMe txt в каталоге D:\Program Files. А если бы таковой переменной не было, система искала бы файл ReadMe.txt в корневом каталоге диска D. Кстати, файловые Windows-функции никогда не добявляют и не изменяют переменные окружения, связанные с именами дисков, а лишь считывают их значения.

NOTE:
Для смены текущего каталога вместо Windows-функции SetCurrentDirectory можно использовать функцию _chdir из библиотеки С Внутренне она тоже обращается к SetCurrentDirettory, но, кроме того, способна добавлять или мо дифицировать переменные окружения, что позволяет запоминать в програм ме текущие каталоги на различных дисках.

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

DWORD GetFullPathName( PCTSTR pszFile, DWORD cchPath, PTSTR pszPath, PTSTR *ppszFilePart);

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

TCHAR szCurDir[MAX_PATH];

DWORD GetFullPathName(TEXT("C."), MAX_PATH, szCurDir, NULL);

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

Определение версии системы

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

Насколько я помню, функция GetVersion есть в API всех версий Windows:

DWORD GetVersion();

С этой простой функцией связана целая история. Сначала ее разработали для 16 разрядной Windows, и она должна была я старшем слове возвращать номер версии MS-DOS, а в младшем — номер версии Windows. Соответственно в каждом слове старший байт сообщал основной номер версии, младший — дополнительный но мер версии

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

Из-за всей этой неразберихи вокруг GetVersion в Windows API включили новую функцию — GetVersionEx:

BOOL GetVersionEx(POSVERSIONINFO pVersionInformation);

Перед обращением к GetVersionEx профамма должна создать структуру OSVER SIONINFOEX, показанную ниже, и передать ее адрес этой функции

typedef struct {
DWORD dwOSVersionInfoSize;
DWORD dwMajorVersion;
DWORD dwMinorVersion;
DWORD dwBuildNumber;
DWORD dwPlatformId;
TCHAR szCSDVersion[128];
WORD wServicePackMajor;
WORD wServicePackMinor;
WORD wSuiteMask;
BYTE wProductType;
BYTE wReserved;
} OSVERSIONINFOEX, *POSVERSIONINFOEX;

Эта структура — новинка Windows 2000 В остальных версиях Windows использу ется структура OSVERSIONINFO, в которой нет последних пяти элементов, присутству ющих в структуре OSVERSIONINFOEX

Обратите внимание, что каждому компоненту номера версии операционной сис темы соответствует свой элемент структуры это сделано специально — чтобы про граммисты не возились с выборкой данных ш всяких там старших-младших байтов слов (и не путались в них), тeпepь программе гораздо проще сравнивать ожидаемый номер версии операционной системы с действительным Назначение каждою элемен та структуры OSVERSIONTNFOFX описано в таблице 4-2

Элемент

Описание

dwOSVersionInfoSjze

Размер структуры, перед обращением к функции GetVertsionEx дол жен быть заполнен вызовом sizeof(OSVERSIONINFO) или Sizeof(OSVERSIONINFOEX)

dwMajorVersion

Основной номер версии операционной системы

dwMinorVersion

Дополнительный номер версии операционной системы

dwBuildNumber

Версия сборки данной системы

dwPlatformId

Идентификатор платформы, поддерживаемой данной системой, его возможные шачепия VFR_PLATFORM_WIN32s (Win32s), VER_PLATFORM_WIN32_WINDOWS (Windows 95/98), VER_PLATFORM_WIN32_NT (Windows NT или Windows 2000), VER_PLATFORM_WIN32_CEHH (Windows CE)

szCSDVersion

Этот элемент содержит текст — дополнительную информацию об установленной операционной системе

wServicePackMajor

Основной номер версии последнего установленного пакета исправ лений (service pack)

wServicePackMinor

Дополнительный номер версии последнего установленного пакета исправлений

Таблица 4-2. Элементы структуры OSVERSIONINFOEX

Элемент

Описание

wSuiteMask

Сообщает, какие программные пакеты (suites) доступны в системе;
его возможные значения

VER_SUITE_SMALLBUSINESS,
VER_SUITE_ENTERPRISE,
VER_SUITE_BACKOFFICE,
VER_SUITE_COMMUNICATIONS,
VER_SUITE_TERMINAL,
VER_SUITE_SMALLBUSINESS_RESTRICTED,
VER_SUITE_EMBEDDEDNT,
VER_SUITE_DATACENTER

wProductType

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

VER_NT_WORKSTATION,
VER_NT_SERVER,
VER_NT_DOMAIN_CONTROLLER

wReserved

Зарезервирован на будущее

В Windows 2000 появилась новая функция, VerifyVersionInfo, которая сравнивает версию установленной операционной системы с тем, что требует Ваше приложение:

BOOL VerifyVersionInfo(
POSVERSIONINFOEX pVersionInformation;
DWORD dwTypeMask;
DWORDLONG dwlConditionMask);

Чтобы использовать эту функцию, соэдайте структуру OSVERSIONINFOEX, запи шите в се элемент dwOSVersionInfoSize размер структуры, а потом инициализируйте любые другие элементы, важные для Вашей программы, При вызове VerifyVersionInfo параметр dwTypeMask указывает, какие элементы структуры Вы инициализировали. Этот параметр принимает любые комбинации следующих флагов: VER_MINORVER SION, VER_MAJORVERSION, VER_BUILDNUMBER, VER_PLATFORMID, VER_SERVICEPACK MINOR, VER_SERVICEPACKMAJOR, VER_SUITENAME и VER_PRODUCT_TYPE, Последний параметр, dwlConditionMask, является 64-разрядным значением, которое управляет тем, как именно функция сравнивает информацию о версии системы с нужными Вам дан ными.

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

VER_SET_CONDITION(
DWORDLONG dwlConditionMask, ULONG dwTy0eBiLMask, ULONG dwConditionMask);

Первый параметр, dwlConditionMask, идентифицирует переменную, битами кото рой Вы манипулируете. Вы не передаете адрес этой переменной, потому что VER_SET_ CONDITION — макрос, а не функция. Параметр dwTypeBitMask указывает один элемент в структуре OSVERSIONINFOEX, который Вы хотите сравнить со своими данными. (Для сравнения нескольких элементов придется обращаться к VER_SETCONDITION не сколько раз подряд.) Флаги, передаваемые в этом параметре, идентичны передавае мым в параметре dwTypeMask функции VerifyVersionInfo.

Последний параметр макроса VER_SET_CONDITION, dwConditionMask, сообщает, как Вы хотите проводить сравнение. Он принимает одно из следующих значений. VER_EQUAL, VER_GREATER, VER_GREATER_EQUAL, VER_LESS или VER_LESS_EQUAL, Вы можете использовать эти значения в сравнениях по VER_PRODUCT_TYPE. Например, значение VER_NT_WORKSTATION меньше, чем VER_NT_SERVER. Но в сравнениях по VER_SUITENAME вместо этих значений применяется VER_AND (должны быть установ лены все программные пакеты) или VER_OR (должен быть установлен хотя бы один из программных пакетов).

Подготовив набор условий, Вы вызываете VerifyVersionlnfo и получаете ненулевое значение, если система отвечает требованиям Вашего приложения, или 0, если она не удовлетворяет этим требованиям или если Вы неправильно вызвали функцию Чтобы определить, почему VenfyVersionlnfo вернула 0, вызовше GetLastError. Если та вернет ERROR_OLD_WIN_VERSION, значит, Вы правильно вызвали функцию Venfy VersionInfo, но система не соответствует предъявленным требованиям.

Вот как проверить, установлена ли Windows 2000;

// готовим структуру OSVERSIONINFOEX, сообщая, что нам нужна Windows 2000
OSVERSIONINFOEX osver = { 0 };
osver.dwOSVersionInfoSize = sizeof(osver);
osver.dwMdjorVersion = 5;
osver.dwMinorVersion = 0;
osver.dwPlatformId = VER_PLATFORM_WIN32_NT;

// формируем маску условии

DWORDLONG dwlConditionMask = 0;
// всегда инициализируйте это элемент так
VER_SET_CONDITION(dwlConditionMask, VER_MAJORVERSION, VER_EQUAL);
VER_SET_CONDITION(dwlConditionMask, VER_MINORVERSION, VER_EQUAL); VER_SET_CONDITION(dwlConditionMask, VER_PLATFORMID, VER_EQUAL);

// проверяем версию
if (VenfyVersionInfo(&osver, VER_MAJORVERSION | VER_MINORVERSION | VER_PLATFORMID,
dwlConditionMask)) {

// хост-система точно соответствует Windows 2000

} else {
// хост-система не является Windows 2000 }

Функция CreateProcess

Процесс создается при вызове Вашим приложением функции CreateProcess

BOOL CreateProcess(
PCTSTR pszApplicationName,
PTSTR pszCommandLine,
PSFCURITY_ATTRIBUTES psaProcess,
PSECURITY_ATTRIBUTES psaThiead,
BOOL bInheritHandles,
DWORD fdwCreate,
PVOID pvEnvironment,
PCTSTR pszCurDir,
PSTARTUPINFO psiStartInfo,
PPROCESS_INFORMATION ppiProcInfo);

Когда поток в приложении вызывает CreateProcess, система создает объект ядра "процесс" с начальным значением счстчика числа его пользователей, равным 1. Этот объект — не сам процесс, а компактная структура данных, через которую операци онная система управляет процессом. (Объект ядра "процесс" следует рассматривать как структуру данных со статистической информацией о процессе.) Затем система создает для нового процесса виртуальное адресное пространство и загружает в него код и данные как для исполняемого файла, тaк и для любых DLL (если таковые требу ются).

Далее система формирует объект ядра "поток" (со счетчиком, равным 1) для пер вичного потоки нового процесса. Как и в первом случае, объект ядра "поток" — это компактная структура данных, через которую система управляет потоком. Первичный поток начинает с исполнения стартового кода из библиотеки С/С++, который в ко нечном счете вызывает функцию WinMain, wWinMain, main или wmain в Вашей про грамме. Если системе удастся создать новый процесс и его первичный поток, Create Process вернет TRUE

NOTE:
CreateProcess возвращает TRUE до окончательной инициализации процесса. Это означает, что на данном этапе загрузчик операционной системы еще нс искал все необходимые DLL. Если он не сможет найти хотя бы одну из DLL или корректно провести инициализацию, процесс завершится. Но, поскольку Create Process уже вернула TRUE, родительский процесс ничего не узнает об этих про блемах.

На этом мы закончим общее описание и перейдем к подробному рассмотрению параметров функции CreateProcess

Параметры pszApplicationName и pszCommandLine

Эти параметры определяют имя исполняемого файла, которым будет пользоваться новый процесс, и командную строку, передаваемую этому процессу. Начнем cpszCom mandLine.

NOTE
Обратите внимание на тип параметра pszCommandLine: PTSTR. Он означает, что CreateProcess ожидает передачи адреса строки, которая не является констан той Дело в том, что CreateProcess в процессе своего выполнения модифици рует переданную командную строку, но перед возвратом управления восста навливает ее.

Это очень важно, если командная строка содержится в той части образа Вашего файла, которая предназначена только для чтения, возникнет ошибка доступа. Например, следующий код приведет к такой ошибке, потому что Visual С++ 6.0 поместит строку "NOTEPAD" в память только для чтения:

STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
CreateProcess(NULL, TEXT("NOTEPAD"), NULL, NULL, FALSE, 0, NULL. NULL, &si, &pi);

Когда CreateProcess попытается модифицировать строку, произойдет ошиб ка доступа. (В прежних версиях Visual C++ эта строка была бы размещена в памяти для чтения и записи, и вызовы CreateProcess не приводили бы к ошиб кам доступа.)

Лучший способ решения этой проблемы — перед вызовом CreateProcess ко пировать константную строку во временный буфер:

STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
TCHAR szComrnandLine[] = TEXT("NOTEPAD");
CreateProcess(NULL, szCommandLine, NULL, NULL, FALSE, 0, NULI, NULL, &si, &pi);

Возможно, Вас заинтересуют ключи /Gf и /GF компилятора Visual C++, ко торые исключают дублирование строк и запрещают их размещение в области только для чтения. (Также обратите внимание на ключ /ZI, который позволяет задействовать отладочную функцию Edit & Continue, поддерживаемую Visual Studio, и подразумевает активизацию ключа /GF.) В общем, лучшее, что може те сделать Вы, — использовать ключ /GF или создать временный буфер. А еще лучше, если Microsoft исправит функцию CreateProcess, чтобы та не морочила нам голову. Надеюсь, в следующей версии Windows так и будет.

Кстати, при вызове ANSI-версии CreateProcess в Windows 2000 таких про блем нет, поскольку в этой версии функции командная строка копируется во временный буфер (см. главу 2)

Параметр pszCommandLme позволяет указать полную командную строку, исполь зуемую функцией CreateProcess при создании нового процесса. Разбирая эту строку, функция полагает, что первый компонент в ней представляет собой имя исполняе мого файла, который Вы хотите запустить. Если в имени этого файла не указано рас ширение, она считает его EXE. Далее функция приступает к поиску заданного файла и делает это в следующем порядке:

  1. Каталог, содержащий ЕХЕ-файл вызывающего процесса.
  2. Текущий каталог вызывающего процесса.
  3. Системный каталог Windows.
  4. Основной каталог Windows.
  5. Каталоги, перечисленные в переменной окружения PATH.

Конечно, если в имени файла указан полный путь доступа, система сразу обраща ется туда и не просматривает эти каталоги. Найдя нужный исполняемый файл, она создает новый процесс и проецирует код и данные исполняемого файла на адресное пространство этого процесса Затем обращается к процедурам стартового кода из библиотеки С/С++. Тот в свою очередь, как уже говорилось, анализирует командную строку процесса и передает (w)WinMain адрес первого (за именем исполняемого фай ла) аргумента как pszCmdLine.

Все, о чем я сказал, произойдет, только если параметр pszApplicationName равен NULL (что и бывает в 99% случаев). Вместо NULL можио передать адрес строки с име нем исполняемого файла, который надо запустить. Однако тогда придется указать не только его имя, но и расширение, поскольку в этом случае имя не дополняется рас ширением EXE автоматически. CreateProcess предполагает, что файл находится в те кущем каталоге (если полный путь не задан). Если в текущем каталоге файла нет, функция не станет искать его в других каталогах, и на этом все закончится

Но даже при указанном в pszApplicationName имени файла CreateProcess все равно передает новому процессу содержимое параметра pszCommandLine как командную строку. Допустим, Вы вызвали CreateProcess так:

// размещаем строку пути в области памяти для чтения и записи
TCHAR szPath[] = TEXT("WORDPAD README.TXT");

// порождаем новый процесс
CreateProcess(TEXT("C:\\WINNr\\SYSrEM32\\NOTEPAD EXE"), szPath );

Система запускает Notepad, а в его командной строке мы видим "WORDPAD README.TXT". Странно, да? Но так уж она работает, эта функция CreateProcess. Упо мянутая возможность, которую обеспечивает пареметр pszApplicationName, на самом деле введена в CreateProcess для поддержки подсистемы POSIX в Windows 2000.

Параметры psaProcess, psaThread и blnheritHandles

Чтобы создать новый процесс, система должна сначала создать объекты ядра "про цесс" и "поток" (для первичного потока процесса). Поскольку это объекты ядра, ро дительский процесс получает возможность связать с ними атрибуты защиты. Пара метры psaProcess и psaThread позволяют определить нужные атрибуты защиты для объектов "процесс" и "поток" соответственно. В эти параметры можно занести NULL, и система закрепит за данными объектами дескрипторы защиты по умолчанию. В качестве альтернативы можно объявить и инициализировать две структуры SECU RITY_ATTRIBlITES; тем самым Вы создадите и присвоите объектам "процесс" и "по ток" свои атрибуты защиты.

Структуры SECURITY_ATTRIBUTES для параметров psaProcess wpsaTbread исполь зуются и для того, чтобы какой-либо из этих двух объектов получил статус наследуе мого любым дочерним процессом. (О теории, на которой построено наследование описателей объектов ядра, я рассказывал в главе 3.)

Короткая программа на рис. 4-2 демонстрирует, как наследуются описатели объек тов ядра. Будем считать, что процесс А порождает процесс В и заносит в параметр psaProcess адрес структуры SECURITY_ATTRIBUTES, в которой элемент blnheritHandle установлен как TRUE. Одновременно параметр psaThread указывает на другую струк туру SECURITY_ATTRIBUTES, в которой значение элемента bInheritHandle — FALSE.

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

Теперь предположим, что процесс А собирается вторично вызвать функцию Create Process, чтобы породить процесс С. Сначала ему нужно определить, стоит ли предос тавлять процессу С доступ к своим объектам ядра. Для этого используется параметр blnberitHandles, Если он приравнен TRUE, система передаст процессу С все наследуе мые описатели В этом случае наследуется и описатель объекта ядра "процесс" про цесса В. А вот описатель объекта "первичный поток" процесса В не наследуется ни при каком значении bInberitHandles. Кроме того, если процесс А вызывает Create Process, передавая через параметр blnberitHandles значение FALSE, процесс С не насле дует никаких описателей, используемых в данный момент процессом А.

Параметр fdwCreate

Параметр fdwCreate определяет флаги, влияющие на то, как именно создается новый процесс Флаги комбинируются булевым оператором OR.

  • Флаг DEBUG_PROCESS даст возможность родительскому процессу проводить отладку дочернего, а также всех процессов, которые последним могут быть порождены Если этот флаг установлен, система уведомляет родительский про цесс (он теперь получает статус отладчика) о возникновении определенных событий в любом из дочерних процессов (а они получают статус отлаживае мых).
  • Флаг DEBUG_ONLY_THIS_PROCESS аналогичен флагу DEBUG_PROCESS с тем исключением, что заставляет систему уведомлять родительский процесс о воз никновении специфических событий только в одном дочернем процессе — его прямом потомке. Тогда, если дочерний процесс создаст ряд дополнительных, отладчик уже нс уведомляется о событиях, "происходящих" в них.
  • Флаг CREATE_SUSPENDED позволяет создать процесс и в то же время приоста новить его первичный поток Это позволяет родительскому процессу модифи цировать содержимое памяти в адресном пространстве дочернего, изменять приоритет его первичного потока или включать этот процесс в задание (job) до того, как он получит шанс на выполнение. Внеся нужные изменения в до черний процесс, родительский разрешает выполнение его кода вызовом фун кции ResumeThread (см. главу 7).
  • Флаг DFTACHED_PROCESS блокирует доступ процессу, инициированному кон сольной программой, к сопданному родительским процессом консольному окну и сообщает системе, что вывод следует перенаправить в новое окно CUI процесс, создаваемый другим CUI-процессом, по умолчанию использует кон сольное окно родительского процесса (Вы, очевидно, заметили, что при за пуске компилятора С из командного процессора новое консольное окно не создается, весь его вывод "подписывается" в нижнюю часть существующего консольного окна ) Таким образом, этот флаг заставляет новый процесс пере направлять свой вывод в новое консольное окно
  • Флаг CREATE_NEW_CONSOLE приводит к созданию нового консольного окна для нового процесса. Имейте в виду, что одновременная установка флагов CREATE_NEW_CONSOLE и DETACHED_PROCESS недопустима.
  • Флаг CREATE_NO_WINDOW не дает создавать никаких консольных окон для данного приложения и тем самым позволяет исполнять его без пользователь ского интерфейса.
  • Флаг CREATE_NEW_PROCESS_GROUP служит для модификации списка процес сов, уведомляемых о нажатии клавиш Ctrl+C и Ctrl+Break Если в системе од новременно исполняется несколько CUI-процессов, то при нажагии одной из упомянутых комбинаций клавиш система уведомляет об этом только процес сы, включенные в группу. Указав этот флаг при создании нового СUI-процес ca, Вы создаете и новую группу
  • Флаг CREATE_DEFAULT_ERROR_MODE сообщает системе, чтo новый процесс не должен наследовать режимы обработки ошибок, установленные в родитель ском (см. раздел, где я рассказывал о функции SetErrorMode).
  • Флаг CREATE_SEPARATE_WOW VDM полезен только при запуске 16-разрядно го Wmdows-приложения в Windows 2000. Если он установлен, система созда ет отдельную виртуальную DOS-машину (Virtual DOS-machine, VDM) и запус кает 16-разрядное Windows-приложение именно в ней (По умолчанию все 16 разрядные Windows-приложения выполняются в одной, общей VDM.) Выпол нение приложения в отдельной VDM дает большое преимущество, "рухнув", приложение уничтожит лишь эту VDM, а программы, выполняемые в других VDM, продолжат нормальную работу. Кроме того, 16-разрядные Windows-при ложения, выполняемые в раздельных VDM, имеют и раздельные очереди вво да. Эго значит, что, если одно приложение вдруг "зависнет", приложения в других VDM продолжат прием ввода. Единственный недостаток работы с нес колькими VDM в том, что каждая из них требуеч значительных объемов физи ческой памяти. Windows 98 выполняет все 16-разрядные Windows-приложения только в одной VDM, и изменить тут ничего нельзя.
  • Флаг CREATE_SHARED_WOW_VDM полезен только при запуске 16-разрядного Windows-приложения в Windows 2000. По умолчанию все 16-разрядные Windows-приложения выполняются в одной VDM, ссли только не указан флаг CREATE_SEPARATEWOW_VDM. Однако стандартное пoвeдeниeWindows 2000 можно изменить, присвоив значение "yes" параметру DefaultSeparateVDM в paздeлe HKEY_LOCAL_MACHINE\System\CurгentControlSet\Contгol\WOW.(Пoc ле модификации этого параметра систему надо перезагрузить.) Установив зна чение "yes", но указав флаг CREATE_SHARED_WOW_VDM, Вы вновь заставите Windows 2000 выполнять все 16-разрядные Windows-приложения в одной VDM.
  • Флаг CREATE_UNICODE_ENVIRONMENT сообщает системе, что блок перемен ных окружения дочернего процесса должен содержать Unicode-символы. По умолчанию блок формируется на основе ANSI-символов
  • Флаг CREATE_FORCEDOS заставляет систему выполнять программу MS-DOS, встроенную в 16-разрядное приложение OS/2
  • Флаг CREATE_BREAKAWAY_FROM_JOB позволяет процессу, включенному в за дание, создать новый процесс, отделенный от этого задания (см. главу 5).

Параметр fdwCreate разрешает задать и класс приоритета процесса Однако это необязательно и даже, как правило, нс рекомендуется, система присваивает новому процессу класс приоритета по умолчанию. Возможные классы приоритета перечис лены в следующей таблице.

Класс приоритета

Флаговый идентификатор

Idle (простаивающий)

IDLE_PRIORITY_CLASS

Below normal (ниже обычного)

BELOW_NORMAL_PRIORITY_CLASS

Normal (обычный)

NORMAL PRIORITY CLASS

Above normal (выше обычного)

ABOVE_NORMAL_PRIORITY_CLASS

High (высокий)

HIGH_PRIORITY_CLASS

Realtime (реального времени)

REALTIME_PRIORITY_CLASS

Классы приоритета влияют на распределение процессорного времени междупро цессами и их потоками. (Подробнее на эту тему см. главу 7.)

NOTE
Классы приоритета BELOW_NORMAL_PRIORITY_CLASS и ABOVE_NORMAL_ PRIORITY_CLASS введены лишь в Windows 2000; они не поддерживаются в Win dows NT 4.0, Windows 95 или Windows 98.

Параметр pvEnvironment

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

PVOID GetEnvironmentStrings();

Она позволяет узнать адрес блока памяти со строками переменных окружения, используемых вызывающим процессом. Полученный адрес можно занести в параметр pvEnvironment функции CreateProcess. (Именно это и делает CreateProcess, если Вы передаете ей NULL вместо pvEnvironment.) Если этот блок памяти Вам больше не ну жен, освободите его, вызнав функцию FreeEnvironmentStrings:

BOOL FreeEnvironmentStrings(PTSTR pszEnvLronmenLBlock);

Параметр pszCurDir

Он позволяет родительскому процессу установить текущие диск и каталог для дочер него процесса. Если его значение — NULL, рабочий каталог нового процесса будет тем же, что и у приложения, его породившего. А если он отличен от NULL, то должен ука зывать на строку (с нулевым символом в конце), содержащую нужный диск и каталог. Заметьте, что в путь надо включать и букву диска.

Параметр psiStartlnfo

Этот параметр указывает на структуру STARTUPINFO:

typedef struct _STARTUPINFO {
DWORD cb;
PSTH lpReserved;
PSTR lpDesktop;
PSTR lpTitle;
DWORD dwX;
DWORD dwY;

DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
DWORD dwFlags;
WORD wShowWindOw;
WORD cbReserved2;
PBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFO, *LPSTARTUPINFO;

Элементы структуры STARTUPINFO используются Windows-функциями при созда нии нового процесса. Надо сказать, что большинство приложений порождает процес сы с атрибутами по умолчанию Но и в этом случае Вы должны инициализировать все элементы структуры STARTUPINFO хотя бы нулевыми значениями, а в элемент сb — заносить размер этой структуры:

STARTUPINFO si = { sizeof(si) }; CreateProcess(.. , &si, ...};

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

Когда Вам понадобится изменить какие-то элементы структуры, делайте это пе ред вызовом CreateProcess. Все элементы зтой структуры подробно рассматриваются в таблице 4-3- Но заметьте, что некоторые элементы имеют смысл, только если до чернее приложение создает перекрываемое (overlapped) окно, а другие — если это приложение осуществляет ввод-вывод на консоль

Элемент

Окно или консоль

Описание

cb

То и другое

Содержит количество байтов, занимаемых структу рой STARTUPINFO. Служит для контроля версий — на тот случай, если Microsoft расширит эту структуру в будущем Программа должна инициализировать cb как sizeof(STARTUPINFO)

lpReserved

То и другое

Зарезервирован Инициализируйте как NULL.

IpDesktop

То и другое

Идентифицирует имя рабочего стола, на котором за пускается приложение Если указанный рабочий стол существует, новый процесс сразу же связывается с ним. В ином случае система сначала создает рабочий стол с атрибутами по умолчанию, присваивает ему имя, указанное в данном элементе структуры, и свя зываем его с новым процессом. Если IpDesktop равен NULL (что чаще всего и бывает), процесс связывается с текущим рабочим столом.

IpTitle

Консоль

Определяет заголовок консольного окна. Если IpTitle — NULL, в заголовок выводится имя исполняе мого файла.

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

Элемент

Окно или консоль

Описание

dwX

dwY

То и другое

Указывают х- и j'-координаты (в пикселах) окна приложения Эти координаты используются, только если дочерний процесс создаст свое первое перекры ваемое окно с идентификатором CW_USEDEFAULT в параметре х функции CreateWindow. В приложениях, создающих консольные окна, данные элементы опре деляют верхний левый угол консольною окна.

dwXSize
dwYSize

То и другое

Определяют ширину и высоту (в пикселах) окна приложения. Эти значения используются, только если дочерний процесс создает свое первое перекрывае мое окно с идентификатором CW_USEUEFAULT в параметре nWtdth функции CreateWindow В приложениях, создающих консольные окна, данные элементы определяют ширину и высоту консольного окна

dwXCountChars dwYCountChars

Консоль

Определяют ширину и высоту (в символах) консольных окон дочернего процесса

dwFillAttnbute

Консоль

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

dwFlags

То и другое

См ниже и следующую таблицу

wSbowWtndow

Окно

Определяет, как именно должно выглядеть первое перекрываемое окно дочернего процесса, если приложение при первом вызове функции ShowWindow передает в параметре nCmdSbow идентификаюр SW_SHOWDEFAULT. В этот элеменn можно записать любой из идентификаторов типа SW_*, обычно используемых при вызове SbowWindoiv.

cbReserved2

То и друюс

Зарезервирован Инициализируйте как 0.

lpReserved2

То и друюс

Зарезервирован. Инициализируйте как NULL.

hStdlnput
hStdOutlput
bStdError

Консоль

Определяют описатели буферов для консольного ввода-вывода. По умолчанию bStdlnpitt идентифицирует буфер клавиатуры, a bStdOutput и bStdError — буфер консольного окна

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

Флаг

Описание

STARTF_USESIZE

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

STARTF_USESHOWWINDOW

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

STARTF_USEPOSITION

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

STARTF_USECOTUNTCHARS

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

STARTF_USEFILLATTRIBUTE

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

STARTF_USESTDHANDLES

Заставляет использовать элементы hStdlnput, hStdOutput и bStdError

Флаг

Описание

STARTF_RUN_FULLSCREEN

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

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

Курсор такой формы подсказывает: можно либо подождать чего-нибудь, что вот вот случится, либо продолжить работу в системе. Если же Вы укажете флаг STARTF_ FORCEOFFFEEDBACK, CreateProcess не станет добавлять "песочные часы" к стандарт ной стрелке.

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

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

В заключение раздела — несколько слов об элементе wShowWindow структуры STARTUPINFO. Этот элемент инициализируется значением, которое Вы передаете в (w)WinMain через ее последний параметр, nCmdShoiv. Он позволяет указать, в каком виде должно появиться главное окно Вашею приложения В качестве значения ис пользуется один из идентификаторов, обычно передаваемых в ShowWindow (чаще всего SW_SHOWNORMAL или SW_SHOWMINNOACTIVE, но иногда и SW_SHOW DEFAULT).

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

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

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

h4-3.jpg

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

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

VOID GetStartupInfo(PSTARTUPINFO pStartupInfo);

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

NOTE
Хотя в документации на Windows об этом четко не сказано, перед вызовом GetStartupInfo нужно инициализировать элемент cb структуры STARTUPINFO:

STARTUPINFO si = { sizeof(si) }, GetStartupInfo(&si)

Параметр ppiProclnfo

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

typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
} PROCESS_TNFORMATION;

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

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

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

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

Созданному объекту ядра "процесс" присваивается уникальный идентификатор; ни у каких других объектов этого типа в системе нс может быть одинаковых иденти фикаторов. Это же касается и объектов ядра "поток". Причем идентификаторы про цесса и потока тоже разные, и их значения никогда не бывают нулевыми. Завершая свою работу, CreateProcess заносит значения идентификаторов в элементы divProcessId и dwThreadld структуры PROCESS_INFORMATION Эти идентификаторы просто облег чают определение процессов и потоков в системе; их используют, как правило, лишь специализированные утилиты вроде Task Manager.

Подчеркну ещс один чрезвычайно важный момент система способна повторно использовать идентификаторы процессов и потоков. Например, при создании про цесса система формирует объект "процесс", присваивая ему идентификатор со зна чением, допустим, 122 Создавая новый объект "процесс", система уже не присвоит ему данный идентификатор. Но после выгрузки из памяти первого объекта следу ющему создаваемому объекту "процесс" может быть присвоен тот же идентификатор — 122.

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

Иногда программе приходится определять свой родительский процесс Однако родственные связи между процессами существуют лишь на стадии создания дочер него процесса Непосредственно перед началом исполнения кода вдочернем процес се Windows перестает учитывать его родственные связи. В предыдущих версиях Win dows не было функций, которые позволяли бы программе обращаться с запросом к ее родительскому процессу. Но ToolHelp-функции, появившиеся в современных вер сиях Windows, сделали это возможным. С этой целью Вы должны использовать струк туру PROCESSENTRY32: ее элемент th32ParentProcessID возвращает идентификатор "родителя" данного процесса. Тем не менее, если Вашей программе нужно взаимодей

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

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

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

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

  • входная функция первичного потока возвращает управление (рекомендуемый способ),
  • один из потоков процесса вызывает функцию ExitProcess (нежелательный спо соб);
  • поток другого процесса вызывает функцию TerminateProcess (тоже нежелательно);
  • все потоки процесса умирают по своей воле (большая редкость),

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

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

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

  • любые С++-объекты, созданные данным потоком, уничтожаются соответству ющими деструкторами;
  • система освобождает память, которую занимал стек потока-,
  • система устанавливает код завершения процесса (поддерживаемый объектом ядра "процесс") — его и возвращает Ваша входная функция;
  • счетчик пользователей данного объекта ядра "процесс" уменьшается на 1.

Функция ExitProcess

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

VOID ExilProcess(UINT fuExitCode);

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

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

ExitProcess, передавая ей значение, возвращенное входной функцией Вот почему воз врат управления входной функцией первичного потока приводит к завершению все го процесса. Обратите внимание, что при завершении процесса прекращается выпол нение и всех других его потоков.

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

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

#include <windows n>
#include <stdio h>

class CSomeObj {
public:
CSomeOtrK) { printf("Constructor\r\n"), }
~CSomeObj() { printf("Destructor\r\n"); }
};

CSomeObj g_GlobalObj;

void main () {
CSomeObj LocalObj;
ExitProcess(0); // этого здесь не должно быть

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

При его выполнении Вы увидите:

Constructor
Constructor

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

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

Constructor
Constructor

Destructor
Destructor

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

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

Функция TerminateProcess

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

BOOL TerminateProcess( HANDLE hProcoss, UINT fuExitCode);

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

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

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

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

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

Когда все потоки процесса уходят

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

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

А происходит вот что.

  1. Выполнение всех потоков в процессе прекращается
  2. Все User- и GDI-объекты, созданные процессом, уничтожаются, а объеюы ядра закрываются (если их не используетдругой процесс).
  3. Код завершения процесса меняется со значения STILL_ACTIVE на код, передан ный в ExitProcess или TerminateProcess.
  4. Объект ядра "процесс" переходит в свободное, или незанятое (signaled), состо яние (Подробнее на эту тему см главу 9 ) Прочие потоки в системе могут при остановить свое выполнение вплоть до завершения данного процесса.
  5. Счетчик объекта ядра "процесс" уменьшается на 1

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

По завершении процесса его код и выделенные ему ресурсы удаляются из памяти. Однако область памяти, выделенная системой для объекта ядра "процесс", не осво бождается, пока счетчик числа его пользователей не достигнет нуля А это произой дет, когда все прочие процессы, создавшие или открывшие описатели для ныне-по койного процесса, уведомят систему (вызовом CloseHandle) о том, что ссылки па этот процесс им больше не нужны.

Описатели завершенного процессса уже мяло на что пригодны. Разве что роди тельский процесс, вызвав функцию GetExitCodeProcess, может проверигь, завершен ли процесс, идентифицируемый параметром hProcess, и, если да, определить код завер шения:

BOOL GetExitCodeProcess( HANDLE hProcess, PDWORD pdwExitCode);

Код завершения возвращается как значение типа DWORD, на которое указывает pdwExitCode. Если па момент вызова GetExitCodeProcess процесс еще не завершился, в DWORD заносится идентификатор STILL_ACTIVE (определенный как 0x103) А если он уничтожен, функция возвращает реальный код его завершения.

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

Дочерние процессы

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

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

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

К сожалению, дочернему процессу, по-видимому, придется оперировать с данны ми, содержащимися в адресном пространстве родительского процесса. Было бы не плохо, чтобы он работал исключительно в своем адресном пространстве, а в "Ва шем" — просто считывал нужные ему данные, тогда он не сможет что-то испортить в адресном пространстве родительского процесса. В Windows предусмотрено несколько способов обмена данными между процессами: DUE (Dynamic Data Exchange), OLE, каналы (pipes), почтовые ящики (mailslots) и т. д, А один из самых удобных способов, обеспечивающих совместный доступ к данным, — использование файлов, проециру емых в память (memory-mapped files) (Подробнее на эту тему см. главу 17.)

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

PROCESS_INFORMATION pi;
DWORD dwExitCode;

// порождаем дочерний процесс
BOOL fSuccess = CreateProcess(..., &pi};

if (fSuccess) {

// закрывайте описатель потока, как только необходимость в нем отпадает!
CloseHandle(pi hThread);


// приостанавливаем выполнение родительского процесса,
// пока не завершится дочерний процесс
WaitForSingleObject(pi hProcess, INFINlTI);

// дочерний процесс завершился; получаем код его завершения
GetExitCodeProcess(pi.hProcess, &dwExitCode);

// закрывайте описатель процесса, как только необходимость в нем отпадает!
CloseHandle(pi.hProcess);

}

В этом фрагменте кода мы создали новый процесс и, если это прошло успешно, вызвали функцию WaitForSingleQbject

DWORD WaitForSingleObject(HANDLE hObject, DWORD dwTimeOut);

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

пока объект, определяемый параметром bObject, не перейдет в свободное (незанятое) состояние. Объект "процесс" переходит в такое состояние при его завершении По этому вызов WaitForSingleObject приостанавливает выполнение потока родительского процесса до завершения порожденного им процесса. Когда WaitForSingleObject вернет управление, Вы узнаете код завершения дочернего процесса через функцию Get ExitCodeProcess.

Обращение к CloseHandle в приведенном выше фрагменте кода заставляет систе му уменьшить значения счетчиков объектов "поток" и "процесс" до нуля и тем самым освободить память, занимаемую этими объектами.

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

Запуск обособленных дочерних процессов

Что ни говори, но чаще приложение все-таки создает другие процессы как обособ ленные (detached processes) Это значит, что после создания и запуска нового процесса родительскому процессу нет нужды с ним взаимодействовать или ждать, пока тот закончит работу Именно так и действует Explorer: запускает для пользователя новые процессы, а дальше его уже не волнует, что там с ними происходит.

Чтобы обрубить все пуповины, связывающие Explorer c дочерним процессом, ему нужно (вызовом CloseHandle) закрыть свои описатели, связанные с новым процессом и его первичным потоком Приведенный ниже фрагмент кода демонстрирует, как, создав процесс, сделать его обособленным

PROCESS_INFORMATION pi;

BOOL fSuccess = CreateProcess( , &pi);
if (fSuccess) {

// разрешаем системе уничтожить обьекты ядра "процесс" и "поток"
// сразу после завершения дочернего процесса
CloseHandle(pi.hTnread);
CloseHandle(pi hProcess);

}

Перечисление процессов, выполняемых в системе

Многие разработчики программного обеспечения пытаются создавать инструмен тальные средства или утилиты для Windows, требующие перечисления процессов, выполняемых в системе Изначально в Windows API не было функций, которые по зволяли бы это делать. Однако в Windows NT ведется постоянно обновляемая база данных Performance Data. В ней содержится чуть ли не тонна информации, доступ ной через функции рссстра вроде RegQueryValueEx, для которой надо указать корне

вой раздел HKEY_PERFORMANCE_DATA. Мало кто из программистов знает об этой базе данных, и причины тому кроются, на мой взгляд, в следующем.

  • Для нее не предусмотрено никаких специфических функций, нужно исполь зовать обычные функции реестра.
  • Ее нет в Windows 95 и Windows 98.
  • Структура информации в этой базе данных очень сложна; многие просто из бегают ею пользоваться (и другим не советуют).

Чтобы упростить работу с этой базой данных, Microsoft создала набор функций под общим названием Performance Data Helper (содержащийся в PDH.dll). Если Вас интересует более подробная информация о библиотеке PDH.dll, ищите раздел по функциям Performance Data Helper в документации Platform SDK

Как я уже упоминал, в Windows 95 и Windows 98 такой базы данных нет. Вместо них предусмотрен набор функций, позволяющих перечислять процессы. Они вклю чены в ToolHelp API За информацией о них я вновь отсылаю Вас к документации Platform SDK — ищите разделы по функциям Process32First и Process32Next,

Но самое смешное, что разработчики Windows NT, которым ToolHelp-функции явно не нравятся, не включили их в Windows NT. Для перечисления процессов они создали свой набор функций под общим названием Process Status (содержащийся в PSAPI.dll). Так что ищите в документации Platform SDK раздел по функции Enum Processes.

Microsoft, которая до сих пор, похоже, старалась усложнить жизнь разработчи кам инструментальных средств и утилит, все же включила ToolHelp-функции в Win dows 2000. Наконец-то и эти разработчики смогут унифицировать свой код хотя бы для Windows 95, Windows 98 и Windows 2000!

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

Эта программа, "04 ProcessInfo.exe" (см листинг на рис. 4-6), демонстрирует, как со здать очень полезную утилиту на основе ToolHelp-функций. Файлы исходного кода и ресурсов программы находятся в каталоге 04-ProcessInfo на компакт-диске, прилага емом к книге. После запуска Processlnfo открывается окно, показанное на рис. 4-4.

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

При просмотре списка процессов становится доступен элемент меню VMMap. (Он отключается, когда Вы переключаетесь на просмотр информации о модулях.) Выб рав элемент меню VMMap, Вы запускаете программу-пример VMMap (см. главу 14). Эта программа "проходит" по адресному пространству выбранного процесса.

В информацию о модулях входит список всех модулей (EXE- и DLL-файлов), спро~ ецированных на адресное пространство текущего процесса. Фиксированным моду лем (fixed module) считается тот, который был неявно загружен при инициализации процесса. Для явпо загруженных DLL показываются счетчики числа пользователей этих DLL. Во втором столбце выводится базовый адрес памяти, на который спроеци

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

h4-4.jpg

Рис. 4-4. ProcessInfo в действии

В дополнение к информации о процессах Вы можете выбрать элемент меню Modu les. Это заставит ProcessInfo перечислить все модули, загруженные в системе, и поме стить их имена в верхний раскрывающийся список Далее ProcessInfo выбирает пер вый модуль и выводит информацию о нем (рис. 4-5).

В этом режиме утилита ProcessInfo позволяет легко определить, в каких процес сах задействован данный модуль. Как видите, полное имя модуля появляется в верх ней части текстового поля, а в разделе Process Information перечисляются все процес сы, содержащие этот модуль. Там же показываются идентификаторы и имена процес сов, в которые загружен модуль, и его базовые адреса в этих процессах.

Всю эту информацию утилита ProcessInfo получает в основном от различных ToolHelp-функций. Чтобы чуточку упростить работу с ToolHelp-функциями, я создал С++-класс CToolhelp (содержащийся в файле Toolhelp.h). Он инкапсулирует все, что связано с получением "моментального снимка" состояния системы, и немного облег чает вызов других TooIHelp-функций.

Особый интерес представляет функция GetModulePreferredBaseAddr в файле Pro cessInfo.cpp:

PVOID GetModulePreferredBaseAddr( DWORD dwProcessId, PVOID pvModuleRemote);

h4-5.jpg

Рис. 4-5. Processlnfo перечисляет все процессы, в адресные пространства которых загружен модуль User32.dll

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

Processlnfo