    ПРОЦЕССЫ 

    глава    3 

    Зта. глава о том, как система контролирует выполняемые приложения. Сначала 
я определю понятие «процесс» и объясню, как система создает объект ядра 
«процесс». Затем я покажу, как управлять процессом, используя сопоставленный с 
ним объект ядра. Далее мы обсудим атрибуты (или свойства) процесса и поговорим 
о нескольких функциях, позволяющих обращаться к этим свойствам и изменять их. 
Я расскажу также о функциях, которые создают (порождают) в системе 
дополнительные процессы. Ну и, конечно, описание процессов было бы неполным, 
если бы я не рассмотрел механизм их завершения. 0'кэй, приступим. 
    Процесс обычно определяют как экземпляр выполняемой программы. В Win32 
процессу отводится 4 Гб адресного пространства. Win32-npoueccbi — в отличие от 
своих аналогов в MS-DOS и 16-разрядной Windows — инертны. Иными словами, 
Win32-npouecc ничего не исполняет — просто_владеет четырехгигабайтовым 
адресным пространством, содержащим код и данные ЕХЕ-файла приложения. В это же 
пространство загружаются код и данные DLL-библиотек, если того требует 
ЕХЕ-файл. Кроме адресного пространства, процессу принадлежат такие ресурсы, 
как файлы, динамически выделяемые области памяти и потоки. Ресурсы, 
создаваемые при жизни процесса, обязательно уничтожаются при его завершении. 
    Как я уже говорил, процессы инертны. Чтобы процесс что-нибудь выполнил, в 
нем нужно создать поток. Именно потоки отвечают за исполнение кода, 
содержащегося в адресном пространстве процесса. В принципе, один процесс может 
владеть несколькими потоками, и тогда они «одновременно» исполняют код в 
адресном пространстве процесса. Для этого каждый поток должен располагать 
собственным набором регистров процессора и собственным стеком, а каждый 
процесс — минимум одним потоком. Если бы у процесса не было ни одного потока, 
ему нечего было бы делать «на этом свете», и система автоматически уничтожила 
бы его вместе с выделенным ему адресным пространством. 
    Чтобы все эти потоки работали, операционная система отводит каждому из них 
определенное процессорное время. Выделяя потокам отрезки времени (называемые 
квантами) по принципу карусели, она создает тем самым иллюзию одновременного 
выполнения потоков (см. рис. 3-1). 

    24 __Windows для профессионалов 

    Поток]—'—Поток j 

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

    При создании Win32-npouecca первый (точнее, первичный) поток создается 
системой автоматически. Далее этот поток может породить другие потоки, те в 
свою очередь — новые и т. д. 
    ^WINDOWS/ Windows NT способна в полной мере использовать возможности машин 
с неkNT/ сколькими процессорами. Например, эту книгу я написал, сидя за 
машиной с \/    двумя процессорами Pentium 90 МГц. Windows NT способна 
закрепить каждый поток за отдельным процессором, и тогда два потока 
исполняются действительно одновременно. Ядро Windows NT полностью поддерживает 
распределение процессорного времени между потоками и управление ими на таких 
системах. Вам не придется делать ничего особенного в своем коде, чтобы 
задействовать преимущества многопроцессорной машины. 
    Windows 95 работает только с одним процессором. Даже если у компьютера 
несколько процессоров, под управлением Windows 95 действует лишь один из них — 
остальные простаивают. 

    Ваше первое Win32-npnnoxeHMe 

    Win32 поддерживает два типа приложений: основанные на графическом 
интерфейсе (GUI) и консольные (CUI). У приложений первого типа внешний 
интерфейс чисто графический. GUI-приложения создают окна, имеют меню, 
взаимодействуют с пользователем через диалоговые окна и вообще пользуются всей 
стандартной «Windows'OBCKO^> начинкой. Типичный пример — стандартные 
программы, поставляемые вместе с Windows: Notepad (Блокнот), Calculator 
(Калькулятор), Wordpad и т. д. Приложения консольного типа больше напоминают 
программы MS-DOS, работающие в текстовом режиме: они дают текстовый вывод, не 
формируют окон, не обрабатывают сообщений и не требуют GUI. И хотя консольные 
приложения на экране тоже размещаются в окне, в нем содержится только текст. 
Командные процессоры вроде CMD.EXE (для Windows NT) или COMMAND.COM (для 
Windows 95) — типичные образцы подобных приложений. 

    _____________________________________Глава 3. Процессы 25 

    Вместе с тем граница между двумя типами приложений весьма условна. Можно, 
например, создать консольное приложение, способное отображать диалоговые окна. 
Скажем, в командном процессоре вполне может быть специальная команда, 
открывающая графическое диалоговое окно со списком команд; вроде мелочь — а 
избавляет от запоминания лишней информации. В то же время можно создать и 
GUI-приложение, выводящее текстовые строки в консольное окно. Я сам часто 
составлял такие программы на основе GUI-интерфейса: открыв консольное окно, я 
пересылал в него отладочную информацию, связанную с исполняемым приложением. 
Но, конечно, графический интерфейс предпочтительнее, чем старомодный 
текстовый. Как показывает опыт, приложения на основе GUI «дружественнее» к 
пользователю, а значит, и более популярны. 
    Единственная часть операционной системы, которую волнует, на какой основе 
построено приложение (GUI или CUI), — системный загрузчик. Загружая программу, 
он прове- . ряет номер подсистемы, хранящийся в ЕХЕ-файле, и определяет, что 
это за программа — GUI или CUI. Если номер указывает на приложение последнего 
типа, загрузчик автоматически создает текстовое консольное окно, а если номер 
говорит о противоположном, загрузчик просто загружает программу в память. 
    Когда Вы создаете GUI-проект на С или C++, Visual C++ устанавливает такой 
ключ для компоновщика, чтобы в ЕХЕ-файле был номер подсистемы, соответствующий 
GUIпрограмме, и чтобы стартовый код из стандартной библиотеки С захотел 
вызвать функцию main (которую Вы, естественно, должны написать). А когда Вы 
создаете GUI-проект на С или C++, Visual C++ аналогичным образом добивается 
того, чтобы в ЕХЕ-файле был номер подсистемы, соответствующий GUI-программе, и 
чтобы стартовый код из стандартной библиотеки С вызывал функцию WinMain (ее 
написание — тоже Ваша обязанность). 
    В этой главе мы обсудим механизмы создания процессов применительно и к 
GUI-, и к консольным приложениям, но главное внимание я уделю первому типу и 
не стану вдаваться в тонкости, связанные с разработкой программ второго типа. 
Если Вас интересует создание консольных приложений, пожалуйста, обратитесь к 
руководству Microsoft Win32 Programmer's Reference. 
    Итак, во всех \Ут32-приложениях на базе GUI должна быть функция WinMain, 
за реализацию которой отвечаете Вы сами, а ее прототип должен выглядеть так: 

    int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE hinstExePrev, LPSTR 
IpszCmdLine. int nCmdShow); 

    Да самом деле эта функция операционной системой не вызывается. Вместо 
этого происходит обращение к стартовой функций из стандартной библиотеки 
C/C++. Компоновщик Visual C++ знает, что ее имя — _WinMainCRTStartup, но, 
запустив компоновщик с параметром /ENTRY, можно заставить его обращаться к 
другой функции. _WinMainCRTStartup отвечает за': 
    1. Считывание указателя на полную командную строку нового процесса. 
    2. Считывание указателя на переменные окружения нового процесса. 
    3. Инициализацию глобальных переменных из стандартной библиотеки С, доступ 
к которым из Вашего кода обеспечивается включением файла STDLIB.H. Список этих 
переменных приведен на рис. 3-2. 
    4. Инициализацию кучи (динамически распределяемой области памяти), 
используемой С-функциями выделения памяти (т. е. malloc и calloc) и другими 
процедурами низкоуровневого ввода/вывода. 
    1. Visual C++ поставляется с исходным кодом стандартной библиотеки С. Код 
функции _WinMainCRTStartup можно найти в файле CRtO.C. 

    26 __________________________Windows для профессионалов 

    Имя переменной   Тип              Описание 

    _osver             unsigned int        Номер выпуска операционной системы. 
Например, у Windows NT 4.0 этот номер — 1381, и поэтому _osver равна 1381. 
    _wmmajor         unsigned int        Основной номер версии Windows в 
шестнадцатеричной форме. Для Windows NT 4.0 это значение равно 4. 
    _winminor         unsigned int        Дополнительный номер версии Windows 
в шестнадцатеричной форме. Для Windows NT 4.0 это значение равно 0. 
    _winver            unsigned int        (_winmajor «8) + _wmminor 
    _arec             unsigned int        Количество аргументов, переданных в 
