Окт 16 2009

Виртуальная память. Адресное пространство процесса

Каждому процессу операционная система выделяет собственное виртуальное адресное пространство. В Win32 его размер составляет 4 Гбайт, что определяется разрядностью регистра команд. Соответственно, 32-битный указатель может быть любым числом в интервале от 0x00000000 до OxFFFFFFFF. Таким образом, адресуется 4 294 967 296 значений, что как раз и перекрывает указанный диапазон памяти.
Верхняя половина этого пространства, то есть адреса от 0x80000000 до OxFFFFFFFF, резервируется за операционной системой, а нижняя половина почти вся доступна процессу.
Виртуальное адресное пространство процесса доступно всем потокам этого процесса. С другой стороны, потоки одного процесса не имеют доступа к адресному пространству других процессов.


Окт 16 2009

События

Событие (event) — самая простая разновидность объектов ядра. Оно содержит счетчик количества пользователей и две булевы переменные. Одна переменная указывает тип данного объекта-события, а другая — его состояние.
События просто уведомляют об окончании какой-либо операции. Объекты-события бывают двух типов: со сбросом вручную (manual-reset events) или с автосбросом (auto-reset events). Первые события позволяют возобновить выполнение сразу нескольких ждущих потоков, а вторые — только одного потока.
Параметр bManualReset определяет тип объекта-события. Значение TRUE создает событие со сбросом вручную, а значение FALSE — событие с автосбросом.
Параметр blnitialState определяет начальное состояние события — свободное (TRUE) или занятое (FALSE).
Параметр pszName содержит указатель на С-строку, в которой указывается имя объекта. Если pszName имеет значение NULL, то создается неименованный объект.
Например, следующий вызов функции CreateEvent из процесса А создает событие с автосбросом и именем EventName:
HANDLE hEvent = CreateEvent(NULL. FALSE. FALSE. "EventName");
На самом деле все обстоит немножко сложнее. При таком вызове система проверяет, не существует ли уже объект ядра с таким именем. Если подобный объект существует, то ядро проверяет тип этого объекта. Допустим, что некий процесс В
уже создал ранее объект-событие с таким же именем EventName. В этом случае система проверяет права доступа процесса А к этому объекту. Если с правами доступа все в порядке, то в таблице дескрипторов процесса А создается новая запись с дескриптором hEvent. To есть процесс А получает свой дескриптор уже существующего именованного объекта-события, а счетчик пользователей этого объекта увеличивается на единицу.
Предположим, что имеет место другая ситуация, когда некий процесс В уже создал ранее объект ядра с таким же именем, но другого типа, например семафор. Тогда функция CreateEvent вернет значение NULL, а если после этого вызвать функцию GetLastError, то она вернет код ошибки, равный 6 (ERROR_INVALID_HANDLE).
Наконец, самая простая ситуация — когда объект ядра с таким именем (EventName) в момент вызова функции CreateEvent не существует. Тогда действительно будет создан новый объект ядра «событие» с именем EventName.
Так что будьте аккуратны с именованием объектов ядра и учитывайте, что пространство имен объектов ядра является общим для объектов всех типов.
Объекты-события могут разделяться разными процессами. Допустим, что некий процесс А создал новое событие hEvent с именем EventName. Потоки из других процессов могут получить доступ к этому объекту несколькими способами:
вызовом функции CreateEvent с передачей в параметре pszName такого же имени (эта ситуация только что рассматривалась);
наследованием дескриптора;
применением функции DuplicateHandle;
вызовом функции OpenEvent с передачей в параметре pszName такого же имени.


Окт 16 2009

Функция Sleep

Поток может попросить у системы не выделять ему процессорное время на определенный период, вызвав функцию Sleep:
VOID SleepCDWORD dwMiПiseconds):
Эта функция приостанавливает выполнение потока на dwMUliseconds миллисекунд. При использовании данной функции следует учитывать несколько дополнительных аспектов.
Вызывая функцию Sleep, поток добровольно отказывается от остатка выделенного ему кванта времени.
Система прекращает выделять потоку процессорное время на период, примерно равный заданному. Вопрос точности, с которой функция обеспечит затребованную задержку/.
Если параметр dwMUliseconds равен нулю, то текущий поток уступает оставшуюся часть своего кванта другому потоку, обладающему равным с ним приоритетом и готовому к выполнению. Однако система может снова запустить текущий поток, если других планируемых потоков с тем же приоритетом нет.


