Синхронизация

Дата публикации 27 июн 2002

Синхронизация — Архив WASM.RU

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

  • критические секции
  • объекты ядра, такие как
    • события
    • мутексы
    • семафоры
    • ожидающие таймеры

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

Немного теории

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

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

Обратите внимание: Я использовал слова: 'OS помещает этот тред в спящее состояние', это значит, что тред не будет тратить системное время, пока он ожидает ресурс. Это обратно так называемому 'busy waiting', когда тред ждет, но на это потребляется системное время. Смотрите код:

Код (Text):
  1.  
  2.         _CSflag         dd 012345678h
  3.           ...
  4.         @nextCheck:
  5.             mov eax,012345678h           ; загружаем ненулевое значение
  6.             xchg eax,dword ptr [_CSflag] ; меняем со значением флага
  7.             or eax,eax                   ; проверяем, сброшен  ли CS
  8.             jnz @nextCheck               ; нет, тратим еще время OS
  9.  
  10.         ; начало критической секции
  11.  
  12.             xor eax,eax                  ; оставляя флаг CS равным нулю
  13.             mov dword ptr [_CSflag],eax
  14.         ; конец критической секции
  15.             ...
  16.        Один тред ждет в цикле, пока друго (только один) в критической секции.
  17.        Взаимоисключение подтвержденно инструкцией xchg.

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

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

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

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

Критические секции

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

Код (Text):
  1.  
  2.   Перед использованием критической секции мы должны создать ее, используя:
  3.  
  4.   push указатель на структуру CRITICAL_SECTION
  5.   call InitializeCriticalSection
  6.  
  7.   Функция ничего не возваращает
  8.  
  9.   Если у вас мультипроцессорная система, вы можете использовать:
  10.  
  11.   push spin count
  12.   push указатель на структуру CRITICAL_SECTION
  13.   call InitializeCriticalSectionAndSpinCount
  14.  
  15.   Функция ничего не возвращает
  16.  
  17.   CRITICAL_SECCTION  struc
  18.           DebugInfo           dd ?
  19.           LockCount           dd ?
  20.           RecursionCount      dd ?
  21.           OwningThread        dd ?
  22.           LockSemaphore       dd ?
  23.           SpinCount           dd ?
  24.   CRITICAL_SECCTION  ends

Итак, наша критическая секция создана, и мы можем использовать ее.

Код (Text):
  1.  
  2.           разделяемый код
  3.           ...
  4.           push указатель на структуру CRITICAL_SECTION
  5.           call EnterCriticalSection
  6.           ; начало критической секции
  7.           ...
  8.           ; здесь может быть только один тред
  9.           ;..
  10.           push указатель на структуру CRITICAL_SECTION
  11.           call LeaveCriticalSection
  12.           ; конец критической секции
  13.           ...

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

Если критическая секция нам больше не нужна, мы удаляем ее:

Код (Text):
  1.  
  2.   push указатель на структуру CRITICAL_SECTION
  3.   call DeleteCriticalSection
  4.  
  5.   Функция ничего не возвращает

Также существует путь, чтобы узнать, можно ли войти в критическую секцию, так, чтобы OS не поместила тред в сон:

Код (Text):
  1.  
  2.   push указатель на структуру CRITICAL_SECTION
  3.   call TryEnterCriticalSection
Функция возвращает ненулевое значение, если удалось войти в критическую секцию.

Последняя функция, которая работает с CS - это SetCriticalSectionSpinCount. Если у вас есть мультипроцессорная система - идите и прочитайте ее описания. У меня такой системы нет, поэтому этот вопрос меня не волнует :smile3:.

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

Синхронизация через объекты ядра

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

Код (Text):
  1.  
  2.   Чтобы поместить тред в сон и подождать, пока объект будет просигнализирован,
  3.   используйте следующий код:
  4.  
  5.   push время ожидания
  6.   push хэндл наблюдаемого объекта
  7.   call WaitForSingleObject
  8.  
  9.  
  10.   push время ожидания
  11.   push булевое значение - ждать всех
  12.   push указатель на массив объектов, которые нужно ждать
  13.   call WaitForMultipleObjects
  14.  
  15.   булевое значение - установка в TRUE(1) укажет функции ждать все хэндлы, в
  16.             противном случае(0) функция возвратится после сигнализации одного
  17.             объекта
  18.  
  19.   время ожидания - время в ms, которое функция будет ожидать сигнализацию
  20.  
  21.   возможный результат работы этих функций:
  22.  
  23.   WAIT_OBJECT_0  = 0     - мы все ждали этого... объект был просигнализирован
  24.                          - при использовании функции XMultipleX с установкой
  25.                            ждать один объект, мы получим индекс этого хэндла
  26.                            в переданном массиве
  27.   WAIT_ABANDONED = 080h  - смотри в главе о мутексах
  28.   WAIT_TIMEOUT   = 0102h - время ожидания вышло... сигнализации объекта не
  29.                            произошло
  30.   WAIT_FAILED    = -1    - вызов функции не удался... используйте
  31.                            GetLastError
  32.  
  33.   Теперь вы знаете, как ждать объект. Давайте взглянем на сами
  34.   синхронизационные объекты.