командной строке. 
    _argv             char **            Массив _яг^с-указателей на 
ANSI-строки. Каждый элемент массива указывает на один из аргументов командной строки. 
    _environ           char **            Массив указателей на ANSI-строки. 
Каждый элемент массива указывает на строку — переменную окружения. 

    Рис. 3-2. Глобальные переменные из стандартной библиотеки С, доступные 
Вашим программам 

    5. Вызов функции WinMain из Вашей программы: 

    GetStartupInfoA(&StartupInfo); 
    int nMainRetVal = WinMain(GetMod.uleHandle(NULL), NULL, IpszCommandLine, 
(StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? StartupInfo.wShowWindow : SW_SHOWDEFAULT); 

    6. После возврата управления функцией WinMain стартовый код обращается к 
функции exit стандартной библиотеки С и передает ей значение, возвращенное 
WinMain (nMainRetVal). Функция exit выполняет кое-какую очистку, а затем 
вызывает ЛУш32-функцию ExitProcess, передавая ей значение, возвращенное WinMain. 
    И в заключение этого раздела мы поговорим об атрибутах, присваиваемых 
новому процессу. 

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

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

    HICON LoadIcon(HINSTANCE hinst, LPCTSTR Ipszlcon): 

    Первый параметр в Loadlcon указывает, в каком файле (ЕХЕ или DLL) 
содержится интересующий Вас ресурс. Многие приложения сохраняют параметр 
hinstExe функции WinMain в глобальной переменной, благодаря чему он доступен 
из любой части кода ЕХЕ-файла. 
    В документации на Win32 утверждается, что некоторые \Ут32-функции требуют 
параметр типа HMODULE. Пример — функция GetModuleFileName: 

    DWORD GetModuleFileName(HMODULE hinstModule, LPTSTR IpszPath, DWORD cchPath); 


    ____________________________________ Глава 3. Процессы 27 

    Однако Win32 API не делает различий между значениями типа HMODULE и 
HINSTANCE, принадлежащими процессу, — для него это одно и то же. Так что, 
встретив в документации на Win32 указание передать какой-то функции параметр 
типа HMODULE, смело применяйте тип HINSTANCE. 
    Истинное значение параметра hinstExe функции WinMain — базовый адрес в 
памяти, определяющий ту область в адресном пространстве процесса, где 
находится образ данного ЕХЕ-файла. Например, если система открывает 
исполняемый файл и загружает его содержимое по адресу 0х00400000, то hinstExe 
функции WinMain получает значение 0х00400000. 
    Базовый адрес, по которому грузится приложение, определяется 
компоновщиком. Разные компоновщики выбирают и разные (по умолчанию) базовые 
адреса. Компоновщик Visual C++ использует по умолчанию базовый адрес 
0х00400000 — самый нижний в Windows 95, начиная с которого в ней допускается 
загрузка образа исполняемого файла. Некоторые, более старые версии 
компоновщиков по умолчанию выбирают другой базовый адрес — 0х00010000, 
поскольку это самый нижний адрес, доступный для загрузки образа исполняемого 
файла в Windows NT. Впрочем, указав параметр /BASE: адрес (в случае 
компоновщика фирмы Microsoft), можно изменить базовый адрес, по которому будет 
грузиться приложение. 
    При попытке загрузить исполняемый файл в Windows 95 по базовому адресу 
ниже 0х00400000 загрузчик переместит его на другой адрес. Это увеличит время 
загрузки приложения, но оно по крайней мере будет выполнено при любых 
обстоятельствах. Разрабатывая программы и для Windows 95, и для Windows NT, 
сделайте так, чтобы приложение загружалось по базовому адресу не ниже 0х00400000. 
    Функция GetModuleHandle: 

    HMODULE GetModuleHandle(LPCTSTR IpszModule); 

    возвращает описатель/базовый адрес, указывающий, куда именно (в адресном 
пространстве процесса) загружается ЕХЕ- или DLL-файл. При вызове этой функции 
имя нужного ЕХЕили DLL-файла передается как строка с нулевым символом в конце. 
Если система находит указанный файл, GetModuleHandle возвращает базовый адрес, 
по которому располагается образ данного файла. Если же файл системой не 
найден, функция возвращает NULL. Кроме того, можно вызвать эту функцию, 
передав ей NULL вместо параметра IpszModule, — тогда Вы узнаете базовый адрес 
ЕХЕ-файла. Именно это и делает стартовый код из стандартной библиотеки С при 
вызове WinMain из Вашей программы. 
    Есть еще две важные вещи, касающиеся GetModuleHandle. Во-первых, она 
проверяет адресное пространство только того процесса, который ее вызвал. Если 
этот процесс не использует никаких функций GDI, то, вызвав GetModuleHandle и 
передав ей аргумент "GDI32", Вы получите NULL — пусть даже модуль GDI32.DLL и 
загружен в адресное пространство какого-то другого процесса. Во-вторых, вызов 
этой функции и передача ей NULL дает в результате базовый адрес ЕХЕ-файла в 
адресном пространстве процесса. Так что, вызывая функцию в виде 
GetModuleHandle(NULL) — даже из кода в DLL, — Вы получаете базовый адрес ЕХЕ-, 
а не DLL-файла. В 16-разрядной Windows эта функция вела себя иначе. 
    /\   В 16-разрядной Windows hModule задачи указывает на базу данных ЕХЕ- 
или / й^\ DLL-модуля (блок информации, используемой внутри системы для 
управле?ss^S^ ния модулем). Запусти Вы хоть 200 копий Notepad, база данных 
модуля все равно будет создана в одном экземпляре, и поэтому все копии 
совместно используют лишь одно значение hmodExe. В 16-разрядной Windows можно 
загрузить одну — и только одну — копию какого-либо DLL-модуля, так что каждому 
загруженному DLL соответствует лишь одно значение hmodExe. 
    Каждому выполняемому экземпляру задачи в 16-разрядной Windows 
присваивается свое, уникальное значение hinstExe. Этот параметр описывает 
стандартный сегмент данных, закрепляемый за задачей. А если Вы запустите все 
те же 200 копий Notepad, система создаст 

    см. след. стр. 

    28 -____________________-_______ Windows для профессионалов 

    и 200 параметров hinstExe— по одному на каждую запущенную копию. Поскольку 
DLL-модули тоже имеют стандартные сегменты данных, то и каждый загруженный DLL 
получает свое, уникальное значение hinstExe. Не исключено, что Вы подумали: 
раз DLL можно загружать только единожды, значит, в 16-разрядной Windows его 
параметры hmodExe и hinstExe могут совпадать. Однако это не так, поскольку 
hmodExe указывает на базу данных DLL-модуля, а hinstExe — на его стандартный 
сегмент данных. 
    В Win32 каждому процессу отводится свое адресное пространство, поэтому 
каждый из них «полагает», что он — единственный в системе; «заметить» 
присутствие другого процесса ему очень непросто. Таким образом, параметры 
процесса hmodExe и hinstExe абсолютно идентичны. Но — по историческим, я бы 
сказал, причинам — эти параметры трактуются в документации на Win32 как два 
разных понятия. 
    Как уже говорилось в предыдущем разделе, параметр hinstExe приложения 
указывает базовый адрес памяти, по которому система грузит код ЕХЕ-файла в 
адресное пространство процесса. Поэтому весьма вероятно, что у многих 
процессов будет одно и то же значение hinstExe. Например, запуск NOTEPAD.EXE 
заставит систему создать адресное пространство размером 4 Гб и загрузить в 
него код и данные этой программы (они могут быть загружены по адресу 
0х00400000). Если теперь запустить второй экземпляр NOTEPAD.EXE, система 
создаст новое адресное пространство размером 4 Гб и вновь загрузит код и 
данные Notepad в память по адресу 0х00400000. Поскольку значение hinstExe 
этого приложения совпадает с базовым адресом в памяти, по которому система 
загружает код его ЕХЕ-модуля, значения hinstExe обоих процессов совпадут (0х00400000). 
    В 16-разрядной Windows можно было вызвать функцию DialogBox, передав ей 
значение hinstExe, принадлежащего другой — не Вашей — задаче: 

    int DialogBox(HINSTANCE hinstance, LPCTSTR IpszTemplate, HWND hwndOwner, 
DLGPROC digprc): 

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

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

    Я уже говорил, что стартовый код из стандартной библиотеки С всегда 
передает в WinMain параметр hinstExePrev как NULL. Этот параметр предусмотрен 
исключительно для совместимости с предыдущими версиями Windows и для 
Win32-пpилoжeний не имеет никакого смысла. 
    В 16-разрядных Windows-приложениях параметр hinstExePrev— это описатель 
предыдущей копии приложения. Если приложение выполняется «в одном экземпляре», 
h i nstExePrev передается как NULL. 16-разрядные Windows-приложения часто 
анализировали этот параметр по двум причинам: 
    •   Чтобы проверить присутствие своей копии и — при ее наличии — завершить 
только что запущенный экземпляр. Этой возможностью обычно пользуются программы 
типа Print Manager, чтобы предотвратить запуск своей второй копии. 
    •  Чтобы определить, нужна ли регистрация оконных классов. В 16-разрядной 
Windows оконные классы можно было регистрировать только по одному разу на 
кажем. след. стр. 

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

    дый модуль. Впоследствии их совместно использовали все экземпляры данного 
приложения. Если дополнительный экземпляр пытался повторно зарегистрировать те 
же оконные классы, вызов функции RegisterClass давал ошибку. В Win32 свои 
оконные классы обязан регистрировать каждый экземпляр приложения, так как этот 
API не предусматривает совместного доступа экземпляров к оконным классам. 
    Чтобы упростить перенос приложений из 16-разрядной Windows в Win32, в 
Microsoft решили всегда передавать NULL через параметр hinstExePrev в функцию 
WinMain. Поскольку многие приложения 16-разрядной Windows проверяют этот 
параметр при регистрации оконных классов, то получается так: обнаружив, что 
hinstExePrev — NULL, они автоматически перерегистрируют эти классы. 
    Хорошо, да не очень: ведь теперь приложения не могут воспользоваться 
hinstExePrev, чтобы предотвратить запуск своей второй копии. Приходится идти 
другим путем. Например, вызывать функцию FindWindow и искать конкретный 
оконный класс, однозначно идентифицирующий данное приложение. Если FindWindow 
возвращает NULL, значит, других экземпляров этого приложения в памяти нет. Еще 
один способ проверки я опишу в главе 12. 

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

    При создании новому процессу передается командная строка, которая почти 
никогда не бывает пустой — как минимум, она содержит имя исполняемого файла, 
использованного при создании этого процесса. Однако, как Вы увидите ниже (при 
обсуждении функции CreateProcess), возможны случаи, когда процесс получает 
командную строку, состоящую из единственного символа — признака конца строки. 
В момент запуска приложения стартовый код из стандартной библиотеки С 
считывает командную строку процесса, пропускает имя исдсищяемрго файла и 
заносит в параметр IpszCmdLine функции WinMain указатель на оставшуюся часть 
командной строки. 
    Параметр IpszCmdLine всегда указывает на ANSI-строку. Поскольку системе не 
известно, какая кодировка символов Вас интересует — ANSI или Unicode, 
Microsoft выбрала передачу строк только в ANSI-кодировке, что упрощает и 
перенос кода из 16-разрядной Windows в Win32: ведь приложения, рассчитанные на 
16-разрядную Windows, работают с ANSI-строками. (О Unicode мы поговорим в 
главе 17.) 
    Программа может анализировать и интерпретировать ANSI-строку, как ей 
«заблагорассудится». Поскольку IpszCmdLine относится к типу LPSTR, а не 
LPCSTR, не стесняйтесь и записывайте строку прямо в буфер, на который 
указывает этот параметр, — но ни при ^ каких условиях не «переступайте» 
границу буфера. Лично я всегда рассматриваю этот буфер ' как «только для 
чтения». Если в командную строку нужно внести изменения, я сначала копирую 
буфер, содержащий командную строку, в локальный буфер (в своей программе), 
который затем и модифицирую. 
    Указатель на полную командную строку процесса можно получить и вызовом 
функции GetCommandLine: 

    LPTSTR GetCommandLine(VOID): 

    Она возвращает указатель на буфер, содержащий полную командную строку, 
включая полное имя (вместе с путем) исполняемого файла. И, вероятно, самый 
веский довод в пользу именно этой функции, а не параметра IpszCmdLine— то, что 
в Win32 существуют две версии GetCommandLine, поддерживающие как ANSI-, так и 
Unicode-кодировку символов. Тогда как IpszCmdLine всегда указывает на буфер, 
содержащий строку с ANSI-символами. 

    30 ______________________________Windows для профессионалов 

    Во многих приложениях безусловно удобнее использовать командную строку, 
предварительно разбитую на отдельные компоненты, доступ к которым приложение 
может получить через глобальные переменные _argc и _argv. Но опять же, _argv — 
это массив символьных указателей на ANSI-, а не Unicode-строки. Выход прост: 
примените \Уш32-функцию, расщепляющую любую строку на отдельные компоненты, — 
CommandLineToArgvW2: 

    LPWSTR CommandLineToArgvW(LPWSTR IpCmdLine, LPINT pArgc); 

    Буква W в конце имени этой функции намекает на «широкие» (wide) символы и 
подсказывает, что функция существует только в Unicode-версии. Параметр 
IpCmdLine указывает на командную строку. Его обычно получают предварительным 
вызовом GetCommandLine. Параметр pArgc — адрес целочисленной переменной, в 
которой возвращается количество аргументов в командной строке. А сама 
CommandLineToArgvW возвращает адрес массива указателей на Unicode-строки. 
    CommandLineToArgvW выделяет нужную память автоматически. Большинство 
приложений не освобождают эту память, полагаясь на операционную систему, 
которая проводит очистку ресурсов по завершении процесса. И такой подход 
вполне приемлем. Но если Вы хотите сами освободить эту память, сделайте так: 

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


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

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

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

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

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

    2. Эта функция появилась в Windows NT, начиная с версии 3.5. 

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

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

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

    Вы получите первую переменную с именем "XYZ ", содержащую строку "Ноте", и 
вторую переменную "XYZ", содержащую строку "Work". 
    Конец каждой переменной окружения (а также их блока) помечается 
дополнительным нулевым байтом. 
    Чтобы создать исходный набор переменных окружения для Windows 95, надо 
модифицировать файл AUTOEXEC.BAT, поместив в него группу строк SET: 

    SET VarName=VarValue 

    При перезагрузке система учтет новое содержимое файла AUTOEXEC.BAT, и 
тогда любые заданные Вами переменные окружения станут доступны всем процессам, 
активизируемым в сеансе работы с Windows 95. 
    При регистрации пользователя на входе в Windows NT система создает 
процессоболочку, связывая с ним группу строк — переменных окружения. Система 
получает начальные значения этих строк, анализируя два параметра в реестре. В 
первом параметре: 

    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control SessionManage\Environment 

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

    HKEY_CURRENT_USER\Environment 

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

    32 ______________________________Windows для профессионалов 

    Кроме того, для модификации реестра Ваша программа может обращаться к 
\Ут32-функциям, позволяющим манипулировать с записями такого рода. Однако, 
чтобы изменения вступили в силу, пользователь должен выйти из системы и вновь 
войти в нее. Некоторые приложения типа Explorer, Task Manager или Control 
Panel могут обновлять свои блоки переменных окружения на базе новых значений в 
реестре — после того как их основные окна получат сообщение WM_WININICHANGE. 
Например, если Вы, обновив реестр, хотите, чтобы какие-то приложения 
соответственно обновили свои блоки переменных окружения, вызовите: 

    SendMessage(HWND_BROADCAST, WM_WININIGHANGE, OL. (LPARAM) "Environment"); 

    Обычно дочерний процесс наследует набор переменных окружения от 
родительского процесса. Однако последний способен управлять тем, какие 
переменные окружения наследуются дочерним процессом, а какие — нет. Но об этом 
я расскажу, когда мы займемся функцией CreateProcess. Наследование я понимаю в 
том смысле, что дочерний процесс получает свою копию блока переменных 
окружения от родительского, а не то, что дочерний и родительский процессы 
совместно используют один и тот же блок. А значит, дочерний процесс может 
добавлять, удалять или модифицировать переменные в своем блоке, и эти 
изменения не затронут блок, принадлежащий родительскому процессу. 
    Переменные окружения обычно применяются для «тонкой» настройки приложения. 
Пользователь создает и инициализирует переменную окружения, затем запускает 
приложение, и оно, анализируя блок, отыскивает одну или несколько «своих» 
переменных; обнаружив, проверяет значение такой переменной и соответствующим 
образом настраивается. 
    Увы, многим пользователям не под силу разобраться в переменных окружения, 
а значит — трудно указать правильные значения. Ведь для этого надо не только 
хорошо знать синтаксис переменных, но и, конечно, понимать, что стоит за теми 
или иными их значениями. С другой стороны, почти все (а может, и все) 
приложения, основанные на GUI, дают возможность «тонкой» настройки через 
диалоговые окна. Такой подход, естественно, нагляднее и проще, а потому и 
воспринимается пользователем с большим энтузиазмом. 
    Ну а теперь, если у Вас еще не пропало желание манипулировать переменными 
окружения, поговорим о предназначенных для этой цели \Ут32-функциях. 
GetEnvironmentVariable позволяет выявлять присутствие той или иной переменной 
окружения и определять ее значение: 

    DWORD GetEnvironrnentVanabled-PCTSTR IpszName, LPTSTR IpszValue, DWORD ccnValue): 

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

    DWORD SetEnvironmentVariable(LPCTSTR IpszName,LPCTSTR IpszValue); 

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

    _____________________________________ Глава 3. Процессы 33 


    Обработка ошибок 

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

    UINT SetErrorMode(UINT fuErrorMode): 

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

    Флаг                             Описание 

    SEM_FAILCRITICALERRORS           Система не выводит окно с сообщением от 