Окт 16 2009

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

В большинстве случаев приложение создает другие процессы как обособленные (detached processes). Это значит, что после создания и запуска нового процесса родительскому процессу нет нужды взаимодействовать с ним или ждать, пока он закончит работу. Именно так и действует Explorer. Это приложение запускает для пользователя новые процессы, а после этого более не следит за их судьбой.
Приведенное ниже приложение CreateMyProcess демонстрирует, как можно вызвать из вашей программы стандартное приложение Windows Калькулятор (calc.exe).
Создавая приложение CreateMyProcess, добавьте к нему ресурс меню с идентификатором IDR_MENU1. Меню должно содержать один пункт с именем Create process и идентификатором IDM_CREATE_PROCESS.


Окт 16 2009

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

Процесс можно завершить четырьмя способами:
Входная функция первичного потока, например WinMain, возвращает управление (предпочтительный способ).
Один из потоков процесса вызывает функцию ExitProcess.
Поток другого процесса вызывает функцию TerminateProcess (нежелательный способ).
Все потоки процесса завершаются по своей воле. Но это случается очень редко.
Рекомендуется проектировать приложение так, чтобы его процесс завершался только после возврата управления функцией первичного потока. Это единственный способ, который гарантирует корректную очистку всех ресурсов, принадлежащих первичному потоку. При таком завершении любые объекты C++, созданные данным потоком, уничтожаются соответствующими деструкторами. Система освобождает память, которую занимал стек потока, и устанавливает код завершения процесса, который и возвращает входная функция.
Вы можете также завершить процесс, вызвав функцию ExitProcess. В справочных материалах MSDN этот способ указан как рекомендуемый. В то же время Дж. Рихтер указывает [5], что возможны ситуации, когда при данном способе завершения процесса не для всех объектов C++ будут вызваны деструкторы. Конечно, операционная система и в этом случае корректно очистит все ресурсы, выделенные процессу. Но при этом весьма вероятна утечка памяти или других ресурсов.
Вызов функции TerminateProcess тоже завершает процесс. Главное отличие этой функции от ExitProcess заключается в том, что ее может вызвать любой поток и завершить при этом любой процесс. Пользуйтесь функцией TerminateProcess лишь в крайнем случае, когда иным способом завершить процесс не удается. Процесс не получает абсолютно никаких уведомлений, что он завершается, и приложение не может выполнить очистку ресурсов или предотвратить свое неожиданное завершение. При этом теряются все данные, которые программа не успела переписать из памяти на диск. Но операционная система и в этом случае освобождает все принадлежавшие процессу ресурсы.
Четвертая ситуация может возникнуть, если все потоки вызвали ExitThread или они были закрыты другими потоками, вызвавшими функцию TerminateThread. Обнаружив, что в процессе нет исполняющихся потоков, операционная система немедленно завершает его. Код завершения процесса приравнивается к коду завершения последнего потока.
В случае завершения процесса системой выполняются следующие действия.
Выполнение всех потоков в процессе прекращается.
Все User- и GDI-объекты, созданные процессом, уничтожаются, а объекты ядра закрываются, если их не использует другой процесс.
Объект ядра «процесс» переходит в свободное, или незанятое (signaled), состояние.
Счетчик пользователей объекта ядра «процесс» уменьшается на единицу.


Окт 16 2009

Управление процессами

Самый распространенный способ начала процесса — это осуществить запуск приложения в Проводнике (Explorer), либо в меню Пуск (Start), либо набрав название программы в командной строке. Кроме того, Win32 API содержит несколько функций, которые можно использовать для создания и управления процессами.


Окт 16 2009

Классы приоритетов процесса и приоритеты потоков