События

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

Код (Text):
  1.  
  2.   Мы можем создать событие следующим образом:
  3.  
  4.   push указатель на имя события
  5.   push булевое значение - начальное состояние
  6.   push булевое значение - режим события
  7.   push указатель на аттрибуты безопасности
  8.   call CreateEvent(A/W)
  9.  
  10.   начальное состояние - событие будет создано в сигнализирующем состоянии(1)
  11.                         или несигнализирующем(0)
  12.  
  13.   режим события - событие может переводиться в несигнализирующее состояние
  14.                   автоматически (после того, как будет выполнена какая-нибудь
  15.                   из функций WaitX) или вручную (мы должны десигнализировать
  16.                   объект самостоятельно)
  17.  
  18.                auto reset   = 0
  19.                manual reset = 1
  20.  
  21.   аттрибуты безопасности - я объясню это в другой статье...

Если один процесс создает событие, другой может получить доступ к этому событию, используя его имя:

Код (Text):
  1.  
  2.   push указатель на имя события
  3.   push булевое значение - наследуется или нет
  4.   push доступ
  5.   call OpenEvent(A/W)
  6.  
  7.   булевое значение - если установлено в TRUE(1), хэндл события может быть
  8.                      унаследован другими созданными процессами
  9.  
  10.   доступ :
  11.   EVENT_ALL_ACCESS   = 01f0003h  - у вас есть полный доступ к событию
  12.   EVENT_MODIFY_STATE = 2         - вы можете использовать только функции
  13.                                    SetEvent и ResetEvent
  14.   SYNCHRONIZE        = 0100000h  - Windows NT: вы можете использовать только
  15.                                    функции WaitX
  16.  
  17.   CreateEvent и OpenEvent возвращают хэндл события

Есть три функции для работы с состоянием событий.

Код (Text):
  1.  
  2.   Чтобы установить событие в сигнализирующее состояние:
  3.  
  4.   push хэндл события
  5.   call SetEvent
  6.  
  7.   Чтобы установить событие в несигнализирующее состояние:
  8.  
  9.   push хэндл события
  10.   call ResetEvent
  11.  
  12.   push хэндл события
  13.   call PulseEvent
  14.  
  15.   Эта функция устанавливает событие, пробуждает ждущий тред, а затем
  16.   сбрасывает событие. Если функция используется на событии, сбрасываемом
  17.   вручную, все ждущие треды будут пробуждены, а если событие сбрасывается
  18.   автоматически, то будет пробуждена только одна ветвь.
  19.  
  20.   Все эти функции возвращают TRUE(1) в случае успеха.

Мутексы

Слово мутекс означает 'mutual exclusion' (взаимное исключение). Это один из легких в использовании и очень полезных синхронизационных объектов. Он похож на критические секции, но его можно использовать во взаимодействии между процессами.

Мы можем создать мутекс следующим образом:

Код (Text):
  1.  
  2.   push указатель на имя мутекса
  3.   push булевое значение  - инциализация
  4.   push указатель на SECURITY_ATTRIBUTES
  5.   call CreateMutex(A/W)
  6.  
  7.   initialization - if TRUE(1) then mutex is created unsignalizated
  8.                    else mutex is avaiable for anyone
  9.  
  10.   инициализация - если TRUE(1), тогда мутекс создается несигнализированным,
  11.                   в противном случае он доступен кому угодно.

Существующий мутекс мы можем открыть так:

Код (Text):
  1.  
  2.   push указатель на имя мутекса
  3.   push булевое значение - наследование
  4.  
  5.   наследование - если установлено в TRUE(1), хэндл события может быть
  6.                  унаследован другим созданными процессоми
  7.  
  8.   доступ :
  9.  
  10.   MUTEX_ALL_ACCESS  =  01f0001h  полный доступ к мутексу
  11.   SYNCHRONIZE       =  0100000h  Windows NT: вы можете использовать только
  12.                                  функции WaitX и функцию ReleaseMutex
  13.                                  (о ней позже)

Обе эти функции возвращают хэндл мутекса, если вызов функции прошел успешно.

Хорошо, предположим, что ваш тред создал мутекс и владеет им. Другие треды ждут мутекса одной из функций WaitX. Если мутекс вам больше не нужен - освободите его с помощью функции ReleaseMutex - один ждущий тред проснется и мутекс снова будет в несигнализированном состоянии. Вы можете освободить только тот мутекс, которым владеете.

Код (Text):
  1.  
  2.   push хэндл мутекса
  3.   call ReleasMutex
  4.  
  5.   В случае успеха функция возвращает ненулевое значение

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

