Июнь 16 2010

Использование проекции файла для реализации разделяемой памяти

Самый низкоуровневый механизм совместного использования данных в одной системе — проецирование файла в память. На нем так или иначе базируются все другие механизмы разделения данных. Поэтому, если вы хотите получить максимальное быстродействие с минимумом издержек, лучше всего применять именно проецирование.
Совместное использование данных в этом случае происходит по следующей схеме. Несколько процессов проецируют в память представления одного и того же объекта «проекция файла», то есть делят одни и те же страницы физической памяти. Поэтому, когда один процесс записывает данные в представление общего объекта «проекция файла», эти изменения немедленно отражаются в других процессах. Но для этого все процессы должны использовать одинаковое имя объекта «проекция файла».
В предыдущем разделе рассматривалось проецирование представления файла, размещенного на диске. Но для целей обмена данными между разными процессами хранение этих данных на диске было бы очень неудобным. К счастью, Win32 API предусматривает возможность проецирования файлов непосредственно на физическую память из страничного файла, а не из специально создаваемого дискового файла.
Этот способ даже проще стандартного, рассмотренного в предыдущем разделе. Во-первых, не нужно вызывать функцию CreateFile. Вы просто вызываете функцию CreateFileMapping и передаете значение INVALID_HANDLE_VALUE (или константу -1) в параметре hFile. Но при вызове функции CreateFileMapping следует передать в последнем ее параметре С-строку, содержащую имя этого объекта. Тогда другие процессы, если им понадобится доступ к разделяемой памяти, смогут вызвать функцию OpenFileMapping и передать ей то же самое имя.
Учтите, что если разделяемая память используется в режиме записи данных более чем одним потоком, то вы должны предусмотреть синхронизацию работы этих потоков. Пример такой синхронизации при помощи объектов-событий рассматривается чуть ниже.
Когда работа с объектом «проекция файла» завершена, процесс должен вызвать функцию CloseHandle. Как только все дескрипторы объекта будут закрыты, система освободит память, переданную из страничного файла.


Окт 16 2009

Модель «клиент-сервер»

Чтобы показать применение механизмов обмена между процессами с помощью разделяемой памяти (файла, проецируемого в память) и с помощью сообщения WM_COPYDATA, мы разработаем две программы, имитирующее функции сервера и клиента.
Клиентом называется объект, запрашивающий доступ к службе или ресурсу. Сервер — это объект, выполняющий некую службу или обладающий ресурсом.
Клиент и сервер могут работать на одной и той же машине, используя локальные механизмы коммуникации, или на разных машинах, применяя для связи сетевые средства.
Поведение клиента и сервера асимметрично. Процесс-сервер инициализируется и переходит в состояние ожидания запросов от возможных клиентов. Как правило, процесс-клиент запускается в интерактивном режиме и посылает запросы серверу. Сервер исполняет полученный запрос, причем это может подразумевать диалог с клиентом, а может — и нет. Затем сервер вновь переходит в состояние ожидания запросов от других клиентов.


Окт 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 переводит поток в состояние готовности и ищет в системе поток с самым высоким приоритетом, находящимся в состоянии готовности. Если в состоянии готовности находятся несколько потоков с приоритетом не ниже предыдущего выполняемого потока, то следующий поток из очереди будет переведен в состояние выполнения и начнет функционировать. Таким образом, потоки с одинаковым уровнем приоритета обслуживаются в циклическом порядке. Впрочем, при отсутствии других претендентов предыдущий поток может получить еще один квант.
Однако выполняемый поток не всегда полностью использует свой квант. Его выполнение может быть прервано при ненулевом кванте в двух ситуациях:
когда появился в состоянии готовности другой поток с более высоким приоритетом; при этом текущий поток вытесняется и переводится в состояние готовности;
текущему потоку потребовался какой-либо системный ресурс (или объект ядра), который в настоящий момент времени является занятым; в этом случае поток переводится в состояние блокировки (ожидания события).
Выбрав новый поток, операционная система переключает контекст. Эта операция заключается в сохранении содержимого регистров процессора для вытесненного потока и загрузке контекста для выбранного потока.


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