Windows поддерживает 32 приоритета (от 0 до 31) — чем больше номер, тем выше приоритет. Приоритет потока складывается из двух составляющих: класса приоритета процесса, его создавшего, и относительного приоритета потока внутри этого класса.
Классы Below normal и Above normal стали использоваться, начиная с Windows 2000. Класс Idle назначается процессу, который должен простаивать в случае активности других процессов, например для приложения — хранителя экрана.
Процессам, запускаемым пользователем, присваивается класс Normal. Это самые многочисленные процессы в системе. Как правило, они являются интерактивными, то есть требуют постоянного взаимодействия с пользователем, как, например, графические или текстовые редакторы. Процессы класса Normal делятся на процессы переднего плана (foreground) и фоновые (background). Для процесса, с которым пользователь в данный момент работает, то есть для процесса переднего плана, уровень приоритета поднимается на две единицы. Это повышает комфортабельность общения пользователя с прикладной программой.
Создавать процессы, относящиеся к классу High, следует с большой осторожностью. Если поток с классом приоритета High занимает процессор достаточно долго, то в это время другие потоки вообще не получат доступа к процессору. Обычно с классом High работают некоторые системные процессы, которые большую часть времени ожидают какого-либо события, например, winlogon.exe. Если в вашем приложении какая-то подзадача требует быстрой реакции на некоторое событие, то вы можете повышать класс приоритета процесса до значения High именно на тот период, когда решается эта подзадача, а затем возвращать его к значению Normal. Для изменения класса приоритета процесса во время работы приложения может применяться функция SetPrioriryClass.
Практически никогда вы не должны использовать класс приоритета Realtime, поскольку в этом случае ваше приложение будет прерывать системные потоки, управляющие мышью, клавиатурой и дисковыми операциями. Система будет фактически парализована. Только в особых случаях, когда программа взаимодействует непосредственно с аппаратурой или решаются короткие подзадачи, для которых нужно гарантировать отсутствие прерываний, класс приоритета Realtime может быть кратковременно использован.


Окт 16 2009

Планирование потоков

Чтобы все потоки работали, операционная система выделяет каждому из них определенное процессорное время. Тем самым создается иллюзия одновременного выполнения потоков. Разумеется, для многопроцессорных систем возможен истинный параллелизм.
В однопроцессорной системе в любой момент времени только один поток может находиться в состоянии выполнения. Все остальные потоки находятся либо в состоянии готовности, либо в состоянии блокировки.
С самого начала поток попадает в очередь готовых к выполнению потоков. Через какое-то время операционная система выделяет в его распоряжение центральный процессор, и поток переходит в состояние выполнения. В системе Windows реализована система вытесняющего планирования на основе приоритетов. Это означает, что освободившийся процессор продолжает обслуживать тот поток из очереди, который обладает наибольшим приоритетом. О правилах назначения приоритетов мы поговорим чуть позже.
Выбранный для выполнения поток работает в течение некоторого периода, называемого квантом. Windows оперирует квантом потока не как отрезком времени, а как целым числом. Обычно поток стартует со значением кванта, равным 6 — для Windows 2000 Professional или 36 — для Windows 2000 Server.
Каждый раз, когда возникает прерывание от системного таймера, из кванта выполняющегося потока вычитается фиксированное значение 3, и так продолжается до тех пор, пока значение кванта не достигнет нуля. Поэтому под управлением Windows 2000 Professional поток будет выполняться в течение двух интервалов системного таймера, а под управлением Windows 2000 Server — в течение 12 интервалов.
Интервал (или период) системного таймера обычно равен 10 мс или около 15 мс, в зависимости от аппаратной платформы. Точное значение этого интервала можно получить с помощью функции GetSystemTimeAdjustment (см. главу 10). Например, для моего компьютера с процессором Intel Celeron CPU 2.0 ГГц период срабатывания системного таймера равен 15,625 мс. При таком периоде кванту потока соответствует временной интервал 15,625 • 2 = 31,25 мс.
Когда после очередного прерывания квант потока становится равным нулю, Windows переводит поток в состояние готовности и ищет в системе поток с самым высоким приоритетом, находящимся в состоянии готовности. Если в состоянии готовности находятся несколько потоков с приоритетом не ниже предыдущего выполняемого потока, то следующий поток из очереди будет переведен в состояние выполнения и начнет функционировать. Таким образом, потоки с одинаковым уровнем приоритета обслуживаются в циклическом порядке. Впрочем, при отсутствии других претендентов предыдущий поток может получить еще один квант.
Однако выполняемый поток не всегда полностью использует свой квант. Его выполнение может быть прервано при ненулевом кванте в двух ситуациях:
когда появился в состоянии готовности другой поток с более высоким приоритетом; при этом текущий поток вытесняется и переводится в состояние готовности;
текущему потоку потребовался какой-либо системный ресурс (или объект ядра), который в настоящий момент времени является занятым; в этом случае поток переводится в состояние блокировки (ожидания события).
Выбрав новый поток, операционная система переключает контекст. Эта операция заключается в сохранении содержимого регистров процессора для вытесненного потока и загрузке контекста для выбранного потока.