обработчика критических ошибок и возвращает ошибку в вызывающий процесс. 
    SEM_NOGPFAULTERRORBOX         Система не выводит окно с сообщением о 
нарушении общей защиты. Этим флагом манипулируют только отладчики, 
самостоятельно обрабатывающие нарушения общей защиты с помощью обработчика исключений. 
    SEM_NOOPENFILEERRORBOX         Система не выводит окно с сообщением об 
отсутствии искомого файла. 
    SEM_NOALIGNMENTFAULTEXCEPT    Система автоматически исправляет нарушения в 
выравнивании данных, и они становятся невидимы приложению. Этот флаг не 
действует на процессорах х86 или Alpha. 

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

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

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

    DWORD GetCurrentDirectory(DWORD cchCurDir. LPTSTR IpszCurDir); 
    BOOL SetCurrentDirectory(LPCTSTR IpszCurDir); 

    К этим функциям мы вернемся в главе 14. 

    34 ______________________________Windows для профессионалов 


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

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

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

    Эти переменные указывают, что текущим каталогом на диске С является 
\Utility\Bin, а на диске D — Program Files. 
    Если Вы вызываете \Ут32-функцию, передавая ей имя диска, отличного от 
текущего, система сначала «заглядывает» в блок переменных окружения и пытается 
найти переменную, связанную с именем указанного диска. Если таковая есть, 
система выбирает текущий каталог на заданном диске в соответствии с ее 
значением, нет — текущим каталогом считается корневой. 
    Скажем, если текущий каталог для процесса — C:\Utility\Bin и Вы вызвали 
