Наследуемые описатели объектов ядра ос windows можно передать через

Для реализации асинхронного ввода-вывода в операционной системе предусмотрен специальный механизм, основанный на так называемых асинхронных вызовах процедур (Аsynchronous Procedure Call, APC). Это один из базовых механизмов, необходимый для нормального функционирования операционной системы.

Асинхронные вызовы процедур

Для реализации асинхронного ввода-вывода в операционной системе предусмотрен специальный механизм, основанный на так называемых асинхронных вызовах процедур (Аsynchronous Procedure Call, APC). Это один из базовых механизмов, необходимый для нормального функционирования операционной системы.

Практика показала, что такой механизм был бы эффективен и для реализации самих приложений. Более того, для реализации асинхронного ввода-вывода с поддержкой функции завершения система уже обязана была предоставить этот механизм. Для реализации этого механизма операционная система ведет списки процедур, которые она должна вызывать в контексте данного потока, с тем ограничением, что прерывать работу занятого потока в произвольный момент времени система не должна. Поэтому для обслуживания накопившихся в очереди процедур необходимо перевести поток в специальное состояние ожидания оповещения (alertable waiting) — для этого Win32 API предусматривает специальный набор функций: например, SleepEx, WaitForSingleObjectEx и др.

Здесь и во многих примерах далее, чтобы не загромождать код примера, опущена проверка ошибок:

VOID CALLBACK ApcProc( ULONG_PTR dwData )
{
  /* ... */
}
int main( void )
{
  QueueUserAPC( ApcProc, GetCurrentThread(), 0 ); 
  /* ... */
  SleepEx( 1000, TRUE );
  return 0;
}

Обычно APC используются самой системой для реализации асинхронного ввода-вывода и некоторых других операций, но разработчикам также предоставлена функция QueueUserAPC, с помощью которой можно ставить в APC очередь запросы для вызова собственных функций.

Процессы, потоки и объекты ядра

Обсуждение реализации мультипрограммирования в операционной системе неизбежно приводит к обсуждению вопросов безопасности.

В мультипрограммной среде может одновременно выполняться значительное количество процессов, представляющих разных пользователей, и ядро операционной системы вынуждено проверять полномочия каждого процесса при их попытках доступа к защищаемым объектам.

Для того чтобы ядро операционной системы могло контролировать доступ к тем или иным объектам, сами объекты должны управляться ядром системы. Это приводит к понятию объектов ядра (kernel objects), которые создаются по запросу процессов ядром системы, управляются ядром, и доступ к которым также контролируется ядром системы.

Объекты ядра

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

не имеют возможности доступа к этим объектам напрямую. Для того чтобы пользовательский процесс мог оперировать такими объектами, введено понятие описатель (handle) объекта ядра. Так, например, объектами ядра являются файлы, процессы, потоки, события, почтовые ящики и многое другое.

Все созданные описатели объектов ядра должны удаляться с помощью функции

BOOL CloseHandle( HANDLE hKernelObject )

которая уменьшает счетчик использования объекта и уничтожает его, когда на объект никто больше не ссылается.

Доступ к защищаемым объектам в Windows задается так называемыми дескрипторами безопасности (Security Descriptor). Дескриптор содержит информацию о владельце объекта и первичной группе пользователей и два списка управления доступом (ACL, Access Control List): один список задает разрешения доступа, другой — необходимость аудита при доступе к объекту. Список содержит записи, указывающие права выполнения действий, и запреты, назначенные конкретным пользователям и группам. При доступе к защищаемым объектам для начала проверяются запреты — если для данного пользователя и группы имеется запрет доступа, то дальнейшая проверка не выполняется и попытка доступа отклоняется. Если запретов нет, то проверяются права доступа — при отсутствии разрешений доступ отклоняется. Запрет обладает более высоким «приоритетом», чем наличие разрешений — это позволяет разрешить доступ, к примеру, целой группе пользователей и выборочно запретить некоторым ее членам.

Объект, осуществляющий доступ (выполняющийся поток), обладает так называемым маркером доступа (access token). Маркер идентифицирует пользователя, от имени которого предпринимается попытка доступа, а также его привилегии и умолчания (например, стандартный ACL объектов, создаваемых этим пользователем). В Windows маркерами доступа обладают как потоки, так и процессы. С процессом связан так называемый первичный маркер доступа, который используется при создании потоков, а вот в дальнейшем поток может работать от имени какого-либо иного пользователя, используя собственный маркер воплощения (impersonation).

Процессы и потоки в Windows являются с одной стороны «представителями» пользователя, выступающими от его имени, а с другой стороны — защищаемыми объектами, при доступе к которым выполняется проверка прав, то есть они обладают одновременно и маркерами доступа, и дескрипторами безопасности.

Описатели объектов ядра в Windows позволяют разным процессам и потокам взаимодействовать с объектами с учетом их контекстов безопасности, располагаемых прав и требуемого режима доступа. Примерами защищаемых объектов являются процессы, потоки, файлы, большинство синхронизирующих примитивов (события, семафоры и т.д.), проекции файлов в память и многое другое.

Для создания большинства объектов ядра используются функции, начинающиеся на слово «Create» и возвращающие описатель созданного объекта, например функции CreateFile, CreateProcess, CreateEvent и т.д. Многие объекты при их создании могут получить собственное имя или остаться неименованными.

Любой процесс или поток может ссылаться на объекты ядра, созданные другим процессом или потоком. Для этого предусмотрено три механизма:

  • Объекты могут быть унаследованы дочерним процессом при его создании. В этом случае объекты ядра должны быть «наследуемыми», и родительский процесс должен принять меры к тому, чтобы потомок мог узнать их описатели. Возможность передавать описатель потомкам по наследованию явно указывается в большинстве функций, так или иначе создающих объекты ядра (обычно такие функции содержат аргумент » BOOL bInheritHandle «, который указывает возможность наследования).
  • Объект может иметь собственное уникальное имя — тогда можно получить описатель этого объекта по его имени. Для разных типов объектов Win32 API предоставляет набор функций, начинающийся на Open... например, OpenMutex, OpenEvent и т.д.
  • Процесс-владелец объекта может передать его описатель любому другому процессу. Для этого процесс-владелец объекта должен получить специальный описатель объекта для «экспорта» в указанный процесс. В Win32 API для этого предназначена функция DuplicateHandle, создающая для объекта, заданного описателем в контексте данного процесса, новый описатель, корректный в контексте нового процесса:
    BOOL DuplicateHandle(
      HANDLE hFromProcess, HANDLE hSourceHandle,
      HANDLE hToProcess, LPHANDLE lpResultHandle,
      DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwOptions
    );

Существенно проследить, чтобы все создаваемые описатели закрывались вызовом функции CloseHandle, включая описатели, созданные функцией DuplicateHandle. Хорошая практика при разработке приложений — проводить мониторинг выделяемых описателей и количества объектов в процессе (например, с помощью таких стандартных средств как менеджер задач или оснастка «производительность» панели управления).

Совместное использование объектов ядра несколькими процессами

Часто возникает необходимость в доступе
к объектам ядра нитями, принадлежащими
разным процессам. Причин тому несколько:

• объекты «проекции файлов» позволяют
двум процессам совместно использовать
одни и те же области памяти;

• почтовые ящики (mail slots)и именованные каналы (named
pipes)дают возможность программам
обмениваться данными с процессами,
исполняемыми на других машинах в сети;

• мьютексы (mutexes),семафоры
(semaphores)и события (events)позволяют синхронизировать нити,
исполняемые в разных процессах, так что
одно приложение может уведомить другое
об окончании той или иной операции.

Поскольку описатели объектов ядра имеют
смысл только в контексте процесса,
совместное использование объектов ядра
между несколькими процессами в
Win32 —задача весьма непростая.

Предусмотрены три механизма, позволяющие
процессам совместно использовать одни
и те же объекты ядра:

  • наследование описателей;

  • дублирование описателей;

  • использование именованных объектов

Наследование описателя объекта

Наследование применимо, только когда
процессы связаны «родственными»
отношениями. Например, родительскому
процессу доступны один или несколько
описателей объектов ядра, и он передает
дочернему процессу право доступа к
своим объектам ядра.

Чтобы такой сценарий наследования
сработал, родительский процесс должен
выполнить несколько операций.

Первое:При создании объекта ядра
следует сообщить системе, что нужен
наследуемый описатель этого объекта.
(наследуютсяописателиобъектов
ядра, носами объектыядра
—нет.)

Чтобы создать наследуемый описатель,
родительский процесс выделяет и
инициализирует структуру
SECURITY_ATTRIBUTES,а затем передает ее адрес
требуемой Createфункции. Следующий код создает наследуемый
объект мьютекс с защитой по умолчанию
и возвращает его описатель:

SECURITY_ATTRIBUTES
sa
sa.Length =
siseof(sa);
sa.lpSecurityDescnptor
=
NULL
sa.bInheritHandle
=
TRUE

HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);

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

Второе: Для наследования
описателей объектов характерно одно
очень странное свойство:
дочерний процесс не имеет ни малейшего
понятия, что он унаследовал какие-то
описатели. Поэтому наследование
описателей объектов ядра полезно должно
сопровождаться передачей информации
о наследуемых описателях.

Для этого в дочерний процесс обычно
передают значение описателя объекта
ядра как аргумент в командной строке.
Инициализирующий код дочернего процесса
анализирует командную строку,извлекает из нее значение описателя, и
дочерний процесс получает доступ к
объекту. При этом механизм наследования
срабатывает только потому, что значение
описателя объекта ядра
в родительском и дочернем процессах
одинаково, —и именно по
этой причине родительский процесс может
передать значение описателя как аргумент
в командной строке.

Для передачи информации о наследуемом
описателе объекта ядра от родительского
процесса дочернему, конечно же, годятся
и другие формы межпроцессной связи.
Один из приемов заключается в том, что
родительский процесс дожидается
окончания инициализации дочернего
(через функцию WaitForInputIdle),а затем посылает сообщение окну,
созданному дочерним процессом.

Еще один прием: родительский процесс
добавляет в свой блок переменных
окружения непустую переменную. Она
должна быть «узнаваема» дочерним
процессом и содержать значение
наследуемого описателя объекта ядра.
Далее родительский процесс создает
дочерний, тот наследует переменные
окружения родительского процесса и,
вызвав GetEnvironmentVariable
получает нужный описатель. Такой
прием особенно хорош, когда дочерний
процесс тоже порождает процессы,
—ведь все переменные окружения
вновь наследуются.

Соседние файлы в папке вар1

  • #
  • #
  • #
  • #
  • #
  • #
  • #

В продолжение разговора об объектах ядра, последней будет тема передачи и использования HANDLE (описателя) объекта в нескольких процессах. Не перепутайте с потоками, ибо внутренние потоки автоматически имеют доступ ко всем описателям внутри текущего процесса.

Передача прав пользования описателями может происходить несколькими путями.

  • Наследованием
  • Именованием
  • Дублированием

В этом случае при создании объекта ядра необходимо создать структуру SECURITY_ATTRIBUTES и задать параметр bInheritHandle=TRUE; После чего передать в параметр SECURITY_ATTRIBUTES функции создающей объект вышеупомянутую структуру защиты.

Однако это еще не все. Наследовать описатель может только дочерний процесс, созданный внутри текущего. При создании такового функцией CreateProcess() в параметр BOOL bInheritHandles присваивается значение TRUE, что говорит системе, скопировать описатели из таблицы родительского в таблицу дочернего процесса. Причем переписывание происходит на те же места, т.е. значение HANDLE остается неизменным.

Наследуются, естественно, только те описатели, которые уже существуют, и попытка попользоваться описателем, созданным после передачи данных в дочерний процесс, закончится ошибкой.

Именование поддерживается не всеми типами объектов ядра. Список поддерживающих: Mutex, Event, Semaphore, WaitableTimer, FileMapping, JobObject.

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

При создании объекта в функции, поддерживающей именование объекта, присутствует параметр PCSTR pszName, который обычно равен NULL, поэтому все объекты получаются безымянными.

Когда мы хотим использовать метод именования, мы должны дать объекту имя. Никакой гарантии нет, что объект с таким именем уже не существует и такая попытка не вызовет ошибку в системе, поэтому старайтесь давать именам префиксы чего-нибудь личного, например, GromObjMutex.

При желании обратиться к такому объекту из другого процесса, зная имя, можно создать CreateMutex() с тем же именем. В случае, если объект уже существует, система проверит права доступа и при разрешении вернет вам HANDLE, указывающий на готовый объект, либо NULL, если объект процессу недоступен. Можно вместо Create функции с таким же успехом использовать Open.

Учтите, что успешное завершение Create функции может произойти и в случае, если объект существует и доступен, и в случае, если объект не существует. В этом случае создастся новый объект ядра с указанным именем. Поэтому, если нужен гарантированный результат присоединения к существующему объекту, используйте функции Open.

Я только скажу, что, просмотрев прототип требуемой функции DuplicateHandle, вы быстро поймете, как работает эта простая и вместе с тем гибкая функция.

Если будет необходимо, я отвечу на вопросы на форуме.

Обязательно стоит обратить внимание на то, что объект будет удален из системы только тогда, когда все процессы, использующие его, вызовут функцию CloseHandle() с параметром описателем этого объекта.

Как я говорил ранее, при всех трех методах совместного использования в процессах объектов ядра счетчик пользователей в самом объекте должен быть сведен к 0.

Предложите, как улучшить StudyLib

(Для жалоб на нарушения авторских прав, используйте

другую форму
)

Ваш е-мэйл

Заполните, если хотите получить ответ

Оцените наш проект

1

2

3

4

5

В продолжение разговора об объектах ядра, последней будет тема передачи и использования HANDLE (описателя) объекта в нескольких процессах. Не перепутайте с потоками, ибо внутренние потоки автоматически имеют доступ ко всем описателям внутри текущего процесса.

В этом случае при создании объекта ядра необходимо создать структуру SECURITY_ATTRIBUTES и задать параметр bInheritHandle=TRUE; После чего передать в параметр SECURITY_ATTRIBUTES функции создающей объект вышеупомянутую структуру защиты.

Однако это еще не все. Наследовать описатель может только дочерний процесс, созданный внутри текущего. При создании такового функцией CreateProcess() в параметр BOOL bInheritHandles присваивается значение TRUE, что говорит системе, скопировать описатели из таблицы родительского в таблицу дочернего процесса. Причем переписывание происходит на те же места, т.е. значение HANDLE остается неизменным.

Наследуются, естественно, только те описатели, которые уже существуют, и попытка попользоваться описателем, созданным после передачи данных в дочерний процесс, закончится ошибкой.

Именование поддерживается не всеми типами объектов ядра. Список поддерживающих: Mutex, Event, Semaphore, WaitableTimer, FileMapping, JobObject.

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

При создании объекта в функции, поддерживающей именование объекта, присутствует параметр PCSTR pszName, который обычно равен NULL, поэтому все объекты получаются безымянными.

Когда мы хотим использовать метод именования, мы должны дать объекту имя. Никакой гарантии нет, что объект с таким именем уже не существует и такая попытка не вызовет ошибку в системе, поэтому старайтесь давать именам префиксы чего-нибудь личного, например, GromObjMutex.

При желании обратиться к такому объекту из другого процесса, зная имя, можно создать CreateMutex() с тем же именем. В случае, если объект уже существует, система проверит права доступа и при разрешении вернет вам HANDLE, указывающий на готовый объект, либо NULL, если объект процессу недоступен. Можно вместо Create функции с таким же успехом использовать Open.

Учтите, что успешное завершение Create функции может произойти и в случае, если объект существует и доступен, и в случае, если объект не существует. В этом случае создастся новый объект ядра с указанным именем. Поэтому, если нужен гарантированный результат присоединения к существующему объекту, используйте функции Open.

Я только скажу, что, просмотрев прототип требуемой функции DuplicateHandle, вы быстро поймете, как работает эта простая и вместе с тем гибкая функция.

Если будет необходимо, я отвечу на вопросы на форуме.

Обязательно стоит обратить внимание на то, что объект будет удален из системы только тогда, когда все процессы, использующие его, вызовут функцию CloseHandle() с параметром описателем этого объекта.

Как я говорил ранее, при всех трех методах совместного использования в процессах объектов ядра счетчик пользователей в самом объекте должен быть сведен к 0.

Источник

Совместное использование объектов ядра несколькими процессами

Время от времени возникает необходимость в разделении объектов ядра между потоками, исполняемыми в разных процессах. Причин тому может быть несколько:

Но поскольку описатели объектов ядра имеют смысл только в конкретном процессе, разделение объектов ядра между несколькими процессами — задача весьма непростая. У Microsoft было несколько веских причин сделать описатели процессно-зависимыми, и самая главная — устойчивость операционной системы к сбоям. Если бы описатели объектов ядра были общесистемными, то один процесс мог бы запросто получить описатель объекта, используемого другим процессом, и устроить в нем (этом процессе) настоящий хаос. Другая причина — защита. Объекты ядра защищены, и процесс, прежде чем оперировать с ними, должен запрашивать разрешение на доступ к ним.

Наследование описателя объекта