Окт 16 2009

Процессы и потоки

При запуске приложения операционная система Windows создает процесс. Процесс (process) — это совокупность ресурсов, необходимых для выполнения программы. Процесс владеет виртуальным адресным пространством, выполняемым кодом, данными, дескрипторами необходимых объектов и иными ресурсами. Однако сам по себе процесс не выполняется. Вместо этого он запускает единственный поток, который часто называют первичным потоком (primary thread). Если процесс имеет только один первичный поток, то, фактически, понятия «процесс» и «поток» совпадают. Первичный поток может создавать другие потоки, те, в свою очередь, новые потоки и т. д.
Поток {thread) — это основная выполняемая единица, для которой операционная система выделяет процессорное время. Каждый поток работает со своим контекстом. Контекст потока {thread context) — это структура, содержащая значения всех регистров процессора. Кроме того, поток имеет доступ ко всем ресурсам своего процесса, включая память, открытые файлы и другие ресурсы.
Обычно приложение содержит только один процесс, поэтому термины «программа» и «процесс» часто используются как синонимы. В то же время любой поток процесса может создать дочерний процесс, выполняющийся одновременно с родительским процессом.


Окт 16 2009

Объекты ядра

Современные процессоры позволяют выполнять программы в одном из двух режимов: Kernel {режим ядра) или User (режим пользователя). В режиме ядра приложению, кроме всего прочего, разрешено выделять память для других приложений. Поэтому изменить важные данные в памяти компьютера, например, управляющие распределением памяти, может только приложение, работающее в режиме ядра.
Архитектура операционной системы Windows NT/2000 состоит из двух основных частей: привилегированной подсистемы режима ядра (privileged kernel mode part) и непривилегированной подсистемы пользовательского режима (nonprivileged user mode part).
Подсистема ядра содержит следующие компоненты:
HAL (Hardware Abstraction Layer) — уровень, абстрагирующий другие компоненты от аппаратных различий, зависимых от платформы.
Микроядро (MicroKernel), которое отвечает за планирование потоков, переключение задач, обработку прерываний и исключительных ситуаций, многопроцессорную синхронизацию.
Драйверы устройств — драйверы оборудования, файловой системы и сетевой поддержки, реализующие пользовательские функции ввода-вывода.
Управление окнами и графическая подсистема реализуют функции графического интерфейса.
Исполнительная часть отвечает за управление памятью, управление процессами и потоками, безопасность, ввод-вывод данных и взаимодействия между процессами.
Объекты ядра используются системой и приложениями для управления самыми разнообразными ресурсами, например процессами, потоками, файлами, семафорами, событиями и многими другими.
Каждый объект ядра — это блок памяти, выделенный ядром и доступный только ему. В блоке размещается структура данных, поля которой содержат информацию об объекте. Некоторые поля (например, дескриптор защиты и счетчик количества пользователей) присутствуют во всех объектах, но большая их часть специфична для объектов конкретного типа. Приложение не имеет прямого доступа к структурам данных, представляющим собой объекты ядра. Оперировать объектами ядра приложение может только через специальные функции Windows.
Когда вы вызываете функцию, создающую объект ядра (например, CreateFile), она возвращает дескриптор созданного объекта. Этот дескриптор может быть использован любым потоком вашего процесса. Для большей надежности операционной системы Microsoft сделала так, что значения дескрипторов объектов ядра действительны только в адресном пространстве процесса, их создавшего. Поэтому все попытки передачи такого дескриптора другому процессу и его использования в другом процессе приводят к ошибкам.
Объекты принадлежат ядру, а не процессу. Другими словами, если процесс создает какой-либо объект ядра, а затем завершает свою работу, то объект ядра может быть и не разрушен. В большинстве случаев объект все же разрушается, но если этим объектом пользуется другой процесс, то ядро не позволит разрушить объект до тех пор, пока второй процесс не откажется от него.
Ядру известно, сколько процессов использует конкретный объект ядра, поскольку в каждом объекте есть счетчик пользователей. В момент создания объекта ядра счетчику присваивается единичное значение. Когда к этому объекту обращается другой процесс, значение счетчика увеличивается на единицу. А когда какой-то процесс завершается, счетчики всех используемых им объектов ядра уменьшаются на единицу. Как только счетчик пользователей объекта обнуляется, ядро уничтожает этот объект.
Многие объекты ядра находятся всегда в одном из двух состояний: свободном состоянии (signaled state) либо несвободном состоянии (nonsignaled state). Переход из одного состояния в другое осуществляется по правилам, определенным Microsoft для каждого из объектов ядра. Эти состояния могут использоваться так называемыми wait-функциями для решения проблем синхронизации выполнения потоков.
Объекты ядра можно защитить с помощью дескриптора защиты (security descriptor). Он содержит информацию о том, кто создал объект и кто имеет права на доступ к нему. Дескрипторы защиты обычно используют при написании серверных приложений. Создавая клиентское приложение, можно просто игнорировать это свойство объектов ядра.
Почти все функции, создающие объекты ядра, принимают в качестве аргумента указатель на структуру SECURITY_ATTRIBUTES. В большинстве случаев на месте соответствующего аргумента можно указать значение NULL, и тогда объект создается с защитой по умолчанию. В этом случае создатель объекта и любой член группы администраторов получают полный доступ к объекту, а все прочие процессы к объекту не допускаются.
Вне зависимости от того, как был создан объект ядра, после окончания работы с ним его нужно закрыть вызовом функции CloseHandle:
BOOL CloseHandle(HANDLE hObject);
Эта функция сначала проверяет таблицу дескрипторов данного процесса, чтобы убедиться, что процесс имеет доступ к объекту hObject. Если доступ разрешен,то система получает адрес структуры данных объекта hObject и уменьшает в ней счетчик количества пользователей. Как только счетчик обнулится, ядро удаляет объект из памяти.
Если же дескриптор неверен, то функция CloseHandle возвращает значение FALSE, а функция GetLastError - код ERROR_INVALID_HANDLE.
Перед самым возвратом управления функция CloseHandle удаляет соответствующую запись из таблицы дескрипторов. После этого дескриптор hObject считается недоступным для данного процесса, и его нельзя более использовать. Но если счетчик пользователей этого объекта не обнулен, то объект остается в памяти. Это означает, что объект используется другими процессами. Когда и остальные процессы завершат свою работу с этим объектом, тоже вызвав функцию CloseHandle, он будет разрушен.
А если вы забыли вызвать CloseHandle — будет ли утечка памяти? И да, и нет. Утечка ресурсов вполне вероятна, пока процесс еще выполняется. Однако по завершении процесса операционная система гарантированно освобождает все ресурсы, принадлежавшие этому процессу.
Ранее указывалось, что объекты ядра используются только в рамках процесса, их создавшего. Но все же иногда возникает необходимость в совместном использовании объектов ядра несколькими процессами, например, в следующих ситуациях:
объект ядра «проекция файла» позволяет двум процессам, исполняемым на одной машине, совместно использовать одни и те же блоки памяти;
почтовые ящики и именованные каналы дают возможность программам обмениваться данными с процессами, исполняемыми на других машинах в сети;
мьютексы, семафоры и события позволяют синхронизировать потоки, исполняемые в разных процессах, чтобы одно приложение могло уведомить другое приложение об окончании той или иной операции.
Win32 API предоставляет три механизма, позволяющие процессам использовать одни и те же объекты ядра: а) наследование дескриптора объекта в дочернем процессе; б) именованные объекты; в) дублирование дескрипторов объектов. В приводимых ниже примерах мы будем активно применять второй из указанных механизмов (совпадающие имена для разделяемых объектов).


« Предыдущая страницаСледующая страница »