функцию CreateFile, чтобы открыть файл D:\ReadMe.Txt, система ищет переменную 
=D:. Поскольку переменная =D: существует, система пытается открыть файл 
ReadMe.Txt в каталоге D:\Program Files. А если бы таковой переменной не было, 
система искала бы файл ReadMe.Txt в корневом каталоге диска D. Кстати, 
файловые УУш32-функции никогда не добавляют и не изменяют переменных 
окружения, связанных с именами дисков, а лишь считывают их значения. 
    /\    Для смены текущего каталога вместо УУш32-функции SetCurrentDirectory 
можно / ' \ использовать функцию _chdir из стандартной библиотеки С. Внутренне 
она /внимАние\ тоже обращается к SetCurrentDirectory, но, кроме того, способна 
добавлять или модифицировать переменные окружения, что позволяет сохранять 
текущие каталоги на различных дисках. 
    Если родительский процесс создает блок переменных окружения и хочет 
передать его дочернему процессу, тот не наследует текущие каталоги 
родительского процесса автоматически. Вместо этого у дочернего процесса 
текущими на всех дисках становятся корневые каталоги. Чтобы дочерний процесс 
унаследовал текущие каталоги родительского, последний должен создать 
соответствующие переменные окружения (и сделать это до порождения другого 
процесса). Родительский процесс может узнать, какие каталоги являются 
текущими, вызвав функцию GetFullPathName: 

    DWORD GetFullPathName(LPCTSTR IpszFile, DWORD cchPath, LPTSTR IpszPath, 
LPTSTR *ppszFilePart); 

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

    TCHAR szCurDir[MAX_PATH]; 
    DWORD GetFullPathName(__TEXT("C:"), MAX_PATH, szCurDir, NULL); 

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

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

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

    _____________________________________ Глава 3. Процессы 35 

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

    DWORD GetVersion(VOID); 

    С этой простой функцией связана целая история. Сначала ее разработали для 
16-разрядной Windows, и она должна была в старшем слове возвращать номер 
версии MS-DOS, а в младшем — номер версии Windows. Соответственно в каждом 
слове старший байт сообщал основной номер версии, младший — дополнительный 
номер версии. 
    Увы, программист, написавший ее код, слегка ошибся, и получилось так, что 
номера версии Windows поменялись местами: в старший байт попадал 
дополнительный номер ее версии, а в младший — основной. Ну а поскольку многие 
программисты уже начали пользоваться этой функцией, Microsoft пришлось 
оставить все, как есть, и изменить документацию с учетом ошибки. 
    Из-за всей этой неразберихи вокруг Get Version в Win32 API включили новую 
функцию —~~GetVersionEx'. 

    BOOL GetVersionEx(LPOSVERSIONINFO IpVerslonlnformation); 
    Перед обращением к ней программа должна создать структуру OSVERSIONINFO: 
    typedef struct { 
    DWORD dwOSVersionInfoSize; 
    DWORD dwMaJorVersion; 
    DWORD dwMinorVersion; 
    DWORD dwBuildNumber: 
    DWORD dwPlatf-ormId; 
    TCHAR szCSDVersion[128]; 
    } OSVERSIONINFO, *LPOSVERSIONINFO; 

    и передать ее адрес этой функции. 
    Обратите внимание, что каждому компоненту номера версии операционной 
системы соответствует свой элемент структуры. Это сделано специально — чтобы 
программисты не возились с выборкой данных из всяких там старших-младших 
байтов-слов (и не путались в них!); теперь программе гораздо проще сравнивать 
ожидаемый номер версии операционной системы с действительным. Назначение 
каждого элемента структуры OSVERSIONINFO описано в таблице: 

    Элемент            Описание 

    dwOSVersionInfoSize    Размер структуры. Перед обращением к функции 
GetVersionEx должен быть заполнен вызовом sizeof(OSVERSIONINFO). 
    dwMaJorVersion        Основной номер версии операционной системы. 
    dwMinorVersion        Дополнительный номер версии операционной системы. 
    dwBuildNumber        Номер выпуска данной системы. 
    dwPlatformId         Идентификатор платформы, поддерживаемой данной системой. 
    Его возможные значения: VER_PLATFORM_WIN32s (WIN32s), 
    VER_PLATFORM_WIN32_WINDOWS (Windows 95), 
    VER_PLATFORM_WIN32_NT (Windows NT) или 
    VER_PLATFORM_WIN32_HH3 (Windows CE). 
    szCSDVersion          Этот элемент содержит текст — дополнительную 
информацию об установленной операционной системе. 
    3. Сокращение «НН» расшифровывается как handheld и подразумевает карманные 
компьютеры, умещающиеся на ладони. 

    36 ______________________________Windows для профессионалов 


    Функция CreateProcess 

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

    BOOL CreateProcess( 
    LPCTSTR IpszApplicatlonName, 
    LPCTSTR IpszCommandLine, 
    LPSECURITY_ATTRIBUTES IpsaProcess, 
    LPSECURITY_ATTRIBUTES IpsaThread, 
    .1  BOOL fInheritHandles, ''. DWORD fdwCreate, 
    LPVOID IpvEnvironment, 
    LPTSTR IpszCurDir, 
    LPSTARTUPINFO IpsiStartInfo, 
    LPPROCESS_INFORMATION IppiProcInfo); 

    Когда поток в приложении вызывает CreateProcess, система создает объект 
ядра «процесс» с начальным значением счетчика числа его пользователей, равным 
1. Этот объект — не сам процесс, а компактная структура данных, через которую 
операционная система управляет процессом. (Объект ядра «процесс» следует 
рассматривать как структуру данных со статистической информацией о процессе.) 
Затем система создает для нового процесса виртуальное адресное пространство 
размером 4 Гб и загружает в него код и данные как для исполняемого файла, так 
и для любых DLL (если таковые требуются). 
    Далее система формирует объект ядра «поток» (со счетчиком, равным 1) для 
первичного потока нового процесса. Как и в первом случае, объект ядра «поток» 
— это компактная структура данных, через которую система управляет потоком. 
Первичный поток начнет с исполнения стартового кода из стандартной библиотеки 
С, который — как всегда — вызовет функцию WinMain в Вашей программе (или main 
— если приложение относится к консольному типу). Если системе удастся создать 
новый процесс и его первичный поток, CreateProcess вернет TRUE. 
    На этом мы закончим общее описание и перейдем к подробному рассмотрению 
параметров функции CreateProcess. 
    Если Вам знакомы две функции 16-разрядной Windows, предназначенные для 
