|
|
|||
|
wm-help.net -> Электронная библиотека -> C++/C#/C -> Джеффри РИХТЕР "Windows для профессионалов" -> Глава 4. ПроцессыГлава 4. ПроцессыЧАСТЬ 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 |