Все о мутексах.

Семафоры

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

Мы можем создать семафор следующим образом:

Код (Text):
  1.  
  2.   push указатель на имя семафора
  3.   push максимальное количество
  4.   push начальное количество
  5.   push указатель на SECURITY_ATTRIBUTES
  6.   call CreateSemaphore(A/W)
  7.  
  8.   максимальное количество - максимальное количество тредов, которое может быть
  9.                            внутри критической секции
  10.   начальное количество - начальное количество тредов внутри критической секции

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

Код (Text):
  1.  
  2.   Конечно, есть функция API, чтобы открыть существующий семафор:
  3.  
  4.   push указатель на имя семафора
  5.   push булевое занчение - наследование
  6.   push доступ
  7.   call OpenSemaphore(A/W)
  8.  
  9.   наследование - если установлено в TRUE(1), хэндл событие может быть
  10.                  унаследовано другими созданными процессами
  11.  
  12.   доступ :
  13.   SEMAPHORE_ALL_ACCESS   = 01f0003h  - полный доступ к семафору
  14.   SEMAPHORE_MODIFY_STATE = 2         - позволяет использование
  15.                                        ReleaseSemaphore
  16.   SYNCHRONIZE            = 0100000h  - позволяет использовать функции WaitX
  17.  
  18.   Обе эти функции возвращают хэндл семафора в случае успеха

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

Код (Text):
  1.  
  2.   push указатель на двойное слово - получаем предыдущее значение счетчика
  3.                                     семафора
  4.   push насколько увеличить значение семафора
  5.   push хэндл семафора
  6.   call ReleaseSemaphore

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

Все о семафорах.

Ждущие таймеры

Ждущие таймеры появились в Windows начиная с NT 4. До сих пор у нас были объекты, которые мы должны были вручную (как правило :smile3: ) переключить в сигнализирующее состояние. Ждущие таймеры - это объекты, которые сигнализируют себя сами после некоторого периода времени. Давайте начнем...

Код (Text):
  1.  
  2.   Ждущий таймер создается следующим образом:
  3.  
  4.   push указатель на имя
  5.   push булевое значение - авто/ручной сброс
  6.   push указатель на SECURITY_ATTRIBUTES
  7.   call CreateWaitableTime(A/W)
  8.  
  9.   авто/ручной сброс - также, как в событиях
  10.  
  11.   Мы можем открыть существующий ждущий таймер так:
  12.  
  13.   push указатель на имя
  14.   push булевое значение - наследование
  15.   push доступ
  16.   call OpenWaitableTimer(A/W)
  17.  
  18.   наследование - если установлено в  TRUE(1), хэндл события может быть
  19.   унаследован другими созданными процессами
  20.  
  21.   доступ :
  22.   TIMER_ALL_ACCESS   = 01f0002h  - полный доступ
  23.   SYNCHRONIZE        = 0100000h  - позволяет использование функций WaitX
  24.   TIMER_MODIFY_STATE = 2         - доступ к SetWaitableTime и
  25.                                    CancelWaitableTimer
  26.  
  27.  
  28.   Обе функции возвращают хэндл ждущего таймера в случае успеха

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

Код (Text):
  1.  
  2.   push булевое значение - продолжение
  3.   push указатель на аргумент завершающей процедуры
  4.   push указатель на завершающую процедуру
  5.   push период
  6.   push указатель на начальное время
  7.   push хэндл ждущего таймера
  8.   call SetWaitableTimer
  9.  
  10.   указатель на начальное время - вам потребуется указать, когда таймер
  11.                                  сработает в первый раз. Дата должна быть в
  12.                                  формате LARGE_INTEGER.
  13.  
  14.   период - устанавливает период срабатываний в наносекундах.
  15.  
  16.   указатель на завершающую процедуру - процедура, выполняющаяся при срабатывании
  17.                                      таймера.
  18.  
  19.     void APCRoutine(LPVOID argToCompletionRoutine,DWORD dwTimerLowValue,
  20.                     DWORD dwTimerHighValue);
  21.  
  22.   продолжение - если вы установите этот параметр в TRUE(1), компьютер
  23.                 выйдет из спящего режима, если он в нем находился... <img src="styles/smiles_s/smile3.gif" class="mceSmilie" alt=":smile3:" title="Smile3    :smile3:">
  24.  
  25.   Последний API... мы можем сбросить настройки любого ждущего таймера:
  26.  
  27.   push хэндл ждущего таймера
  28.   call CancelWaitableTimer

Все о ждущих таймерах.

Напоследок

Я думаю, что большинство новых вирусов будут использовать какой-нибудь вид IPC, а значит и один из видов синхронизации. Эта статья дает только обзор того, что Windows предлагает для синхронизации, эта тема довольно сложна и не может быть полностью объяснена в одной или двух статьях... идите и синхронизируйте... :smile3: © mort[MATRiX], пер. Aquila


0 847
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532