Наследование применимо, только когда процессы связаны родственными отношениями (родительский-дочерний). Например, родительскому процессу доступен один или несколько описателей объектов ядра, и он решает, породив дочерний процесс, передать ему по наследству доступ к своим объектам ядра. Чтобы такой сценарий наследования сработал, родительский процесс должен выполнить несколько операций.

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

Чтобы создать наследуемый описатель, родительский процесс выделяет и инициализирует структуру SECURITY_ATTRIBUTES, а затем передает ее адрес требуемой Create-функции. Следующий код создаст объект-мьютекс и возвращает его описатель:

SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecuntyDescriptor = NULL;
sa.bInheritHandle =TRUE; // делаем возвращаемый описатель наследуемым
HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);

Этот код инициализирует структуру SECURTY_ATTRIBUTES, указывая, что объект следует создать с защитой по умолчанию (в Windows 98 это игнорируется) и что возвращаемый описатель должен быть наследуемым. Хотя Windows 98 не полностью поддерживает защиту, она все же поддерживает наследование и поэтому корректно обрабатывает элемент bInheritHandle.

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

Для этого в дочерний процесс обычно передают значение ожидаемого им описателя объекта ядра, как аргумент в командной строке. Инициализирующий код дочернего процесса анализирует командную строку (чаще всего вызовом sscanf), извлекает из нее значение описателя, и дочерний процесс получает неограниченный доступ к объекту. При этом механизм наследования срабатывает только потому, что значение описателя общего объекта ядра в родительском и дочернем процессах одинаково, — и именно по этой причине родительский процесс может передать значение описателя, как аргумент в командной строке.

Для наследственной передачи описателя объекта ядра от родительского процесса дочернему, конечно же, годятся и другие формы межпроцессной связи. Один из приемов заключается в том, что родительский процесс дожидается окончания инициализации дочернего (через функцию WaitForInputIdle), а затем посылает (синхронно или асинхронно) сообщение окну, созданному потоком дочернего процесса.

Еще один прием: родительский процесс добавляет в свой блок переменных окружения новую переменную. Она должна быть «узнаваема» дочерним процессом и содержать значение наследуемого описателя объекта ядра, Далее родительский процесс создает дочерний, тот наследует переменные окружения родительского процесса и, вызвав GetEnvironmentVariable, получает нужный описатель. Такой прием особенно хорош, когда дочерний процесс тоже порождает процессы, — ведь все переменные окружения вновь наследуются.

Изменение флагов описателя

Иногда встречаются ситуации, в которых родительский процесс создает объект ядра с наследуемым описателем, а затем порождает два дочерних процесса. Но наследуемый описатель нужен только одному из них. Иначе говоря, время от времени возникает необходимость контролировать, какой из дочерних процессов наследует описатели объектов ядра. Для этого модифицируйте флаг наследования, связанный с описателем, вызовом SetHandleInformation:

BOOL SetHandleInformation(
HANDLE hObject,
DWORD dwMask,
DWORD dwFlags);

Функция принимает три параметра. Первый (bObject) идентифицирует допустимый описатель. Второй (dwMask) сообщает функции, какой флаг (или флаги) необходимо изменить. На сегодняшний день с каждым описателем связано два флага:

#define HANDLE FLAG_INHERIT 0x00000001
#define HANDLE FLAG_PROTECT_FROM_CLOSE 0x00000002

Чтобы изменить сразу все флаги объекта, нужно объединить их побитовой операцией OR. И, наконец, третий параметр функции SetHandleInformation — dwFlags — указывает, в какое именно состояние следует перевести флаги. Например, чтобы установить флаг наследования для описателя объекта ядра

SetHandleInformation(hobj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);

а чтобы сбросить этот флаг:

SetHandleInformation(hobj, HANDLE_FLAG_INHERIT, 0);

Флаг HANDLE_FLAGPROTECT_FROM_CLOSE сообщает системе, что данный описатель закрывать нельзя:

SetHandleInformation(hobj, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
CloseHandle(hobj); // генерируется исключение

Если какой-нибудь поток попытается закрыть защищенный описатель, CloseHandle приведет к исключению. Необходимость в такой защите возникает очень редко. Однако этот флаг весьма полезен, когда процесс порождает дочерний, а тот в свою очередь — еще один процесс. При этом родительский процесс может ожидать, что его «внук» унаследует определенный описатель объекта, переданный дочернему. Но тут вполне возможно, что дочерний процесс, прежде чем породить новый процесс, закрывает нужный описатель. Тогда родительский процесс теряет связь с «внуком», поскольку тот не унаследовал требуемый объект ядра. Защитив описатель от закрытия, можно исправить ситуацию, и «внук» унаследует предназначенный ему объект.

У этого подхода, впрочем, есть один недостаток. Дочерний процесс, вызвав:

SetHandleInformation(hobj, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0);
CloseHandle(hobj);

может сбросить флаг HANDLE_FLAG_PROTECT_FROM_CLOSE и закрыть затем соответствующий описатель. Родительский процесс ставит на то, что дочерний не исполнит этот код. Но одновременно он ставит и на то, что дочерний процесс породит ему «внука», поэтому в целом ставки не слишком рискованны.

Для полноты картины стоит, пожалуй, упомянуть и функцию GetHandleInformation:

BOOL GetHandleInformation(
HANDLE hObj,
PDWORD pdwFlags);

Эта функция возвращает текущие флаги для заданного описателя в переменной типа DWORD, на которую укапывает pdwFlags. Чтобы проверить, является ли описатель наследуемым, сделайте так:

Именованные объекты

Второй способ, позволяющий нескольким процессам совместно использовать одни и те же объекты ядра, связан с именованием этих объектов. Именование допускают многие (но не все) объекты ядра. Например, следующие функции создают именованные объекты ядра:

HANDLE CreateMutex(
PSECURITY_ATTRIBUTES psa,
BOOL bInitialOwner,
PCTSTR pszName);

HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
BOOL bInitialState,
PCTSTR pszName);

HANDLE CreateSemaphore(
PSECURITY_ATTRIBUTES psa,
LONG lInitialCount,
LONG lMaximumCount,
PCTSTR pszName);

HANDLE CreateWaitableTimer(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
PCTSTR pszName);

Последний параметр, pszName, у всех этих функций одинаков. При передачи в нем NULL, создается безымянный (анонимный) объект ядра. В этом случае объект может разделяться между процессами либо через наследование (см. предыдущий раздел), либо с помощью DuplicateHandle (см. следующий раздел). А чтобы разделять объект по имени, необходимо присвоить ему какое-нибудь имя. Тогда вместо NULL в параметре pszName нужно передать адрес строки с именем, завершаемой нулевым символом. Имя может быть длиной до MAX_PATH знаков (это значение определено как 260). К сожалению, Microsoft ничего не сообщает о правилах именования объектов ядра. Например, создавая объект с именем JeffObj, никто не застрахован от того, что в системе еще нет объекта ядра с таким именем. И что хуже, все эти объекты делят единое пространство имен. Из-за этого следующий вызов CreateSemaphore будет всегда возвращать NULL:

HANDLE hMutex = CreateMutex(NULL. FALSE, «JeffObj»);
HANDLE hSem = CreateSemaphore(NULL, 1, 1, «JeffObj»);
DWORD dwErrorCode = GetLastError();

После выполнения этого фрагмента значение dwErrorCode будет равно 6 (ERROR_INVALID_HANDLE). Полученный код ошибки не слишком вразумителен, но другого не дано.

Рассмотрим, как разделять объекты между процессами по именам. Допустим, после запуска процесса А вызывается функция:

HANDLE hMutexProcessA = CreateMutex(NULL, FALSE, «JeffMutex»);

Этот вызов заставляет систему создать новый объект ядра «мъютекс» и присвоить ему имя JeffMutex. Заметьте, что описатель hMutexProcessA в процессе А не является наследуемым, — он и не должен быть таковым при простом именовании объектов.

Спустя какое-то время некий процесс порождает процесс В. Необязательно, чтобы последний был дочерним от процесса А; он может быть порожден Explorer или любым другим приложением. (В этом, кстати, и состоит преимущество механизма именования объектов перед наследованием.) Когда процесс В приступает к работе,
исполняется код:

HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, «JeffMutex»);

При этом вызове система сначала проверяет, не существует ли уже объект ядра с таким именем. Если да, то ядро проверяет тип этого объекта. Поскольку мы пытаемся создать мьютекс и его имя тоже JeffMutex, система проверяет права доступа вызывающего процесса к этому объекту. Если у него есть все права, в таблице описателей, принадлежащей процессу В, создается новая запись, указывающая на существующий объект ядра. Если же вызывающий процесс не имеет полных прав на доступ к объекту или если типы двух объектов с одинаковыми именами не совпадают, вызов CreateMutex заканчивается неудачно и возвращается NULL.

