Статьи по Assembler

       

Взаимодействие экземпляров приложения (вариант)


Этот материал предоставлен Геннадием Майко. Он дополняет публикацию "Взаимодействие экземпляров приложения", в которой assembler.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.


Этот материал предоставлен Сергеем (AKA The Byte Reaper) http://www.neptunix.com. Он дополняет публикацию "Взаимодействие экземпляров приложения", в которой assembler.ru излагал суть проблемы и рассматривал некоторые способы ее решения. Вариант, предложенный Сергеем, в отличие от нашего, решает одну вполне конкретную задачу: предотвращает повторный запуск приложения, то есть реализует вторую стратегию из трех перечисленных в статье.

 Здравствуйте. Прочитал данную статью и ее обсуждение и решил предложить еще один вариант, может быть, не такой элегантный, как оба предыдущих, но все-таки работающий, короткий и примененный мною в нашей с MemoBreaker'ом программе UIN2IP.

=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= secAttrib SECURITY_ATTRIBUTES <> ;... ;... ;... mov secAttrib.nLength,SIZEOF secAttrib mov secAttrib.lpSecurityDescriptor,NULL mov secAttrib.bInheritHandle,TRUE invoke CreateMutex,ADDR secAttrib,1,ADDR mutex invoke GetLastError .if eax > 0 ; Вообще-то правильнее было бы сверять еах ; c ERROR_ALREADY_EXISTS, ну да ладно... invoke ExitProcess,0 ; Если такой мьютекс уже существует - .endif ; завершаем приложение =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=



Не совсем, конечно, использование по прямому назначению, но чем не синхронизация? ;)

Всего наилучшего, Сергей AKA The Byte Reaper

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




Упомянутый в письме проект находится в файле ms.zip размером 8851 байт.

Вот исходный текст приложения (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. Потому что, передавая друг другу данные, процессы должны обеспечить согласованный доступ к разделяемой памяти, то есть сообщать друг другу о готовности к передаче и приему данных и не допускать обращения к неготовым или изменяемым в данный момент данным.

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


Содержание раздела