Взаимодействие экземпляров приложения (вариант) — Архив WASM.RU

Все статьи

Взаимодействие экземпляров приложения (вариант) — Архив WASM.RU

  Этот материал предоставлен Геннадием Майко. Он дополняет публикацию "Взаимодействие экземпляров приложения", в которой мы излагали суть проблемы и рассматривал некоторые способы ее решения. Вариант, предложенный Геннадием, изящен и предельно прост в реализации. Приносим ему свою благодарность. Итак, слово автору:

Добрый день,

Хотел бы предложить еще один вариант (на мой взгляд, весьма эффективный) проверки того, что приложение, написанное под Win32, было уже запущено. Этот пример навеян очень интересной и полезной книгой Jeffrey Richter "Advanced Windows" (если я не ошибаюсь, есть ее перевод на русский язык).

Идея состоит в следующем. Для приложения или DLL можно определить некоторую секцию (сегмент) как "разделяемую" (shared). Это значит, что ее содержимое будет использоваться совместно всеми экземплярами приложения. Этим можно воспользоваться для проверки факта запуска приложения во второй и более раз.

Определим в некотором отдельном сегменте переменную Cnt и проинициализируем ее 1 (или любым другим ненулевым значением). В самом начале работы приложение проверяет значение этой переменной и, если оно равно 1, присваивает ей 0. Понятно, что эта операция должна быть атомарной (во-первых, к этой переменной могут обращаться несколько программ одновременно, во-вторых, мы можем работать на многопроцессорной машине). Это можно сделать, например, с помощью команды XCHG или LOCK CMPXCGH (в последнем случае необходимо обязательно использовать префикс LOCK и определить в программе директиву .486).

Если значение этой переменной равно 1, значит программа работает первый раз. Если значение этой переменной равно 0, была запущено вторая (или более) копия приложения.

Для того, чтобы объявить некоторую секцию разделяемой, необходимо добавить к опциям компоновщика следующее:
link /section:<имя_разделяемого_сегмента>,RWS
Здесь ключевым является 'S' (SHARED) в списке атрибутов сегмента.

В приложении к этому письму Вы можете найти проект для Visual C++ 6.0 с соответствующим примером (я воспользовался Вашими рекомендациями для его создания). Текст программы с соответствующими комментариями находится в файле ms.asm. В каталоге Debug есть исполняемый файл ms.exe. Запустив его один раз и открыв Task Manager, мы можем найти Image Name ms.exe в списке Processes. При втором, третьем и т.д. запуске этой программы новых процессов ms.exe в этот список не добавляется. Если в программе заменить команду jnz на jmp и пересобрать проект, то при втором и т.д. запусках добавляются новые процессы ms.exe.

Обратите внимание, что запущенная программа будет работать бесконечно, поэтому, чтобы ее окончить, необходимо принудительно закончить соответствующий процесс с помощью Task Manager'a.

Работа программы проверена на двухпроцессорной машине под Windows NT 4.0.

С уважением,
Геннадий Майко.

  Вот исходный текст приложения (ms.asm):

; Простое приложение, которое может быть запущено только один раз.
; Однажды стартовав, работает бесконечно. Для завершения:
;   NT:    Откройте Task Manager. На вкладке Processes выберите ms.exe.
;          Нажмите кнопку End Process.
;   95/98: Нажмите Ctrl+Alt+Del. Выберите в списке приложение Ms.
;          Нажмите кнопку End Task.
; При построении проекта в командную строку link.exe следует
; включить опцию /SECTION:SHS,RWS

.386
.Model flat,stdcall

       ; Разделяемый сегмент
SHS    SEGMENT
Cnt    dd 1        ; Флаг: 1 - работает первый экземпляр приложения;
                   ;       0 - запущен второй (и последующие) экземпляры.
SHS    ENDS    

       ; Код
.code
WinMain PROC PUBLIC hinst,prev_hinst,command_line,cmd_show
       xor   eax,eax     ; eax = 0
       xchg  eax,Cnt     ; eax <--> Cnt. Атомарный обмен eax и Cnt
       or    eax,eax     ; проверить флаг
       jnz   L_Cont      ; переход к коду для первого экземпляра

       ; Код для второго (и последующих) экземпляров
            
       ret               ; Завершение второго (и последующих) экземпляров

       ; Код для первого экземпляра приложения

