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

Все статьи

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

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

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

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

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

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

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

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

        _CSflag         dd 012345678h
          ...
        @nextCheck:
            mov eax,012345678h           ; загружаем ненулевое значение
            xchg eax,dword ptr [_CSflag] ; меняем со значением флага
            or eax,eax                   ; проверяем, сброшен  ли CS
            jnz @nextCheck               ; нет, тратим еще время OS

        ; начало критической секции

            xor eax,eax                  ; оставляя флаг CS равным нулю
            mov dword ptr [_CSflag],eax
        ; конец критической секции
            ...
       Один тред ждет в цикле, пока друго (только один) в критической секции.
       Взаимоисключение подтвержденно инструкцией xchg.

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

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

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

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

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

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

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

  push указатель на структуру CRITICAL_SECTION
  call InitializeCriticalSection

  Функция ничего не возваращает

  Если у вас мультипроцессорная система, вы можете использовать:

  push spin count
  push указатель на структуру CRITICAL_SECTION
  call InitializeCriticalSectionAndSpinCount

  Функция ничего не возвращает

  CRITICAL_SECCTION  struc
          DebugInfo           dd ?
          LockCount           dd ?
          RecursionCount      dd ?
          OwningThread        dd ?
          LockSemaphore       dd ?
          SpinCount           dd ?
  CRITICAL_SECCTION  ends

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

          разделяемый код
          ...
          push указатель на структуру CRITICAL_SECTION
          call EnterCriticalSection
          ; начало критической секции
          ...
          ; здесь может быть только один тред
          ;..
          push указатель на структуру CRITICAL_SECTION
          call LeaveCriticalSection
          ; конец критической секции
          ...

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

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

  push указатель на структуру CRITICAL_SECTION
  call DeleteCriticalSection

  Функция ничего не возвращает

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

  push указатель на структуру CRITICAL_SECTION
  call TryEnterCriticalSection
Функция возвращает ненулевое значение, если удалось войти в критическую секцию.

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

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

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

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

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

  push время ожидания
  push хэндл наблюдаемого объекта
  call WaitForSingleObject


  push время ожидания
  push булевое значение - ждать всех
  push указатель на массив объектов, которые нужно ждать
  call WaitForMultipleObjects

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

  время ожидания - время в ms, которое функция будет ожидать сигнализацию

  возможный результат работы этих функций:

  WAIT_OBJECT_0  = 0     - мы все ждали этого... объект был просигнализирован
                         - при использовании функции XMultipleX с установкой
                           ждать один объект, мы получим индекс этого хэндла
                           в переданном массиве
  WAIT_ABANDONED = 080h  - смотри в главе о мутексах
  WAIT_TIMEOUT   = 0102h - время ожидания вышло... сигнализации объекта не
                           произошло
  WAIT_FAILED    = -1    - вызов функции не удался... используйте
                           GetLastError

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

События

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

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

  push указатель на имя события
  push булевое значение - начальное состояние
  push булевое значение - режим события
  push указатель на аттрибуты безопасности
  call CreateEvent(A/W)

  начальное состояние - событие будет создано в сигнализирующем состоянии(1)
                        или несигнализирующем(0)

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

               auto reset   = 0
               manual reset = 1

  аттрибуты безопасности - я объясню это в другой статье...

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

  push указатель на имя события
  push булевое значение - наследуется или нет
  push доступ
  call OpenEvent(A/W)

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

  доступ :
  EVENT_ALL_ACCESS   = 01f0003h  - у вас есть полный доступ к событию
  EVENT_MODIFY_STATE = 2         - вы можете использовать только функции
                                   SetEvent и ResetEvent
  SYNCHRONIZE        = 0100000h  - Windows NT: вы можете использовать только
                                   функции WaitX

  CreateEvent и OpenEvent возвращают хэндл события

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

  Чтобы установить событие в сигнализирующее состояние:

  push хэндл события
  call SetEvent

  Чтобы установить событие в несигнализирующее состояние:

  push хэндл события
  call ResetEvent

  push хэндл события
  call PulseEvent

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

  Все эти функции возвращают TRUE(1) в случае успеха.

