ЧАСТЬ II НАЧИНАЕМ РАБОТАТЬ
Эта глава о том, как система управляет выполняемыми приложениями. Сначала я определю понятие "процесс" и объясню, как система создает объект ядра "процесс" Затем я покажу, как управлять процессом, используя сопоставленный с ним объект ядра Далее мы обсудим атрибуты (или свойства) процесса и поюворим о несколь ких функциях, позволяющих обращаться к этим свойствам и изменять их. Я расскажу также о функциях, которые создают (порождают) в системе дополнительные процес сы Ну и, конечно, описание процессов было бы неполным, если бы я не рассмотрел механизм их завершения. О'кэй, приступим
Процесс обычно определяют как экземпляр выполняемой программы, и он состо ит из двух компонентов
Рис. 4-1. Операционная система выделяет потокам кванты времени по принципу карусели
Процессы
инертны Чтобы процесс что-нибудь выполнил, в нем нужно создать поток Именно
потоки отвечаю за исполнение кодя, содержащегося в адресном про странстве
процесса В принципе, один процесс может владеть несколькими потока ми, и тогда
они "одновременно" исполняют код в адресном пространстве процесса.
Для этого
каждый поток должен располагать собственным набором регистров про цессора и
собственным стеком В каждом процессе есть минимум один поток. Если бы у процесса
не было ни одного потока, ему нечего было бы делать на этом свете, и система
автоматически уничтожила бы его вместе с выделенным ему адресным про странством.
Чтобы всс эти
потоки работали, операционная система отводит каждому из них определенное
процессорное время. Выделяя потокам отрезки времени (называемые
квантами) по принципу карусели, она создает тем самым иллюзию
одновременного выполненияпотоков Рис 4-1 иллюстрирует распределение
процессорноговремени между потоками па машине с одним процессором. Если в машине
установлено более одного процессора, алгоритм работы операционной системы
значительно усложняет ся (в этом случае система стремится сбалансировать
нагрузку между процессорами). При создании
процесса первый (точнее, первичный) поток создается системой автоматически.
Далее этот поток может породить другие потоки, те в свою очередь — новые и т. д.
WINDOWS 2000
WINDOWS 98
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 В этот момент раз
работчик может выбрать один из четырех вариантов дальнейших действий:
Все стартовые
функции из библиотеки С/С++ делают практически одно и то же. Разница лишь в том,
какие строки они обрабатывают (в ANSI или Unicode) и какую входную функцию
вызывают после инициализации библиотеки. Кстати, с Visual C++ поставляется
исходный код этой библиотеки, и стартовые функции находятся в фай ле CRt0.c. А
теперь рассмотрим, какие операции они выполняют: Закончив эти
операции, стартовая функция обращается к входной функции в Вашей программе. Если
Вы написали ее в виде 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
выполняет следующие операции: Имя
переменной Тип
Описание _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:
Истинное
значение параметра 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( Поскольку у
второго параметра нет имени, компилятор не выдает предупрежде ние "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;
// используйте
эти аргументы if (*ppArgv[1] == L x ) { С любым
процессом связан блок переменных окружения — область памяти, выделен ная в
адресном пространстве процесса Каждый блок содержит группу строк такого вида
VarName1-VarValue1\0 VarName2-VarValue2\0 VarName3=VarValue3\0 \0
Первая часть
каждой строки — имя переменной окружения. Зa ним следует знак равенства и
значение, присваиваемое переменной Строки в блоке переменных ок ружения должны
бьпь отсортированы в алфавитном порядке по именам переменных Знак
равенства разделяет имя переменной и ее значение, так что его нельзя ис
пользовать как символ в имени переменной Важную роль играют и пробелы Например,
объявив две переменные XYZ= Windows
(обратите внимание на пробел за знаком равенства) ABC=Windows
и сравнив
значения переменных ХУZ и АВС, Вы увидите, что система их различает, — она
учитывает любой пробел, поставленный перед знаком равенства или после него
Вот что будет, если записать, скажем, так XYZ =Home
(обратите внимание на пробел перед знаком равенства) XYZ=Work
Вы получите
первую переменную с именем "XYZ", содержащую строку "Home", и вторую переменную
"XYZ", содержащую строку "Work" Конец блока
переменных окружения помечается дополнительным нулевым сим волом WINDOWS 98
SET
VarName=VarValue При
перезагрузке система учтет новое содержимое файла Autoexecbat, и тогда любые
заданные Вами переменные окружения станут доступны всем процессам,
инициируемым в сеансе работы с Windows 98 WINDOWS 2000
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\
SessionManager\Environment содержится
список переменных окружения, относящихся к системе, а во втором: HKEY_CURRENT_USER\Environment находится
список переменных окружения, относящихся к пользователю, ко торый в настоящее
время зарегистрирован в системе. Пользователь
может добавлять, удалять или изменять любые переменные через апплет System из
Control Panel B этом апплете надо открыть вкладку Advanced и щелкнуть кнопку
Environment Variables — тогда на экране появит ся следующее диалоговое окно.
Модифицировать переменные из списка 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 На моей машине
эта переменная выглядит так: 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( Она
устанавливает ту переменную, на чье имя указывает параметр 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:
Если
родительский процесс создает блок переменных окружения и хочст пере дать его
дочернему процессу, тот не наследует текущие каталоги родительского про цесса
автоматически. Вместо этого у дочернего процесса текущими на всех дисках
становятся корневые каталоги. Чтобы дочерний процесс унаследовал текущие ката
логи родительского, последний должен создать соответствующие переменные окру
жения (и сделать это до порождения другого процесса). Родительский процесс мо
жет узнать, какие каталоги являются текущими, вызвав
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
{ Эта структура
— новинка 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, wProductType Сообщает, какой именно вариант операционной системы
установлен; его возможные значения: VER_NT_WORKSTATION, wReserved Зарезервирован на будущее В Windows
2000 появилась новая функция, VerifyVersionInfo, которая сравнивает
версию установленной операционной системы с тем, что требует Ваше
приложение: BOOL
VerifyVersionInfo( Чтобы
использовать эту функцию, соэдайте структуру 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( Первый
параметр, 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
// формируем
маску условии DWORDLONG
dwlConditionMask = 0; // проверяем
версию //
хост-система точно соответствует Windows 2000 } else {
Процесс
создается при вызове Вашим приложением функции CreateProcess
BOOL
CreateProcess( Когда поток в
приложении вызывает CreateProcess, система создает объект ядра "процесс"
с начальным значением счстчика числа его пользователей, равным 1. Этот объект —
не сам процесс, а компактная структура данных, через которую операци онная
система управляет процессом. (Объект ядра "процесс" следует рассматривать как
структуру данных со статистической информацией о процессе.) Затем система
создает для нового процесса виртуальное адресное пространство и загружает в него
код и данные как для исполняемого файла, тaк и для любых DLL (если таковые требу
ются). Далее система
формирует объект ядра "поток" (со счетчиком, равным 1) для пер вичного потоки
нового процесса. Как и в первом случае, объект ядра "поток" — это компактная
структура данных, через которую система управляет потоком. Первичный поток
начинает с исполнения стартового кода из библиотеки С/С++, который в ко нечном
счете вызывает функцию WinMain, wWinMain, main или wmain в Вашей
про грамме. Если системе удастся создать новый процесс и его первичный поток,
Create Process вернет TRUE NOTE:
На этом мы
закончим общее описание и перейдем к подробному рассмотрению параметров функции
CreateProcess Эти параметры
определяют имя исполняемого файла, которым будет пользоваться новый процесс, и
командную строку, передаваемую этому процессу. Начнем cpszCom
mandLine. NOTE
Это очень
важно, если командная строка содержится в той части образа Вашего файла, которая
предназначена только для чтения, возникнет ошибка доступа. Например, следующий
код приведет к такой ошибке, потому что Visual С++ 6.0 поместит строку "NOTEPAD"
в память только для чтения: STARTUPINFO si
= { sizeof(si) }; Когда
CreateProcess попытается модифицировать строку, произойдет ошиб ка
доступа. (В прежних версиях Visual C++ эта строка была бы размещена в памяти для
чтения и записи, и вызовы CreateProcess не приводили бы к ошиб кам
доступа.)
Лучший способ
решения этой проблемы — перед вызовом CreateProcess ко пировать
константную строку во временный буфер: STARTUPINFO si
= { sizeof(si) }; Возможно, Вас
заинтересуют ключи /Gf и /GF компилятора Visual C++, ко торые исключают
дублирование строк и запрещают их размещение в области только для чтения. (Также
обратите внимание на ключ /ZI, который позволяет задействовать отладочную
функцию Edit & Continue, поддерживаемую Visual Studio, и подразумевает
активизацию ключа /GF.) В общем, лучшее, что може те сделать Вы, — использовать
ключ /GF или создать временный буфер. А еще лучше, если Microsoft исправит
функцию CreateProcess, чтобы та не морочила нам голову. Надеюсь, в
следующей версии Windows так и будет. Кстати, при
вызове ANSI-версии CreateProcess в Windows 2000 таких про блем нет,
поскольку в этой версии функции командная строка копируется во временный буфер
(см. главу 2) Параметр
pszCommandLme позволяет указать полную командную строку, исполь зуемую
функцией CreateProcess при создании нового процесса. Разбирая эту строку,
функция полагает, что первый компонент в ней представляет собой имя исполняе
мого файла, который Вы хотите запустить. Если в имени этого файла не указано рас
ширение, она считает его EXE. Далее функция приступает к поиску заданного файла
и делает это в следующем порядке: Конечно, если
в имени файла указан полный путь доступа, система сразу обраща ется туда и не
просматривает эти каталоги. Найдя нужный исполняемый файл, она создает новый
процесс и проецирует код и данные исполняемого файла на адресное пространство
этого процесса Затем обращается к процедурам стартового кода из библиотеки
С/С++. Тот в свою очередь, как уже говорилось, анализирует командную строку
процесса и передает (w)WinMain адрес первого (за именем исполняемого фай
ла) аргумента как pszCmdLine. Все, о чем я
сказал, произойдет, только если параметр pszApplicationName равен NULL
(что и бывает в 99% случаев). Вместо NULL можио передать адрес строки с
име нем исполняемого файла, который надо запустить. Однако тогда придется
указать не только его имя, но и расширение, поскольку в этом случае имя не
дополняется рас ширением EXE автоматически. CreateProcess предполагает,
что файл находится в те кущем каталоге (если полный путь не задан). Если в
текущем каталоге файла нет, функция не станет искать его в других каталогах, и
на этом все закончится Но даже при
указанном в pszApplicationName имени файла CreateProcess все равно
передает новому процессу содержимое параметра pszCommandLine как
командную строку. Допустим, Вы вызвали CreateProcess так: // размещаем
строку пути в области памяти для чтения и записи // порождаем
новый процесс Система
запускает Notepad, а в его командной строке мы видим "WORDPAD README.TXT".
Странно, да? Но так уж она работает, эта функция CreateProcess. Упо
мянутая возможность, которую обеспечивает пареметр pszApplicationName, на
самом деле введена в CreateProcess для поддержки подсистемы POSIX в
Windows 2000. Чтобы создать
новый процесс, система должна сначала создать объекты ядра "про цесс" и "поток"
(для первичного потока процесса). Поскольку это объекты ядра, ро дительский
процесс получает возможность связать с ними атрибуты защиты. Пара метры
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 определяет флаги, влияющие на то, как именно создается новый
процесс Флаги комбинируются булевым оператором OR. Параметр
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
Параметр
pvEnvironment указывает на блок памяти, хранящий строки переменных
окружения, которыми будет пользоваться новый процесс. Обычно вместо этого па
раметра передается NULL, в результате чего дочерний процесс наследует строки пе
рсмснных окружения от родительского процесса. В качестве альтернативы можно
вызвать функцию GetEnvironmentStrings PVOID
GetEnvironmentStrings(); Она позволяет
узнать адрес блока памяти со строками переменных окружения, используемых
вызывающим процессом. Полученный адрес можно занести в параметр pvEnvironment
функции CreateProcess. (Именно это и делает CreateProcess,
если Вы передаете ей NULL вместо pvEnvironment.) Если этот
блок памяти Вам больше не ну жен, освободите его, вызнав функцию
FreeEnvironmentStrings: BOOL
FreeEnvironmentStrings(PTSTR pszEnvLronmenLBlock); Он позволяет
родительскому процессу установить текущие диск и каталог для дочер него
процесса. Если его значение — NULL, рабочий каталог нового процесса будет тем
же, что и у приложения, его породившего. А если он отличен от NULL, то должен
ука зывать на строку (с нулевым символом в конце), содержащую нужный диск и
каталог. Заметьте, что в путь надо включать и букву диска. Этот параметр
указывает на структуру STARTUPINFO: typedef struct
_STARTUPINFO { DWORD dwXSize;
Элементы
структуры 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 То и
другое Определяют ширину и высоту (в пикселах) окна приложения. Эти
значения используются, только если дочерний процесс создает свое первое
перекрывае мое окно с идентификатором CW_USEUEFAULT в параметре nWtdth
функции CreateWindow В приложениях, создающих консольные окна,
данные элементы определяют ширину и высоту консольного окна
dwXCountChars dwYCountChars Консоль Определяют ширину и высоту (в символах) консольных окон
дочернего процесса dwFillAttnbute Консоль Задает
цвет текста и фона в консольных окнах дочернего
процесса dwFlags То и
другое См ниже
и следующую таблицу wSbowWtndow Окно Определяет, как именно должно выглядеть первое перекрываемое окно
дочернего процесса, если приложение при первом
вызове функции ShowWindow передает в
параметре nCmdSbow идентификаюр SW_SHOWDEFAULT.
В этот элеменn можно записать любой из
идентификаторов типа SW_*, обычно используемых при
вызове SbowWindoiv. cbReserved2 То и
друюс Зарезервирован Инициализируйте как 0. lpReserved2 То и
друюс Зарезервирован. Инициализируйте как NULL. hStdlnput Консоль Определяют описатели буферов для консольного ввода-вывода. По
умолчанию 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, Таким
образом, пользователь может легко выбирать, в каком окне запускать про грамму —
нормальном, свернутом или развернутом.
Рис. 4-3. Окно свойств для ярлыка Notepad Наконец,
чтобы получить копию структуры STARTUPINFO, инициализированной родительским
процессом, приложение может вызвать: VOID
GetStartupInfo(PSTARTUPINFO pStartupInfo); Анализируя
эту структуру, дочерний процесс может изменять свое поведение в зависимости oi
значений ее элементов NOTE STARTUPINFO si
= { sizeof(si) }, GetStartupInfo(&si) Параметр
ppiProcInfo указывает на структуру PROCESS_INFORMATION, которую Вы должны
предварительно создать; ее элементы инициализируются самой функцией
CreateProcess. Структура представляет собой следующее typedef struct
_PROCESS_INFORMATION { Как я уже
говорил, создание нового процесса влечет за собой создание объектов ядра
"процесс" и "поток" В момент создания система присваивает счетчику каждого
объекта начальное значение — единицу, Далее функция 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: VOID
ExilProcess(UINT fuExitCode); Эта функция
завершает процесс и заносит в параметр fuExitCode код завершения
процесса. Возвращаемого значения у ExitProcess нет, так как результат ее
действия — завершение процесса. Если за вызовом этой функции в программе
присутствует ка кой-нибудь код, он никогда не исполняется. Когда входная
функция (WinMain, wWinMain, main или wmairi) в Вашей програм ме
возвращает управление, оно передастся стартовому коду из библиотеки С/C++, и тот
проводит очистку всех ресурсов, выделенных им процессу, а затем обращается к
ExitProcess, передавая ей значение, возвращенное входной функцией
Вот почему воз врат управления входной функцией первичного потока приводит к
завершению все го процесса. Обратите внимание, что при завершении процесса
прекращается выпол нение и всех других его потоков. Кстати, в
документации из Platform SDK утверждается, что процесс не завершает ся до тех
пор, пока не завершится выполнение всех его потоков. Это, конечно, верно, но тут
есть одна тонкость. Стартовый код ил библиотеки С/С++ обеспечивает завер шение
процесса, вызывая ExitProcess после того, как первичный поток Вашего
прило жения возвращается из входной функции. Однако, вызвав из нее функцию
ExitThread (вместо того чтобы вызвать ExitProcess или просто
вернуть управление), Вы заверши те первичный поток, но не сам процесс — если в
нем еще выполняется какой-то дру гой поток (или потоки). Заметьте, что
такой вызов ExitProcess или ExitTbread приводит к уничтожению
процесса или потока, ко1да выполнение функции еще не завершилось. Что касается
операционной системы, то здесь все в порядке: она корректно очистит все ресурсы,
выделенные процессу или потоку Но в приложении, написанном на С/С++, следует
избегать вызова этих функций, так как библиотеке С/С++ скорее всего нс удастся
провести должную очистку. Взгляните на этот код: #include
<windows n> class CSomeObj
{ CSomeObj
g_GlobalObj; void main () {
// в конце
этой функции компилятор автоматически вставил код // дли вызова деструктора
LocalObj, но ExitProcess не дает его выполнить } При его
выполнении Вы увидите: Constructor
Код
конструирует два объекта: глобальный и локальный Но Вы никогда не увиди те
строку Destructor С++-объекты не разрушаются должным образом из-за того,
что ExitProcess форсирует уничтожение процесса и библиотека С/С++ не
получает шанса на очистку. Как я уже
говорил, никогда не вызывайте ExitProcess в явном виде. Если я уберу из
предыдущего примера вызов ExttProcess, программа выведет такие строки:
Constructor
Destructor
Простой
возврат управления от входной функции первичного потока позволил библиотеке
С/С++ провести нужную очистку и корректно разрушить С++-объекты. Кстати, все, о
чем я рассказал, относится не только к объектам, но и ко многим дру гим вещам,
которые библиотека С/С++ делает для Вашего процесса. NOTE
Вызов функции
TerminateProcess тоже завершает процесс: BOOL
TerminateProcess( HANDLE hProcoss, UINT fuExitCode); Главное
отличие этой функции от ExitProcess в том, что ее может вызвать любой
поток и завершить любой процесс. Параметр bProcess идентифицирует
описатель завершаемого процесса, а в параметре fuExitCode возвращается
код завершения про цесса. Пользуйтесь
TerminateProcess лишь в том случае, когда иным способом завершить процесс
не удается. Процесс не получает абсолютно никаких уведомлений о том, что он
завершается, и приложение не может ни выполнить очистку, ни предотвратить свое
неожиданное завершение (если оно, конечно, не использует механизмы защиты). При
этом теряются все данные, которые процесс не успел переписать из памяти на диск.
Процесс
действительно не имеет ни малейшего шанса самому провести очистку, но
операционная система высвобождает всс принадлежавшие ему ресурсы: возвраща ет
себе выделенную им память, закрывает любые открытые файлы, уменьшает счет чики
соответствующих объектов ядра и разрушает все его User- и GDI-объекты.
По завершении
процесса (не важно каким способом) система гарантирует: после него ничего не
останется — даже намеков на то, что он когда-то выполнялся. Завер шенный
процесс не оставляет за собой никаких следов. Надеюсь, я сказал
ясно.
NOTE В такой
ситуации (а она может возникнуть, если все потоки вызвали ExitTbread или
их закрыли вызовом TermirmteTbread) операционная система больше не
считает нуж ным "содержать" адресное пространство данного процесса. Обнаружив,
что в процес се не исполняется ни один поток, она немедленно завершает его. При
этом код за вершения процесса приравнивается коду завершения последнего потока.
А происходит
вот что. Связанный с
завершаемым процессом объект ядра не высвобождается, пока не будут закрыты
ссылки на него и из других процессов. В момент завершения процесса система
автоматически уменьшает счетчик пользователей этого объекта на 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; // порождаем
дочерний процесс if (fSuccess)
{ // закрывайте
описатель потока, как только необходимость в нем отпадает! // дочерний
процесс завершился; получаем код его завершения
// закрывайте
описатель процесса, как только необходимость в нем отпадает!
}
В этом
фрагменте кода мы создали новый процесс и, если это прошло успешно, вызвали
функцию 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); // разрешаем
системе уничтожить обьекты ядра "процесс" и "поток" }
Многие
разработчики программного обеспечения пытаются создавать инструмен тальные
средства или утилиты для Windows, требующие перечисления процессов, выполняемых
в системе Изначально в Windows API не было функций, которые по зволяли бы это
делать. Однако в Windows NT ведется постоянно обновляемая база данных
Performance Data. В ней содержится чуть ли не тонна информации, доступ ной через
функции рссстра вроде RegQueryValueEx, для которой надо указать корне
вой раздел
HKEY_PERFORMANCE_DATA. Мало кто из программистов знает об этой базе данных, и
причины тому кроются, на мой взгляд, в следующем. Чтобы
упростить работу с этой базой данных, 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! Эта
программа, "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) и
его приоритет.
Рис. 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);
Рис. 4-5. Processlnfo перечисляет все процессы, в адресные пространства
которых загружен модуль User32.dll Принимая
идентификатор процесса и адрес модуля в этом процессе, она просмат ривает его
адресное пространство, находит модуль и считывает информацию из заго ловка
модуля, чтобы определить, какой базовый адрес для него предпочтителен. Мо дуль
должен всегда загружаться именно по этому адресу, а иначе приложения, исполь
зующие данный модуль, потребуют больше памяти и будут инициализироваться мед
леннее. Поскольку такая ситуация крайне нежелательна, моя утилита сообщает о слу
чаях, когда модуль загружен не по предпочтительному базовому адресу. Впрочем, на
эти темы мы поговорим в главе 20 (в разделе "Модификация базовых адресов
модулей").
Windows 2000 в полной мере использует возможности машин с
несколькими процессорами. Например, эту книгу я писал, сидя за машиной с двумя
процес сорами. Windows 2000 способна закрепить каждый поток за отдельным про
цессором, и тогда два потока исполняются действительно одновременно. Ядро
Windows 2000 полностью поддерживает распределение процессорного време ни между
потоками и управление ими на таких системах. Вам не придется делать ничего
особенного в своем коде, чтобы задействовать преимущества многопроцессорной
машины.
Windows 98 работает только с одним процессором. Даже если у
компьютера несколько процессоров, под управлением Windows 98 действует лишь
один из них — остальные простаивают.Ваше первое Windows-приложение
Описатель экземпляра процесса
Как
оказалось, HMODULE и HINSTANCE — это идно и то же. Встретив в доку ментации
указание передать какой-то функции HMODULE, смело передавайте HINSTANCE, и
наоборот. Они существуют в таком виде лишь потому, что в l6 разрядпой Windows
идентифицировали совершенно разные вещи.
Описатель предыдущего экземпляра процесса
HINSTANCE hinstExe,
HINSTANCE, PSTR pszCmdLine, int
nCmdShow); Командная строка процесса
PWSTR *ppArgv = CommandLineToArgvW(GetCommandLineW(), &pNumArgs);
// освободите блок памяти
HeapFree(GetProcessHeap() 0 ppArgv);Переменные окружения
...
VarNameX=VarValueX\0
Чтобы создать исходный набор переменных окружения для Windows 98,
надо модифицировать файл Autoexec bat, поместив в него группу строк SET в виде
При
регистрации пользователя на входе в Windows 2000 система создает npo
цесс-оболочку, связывая с ним группу строк — переменных окружения. Систе ма
получает начальные значения этих строк, анализируя два раздела в рссст pe. В
первом:
%USERPROFILE%\My Documents
PCTSTR pszName,
PCTSTR pszValue);
Привязка к процессорам
Режим обработки ошибок
Текущие диск и каталог для процесса
Текущие
каталоги для процесса
Для
смены текущего каталога вместо Windows-функции SetCurrentDirectory можно
использовать функцию _chdir из библиотеки С Внутренне она тоже обращается к
SetCurrentDirettory, но, кроме того, способна добавлять или мо дифицировать
переменные окружения, что позволяет запоминать в програм ме текущие каталоги
на различных дисках.
Определение версии системы
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;
его возможные значения
VER_SUITE_ENTERPRISE,
VER_SUITE_BACKOFFICE,
VER_SUITE_COMMUNICATIONS,
VER_SUITE_TERMINAL,
VER_SUITE_SMALLBUSINESS_RESTRICTED,
VER_SUITE_EMBEDDEDNT,
VER_SUITE_DATACENTER
VER_NT_SERVER,
VER_NT_DOMAIN_CONTROLLER
POSVERSIONINFOEX pVersionInformation;
DWORD
dwTypeMask;
DWORDLONG dwlConditionMask);
DWORDLONG dwlConditionMask, ULONG dwTy0eBiLMask,
ULONG dwConditionMask);
OSVERSIONINFOEX osver = { 0 };
osver.dwOSVersionInfoSize =
sizeof(osver);
osver.dwMdjorVersion = 5;
osver.dwMinorVersion = 0;
osver.dwPlatformId = VER_PLATFORM_WIN32_NT;
// всегда инициализируйте это элемент так
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 } Функция 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 возвращает TRUE до окончательной инициализации
процесса. Это означает, что на данном этапе загрузчик операционной системы еще
нс искал все необходимые DLL. Если он не сможет найти хотя бы одну из DLL или
корректно провести инициализацию, процесс завершится. Но, поскольку Create
Process уже вернула TRUE, родительский процесс ничего не узнает об этих про
блемах.
Параметры pszApplicationName и pszCommandLine
Обратите внимание на тип параметра pszCommandLine: PTSTR. Он
означает, что CreateProcess ожидает передачи адреса строки, которая не
является констан той Дело в том, что CreateProcess в процессе своего
выполнения модифици рует переданную командную строку, но перед возвратом
управления восста навливает ее.
PROCESS_INFORMATION pi;
CreateProcess(NULL,
TEXT("NOTEPAD"), NULL, NULL, FALSE, 0, NULL. NULL, &si,
&pi);
PROCESS_INFORMATION pi;
TCHAR szComrnandLine[] =
TEXT("NOTEPAD");
CreateProcess(NULL, szCommandLine, NULL, NULL, FALSE, 0,
NULI, NULL, &si, &pi);
TCHAR szPath[] =
TEXT("WORDPAD README.TXT");
CreateProcess(TEXT("C:\\WINNr\\SYSrEM32\\NOTEPAD EXE"),
szPath ); Параметры psaProcess, psaThread и blnheritHandles
Параметр fdwCreate
Классы приоритета BELOW_NORMAL_PRIORITY_CLASS и ABOVE_NORMAL_
PRIORITY_CLASS введены лишь в Windows 2000; они не поддерживаются в Win dows
NT 4.0, Windows 95 или Windows 98. Параметр pvEnvironment
Параметр pszCurDir
Параметр psiStartlnfo
DWORD cb;
PSTH lpReserved;
PSTR lpDesktop;
PSTR
lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD
dwFillAttribute;
DWORD dwFlags;
WORD wShowWindOw;
WORD
cbReserved2;
PBYTE lpReserved2;
HANDLE hStdInput;
HANDLE
hStdOutput;
HANDLE hStdError;
} STARTUPINFO, *LPSTARTUPINFO;
dwYSize
hStdOutlput
bStdError
Хотя в документации на Windows об этом четко не сказано, перед
вызовом GetStartupInfo нужно инициализировать элемент cb структуры
STARTUPINFO: Параметр ppiProclnfo
HANDLE hProcess;
HANDLE hThread;
DWORD
dwProcessId;
DWORD dwThreadId;
} PROCESS_TNFORMATION;
Не забывайте закрывать описатели дочернего процесса и его
первичного по тока, иначе, пока Вы не закроете свое приложение, будет
происходить утечка ресурсов. Конечно, система высвободит все эти ресурсы после
завершения Вашего процесса, по хорошо написанная программа должна сама
закрывать описатели дочернего процесса и его первичного потока, как только
необхо димость в них отпадает Пропуск этой операции — одна из самых частых
ошибок. Завершение процесса
Возврат управления входной функцией первичного потока
Функция ExitProcess
#include <stdio h>
public:
CSomeOtrK) { printf("Constructor\r\n"), }
~CSomeObj() {
printf("Destructor\r\n"); }
};
CSomeObj LocalObj;
ExitProcess(0); // этого здесь не должно быть
Constructor
Constructor
Destructor
Явные вызовы ExitProcess и ExitTbread — распространенная ошибка,
которая мешает правильной очистке ресурсов. В случае ExitTbread процесс
продолжа ет работать, но при этом весьма вероятна утечка памяти или других
ресурсов. Функция TerminateProcess
TerminateProcess — функция асинхронная,
т. e. она сообщает системе, что Вы хотите завершить процесс, но к тому
времени, когда она вернет управление, процесс может быть еще не уничтожен. Так
что, если Вам нужно точно знать момент завершения процесса, используйте
WaitForSingleObject (см. главу 9) или аналогичную функцию, передав ей
описатель этого процесса. Когда все потоки процесса уходят
Что происходит при завершении процесса
Дочерние процессы
DWORD dwExitCode;
BOOL fSuccess = CreateProcess(..., &pi};
CloseHandle(pi
hThread);
//
приостанавливаем выполнение родительского процесса,
// пока не завершится
дочерний процесс
WaitForSingleObject(pi hProcess, INFINlTI);
GetExitCodeProcess(pi.hProcess, &dwExitCode);
CloseHandle(pi.hProcess);
Запуск обособленных дочерних процессов
if (fSuccess) {
// сразу после
завершения дочернего процесса
CloseHandle(pi.hTnread);
CloseHandle(pi
hProcess); Перечисление процессов, выполняемых в системе
Программа-пример Processlnfo