L_Cont:
       jmp   L_Cont      ; Бесконечный цикл
WinMain ENDP

end

  Несколько комментариев от assembler.ru:

  1. Опция командной строки компоновщика link.exe /SECTION:name,[E][R][W][S][D][K][L][P][X] позволяет принудительно назначать атрибуты секциям PE-файла. В данном случае секции, образованной из сегмента SHS, устанавливаются атрибуты R (доступна для чтения), W (доступна для записи), S (разделяемая). Атрибут S означает, что все процессы, запущенные с помощью одного и того же исполняемого файла ("image" в терминах PE-файла), получат общий доступ к области памяти, содержащей переменную Cnt. Всякое изменение этой переменной одним процессом будет наблюдаться другими процессами.
    Возможность объявлять секцию разделяемой, вероятнее всего, была заложена в архитектуру Windows именно с целью обеспечить взаимодействие экземпляров одного и того же приложения. Какое-либо иное применение этого механизма как-то не приходит в голову.
    Ms.exe демонстрирует одну интересную особенность операционной среды. Как видим, разделяемая секция, содержащая инициализированные данные, при загрузке приложения инициализируется только в случае, когда загружается первый экземпляр приложения. При загрузке же последующих экземпляров инициализации переменных не происходит, а они получают значения, установленные к этому моменту предыдущими экземплярами приложения. Это логично, и странно было бы, если бы было по-другому, но как же все-таки производители операционных систем умудряются все предусмотреть?
  2. Обсуждаемое Геннадием требование атомарности обращения программы к переменной Cnt, в целом справедливое для любых операций с разделяемой памятью, применительно к данному случаю, по нашему мнению, не является строгим.
    Немного найдется пользователей, которые способны запустить два экземпляра приложения один за другим с достаточно малым интервалом времени так, чтобы они успели вступить в конфликт при обращении к переменной Cnt. Однако настоящий ассемблерщик, конечно же, обязательно поставит префикс lock.
    Про префикс lock Геннадий представил следующее пояснение. Как известно, этот префикс имеет смысл в многопроцессорных системах. Он блокирует на время выполнения команды, перед которой стоит, доступ к памяти со стороны других процессоров. Так вот, команда xchg гарантированно формирует сигнал LOCK# вне зависимости от того, имеется или нет при ней префикс lock (см. "Intel Architecture Software Developer's Manual. vol.2: Instruction Set Reference"). А вот команда cmpxchg такого сигнала не формирует, поэтому в случаях, когда в многопроцессорных системах имеется опасность одновременного доступа нескольких процессоров к одним и тем же данным, применение префикса для нее обязательно.
  3. Рассматриваемый способ обеспечения уникальности экземпляра приложения имеет органически присущее ему отличие от способов, основанных на средствах межпроцессного обмена. Если скопировать ms.exe в другую папку и попытаться запустить обе копии приложения, мы обнаружим, что обе они успешно запустятся, потому что это разные image'ы, и разделения секции SHS между ними не происходит.
    В контексте стоящей задачи, конечно, это несущественно, так как даже в эпоху морального устаревания 8-гигабайтных бытовых винчестеров, опять же, весьма немногие пользователи увлекаются содержанием на диске нескольких копий приложений. Но все равно интересно.
  4. Разделение сегмента между экземплярами приложения можно использовать не только для обеспечения уникальности экземпляров, но и для реализации сколь угодно сложного обмена информацией между ними (третья стратегия), ведь мы можем добавить в разделяемый сегмент любой набор необходимых нам для этого переменных. Это даже удобнее, чем File Mapping, так как не требует специальной поддержки программой, не связано с использованием файловой системы, и к этим переменным можно обращаться по именам, а не по смещениям.
    А вот здесь уж точно без атомарности не обойтись. Точнее, без средств межпроцессного взаимодействия. Еще точнее - без средств синхронизации процессов. И уж совсем точно - без объектов mutex и/или event. Потому что, передавая друг другу данные, процессы должны обеспечить согласованный доступ к разделяемой памяти, то есть сообщать друг другу о готовности к передаче и приему данных и не допускать обращения к неготовым или изменяемым в данный момент данным.

  В целом решение, предложенное Геннадием, следует признать очень интересным и совершенно функциональным. Еще раз большое ему спасибо.

2002-2013 (c) wasm.ru