создания процесса, — WinExec и LoadModule, — то, сравнив число их параметров с 
тем, что есть в новой функции CreateProcess, Вы сразу поймете, насколько шире 
ее возможности в контроле за созданием процесса. В 32-разрядной Windows 
функции WinExec и LoadModule оставлены исключительно для совместимости с 
16-разрядной Windows и реализованы через внутренние вызовы CreateProcess. 
Однако для старых функций не предусмотрено версий, способных работать с 
Unicode, — так что в них можно передавать только ANSI-строки. 

    Параметры IpszAppHcationName и /pszCommandlfne 

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

    _____________________________________Глава 3. Процессы 37 

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

    Конечно, если в имени файла указан полный путь доступа, система сразу 
обращается туда и не просматривает упомянутые каталоги. Найдя нужный 
исполняемый файл, она создает новый процесс и проецирует код и данные 
исполняемого файла на адресное пространство этого процесса. Затем обращается к 
процедурам стартового кода из стандартной библиотеки. С,., Тот в свою очередь, 
как уже говорилось, анализирует командную строку процесса и передает WinMain 
адрес первого (за именем исполняемого файла) аргумента как IpszCmdLine. 
    Все, о чем я сказал, произойдет, только если параметр IpszApplicationName 
— NULL (что и бывает в 99% случаев). Вместо NULL можно передать адрес строки с 
именем исполняемого файла, который надо запустить. Однако тогда придется 
указать не только его имя, но и расширение, поскольку в этом случае имя не 
дополняется расширением ЕХЕ автоматически. CreateProcess предполагает, что 
файл находится в текущем каталоге (если полный путь не задан). Если в текущем 
каталоге файла нет, функция не станет искать его в других каталогах, и на этом 
все закончится. 
    Но даже при указанном в IpszApplicationName имени файла CreateProcess все 
равно передает новому процессу содержимое параметра IpszCommandLine как 
командную строку. Допустим, Вы вызвали CreateProcess так: 

    CreateProcess("C:\\WINNT\\SYSTEM32\\NOTEPAD.EXE", "WORDPAD README.TXT"); 

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

    Параметры /psaProcess, /psaThreadn fInheritHandles 

    Чтобы создать новый процесс, система должна сначала создать объекты ядра 
«процесс» и «поток» (для первичного потока процесса). Поскольку это объекты 
ядра, родительский процесс получает возможность связать с ними атрибуты 
защиты. Параметры IpsaProcess и Ipsa Thread позволяют определить нужные 
атрибуты защиты для объектов «процесс» и «поток» соответственно. В эти 
параметры можно занести NULL, и система закрепит за данными объектами 
дескрипторы защиты по умолчанию. В качестве альтернативы можно объявить и 
инициализировать две структуры SECURITY_ATTRIBUTES; тем самым Вы создадите и 
присвоите объектам «процесс» и «поток» их собственные атрибуты защиты. 
    Структуры SECURITY_ATTRIBUTES для параметров IpsaProcess и IpsaThread 
используются и тогда, когда нужно, чтобы какой-либо из этих двух объектов 
получил статус наследуемого любым дочерним процессом. (О теории, на которой 
построено наследование описателей объектов ядра, я рассказывал в главе 2.) 
    На рис. 3-3 Вы найдете короткую программу, демонстрирующую, как 
наследуются описатели объектов ядра. Будем считать, что процесс А порождает 
процесс В и заносит в параметр IpsaProcess адрес структуры 
SECURITY_ATTRIBUTES, в которой элемент bInheritHandle установлен как TRUE. 
Одновременно параметр IpsaThread указывает на другую структуру 
SECURITY_ATTRIBUTES, в которой значение элемента bInheritHandle — FALSE. 

    38 _____________________________Windows для профессионалов 

    Создавая процесс В, система формирует объекты ядра «процесс» и «поток», а 
затем — в структуре, на которую указывает параметр IppiProcInfo, — возвращает 
их описатели процессу А, и с этого момента тот может манипулировать только что 
созданными объектами «процесс» и «поток». 
    Теперь предположим, что процесс А собирается вторично вызвать 