Мутексы

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

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

  push указатель на имя мутекса
  push булевое значение  - инциализация
  push указатель на SECURITY_ATTRIBUTES
  call CreateMutex(A/W)

  initialization - if TRUE(1) then mutex is created unsignalizated
                   else mutex is avaiable for anyone

  инициализация - если TRUE(1), тогда мутекс создается несигнализированным,
                  в противном случае он доступен кому угодно.

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

  push указатель на имя мутекса
  push булевое значение - наследование

  наследование - если установлено в TRUE(1), хэндл события может быть
                 унаследован другим созданными процессоми

  доступ :

  MUTEX_ALL_ACCESS  =  01f0001h  полный доступ к мутексу
  SYNCHRONIZE       =  0100000h  Windows NT: вы можете использовать только
                                 функции WaitX и функцию ReleaseMutex
                                 (о ней позже)

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

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

  push хэндл мутекса
  call ReleasMutex

  В случае успеха функция возвращает ненулевое значение

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

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

Семафоры

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

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

  push указатель на имя семафора
  push максимальное количество
  push начальное количество
  push указатель на SECURITY_ATTRIBUTES
  call CreateSemaphore(A/W)

  максимальное количество - максимальное количество тредов, которое может быть
                           внутри критической секции
  начальное количество - начальное количество тредов внутри критической секции

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

  Конечно, есть функция API, чтобы открыть существующий семафор:

  push указатель на имя семафора
  push булевое занчение - наследование
  push доступ
  call OpenSemaphore(A/W)

  наследование - если установлено в TRUE(1), хэндл событие может быть
                 унаследовано другими созданными процессами

  доступ :
  SEMAPHORE_ALL_ACCESS   = 01f0003h  - полный доступ к семафору
  SEMAPHORE_MODIFY_STATE = 2         - позволяет использование
                                       ReleaseSemaphore
  SYNCHRONIZE            = 0100000h  - позволяет использовать функции WaitX

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

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

  push указатель на двойное слово - получаем предыдущее значение счетчика
                                    семафора
  push насколько увеличить значение семафора
  push хэндл семафора
  call ReleaseSemaphore

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

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

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

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

  Ждущий таймер создается следующим образом:

  push указатель на имя
  push булевое значение - авто/ручной сброс
  push указатель на SECURITY_ATTRIBUTES
  call CreateWaitableTime(A/W)

  авто/ручной сброс - также, как в событиях

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

  push указатель на имя
  push булевое значение - наследование
  push доступ
  call OpenWaitableTimer(A/W)

  наследование - если установлено в  TRUE(1), хэндл события может быть
  унаследован другими созданными процессами

  доступ :
  TIMER_ALL_ACCESS   = 01f0002h  - полный доступ
  SYNCHRONIZE        = 0100000h  - позволяет использование функций WaitX
  TIMER_MODIFY_STATE = 2         - доступ к SetWaitableTime и
                                   CancelWaitableTimer


  Обе функции возвращают хэндл ждущего таймера в случае успеха

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

  push булевое значение - продолжение
  push указатель на аргумент завершающей процедуры
  push указатель на завершающую процедуру
  push период
  push указатель на начальное время
  push хэндл ждущего таймера
  call SetWaitableTimer

  указатель на начальное время - вам потребуется указать, когда таймер
                                 сработает в первый раз. Дата должна быть в
                                 формате LARGE_INTEGER.

  период - устанавливает период срабатываний в наносекундах.

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

    void APCRoutine(LPVOID argToCompletionRoutine,DWORD dwTimerLowValue,
                    DWORD dwTimerHighValue);

  продолжение - если вы установите этот параметр в TRUE(1), компьютер
                выйдет из спящего режима, если он в нем находился... :)

  Последний API... мы можем сбросить настройки любого ждущего таймера:

  push хэндл ждущего таймера
  call CancelWaitableTimer

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

Напоследок

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

2002-2013 (c) wasm.ru