Однако, хотя процесс В успешно вызвал CreateMutex, новый объект-мьютекс он не создал. Вместо этого он получил свой описатель существующего объекта-мьютекса. Счетчик объекта, конечно же, увеличился на 1, и теперь этот объект не разрушится, пока его описатели не закроют оба процесса — А и В. Заметьте, что значения описателей объекта в обоих процессах скорее всего разные, но так и должно быть, каждый процесс будет оперировать с данным объектом ядра, используя свой описатель.

Вызывая CreateMutex, процесс В передает ей атрибуты защиты и второй параметр. Так вот, эти параметры игнорируются, если объект с указанным именем уже существует! Приложение может определить, что оно делает: создает новый объект ядра или просто открывает уже существующий, — вызвав GetLastError сразу же после вызова одной из Create-функций:

HANDLE hMutex = CreateMutex(&sa, FALSE, «JeffObj»);
if (GetLastError() == ERROR_ALREADY_EXISTS) <
// открыт описатель существующего объекта sa.lpSecurityDescriptor и второй параметр (FALSE) игнорируются
> else <
// создан совершенно новый объект sa.lpSecurityDescriptor и второй параметр (FALSE) используются при создании объекта
>

Есть и другой способ разделения объектов по именам. Вместо вызова Create-функции процесс может обратиться к одной из следующих Open-функций:

HANDLE OpenMutex(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenEvent(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

HANDLE OpenSemaphore(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName),

HANDLE OpenWaitableTimer(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
PCTSTR pszName);

Заметьте: все эти функции имеют один прототип. Последний параметр, pszName, определяет имя объекта ядра. В нем нельзя передать NULL — только адрес строки с нулевым символом в конце. Эти функции просматривают единое пространство имен объектов ядра, пытаясь найти совпадение. Если объекта ядра с указанным именем нет, функции возвращают NULL, a GetLastError — код 2 (ERROR_FILE_NOT_FOUND). Но если объект ядра с заданным именем существует и если его тип идентичен тому, что указан, система проверяет, разрешен ли к данному объекту доступ запрошенного вида (через параметр dwDesiredAccess). Если такой вид доступа разрешен, таблица описателей в вызывающем процессе обновляется, и счетчик числа пользователей объекта возрастает на единицу. Если присвоить параметру bInheritHandle значение TRUE, то будет возвращен наследуемый описатель.

Главное отличие между вызовом Create- и Open-функций в том, что при отсутствии указанного объекта Create-функция создает его, а Open-функция просто уведомляет об ошибке.

Microsoft ничего не сообщает о правилах именования объектов ядра. Но представьте себе, что пользователь запускает две программы от разных компаний и каждая программа пытается создать объект с именем «MyObject». Ничего хорошего из этого не выйдет. Чтобы избежать такой ситуации, можно создавать GUID и использовать его строковое представление как имя объекта.

Именованные объекты часто применяются для того, чтобы не допустить запуска нескольких экземпляров одного приложения. Для этого просто вызывается одна из Create-функций в своей функции main или WinMain и создаете некий именованный объект. Какой именно — не имеет ни малейшего значения. Сразу после Create-функции необходимо вызвать GetLastError. Если она вернет ERROR_ALREADY_EXISTS, значит, один экземпляр приложения уже выполняется и новый его экземпляр можно закрыть. Вот фрагмент кода, иллюстрирующий этот прием:

int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE, PSTR pszCmdLine, int nCmdShow> <
HANDLE h = CreateMutex(NULL, FALSE, ««);
lf (GetLastError() == ERROR_ALREADY_EXISTS) <
// экземпляр этого приложения уже выполняется
return(0),
>

// запущен первый экземпляр данного приложения
// перед выходом закрываем объект
CloseHandle(h),
return(0);

Источник

Межпроцессный обмен

Организация каналов в ОC Windows

Анонимные каналы

Анонимные каналы создаются процессом сервером при помощи функции CreatePipe :

Функция CreatePipe возвращает два описателя (дескриптора) для чтения и записи в канал. После создания канала необходимо передать клиентскому процессу эти дескрипторы (или один из них), что обычно делается с помощью механизма наследования.

Другим способом является создание наследуемого дубликата имеющегося описателя при помощи функции DuplicateHandle и последующая передача его создаваемому процессу через командную строку или каким-либо иным образом.

Прогон программы общения процесса через анонимный канал с самим собой

В приведенной программе создается анонимный канал, в него записывается строка цифр, затем часть этой строки читается и выводится на экран.

Прогон программы общения через анонимный канал клиента и сервера

Именованные каналы

Использование именованных каналов

Сервер создает именованный канал при помощи функции CreateNamedPipe (см. MSDN).

Помимо имени канала в форме, описанной выше, в число параметров функции входят: флаг, указывающий модель передачи данных; параметр, определяющий синхронный или асинхронный режим работы канала, а также указывающий, должен ли канал быть односторонним или двухсторонним. Кроме того, имеется необязательный дескриптор защиты, запрещающий несанкционированный доступ к именованному каналу, и параметр, определяющий максимальное число одновременных соединений по данному каналу.

Прогон программы общения двух процессов через именованный канал

В данном примере сервер создает канал, затем запускает процесс-клиент и ждет соединения. Далее он читает сообщение, посланное клиентом.

Заключение

Источник

Межпроцессный обмен

Организация каналов в ОC Windows

Анонимные каналы

Анонимные каналы создаются процессом сервером при помощи функции CreatePipe :

Функция CreatePipe возвращает два описателя (дескриптора) для чтения и записи в канал. После создания канала необходимо передать клиентскому процессу эти дескрипторы (или один из них), что обычно делается с помощью механизма наследования.

Другим способом является создание наследуемого дубликата имеющегося описателя при помощи функции DuplicateHandle и последующая передача его создаваемому процессу через командную строку или каким-либо иным образом.

Прогон программы общения процесса через анонимный канал с самим собой

В приведенной программе создается анонимный канал, в него записывается строка цифр, затем часть этой строки читается и выводится на экран.

Прогон программы общения через анонимный канал клиента и сервера

Именованные каналы

Использование именованных каналов

Сервер создает именованный канал при помощи функции CreateNamedPipe (см. MSDN).

Помимо имени канала в форме, описанной выше, в число параметров функции входят: флаг, указывающий модель передачи данных; параметр, определяющий синхронный или асинхронный режим работы канала, а также указывающий, должен ли канал быть односторонним или двухсторонним. Кроме того, имеется необязательный дескриптор защиты, запрещающий несанкционированный доступ к именованному каналу, и параметр, определяющий максимальное число одновременных соединений по данному каналу.

Прогон программы общения двух процессов через именованный канал

В данном примере сервер создает канал, затем запускает процесс-клиент и ждет соединения. Далее он читает сообщение, посланное клиентом.

Заключение

Источник

Межпроцессный обмен

Организация каналов в ОC Windows

Анонимные каналы

Анонимные каналы создаются процессом сервером при помощи функции CreatePipe :

Функция CreatePipe возвращает два описателя (дескриптора) для чтения и записи в канал. После создания канала необходимо передать клиентскому процессу эти дескрипторы (или один из них), что обычно делается с помощью механизма наследования.

Другим способом является создание наследуемого дубликата имеющегося описателя при помощи функции DuplicateHandle и последующая передача его создаваемому процессу через командную строку или каким-либо иным образом.

Прогон программы общения процесса через анонимный канал с самим собой

В приведенной программе создается анонимный канал, в него записывается строка цифр, затем часть этой строки читается и выводится на экран.

Прогон программы общения через анонимный канал клиента и сервера

Именованные каналы

Использование именованных каналов

Сервер создает именованный канал при помощи функции CreateNamedPipe (см. MSDN).

Помимо имени канала в форме, описанной выше, в число параметров функции входят: флаг, указывающий модель передачи данных; параметр, определяющий синхронный или асинхронный режим работы канала, а также указывающий, должен ли канал быть односторонним или двухсторонним. Кроме того, имеется необязательный дескриптор защиты, запрещающий несанкционированный доступ к именованному каналу, и параметр, определяющий максимальное число одновременных соединений по данному каналу.

Прогон программы общения двух процессов через именованный канал

В данном примере сервер создает канал, затем запускает процесс-клиент и ждет соединения. Далее он читает сообщение, посланное клиентом.

Заключение

Источник

Written on 04 Февраля 2007. Posted in Win32

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

Процессы

Процессом обычно называют экземпляр выполняемой программы.

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

— Структура данных, содержащая всю информацию о процессе, в том числе список открытых дескрипторов различных системных ресурсов, уникальный идентификатор процесса, различную статистическую информацию и т.д.;

— Адресное пространство — диапазон адресов виртуальной памяти, которым может пользоваться процесс;

— Исполняемая программа и данные, проецируемые на виртуальное адресное пространство процесса.

Потоки

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

— Уникальный идентификатор потока;

— Содержимое набора регистров процессора, отражающих состояние процессора;

— Два стека, один из которых используется потоком при выполнении в режиме ядра, а другой — в пользовательском режиме;

— Закрытую область памяти, называемую локальной памятью потока (thread local storage, TLS) и используемую подсистемами, run-time библиотеками и DLL.

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

Чтобы все потоки работали, операционная система отводит каждому из них определенное процессорное время. Тем самым создается иллюзия одновременного выполнения потоков (разумеется, для многопроцессорных компьютеров возможен истинный параллелизм). В Windows реализована система вытесняющего планирования на основе приоритетов, в которой всегда выполняется поток с наибольшим приоритетом, готовый к выполнению. Выбранный для выполнения поток работает в течение некоторого периода, называемого квантом. Квант определяет, сколько времени будет выполняться поток, пока операционная система не прервет его. По окончании кванта операционная система проверяет, готов ли к выполнению другой поток с таким же (или большим) уровнем приоритета. Если таких потоков не оказалось, текущему потоку выделяется еще один квант. Однако поток может не полностью использовать свой квант. Как только другой поток с более высоким приоритетом готов к выполнению, текущий поток вытесняется, даже если его квант еще не истек.

Квант не измеряется в каких бы то ни было единицах времени, а выражается целым числом. Для каждого потока хранится текущее значение его кванта. Когда потоку выделяется квант процессорного времени, это значит, что его квант устанавливается в начальное значение. Оно зависит от операционной системы. Например, для Win2000 Professional начальное значение кванта равно 6, а для Win2000 Server — 36.

Это значение можно изменить вызвав Control Panel — > System -> Advanced -> Performance options. Значение «Applications» — как для Win2000 Professional; «Background Services» — как для Win2000 Server.
Или напрямую в ключе реестра HKLM Win32PrioritySeparation.

Всякий раз, когда возникает прерывание от таймера, из кванта потока вычитается 3, и так до тех пор, пока он не достигнет нуля. Частота срабатывания таймера зависит от аппаратной платформы. Например, для большинства однопроцессорных x86 систем он составляет 10мс, а на большинстве многопроцессорных x86 систем — 15мс.

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

Планирование в Windows осуществляется на уровне потоков, а не процессов. Это кажется понятным, так как сами процессы не выполняются, а лишь предоставляют ресурсы и контекст для выполнения потоков. Поэтому при планировании потоков, система не обращает внимания на то, какому процессу они принадлежат. Например, если процесс А имеет 10 готовых к выполнению потоков, а процесс Б — два, и все 12 потоков имеют одинаковый приоритет, каждый из потоков получит 1/12 процессорного времени.

Приоритеты

В Windows существует 32 уровня приоритета, от 0 до 31. Они группируются так: 31 — 16 уровни реального времени; 15 — 1 динамические уровни; 0 — системный уровень, зарезервированный для потока обнуления страниц (zero-page thread).

При создании процесса, ему назначается один из шести классов приоритетов:

Real time class (значение 24),

High class (значение 13),

Above normal class (значение 10),

Normal class (значение 8),

Below normal class (значение 6),

и Idle class (значение 4).

В Windows NT/2000/XP можно посмотреть приоритет процесса в Task Manager.

Above normal и Below normal появились начиная с Win2000.

Приоритет каждого потока (базовый приоритет потока) складывается из приоритета его процесса и относительного приоритета самого потока. Есть семь относительных приоритетов потоков:

Normal: такой же как и у процесса;

Above normal: +1 к приоритету процесса;

Below normal: -1;

Highest: +2;

Lowest: -2;

Time critical: устанавливает базовый приоритет потока для Real time класса в 31, для остальных классов в 15.

Idle: устанавливает базовый приоритет потока для Real time класса в 16, для остальных классов в 1.

В следующей таблице показаны приоритеты процесса, относительный и базовый приоритеты потока.

Приоритет потока

Класс процесса

Класс процесса

   

Idle class

Below normal class

Normal class

Above normal class

High class

Real time class

1

 

Idle

Idle

Idle

Idle

Idle

 

2

 

Lowest

         

3

 

Below …

         

4

Idle class

Normal

Lowest

       

5

 

Above …

Below …

       

6

Below normal class

Highest

Normal

Lowest

     

7

   

Above …

Below …

     

8

Normal class

 

Highest

Normal

Lowest

   

9

     

Above …

Below …

   

10

Above normal class

   

Highest

Normal

   

11

       

Above …

Lowest

 

12

       

Highest

Below …

 

13

High class

       

Normal

 

14

         

Above …

 

15

         

Highest

 

15

 

Time critical

Time critical

Time critical

Time critical

Time critical

 

16

           

Idle

17

             

18

             

19

             

20

             

21

             

22

           

Lowest

23

           

Below …

24

Real time class

         

Normal

25

           

Above …

26

           

Highest

27

             

28

             

29

             

30

             

31

           

Time critical

Привязка к процессорам

Если операционная система выполняется на машине, где установлено более одного процессора, то по умолчанию, поток выполняется на любом доступном процессоре. Однако в некоторых случаях, набор процессоров, на которых поток может работать, может быть ограничен. Это явление называется привязкой к процессорам (processor affinity). Можно изменить привязку к процессорам программно, через Win32-функции планирования.

Память

Каждому процессу в Win32 доступно линейное 4-гигабайтное (2^32 = 4 294 967 296) виртуальное адресное пространство. Обычно верхняя половина этого пространства резервируется за операционной системой, а вторая половина доступна процессу.

Виртуальное адресное пространство процесса доступно всем потокам этого процесса. Иными словами, все потоки одного процесса выполняются в едином адресном пространстве.

С другой стороны, механизм виртуальной памяти позволяет изолировать процессы друг от друга. Потоки одного процесса не могут ссылаться на адресное пространство другого процесса.

Виртуальная память может вовсе не соответствовать структуре физической памяти. Диспетчер памяти транслирует виртуальные адреса на физические, по которым реально хранятся данные. Поскольку далеко не всякий компьютер в состоянии выделить по 4 Гбайт физической памяти на каждый процесс, используется механизм подкачки (swapping). Когда оперативной памяти не хватает, операционная система перемещает часть содержимого памяти на диск, в файл (swap file или page file), освобождая, таким образом, физическую память для других процессов. Когда поток обращается к странице виртуальной памяти, записанной на диск, диспетчер виртуальной памяти загружает эту информацию с диска обратно в память.

Создание процессов

Создание Win32 процесса осуществляется вызовом одной из таких функций, как CreateProcess, CreateProcessAsUser (для Win NT/2000) и CreateProcessWithLogonW (начиная с Win2000) и происходит в несколько этапов:

— Открывается файл образа (EXE), который будет выполняться в процессе. Если исполняемый файл не является Win32 приложением, то ищется образ поддержки (support image) для запуска этой программы. Например, если исполняется файл с расширением .bat, запускается cmd.exe и т.п.

В WinNT/2000 для отладки программ реализовано следующее. CreateProcess, найдя исполняемый Win32 файл, ищет в SOFTWAREMicrosoftWindows NTCurrentVersionImage File Execution Option раздел с именем и расширением запускаемого файла, затем ищет в нем параметр Debugger, и если строка не пуста, запускает то, что в ней написано вместо данной программы.

— Создается объект Win32 «процесс».

— Создается первичный поток (стек, контекст и объект «поток»).

— Подсистема Win32 уведомляется о создании нового процесса и потока.

— Начинается выполнение первичного потока.

— В контексте нового процесса и потока инициализируется адресное пространство (например, загружаются требуемые DLL) и начинается выполнение программы.

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

Процесс завершается если:

— Входная функция первичного потока возвратила управление.

— Один из потоков процесса вызвал функцию ExitProcess.

— Поток другого процесса вызвал функцию TerminateProcess.

Когда процесс завершается, все User- и GDI-объекты, созданные процессом, уничтожаются, объекты ядра закрываются (если их не использует другой процесс), адресное пространство процесса уничтожается.

Пример 1. Программа создает процесс «Калькулятор».


#include <windows.h>
int main(int argc, char* argv[])
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory( &si, sizeof(si) );
si.cb = sizeof(si);
ZeroMemory( &pi, sizeof(pi) );
if( !CreateProcess( NULL, "c:/windows/calc.exe", NULL, NULL, FALSE,
0, NULL, NULL, &si, &pi))

return 0;
// Close process and thread handles.
CloseHandle( pi.hProcess );
CloseHandle( pi.hThread );
return 0;
}

Создание потоков

Первичный поток создается автоматически при создании процесса. Остальные потоки создаются функциями CreateThread и CreateRemoteThread (только в Win NT/2000/XP).

Завершение потоков

Поток завершается если

— Функция потока возвращает управление.

— Поток самоуничтожается, вызвав ExitThread.

— Другой поток данного или стороннего процесса вызывает TerminateThread.

— Завершается процесс, содержащий данный поток.

Если вы используете в своем приложении LibCMt C run-time библиотеку, Microsoft настоятельно рекомендует вместо Win32 API функций использовать их аналоги из C run-time: _beginthread, _beginthreadex, _endthread и _endthreadex.

Объекты ядра

Эти объекты используются системой и пользовательскими приложениями для управления множеством самых разных ресурсов: процессами, потоками, файлами и т.д. Windows позволяет создавать и оперировать с несколькими типами таких объектов, в том числе:

Kernel object

Объект ядра

Kernel object

Объект ядра

Access token

Маркер доступа

Module

Подгружаемый модуль (DLL)

Change notification

Уведомление об изменениях на диске

Mutex

Мьютекс

I/O completion ports

Порт завершения ввода-вывода

Pipe

Канал

Event

Событие

Process

Процесс

File

Файл

Semaphore

Семафор

File mapping

Проекция файла

Socket

Сокет

Heap

Куча

Thread

Поток

Job

Задание

Timer

Ожидаемый таймер

Mailslot

Почтовый слот

   

Объект ядра это, по сути, структура, созданная ядром и доступная только ему. В пользовательское приложение передается только описатель (handle) объекта, а управлять объектом ядра можно с помощью функций Win32 API.

Wait функции

Как можно приостановить работу потока? Существует много способов. Вот некоторые из них.

Функция Sleep() приостанавливает работу потока на заданное число миллисекунд. Если в качестве аргумента вы укажите 0 ms, то произойдет следующее. Поток откажется от своего кванта процессорного времени, однако тут же появится в списке потоков готовых к выполнению. Иными словами произойдет намеренное переключение потоков. (Вернее сказать, попытка переключения. Ведь следующим для выполнения потоком вполне может стать тот же самый.)

Функция WaitForSingleObject() приостанавливает выполнение потока до тех пор, пока не произойдет одно из двух событий:

— истечет таймаут ожидания;

— ожидаемый объект перейдет в сигнальное (signaled) состояние.

По возвращаемому значению можно понять, какое из двух событий произошло. Ожидать с помощью wait-функций можно большинство объектов ядра, например, объект «процесс» или «поток», чтобы определить, когда они завершат свою работу.

Функции WaitForMultipleObjects передается сразу массив объектов. Можно ожидать срабатывания сразу всех объектов или какого-то одного из них.

Пример 2. Программа создает два одинаковых потока и ожидает их завершения.
Потоки просто выводят текстовое сообщение, которое передано им при инициализации.


#include <windows.h>
#include <process.h>

unsigned __stdcall ThreadFunc( void * arg) // Функция потока
{
char ** str = (char**)arg;
MessageBox(0,str[0],str[1],0);
_endthreadex( 0 );
return 0;
};
int main(int argc, char* argv[])
{
char * InitStr1[2] = {"First thread running!","11111"};// строка для первого потока
char * InitStr2[2] = {"Second thread running!","22222"};// строка для второго потока
unsigned uThreadIDs[2];

HANDLE hThreads[2];
hThreads[0] = (HANDLE)_beginthreadex( NULL, 0, &ThreadFunc, InitStr1, 0,&uThreadIDs[0]);
hThreads[1] = (HANDLE)_beginthreadex( NULL, 0, &ThreadFunc, InitStr2, 0,&uThreadIDs[1]);

// Ждем, пока потоки не завершат свою работу
WaitForMultipleObjects(2, hThreads, TRUE, INFINITE ); // Set no time-out
// Закрываем дескрипторы
CloseHandle( hThreads[0] );
CloseHandle( hThreads[1] );
return 0;
}

Синхронизация потоков

Работая параллельно, потоки совместно используют адресное пространство процесса. Также все они имеют доступ к описателям (handles) открытых в процессе объектов. А что делать, если несколько потоков одновременно обращаются к одному ресурсу или необходимо как-то упорядочить работу потоков? Для этого используют объекты синхронизации и соответствующие механизмы.

Мьютексы

Мьютексы (Mutex) это объекты ядра, которые создаются функцией CreateMutex(). Мьютекс бывает в двух состояниях — занятом и свободном. Мьютексом хорошо защищать единичный ресурс от одновременного обращения к нему разными потоками.

Пример 3. Допустим, в программе используется ресурс, например, файл или буфер в памяти. Функция WriteToBuffer() вызывается из разных потоков. Чтобы избежать коллизий при одновременном обращении к буферу из разных потоков, используем мьютекс. Прежде чем обратиться к буферу, ожидаем <освобождения> мютекса.


HANDLE hMutex;

int main()
{
hMutex = CreateMutex( NULL, FALSE, NULL); // Создаем мьютекс в свободном состоянии
...
// Создание потоков, и т.д.
...
}
BOOL WriteToBuffer()
{
DWORD dwWaitResult;
// Ждем освобождения мьютекса перед тем как обратиться к буферу.
dwWaitResult = WaitForSingleObject( hMutex, 5000L); // 5 секунд на таймаут
 
if (dwWaitResult == WAIT_TIMEOUT) // Таймаут. Мьютекс за это время не освободился.
{
return FALSE;
}
else // Мьютекс освободился, и наш поток его занял. Можно работать.
{
Write_to_the_buffer().
...
ReleaseMutex(hMutex); // Освобождаем мьютекс.
}
return TRUE;
}

Семафоры

Семафор (Semaphore) создается функцией CreateSemaphore(). Он очень похож на мьютекс, только в отличие от него у семафора есть счетчик. Семафор открыт если счетчик больше 0 и закрыт, если счетчик равен 0. Семафором обычно «огораживают» наборы равнозначных ресурсов (элементов), например очередь, список и т.п.

Пример 4. Классический пример использования семафора это очередь элементов, которую обрабатывают несколько потоков. Потоки «разбирают» элементы из очереди. Если очередь пуста, потоки должны «спать», ожидая появления новых элементов. Для учета элементов в очереди используется семафор.


class CMyQueue
{
HANDLE m_hSemaphore; // Семафор для учета элементов очереди
// Описание других объектов для хранения элементов очереди

public:
CMyQueue()
{
m_hSemaphore = CreateSemaphore(NULL, 0, 1000, NULL); //начальное значение счетчика = 0

//максимальное значение = 1000


// Инициализация других объектов
}
~CMyQueue()
{
CloseHandle( m_hSemaphore);
// Удаление других объектов
}
void AddItem(void * NewItem)
{
// Добавляем элемент в очередь
// Увеличиваем счетчик семафора на 1.
ReleaseSemaphore(m_hSemaphore,1, NULL);
}
void GetItem(void * Item)
{
// Если очередь пуста, то потоки, вызвавшие этот метод,
// будут находиться в ожидании...

WaitForSingleObject(m_hSemaphore,INFINITE);
 
// Удаляем элемент из очереди
}
};

Замечание. В этом примере мы считаем, что сами процедуры добавления элемента в очередь и удаления из очереди безопасны с точки зрения многопоточности. Не будем пока касаться деталей их реализации. Подробнее мы рассмотрим это в примере 9.

События

События (Event), также как и мьютексы имеют два состояния — установленное и сброшенное. События бывают со сбросом вручную и с автосбросом. Когда поток дождался (wait-функция вернула управление) события с автосбросом, такое событие автоматически сбрасывается. В противном случае событие нужно сбрасывать вручную, вызвав функцию ResetEvent(). Допустим, сразу несколько потоков ожидают одного и того же события, и событие сработало. Если это было событие с автосбросом, то оно позволит работать только одному потоку (ведь сразу же после возврата из его wait-функции событие сбросится автоматически!), а остальные потоки останутся ждать. Если же это было событие со сбросом вручную, то все потоки получат управление, а событие так и останется в установленном состоянии, пока какой-нибудь поток не вызовет ResetEvent().

Пример 5. Вот еще один пример многопоточного приложения. Программа имеет два потока; один готовит данные, а второй отсылает их на сервер. Разумно распараллелить их работу. Здесь потоки должны работать по очереди. Сначала первый поток готовит порцию данных. Потом второй поток отправляет ее, а первый тем временем готовит следующую порцию и т.д. Для такой синхронизации понадобится два event’а с автосбросом.


unsigned __stdcall CaptureThreadFunc( void * arg) // Поток, готовящий данные
{
while (bSomeCondition)
{
WaitForSingleObject(m_hEventForCaptureTh,INFINITE); // Ждем своего события
... // Готовим данные
SetEvent(hEventForTransmitTh); // Разрешаем работать второму потоку
}
_endthreadex( 0 );
return 0;
};

unsigned __stdcall TransmitThreadFunc( void * arg) // Поток, отсылающий данные.
{
while (bSomeCondition)
{
WaitForSingleObject(m_hEventForTransmitTh,INFINITE); // Ждем своего события
... // Данные готовы, формируем из них пакет для отправки
SetEvent(hEventForCaptureTh); // Разрешаем работать первому потоку, а сами...
... // отправляем пакет
}
_endthreadex( 0 );
return 0;
};

int main(int argc, char* argv[]) // Основной поток
{
// Создаем два события с автосбросом, со сброшенным начальным состоянием
hEventForCaptureTh = CreateEvent(NULL,FALSE,FALSE,NULL);
hEventForTransmitTh = CreateEvent(NULL,FALSE,FALSE,NULL);

// Создаем потоки
hCaptureTh = (HANDLE)_beginthreadex( NULL, 0, &CaptureThreadFunc, 0, 0,&uTh1);
hTransmitTh = (HANDLE)_beginthreadex( NULL, 0, &TransmitThreadFunc, 0, 0,&uTh2);
// Запускаем первый поток
SetEvent(hEventForCaptureTh);

....
}

Пример 6. Другой пример. Программа непрерывно в цикле производит какие-то вычисления. Нужно иметь возможность приостановить на время ее работу. Допустим, это просмотрщик видео файлов, который в цикле, кадр за кадром отображает информацию на экран. Не будем вдаваться в подробности видео функций. Реализуем функции Pause и Play для программы. Используем событие со сбросом вручную.


// Главная функция потока, которая в цикле отображает кадры
unsigned __stdcall VideoThreadFunc( void * arg)
{
while (bSomeCondition)
{
WaitForSingleObject(m_hPauseEvent,INFINITE); // Если событие сброшено, ждем
... // Отображаем очередной кадр на экран
}
_endthreadex( 0 );
return 0;
};

void Play()
{
SetEvent(m_hPauseEvent);
};

void Pause()
{
ResetEvent(m_hPauseEvent);
};

Функция PulseEvent() устанавливает событие и тут же переводит его обратно в сброшенное состояние; ее вызов равнозначен последовательному вызову SetEvent() и ResetEvent(). Если PulseEvent вызывается для события со сбросом в ручную, то все потоки, ожидающие этот объект, получают управление. При вызове PulseEvent для события с автосбросом пробуждается только один из ждущих потоков. А если ни один из потоков не ждет объект-событие, вызов функции не дает никакого эффекта.

Пример 7. Реализуем функцию NextFrame() для предыдущего примера для промотки файла вручную по кадрам.


void NextFrame()
{
PulseEvent(m_hPauseEvent);
};

Ожидаемые таймеры

Пожалуй, ожидаемые таймеры — самый изощренный объект ядра для синхронизации. Появились они, начиная с Windows 98. Таймеры создаются функцией CreateWaitableTimer и бывают, также как и события, с автосбросом и без него. Затем таймер надо настроить функцией SetWaitableTimer. Таймер переходит в сигнальное состояние, когда истекает его таймаут. Отменить «тиканье» таймера можно функцией CancelWaitableTimer. Примечательно, что можно указать callback функцию при установке таймера. Она будет выполняться, когда срабатывает таймер.

Пример 8. Напишем программу-будильник используя WaitableTimer’ы. Будильник будет срабатыват раз в день в 8 утра и «пикать» 10 раз. Используем для этого два таймера, один из которых с callback-функцией.


#include <process.h>
#include <windows.h>
#include <stdio.h>
#include <conio.h>

#define HOUR (8) // время, когда срабатывает будильник (только часы)
#define RINGS (10) // сколько раз пикать

HANDLE hTerminateEvent ;

// callback функция таймера
VOID CALLBACK TimerAPCProc(LPVOID, DWORD, DWORD)
{
Beep(1000,500); // звоним!
};
 
// функция потока
unsigned __stdcall ThreadFunc(void *)
{
HANDLE hDayTimer = CreateWaitableTimer(NULL,FALSE,NULL);
HANDLE hAlarmTimer = CreateWaitableTimer(NULL,FALSE,NULL);
HANDLE h[2]; // мы будем ждать эти объекты
h[0] = hTerminateEvent; h[1] = hDayTimer;
int iRingCount=0; // число "звонков"
int iFlag;
DWORD dw;

// немного помучаемся со временем,
//т.к. таймер принимает его только в формате FILETIME

LARGE_INTEGER liDueTime, liAllDay;
liDueTime.QuadPart=0;
// сутки в 100-наносекундных интервалах = 10000000 * 60 * 60 * 24 = 0xC92A69C000
liAllDay.QuadPart = 0xC9;
liAllDay.QuadPart=liAllDay.QuadPart << 32;
liAllDay.QuadPart |= 0x2A69C000;
SYSTEMTIME st;
GetLocalTime(&st); // узнаем текущую дату/время
iFlag = st.wHour > HOUR; // если назначенный час еще не наступил,
//то ставим будильник на сегодня, иначе - на завтра

st.wHour = HOUR;
st.wMinute = 0;
st.wSecond =0;
FILETIME ft;
SystemTimeToFileTime( &st, &ft);
if (iFlag)
((LARGE_INTEGER *)&ft)->QuadPart =
((LARGE_INTEGER *)&ft)->QuadPart +liAllDay.QuadPart ;

LocalFileTimeToFileTime(&ft,&ft);
// Устанавливаем таймер,


// он будет срабатывать раз в сутки (24*60*60*1000ms),
// начиная со следующего "часа пик" - HOUR

SetWaitableTimer(hDayTimer, (LARGE_INTEGER *) &ft, 24*60*60000, 0, 0, 0);
do {
dw = WaitForMultipleObjectsEx(2,h,FALSE,INFINITE,TRUE);
if (dw == WAIT_OBJECT_0 +1) // сработал hDayTimer
{
// Устанавливаем таймер, он будет вызывать callback ф-ию раз в секунду,
// начнет с текущего момента

SetWaitableTimer(hAlarmTimer, &liDueTime, 1000, TimerAPCProc, NULL, 0);
iRingCount=0;
}
if (dw == WAIT_IO_COMPLETION) // закончила работать callback ф-ия
{
iRingCount++;
if (iRingCount==RINGS)
CancelWaitableTimer(hAlarmTimer);
}
}while (dw!= WAIT_OBJECT_0); // пока не сработало hTerminateEvent крутимся в цикле

// закрывае handles, выходим


CancelWaitableTimer(hDayTimer);
CancelWaitableTimer(hAlarmTimer);
CloseHandle(hDayTimer);
CloseHandle(hAlarmTimer);
_endthreadex( 0 );
return 0;
};

int main(int argc, char* argv[])
{
// это событие показывае потоку когда надо завершаться
hTerminateEvent = CreateEvent(NULL,FALSE,FALSE,NULL);
unsigned uThreadID;
HANDLE hThread;
// создаем поток
hThread = (HANDLE)_beginthreadex( NULL, 0, &ThreadFunc, 0, 0,&uThreadID);
puts("Press any key to exit.");
// ждем any key от пользователя для завершения программы
getch();
// выставляем событие
SetEvent(hTerminateEvent);
// ждем завершения потока
WaitForSingleObject(hThread, INFINITE );
// закрываем handle
CloseHandle( hThread );
return 0;
}

Критические секции. Синхронизация в пользовательском режиме

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

Пример 9. Снова рассмотрим очередь элементов. Один из вариантов ее реализации — двусвязный список. С точки зрения многопоточности, опасными являются операции добавления и удаления элементов из очереди. Существует вероятность, что несколько потоков одновременно начнут перестраивать указатели и связность очереди нарушится. Чтобы этого избежать, используем критическую секцию.


typedef ... ItemData;

// Элемент очереди: данные и два указателя на предыдущий и следующий элементы
typedef struct _ItemStruct
{
ItemData data;
struct _ItemStruct * prev,*next;
} Item;

// Описание класса "Очередь"
class CMyQueue
{
CRITICAL_SECTION m_crisec; // Критическая секция
Item * m_Begin; // Указатель на первый элемент
Item * m_End; // Указатель на последний элемент
int m_Count; // Количество элементов


public:
CMyQueue()
{
// Инициализируем критическую секцию
InitializeCriticalSection(&m_crisec);
// Инициализируем переменные
m_Count = 0;
m_Begin = m_End = NULL;
}

~CMyQueue()
{
// Удаляем все элементы очереди
while(m_Count) GetItem();
// Удаляем критическую секцию
DeleteCriticalSection(&m_crisec);
}

void AddItem(ItemData data)
{
Item * NewItem;
Item * OldFirstItem;
NewItem = new Item(); // New item
NewItem->next = NULL;
NewItem->prev = NULL;
NewItem->data = data;
// ------------------------ Этот кусок не может выполняться параллельно
EnterCriticalSection(&m_crisec); // Заходим в к.с. (ждем входа)
OldFirstItem = m_Begin;
m_Begin = NewItem;
NewItem->next = OldFirstItem;
if (OldFirstItem)
OldFirstItem->prev = NewItem;
else
m_End = NewItem;
m_Count++;
LeaveCriticalSection(&m_crisec); // Выходим из к.с.
// ------------------------ Этот кусок не может выполняться параллельно
}

ItemData GetItem()
{
ItemData data;
// ------------------------ Этот кусок не может выполняться параллельно
EnterCriticalSection(&m_crisec); // Заходим в к.с. (ждем входа)
if (!m_End)
data = NULL;
else
{
data = m_End->data ;
if (m_End->prev )
{
m_End->prev ->next = NULL;
m_End = m_End->prev ;
}
else
{
m_End = NULL;
m_Begin = NULL;
}
m_Count --;
}
LeaveCriticalSection(&m_crisec); // Выходим из к.с.
// ------------------------ Этот кусок не может выполняться параллельно
return data;
};
};

Синхронизация процессов

Описатели объектов ядра зависимы от конкретного процесса (process specific). Проще говоря, handle объекта, полученный в одном процессе, не имеет смысла в другом. Однако существуют способы работы с одними и теми же объектами ядра из разных процессов.

Во-первых, это наследование описателя. При создании объекта можно указать будет ли его описатель наследоваться дочерними (порожденными этим процессом) процессами.

Во-вторых, дублирование описателя. Функция DuplicateHandle дублирует описатель объекта одного процесса в другой, т.е. по сути, берет запись в таблице описателей одного процесса и создает ее копию в таблице другого.

И, наконец, именование объекта ядра. При создании объекта ядра для синхронизации (мьютекса, семафора, ожидаемого таймера или события) можно задать его имя. Оно должно быть уникальным в системе. Тогда другой процесс может открыть этот объект ядра, указав в функции Open…(OpenMutex, OpenSemaphore, OpenWaitableTimer, OpenEvent) это имя.

На самом деле, при вызове функции Create… () система сначала проверяет, не существует ли уже объект ядра с таким именем. Если нет, то создается новый объект. Если да, ядро проверяет тип этого объекта и права доступа. Если типы не совпадают или же вызывающий процесс не имеет полных прав на доступ к объекту, вызов Create… функции заканчивается неудачно и возвращается NULL. Если все нормально, то просто создается новый описатель (handle) существующего уже объекта ядра. По коду возврата функции GetLastError() можно понять что произошло: создался новый объект или Create() вернула уже существующий.

Поэтому, синхронизировать потоки внутри разных процессов можно точно также как и в пределах одного. Нужно только правильно передать описатель синхронизирующего объекта от одного процесса к другому любым из перечисленных выше способов.

Пример 10. Многие приложения при запуске проверяют, запущен ли еще один экземпляр этой программы. Стандартный способ реализации этой проверки — использование поименованного Мьютекса.


int main(int argc, char* argv[])
{
HANDLE Mutex;
Mutex = CreateMutex(NULL,FALSE,"MyMutex");
if (ERROR_ALREADY_EXISTS == GetLastError()) // Такой мьютекс уже кем-то создан...
{
MessageBox(0,"Приложение уже запущено","Error",0);
CloseHandle( Mutex);
exit(0);
}
...
}

Взаимодействие между процессами

Потоки одного процесса не имеют доступа к адресному пространству другого процесса. Однако существуют механизмы для передачи данных между процессами.

Разделяемая память

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

Такие объекты называются проекциями файлов (на оперативную память) (file-mapping object). Для создания проекции файла сначала вызывается функция CreateFileMapping(). Ей передается дескриптор (уже открытого) файла или указывается, что нужно использовать page file операционной системы. Кроме этого, в параметрах ей передается флаг защиты, максимальный размер проекции и имя объекта. Затем вызывается функция MapViewOfFile(). Она отображает представление файла (view of a file) в адресное пространство процесса. По окончании работы вызывается функция UnmapViewOfFile(). Она освобождает память и записывает данные в файл (если это не файл подкачки). Чтобы записать данные на диск немедленно, используется функция FlushViewOfFile(). Проекция файла, как и другие объекты ядра, может использоваться другими процессами через наследование, дублирование дескриптора или по имени.

Пример 11. Вот пример программы, которая создает проекцию в page file и записывает в нее данные.


#include <windows.h>

void main()
{
HANDLE hMapping;
char* lpData;
char* lpBuffer;
...
//Создание или открытие существующей проекции файла
hMapping = CreateFileMapping( (HANDLE)(-1), // используем page file
NULL, PAGE_READWRITE, 0, 0x0100, "MyShare");
if (hMapping == NULL) exit(0);
// Размещаем проекцию hMapping в адресном пространстве нашего процесса;
// lpData получает адрес размещения
lpData = (char*) MapViewOfFile(hMapping, FILE_MAP_ALL_ACCESS,0,0,0);
if (lpData == NULL) exit(0);
// Копируем в проекцию данные
memcpy ( lpData , lpBuffer );
...
// Заканчиваем работу. Освобождаем память.
UnmapViewOfFile(lpData);
// Закрываем объект ядра
CloseHandle(hMapping);
};

Прочие механизмы (сокеты, pipe)

Кроме разделяемой памяти, в Windows есть и другие способы передачи информации между процессами, например, каналы, поименованные каналы и сокеты. Все они имеют сходный принцип и представляют собой своеобразный канал или соединение, «трубу», соединяющую процессы. Программа, имея один конец такого соединения, может читать и/или писать в него данные, обмениваясь таким образом информацией с программой на другом конце.

Каналы используются для пересылки данных в одном направлении между дочерним и родительским процессами или между двумя дочерними процессами. Операции чтения/записи в канал похожи на подобные операции при работе с файлами.

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

Сокет — это абстрактный объект для обозначения одного из концов сетевого соединения, в том числе и через Internet. Сокеты Windows бывают двух типов: сокеты дейтаграмм и сокеты потоков. Интерфейс Windows Sockets (WinSock) основан на BSD-версии сокетов, но в нем имеются также расширения, специфические для Windows.

Сообщения в Windows (оконные сообщения)

Говоря о Windows нельзя не упомянуть о таких понятиях как windows (окна), messages (сообщения), message queue (очередь сообщений) и т.д.

Window — это (прямоугольная) область экрана в которой приложение отображает информацию (если оно видимо, конечно) и получает информацию от пользователя. Окна принадлежат потокам. Поток, создавший окно считается владельцем этого окна. Поток может быть владельцем нескольких окон.

Окна управляются сообщениями. Все события, происходящие с окном, сопровождаются посылкой ему сообщений: создание и уничтожение окна, ввод с клавиатуры, перемещение мыши, перерисовка и перемещение окна и т.д. Сообщения окну могут посылаться как самой системой, так и пользовательскими приложениями. Каждому окну приписана функция, называемая оконной процедурой (window procedure), которая и вызывается при обработке сообщения.

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

Если описатели объектов ядра процессо-зависимы, то описатели окон уникальны в пределах Deskop. Поэтому одному процессу не составляет никакого труда получить и использовать описатель окна принадлежащему потоку другого процесса.

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

Пример 12. Программа находит окно с заголовком “Калькулятор” и закрывает его, посылая сообщение WM_CLOSE.


#include <windows.h>

int main(int argc, char* argv[])
{
HWND hwnd = FindWindow( NULL , "Калькулятор");
if (NULL != hwnd) PostMessage(hwnd, WM_CLOSE, 0, 0 );
return 0;
}

Тема оконных сообщений в Windows весьма обширна и требует для рассмотрения, по крайней мере, отдельной статьи.

Заключение

В этой статье коротко были рассмотрены основные моменты многозадачной работы в Windows: создание и завершение процессов, синхронизация и межпроцессное общение.

Статья ни в коем случае не претендует на полноту и предназначена дать лишь общие сведения или указать направление для поиска нужной информации в огромной документации. Чтобы узнать более подробно о приведенных в статье объектах и функциях, читайте первоисточники:

— Microsoft Platform SDK

— Jeffrey Richter. Programming Applications for Microsoft® Windows. ISBN 1-57231-996-8

— Соломон, Руссинович. Внутреннее устройство MS Windows 2000. ISBN 5-7502-0136-8

Понравилась статья? Поделить с друзьями:
  • Насколько хорош встроенный антивирус в windows 10
  • Настроить биос gigabyte для загрузки с флешки windows 10
  • Насколько можно увеличить файл подкачки в windows 10
  • Настроить беспроводное сетевое соединение windows xp
  • Насколько windows 10 требовательнее windows 7