CreateProcess, чтобы породить процесс С. Сначала ему нужно определить, стоит 
ли предоставлять процессу С доступ к своим объектам ядра. Для этого 
используется параметр flnheritHandles. Если он приравнен TRUE, система 
передает процессу С все наследуемые описатели. В этом случае наследуется и 
описатель объекта ядра «процесс» процесса В. А вот описатель объекта 
«первичный поток» процесса В не наследуется ни при каком значении 
flnheritHandles. Кроме того, если процесс А вызывает CreateProcess, передавая 
через параметр flnheritHandles значение FALSE, процесс С не наследует никаких 
описателей, используемых в данный момент процессом А. 

    INHERIT.C 
   
    Модуль: Inherit.С Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    ((include <windows.h> 
    int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE hinstExePrev, LPSTR 
IpszCmdLine, int nCmdShow) { 
    STARTUPINFO si; 
    SECURITY_ATTRIBUTES saProcess, saThread; 
    PROCESS_INFORMATION piProcessB, piProcessC; 
    // готовим структуру STARTUPINFO для порождаемых процессов 
    ZeroMemory(&si, sizeof(si)); 
    si.cb = sizeof(si); 
    // готовимся к созданию процесса В из процесса А; 
    // описатель, идентифицирующий новый объект "процесс", 
    // должен быть наследуемым 
    saProcess.nLengtn = sizeof(saProcess); 
    saProcess.IpSecurityDescriptor = NULL; 
    saProcess.bInheritHandle = TRUE: 
    // описатель, идентифицирующий новый объект "поток", 
    // НЕ должен быть наследуемым 
    saThread.nLengtn = sizeof(saThread): 
    saThread.IpSecurityDescriptor = NULL; 
    saThread.bInheritHandle = FALSE; 

    см. след. стр. 


    Рис. 3-3. Пример, иллюстрирующий наследование описателей объектов ядра 


    _____________________________________ Глава 3. Процессы 39 


    РИС. 3-3 (продолжение) 

    // порождаем процесс В 
    CreateProcess(NULL, "ProcessB", &saProcess, &saThread, FALSE, 0, NULL, 
NULL, &si. &piProcessB); 
    // Структура pi содержит два описателя, относящиеся к процессу А: 
    // hProcess, который идентифицирует объект "процесс" процесса В 
    // и является наследуемым, и hThread, который идентифицирует объект 
    // "первичный поток" процесса В и НЕ является наследуемым, 
    // Готовимся создать процесс С из процесса А. 
    // Так как в IpsaProcess и IpsaThread передаются NULL, описатели 
    // объектов "процесс" и "первичный поток" процесса С считаются 
    // ненаследуемыми по умолчанию. 
    
    // Если процесс А создаст еще один процесс, тот НЕ унаследует 
    // описатели объектов "процесс" и "первичный поток" процесса С. 
    
    /'/ Поскольку в параметре TinheritHandles передается TRUE, 
    // процесс С унаследует описатель, идентифицирующий объект 
    // "процесс" процесса В, но НЕ описатель, идентифицирующий объект 
    // "первичный поток" того же процесса. 
    CreateProcess(NULL, "ProcessC", NULL, NULL, TRUE, 0, NULL, NULL, &si, &piProcessC); 
    return(O): 
    i 


    Параметр fdwCreate 

    Параметр fdwCreate определяет флаги, влияющие на то, как именно создается 
новый процесс. Несколько флагов комбинируются Булевым оператором OR. 
    Флаг DEBUG_PROCESS позволяет родительскому процессу проводить отладку 
дочернего, а также всех процессов, которые последним могут быть порождены. 
Если этот флаг установлен^система уведомляет родительский процесс (он теперь 
получает статус отладчика) 6 возникновении определенных событий в любом из 
дочерних процессов (а они получаюТстатус отлаживаемых). 
    Флаг DEBUG_ONLY_THIS_PROCESS аналогичен флагу DEBUG_PROCESS с тем 
исключением, что заставляет систему уведомлять родительский процесс о 
возникновении специфических событий только в одном дочернем процессе — его 
прямом потомке. И тогда, если дочерний процесс создаст ряд дополнительных, 
отладчик уже не уведомляется о событиях, «происходящих» в них. 
    Флаг CREATE_SUSPENDED позволяет создать процесс и в то же время 
приостановить его первичный поток. Этим флагом обычно пользуются отладчики. 
Получив команду~за~ грузить отлаживаемую программу, отладчик должен сообщить 
системе, чтобы та инициализировала новый процесс и его первичный поток, но 
исполнение этого потока пока задержала. Благодаря этому флагу пользователь, 
проводя отладку приложения, может, расставив по всей программе точки 
прерывания, разрешить перехват определенных событий, а затем дать отладчику 
команду приступить к исполнению первичного потока. 

    40 __________ ___________________Windows для профессионалов 

    Флаг DETACHED_PROCESS блокирует доступ процессу, активизированному 
консольной программой, к созданному родительским процессом окну и сообщает, 
что вывод следует перенаправить в новое окно. Если процесс этого типа 
создается другим процессом, то — по умолчанию — новый будет использовать окно 
родительского процесса. (Вы, очевидно, заметили, что при запуске компилятора С 
из командного процессора новое консольное окно не создается; весь его вывод 
«подписывается» в нижнюю часть существующего консольного окна.) Так вот, этот 
флаг заставляет новый процесс перенаправлять свой вывод в новое консольное окно. 
    Флаг CREATE_NEW_CONSOLE приводит к созданию нового консольного окна для 
нового процесса. Имейте в виду: одновременная установка флагов 
CREATE_NEW_CONSOLE и DETACHED_PROCESS недопустима. 
    Флаг CREATE_NO_WINDOW не дает создавать никаких консольных окон для 
данного приложения и тем самым позволяет исполнять консольные программы без 
пользовательского интерфейса. 
    Флаг CREATE_NEW_PROCESS_GROUP служит для модификации списка процессов, 
уведомляемых о нажатии клавиш Ctrl+C и Ctrl+Break. Если в системе одновременно 
исполняется несколько процессов консольного типа, то при нажатии одной из 
упомянутых комбинаций клавиш система уведомляет об этом только процессы, 
включенные в группу. Указав этот флаг при создании нового процесса консольного 
типа, Вы создаете и новую группу. Таким образом, на нажатие клавиш Ctrl+C и 
Ctrl+Break реагировать будут лишь этот процесс и процессы, им порожденные. 
    Флаг CREATE_DEFAULT_ERROR_MODE сообщает системе, что новый процесс не 
должен наследовать режимы обработки ошибок, установленные в родительском. 
(Подробнее об обработке ошибок см. выше описание функции SetErrorMode.) 
    Флаг CREATE_SEPARATE_WOW_VDM полезен только при запуске 16-разрядного 
Windows-приложения в Windows NT. Если он установлен, система создает отдельную 
виртуальную DOS-машину (Virtual DOS-machine, VDM) и запускает 16-разрядное 
Windowsприложение именно в ней. (По умолчанию все 16-разрядные 
Windows-приложения выполняются в одной, общей VDM.) Выполнение приложения в 
отдельной VDM дает большое преимущество: «рухнув», приложение уничтожит лишь 
эту VDM, а программы, выполняемые в других VDM, продолжат нормальную работу. 
Кроме того, 16-разрядные Windowsприложения, выполняемые в раздельных VDM, 
имеют и раздельные очереди ввода. Это значит, что, если одно приложение вдруг 
«зависнет», приложения в других VDM продолжат прием ввода. Единственный 
недостаток работы с несколькими VDM в том, что каждая из них требует 
значительных объемов физической памяти. Windows 95 выполняет все 16-разрядные 
Windows-приложения только в одной VDM, и изменить тут ничего нельзя. 
    Флаг CREATE_SHARED_WOW_VDM полезен только при запуске 16-разрядного 
Windows-приложения в Windows NT. По умолчанию все 16-разрядные 
Windows-приложения выполняются в одной VDM, если только не указан флаг 
CREATE_SEPARATE_WOW_VDM. Однако стандартное поведение Windows NT можно 
изменить, если присвоить значение «yes» параметру DefaultSeparateVDM в разделе 
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\WOW. (После модификации 
этого параметра Windows NT нужно перезагрузить.) Установив значение «yes», но 
указав флаг CREATE_SHARED_WOW_VDM, Вы вновь заставите Windows NT выполнять все 
16-разрядные Windows-приложения в одной VDM. 
    Флаг CREATE_UNICODE_ENVIRONMENT сообщает системе, что блок переменных 
окружения дочернего процесса должен содержать Unicode-символы. По умолчанию 
блок формируется на основе ANSI-символов. 
    При создании процесса можно задать и класс его приоритета. Однако это 
необязательно и даже, как правило, не рекомендуется; система присваивает 
новому процессу класс приоритета по умолчанию. Вот какие классы приоритета существуют: 

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

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

    Idle (простаивающий)          IDLE_PRIORITY_CLASS 
    Normal (обычный)             NORMAL_PRIORITY_CLASS 
    High (высокий)                HIGH_PRIORITY_CLASS 
    Realtime (реального времени)    REALTIME_PRIORITY_CLASS 

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

    Параметр IpvEnvironment 

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

    LPVOID GetEnvironmentStrings(VOID); 

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

    Параметр IpszCurDir ^ 

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

    Параметр IpsiStartfnfo 

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

    typedef struct _STARTUPINFO { 
    DWORD cb; 
    LPSTR IpReserved; 
    LPSTR IpDesktop; 
    LPSTR IpTitle: 
    DWORD dwX; 
    DWORD dwY; 
    DWORD dwXSize: 
    DWORD dwYSize: 
    DWORD dwXCountChars; 
    DWORD dwYCountChars; 
    DWORD dwFillAttribute; 
    DWORD dwFlags: 
    WORD  wShowWindow: 
    WORD  cbReserved2; 
    LPBYTE lpReserved2; 
    см. след. стр. 


    42 ______________________________Windows для профессионалов 


    HANDLE hStdInput; 
    HANDLE hStdOutput; 
    HANDLE hStdError: 
    } STARTUPINFO, *LPSTARTUPINFO; 

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

    STARTUPINFO Si; 
    ZeroMemory(&si, sizeof(si)); 
    si.cb = sizeof(si); 
    CreateProcess(..., &si, ...); 

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

    Окно 

    Элемент          или консоль    Описание 

    сЪ                то и другое     Содержит количество байтов, занимаемых 
структурой STARTUPINFO. Служит для контроля версии — на тот случай, если 
Microsoft расширит эту структуру в будущем Win32. Программа должна 
инициализировать с&как sizeof(STARTUPINFO). 
    IpReserved          то и другое     Зарезервирован. Инициализируйте как NULL. 
    IpDesktop          то и другое     Идентифицирует имя рабочего стола, на 
котором запускается приложение. Если указанный рабочий стол существует, новый 
процесс сразу же связывается с ним. В ином случае система сначала создает 
рабочий стол с атрибутами по умолчанию, присваивает ему имя, указанное в 
данном элементе структуры, и связывает его с новым процессом. Если IpDesktop 
равен NULL (что чаще всего и бывает), процесс связывается с текущим рабочим столом. 
    IpTitle              консоль         Определяет заголовок консольного 
окна. Если IpTitle — 
    NULL, в заголовок выводится имя исполняемого файла. 
    dwX              то и другое     Указывают х- и у-координаты (в пикселах) 
области экрана, 
    dwY                             в которой размещается окно приложения. Эти 
координаты используются, только если дочерний процесс создает свое первое 
перекрываемое окно с идентификатором CW_USEDEFAULT в параметре х функции 
CreateWindow. В приложениях, создающих консольные окна, данные элементы 
определяют верхний левый угол консольного окна. 

    см. след. стр. 

    Рис. 3-4. Элементы структуры STARTUPINFO 


    ____________________________________Глава 3. Процессы 43 


    РИС. 3-4 (продолжение) 

    Окно Элемент или консоль    Описание 

    dwXSize           то и другое     Определяют ширину и высоту (в пикселах) 
окна приложения. 
    dwYSize                          Эти значения используются, только если 
дочерний процесс создает свое первое перекрываемое окно с идентификатором 
CW_USEDEFAULT в параметре nWidth функции CreateWindow. 
    В приложениях, создающих консольные окна, данные элементы определяют 
ширину и высоту консольного окна. 
    dwXCountChars     консоль        Определяют ширину и высоту (в символах) 
консольных окон 
    dwYCountChars                    дочернего процесса. 
    dwFiUAttribute      консоль        Задает цвет текста и фона в консольных 
окнах дочернего процесса. 
    dwFlags           то и другое     См. ниже и следующую таблицу. 
    wShowWindow      окно           Определяет, как именно должно выглядеть первое 
    перекрываемое окно дочернего процесса, если приложение при первом вызове 
функции ShowWindow передает в параметре nCmdShow идентификатор SW_SHOWDEFAULT. 
В этот элемент можно записать любой из идентификаторов типа SW_*, обычно 
допустимых при вызове ShowWindow. 
    cbReserved2         то и другое     Зарезервирован. Инициализируйте как 0. 
    lpReserved.2         то и другое     Зарезервирован. Инициализируйте как NULL. 
    hStdInput          консоль        Определяют описатели буферов для консольного 
    hStdOutput                        ввода/вывода. По умолчанию hStdInput идентифицирует 
    hStdError                          буфер клавиатуры, а hStdOutput и 
hStdError — буфер консольного окна. 
    Теперь, как я и обещал, обсудим элемент dwFlags. Он содержит набор флагов, 
позволяющих управлять созданием дочернего процесса. Большая часть флагов 
просто сообщает функции CreateProcess, содержат ли прочие элементы структуры 
STARTUPINFO полезную информацию или некоторые из них можно игнорировать. В 
таблице приведен список допустимых флагов и описано их назначение: 

    Флаг                     Описание 

    STARTF_USESIZE             Заставляет использовать элементы dwXSize и dwYSize. 
    STARTF_USESHOWWINDOW  Заставляет использовать элемент wShowWindow. 
    STARTF_USEPOSITION        Заставляет использовать элементы dwX и dwY. 
    STARTF_USECOUNTCHARS    Заставляет использовать элементы dwXCountChars и dwYCountChars. 
    STARTF_USEFILLATTRIBUTE   Заставляет использовать элемент dwFillAttribute. 
    STARTF_USESTDHANDLES    Заставляет использовать элементы hStdInput, 
hStdOutput и hStdError. 
    Два дополнительных флага — STARTF_FORCEONFEEDBACK и 
STARTF_FORCEOFFFEEDBACK — позволяют контролировать форму курсора мыши в момент 
запуска нового процесса. Поскольку Windows 95 и Windows NT поддерживают 
истинную вытесняю

    44 _______________________________Windows для профессионалов 

    щую многозадачность, можно запустить одно приложение и, пока оно 
инициализируется, поработать с другой программой. Для визуальной обратной 
связи с пользователем функция CreateProcess временно изменяет форму системного 
курсора мыши: 
    Курсор такой формы подсказывает: можно либо подождать чего-то, что вот-вот 
случится, либо продолжить работу в системе. В самых первых бета-версиях 
Windows NT такого курсора не было — CreateProcess вообще не меняла форму 
курсора. Это несколько сбивало с толку. Я сам часто сталкивался с подобными 
ситуациями. Запущу какую-нибудь программу из Program Manager, и что я вижу? 
Окно приложения еще не появилось, а курсор выглядит стандартной стрелкой. 
Решив, что моя команда почему-то проигнорирована, я снова щелкаю значок 
программы в Program Manager. И вот на экране долгожданное окно приложения, а 
за ним... еще, и еще, и еще. Теперь я вынужден переходить из одного окна в 
другое и закрывать лишние копии приложения — все по очереди. Такая проблема 
из-за, казалось бы, сущего пустяка! И вот ведь еще что: при загрузке 
приложения в 16-разрядной Windows курсор-то превращался в «песочные часы». 
Подсознательно ожидая той же реакции и от Windows NT, я думал, что она 
неправильно работает, — от старых привычек избавиться очень трудно. 
    Поэтому теперь в CreateProcess предусмотрена возможность управления формой 
курсора при запуске процесса. Если же Вы укажете флаг STARTF_FORCEOFFFEEDBACK, 
CreateProcess не станет трансформировать курсор в «песочные часы» — останется 
стандартная стрелка. 
    Флаг STARTF_FORCEONFEEDBACK, напротив, заставляет CreateProcess 
отслеживать инициализацию нового процесса и в зависимости от результата 
проверки изменять форму курсора. Когда CreateProcess вызывается с этим флагом, 
курсор преобразуется в «песочные часы». Если спустя 2 секунды от нового 
процесса не поступает GUI-вызова, она восстанавливает исходную форму курсора.  
    Если же в течение 2 секунд процесс все-таки делает GUI-вызов, 
CreateProcess ждет, когда приложение откроет свое окно. Это должно произойти в 
течение 5 секунд после GUIвызова. Если окно не появилось, CreateProcess 
восстанавливает курсор, а появилось — сохраняет его в виде «песочных часов» 
еще на 5 секунд. Как только приложение вызовет функцию GetMessage, сообщая тем 
самым, что оно закончило инициализацию, CreateProcess немедленно изменит 
курсор на стандартный и прекратит мониторинг нового процесса. 
    И последний флаг, который мы обсудим, — STARTF_SCREENSAVER — подсказывает 
системе, что данное приложение — экранная заставка; это заставляет ее 
инициализировать программу весьма своеобразно. Когда процесс начнет 
исполняться, система разрешит его инициализацию с классом приоритета, 
указанным при вызове CreateProcess. А когда процесс обратится к GetMessage или 
PeekMessage, система автоматически сменит класс его приоритета на 
«простаивающий» (idle). 
    Если программа — экранная заставка активна и пользователь нажимает клавишу 
или двигает мышь, система автоматически возвращает этой программе исходный 
класс приоритета (указанный в свое время при вызове CreateProcess). 
    Для запуска экранной заставки функцию CreateProcess следует вызывать с 
флагом NORMAL_PRIORITY_CLASS, что дает такой эффект: 
    1. Программа — экранная заставка будет инициализирована перед тем, как 
«впадет в спячку». Если бы она выполнялась в таком состоянии все свое время, 
ее вытеснили бы процессы с приоритетами normal и realtime, и заставка никогда 
не получила бы шанса на инициализацию. 
    2. Выполнение программы — экранной заставки обычно завершается, когда 
пользователь начинает работать с каким-нибудь приложением. Ведь последнее 
скорее всего 

    _____________________________________Глава 3. Процессы 45 

    имеет нормальный приоритет, а это могло бы привести к повторному 
вытеснению потоков в программе — экранной заставке, и в результате ее не 
удалось бы завершить. 
    В заключение раздела — несколько слов об элементе wShowWindow структуры 
STARTUPINFO. Этот элемент инициализируется значением, которое Вы передаете в 
WinMain через ее последний параметр — nCmdShow. Он позволяет указать, в каком 
виде должно появиться основное окно Вашего приложения. В качестве значения 
используется один из идентификаторов, обычно передаваемых в ShowWindow (чаще 
всего SW_SHOWNORMAL или SW_SHOWMINNOACTIVE, но иногда и SW_SHOWDEFAULT). 
    После запуска программы двойным щелчком из Explorer ее функция WinMain 
вызывается с SW_SHOWNORMAL в параметре nCmdShow. Если же программа запускается 
двойным щелчком при нажатой клавише Shift, в этом параметре передается 
идентификатор SW^SHOWMINNOACTIVE. Благодаря этому пользователь может легко 
выбрать, в каком окне запустить программу — нормальном или свернутом. 
    Наконец, чтобы получить копию структуры STARTUPINFO, инициализированной 
родительским процессом, приложение может вызвать: 

    VOID GetStartupInfo (LPSTARTUPINFO IpStartupInfo); 

    Анализируя эту структуру, дочерний процесс способен изменять свое 
поведение в зависимости от значений ее элементов. Но учтите: хотя в 
документации на Win32 об этом четко насказано, перед вызовом GetStartupInfo 
нужно инициализировать элемент cb структуры STARTUPINFO: 

    STARTUPINFO si; 
    si.cb = sizeof(si); 
    GetStartupInfo(&si); 


    Параметр IppiProdnfo 

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

    typedef struct _PROCESS_INFORMATION { 
    RANDLE hProcess: 
    RANDLE hThread; 
    DWORD dwProcessId: 
    DWORD dwThreadId: 
    } PROCESS_INFORMATION: 

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

    46  _________________________.Windows для профессионалов 

    Не забывайте закрывать упомянутые описатели. Пропуск этой операции — одна 
из самых частых ошибок; она приводит к утечке системной памяти вплоть до 
завершения процесса, вызвавшего CreateProcess. 
    Созданному процессу присваивается уникальный идентификатор; ни у каких 
процессов, выполняемых в системе, не может быть одинаковых идентификаторов. То 
же касается и потоков. Завершая свою работу, CreateProcess заносит значения 
идентификаторов в элементы dwProcessId и dwThreadId структуры 
PROCESS_INFORMATION. Используя их, родительский процесс может обращаться к дочернему. 
    Подчеркну еще один чрезвычайно важный момент: система способна повторно 
использовать идентификаторы процессов и потоков. Например, при создании 
процесса система формирует объект «процесс», присваивая ему идентификатор со 
значением, допустим, 0х22222222. Создавая новый объект «процесс», система уже 
не присвоит ему данный идентификатор. Но после выгрузки из памяти первого 
объекта следующему создаваемому объекту «процесс» может быть присвоен тот же 
идентификатор — 0х22222222. 
    Эту особенность нужно учитывать при написании кода, избегая ссылок на 
неверный объект «процесс» (или «поток»). Действительно, затребовать и 
сохранить идентификатор процесса несложно, но задумайтесь, что получится, если 
в следующий момент этот процесс будет завершен, а новый получит тот же 
идентификатор: сохраненный ранее идентификатор уже связан совсем с другим процессом. 
    Чтобы избавиться от подобных неприятностей, доступ к объекту «процесс» 
нужно блокировать. Иначе говоря, Вы должны обязательно увеличивать значение 
счетчика, связанного с объектом «процесс», — ведь система никогда не выгрузит 
из памяти объект, счетчик которого отличен от нуля. Впрочем, в большинстве 
случаев счетчик увеличивается и без Вашего участия, как, например, после 
вызова CreateProcess. 
    Зная, что значение счетчика выше нуля, можно свободно оперировать 
идентификатором процесса. А когда необходимость в нем отпадет, вызовите 
CloseHandle -— чтобы уменьшить счетчик объекта «процесс». Затем 
удостоверьтесь, что этот идентификатор больше нигде не используется. 

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

    Процесс можно завершить тремя способами: 
    1. Один из потоков процесса вызывает функцию ExitProcess (самый 
распространенный способ). 
    2. Поток другого процесса вызывает функцию TerminateProcess (этого надо избегать). 
    3. Все потоки процесса «умирают» по своей «воле» (большая редкость). 
    В этом разделе мы и обсудим только что перечисленные способы завершения 
процесса, а также рассмотрим, что на самом деле происходит в момент его окончания. 

    Функция ExitProcess 

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

    VOID ExitProcess(UINT fuExitCode): 

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

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

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

    Функция TerminafeProcess 

    Вызов TerminateProcess тоже завершает процесс: 

    BOOL TerminateProcess(HANDLE hProcess, UINT fuExitCode); 

    Главное отличие этой функции от ExitProcess в том, что ее может вызвать 
любой поток и завершить любой процесс. Параметр hProcess идентифицирует 
описатель завершаемого процесса, а в параметр fuExitCode помещается код 
завершения процесса. 
    Пользоваться TerminateProcess вообще-то не рекомендуется; к ней можно 
прибегнуть лишь в том случае, когда иным способом процесс завершить не 
удается. При нормальном ходе вещей система уведомляет о завершении процесса 
все связанные с ним DLL-модули. Однако этого не происходит, если Вы 
обращаетесь к TerminateProcess, — а значит, не исключено некорректное 
завершение процесса. Например, Ваша программа может задействовать DLL-модуль, 
который при отключении от процесса сбрасывает данные из какого-то буфера в 
дисковый файл. В обычных условиях отключение DLL происходит при его выгрузке с 
помощью функции FreeLibrary. Ну а поскольку при обращении к TerminateProcess 
DLL-модуль об отключении не уведомляется, он не сможет выполнить своей задачи. 
Так что система сообщает DLL-модулям о завершении процесса только при 
нормальном его прекращении или при вызове ExitProcess. (Подробнее о DLL см. 
главу 12.) 
    Несмотря на все сказанное, надо заметить, что при любом способе завершения 
процесса система гарантирует освобождение занятой процессом памяти и объектов 
User или GDI, а также закрытие всех открытых файлов и уменьшение счетчиков 
объектов ядра. 

    Если все потоки процесса «уходят» 

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

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

    А происходит вот что: 
    1. Выполнение всех потоков в процессе прекращается. 
    2. Все объекты User и GDI, созданные процессом, уничтожаются, а объекты 
ядра закрываются. 

    48 _____________________________Windows для профессионалов 

    3. Объект ядра «процесс» переходит в свободное, или незанятое (signaled), 
состояние. (Подробнее на эту тему см. главу 10.) Прочие потоки в системе могут 
приостановить свое выполнение вплоть до завершения данного процесса. 
    4. Код завершения процесса меняется со значения STILL_ACTIVE на код, 
переданный в ExitProcess или TerminateProcess. 
    5. Счетчик объекта ядра «процесс» уменьшается на 1. 
    Связанный с завершаемым процессом объект ядра «процесс» не высвобождается, 
пока не будут закрыты все ссылки на него. Кроме того, закрытие процесса не 
приводит к автоматическому завершению порожденных им процессов. 
    По завершении процесса его код и выделенные ему ресурсы удаляются из 
памяти. Однако закрытая память, выделенная системой для объекта ядра 
«процесс», не освобождается, пока счетчик числа его пользователей не достигнет 
нуля. А это произойдет, только если все прочие процессы, создавшие или 
открывшие описатели для «ныне покойного» процесса, уведомят систему (вызовом 
CloseHandk) о том, что ссылки на этот процесс им больше не нужны. 
    Описатели завершенного процессса уже мало на что пригодны. Разве что 
родительский процесс, вызвав GetExitCodeProcess, может проверить, завершен ли 
процесс, идентифицируемый параметром hProcess, и, если да, определить код завершения: 

    BOOL GetExitCodeProcess(HANDLE hProcess, LPDWORD IpdwExltCode); 

    Код завершения возвращается как DWORD, на которое указывает IpdwExitCode. 
Если на момент вызова GetExitCodeProcess процесс еще не завершился, в DWORD 
заносится идентификатор STILL_ACTIVE (определенный как 0х103). Если функция 
выполнена успешно, она возвращает TRUE. В главе 10 я подробнее расскажу о том, 
как с помощью описателя дочернего процесса определить, завершился ли этот процесс. 

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

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

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

    он работал в собственном адресном пространстве, а в «Вашем» — просто 
считывал нужные ему данные; тогда он не сможет что-то испортить в адресном 
пространстве родительского процесса. В Win32 предусмотрено несколько способов 
обмена данными между процессами: 
    DDE (Dynamic Data Exchange — динамический обмен данными), OLE (Object 
Linking and Embedding — связывание и внедрение объектов), каналы (pipes), 
почтовые ящики (mailslots) и т. д. А один из самых удобных способов, 
обеспечивающих совместный доступ к данным, — использование файлов, 
проецируемых в память (memory-mapped files). (Подробнее на эту тему см. главу 8.) 
    Если Вы хотите создать новый процесс, заставить его выполнить какие-либо 
операции и дождаться их результатов, напишите примерно такой код: 

    PROCESS_INFORMATION pi; 
    DWORD dwExitCode; 
    BOOL fSuccess = CreateProcess(..., &pi): 
    if (fSuccess) { 
    // закрывайте описатель потока, как только необходимость в нем отпадает! 
    GloseHandle(pi.hThread): 
    WaitForSingleObject(pi.hProcess, INFINITE): 
    // процесс завершился 
    GetExitCodeProcess(pi.hProcess, SdwExitCode); 
    // закрывайте описатель процесса, как только необходимость в нем отпадает! 
    CloseHandle(pi.hProcess): 
    } 

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

    DWORD WaitForSingleObject(HANDLE hObject, DWORD dwTimeOut): 

    Подробное рассмотрение данной функции мы отложим до главы 10, а сейчас 
ограничимся одним соображением. Функция задерживает выполнение кода пока 
объект, определяемый параметром hObject, не перейдет в свободное (незанятое) 
состояние. (Объекты «процесс» переходят в такое состояние при завершении 
процесса.) Итак, вызов WaitForSingleObject приостанавливает выполнение потока 
родительского процесса до завершения порожденного им процесса. Когда 
WaitForSingleObject вернет управление, Вы узнаете код завершения дочернего 
процесса через функцию GetExitCodeProcess. 
    Обращение к CloseHandle в приведенном выше фрагменте кода заставляет 
систему уменьшить значения счетчиков объектов «поток» и «процесс» до нуля и 
тем самым освободить память, занимаемую этими объектами. 
    Вы, наверное, заметили, что в этом фрагменте я закрыл описатель объекта 
ядра «первичный поток» (принадлежащий дочернему процессу) сразу после возврата 
из CreateProcess. Это не приводит к завершению первичного потока дочернего 
процесса — просто уменьшает счетчик, связанный с упомянутым объектом. А вот 
почему это делается — и, кстати, даже рекомендуется делать — именно так, 
станет ясно из простого примера. Допустим, первичный поток дочернего процесса 
порождает еще один поток, а сам после этого завершается. В этот момент система 
может высвободить объект «первичный поток» дочернего процесса из памяти, если 
у родительского процесса нет описателя данного объекта. Но если родительский 
процесс располагает таким описателем, система не сможет удалить этот объект из 
памяти до тех пор, пока и родительский процесс не закроет его описатель. 

    50 _______-____________________Windows для профессионалов 


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

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

    PROCESS_INFORMATION pi: 
    BOOL fSuccess = CreateProcess(.... &pi); 
    if (fSuccess) { 
       GloseHandle(pi.hThread); 
       CloseHandle(pi.hProcess); 

