ПРОГРАММИРОВАНИЕ НА VISUAL C++

Выпуск No. 36 от 11 марта 2001 г.

Здравствуйте!

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

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

IPC: основы межпроцессного взаимодействия
Обзор технологий

Введение

Любая операционная система была бы весьма ущербна, если бы замыкала выполняющееся приложение в собственном темном мирке без окон и дверей, без какой-либо возможности сообщить другим программам какую-либо информацию. Если посмотреть внимательно, можно заметить, что далеко не все приложения являются самодостаточными. Очень многим, если не большей части, требуется информация от других приложений, либо они должны эту информацию сообщать. Именно поэтому в операционную систему встраивается множество механизмов, которые обеспечивают т.н. Interproccess Communication (IPC) - то есть межпроцессное взаимодействие.

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

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

C:\>DIR|MORE

происходило следующее: выполнялась команда DIR и ее вывод записывался во временный текстовый файл. После этого содержимое файла подавалось на вход команды MORE. В результате вы получали листинг каталогов, который в случае большого количества каталогов не уезжал мгновенно за экран, а мог скроллироваться с помощью клавиши Enter. Конечно же это очень примитивный IPC, но его наличие показывает, что уже тогда такой механизм был востребован и в какой-то мере реализован.

Примеры использования IPC охватывают гораздо большее количество программ и приложений, чем вы скорее всего думаете. Когда вы выходите в интернет, ваш браузер - одна программа (процесс) - взаимодействует с web-сервером - другой программой (процессом). Эти программы выполняются на разных компьютерах; браузер на вашем, сервер - где-то еще. И вас не волнует, какая ОС установлена на сервере и какая там платформа.
Или, например, вы работаете с удаленной базой данных. Ваше клиентское приложение - это один процесс, на сервере базы данных запущен другой процесс. Процесс на сервере выполняет запросы к БД, поступающие от вашего процесса.

ПРИМЕЧАНИЕ
Вообще, для сетевых форм IPC (но не обязательно только для них) очень часто используется концепция "клиент-сервер". Как вы понимаете, "клиент" - это приложение, которому требуются данные, "сервер" - приложение, предоставляющее данные.

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

Средств, обеспечивающих взаимодействие между процессами, создано достаточно много. Огромное их количество реализовано в Windows 9x, еще больше - в Windows NT/2000. Теперь нужно приличное количество времени, чтобы хотя бы познакомиться со всеми! Замечу, что нет, и наверное в принципе не может быть универсального способа обмена данными, который годился бы на все случаи жизни - все равно в некоторых случаях использование другого способа будет предпочтительнее. Но я надеюсь, что после прочтения этой статьи вы сможете достаточно уверенно ориентироваться в мире IPC и обоснованно выбирать тот или иной метод.

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

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

Теперь давайте рассмотрим основные виды IPC и случаи, в которых они используются.

Буфер обмена (clipboard)
Это одна из самых примитивных и хорошо известных форм IPC. Он появился еще в самых ранних версиях Windows. Основная его задача - обеспечивать обмен данными между программами по желанию и под контролем пользователя. Впрочем, вы наверняка сами неплохо знаете, как используется буфер обмена... ;-) Не рекомендуется использовать его для внутренних нужд приложения, и не стоит помещать туда то, что не предназначено для прямого просмотра пользователем.
Сообщение WM_COPYDATA
Стандартное сообщение для передачи участка памяти другому процессу. Работает однонаправленно, принимающий процесс должен расценивать полученные данные как read only. Посылать это сообщение необходимо только с помощью SendMessage, которая (напомню) в отличие от PostMessage ждет завершения операции. Таким образом, посылающий поток "подвисает" на время передачи данных. Вы сами должны решить, насколько это приемлемо для вас. Это не имеет значения для небольших кусков данных, но для больших объемов данных или для real-time приложений этот способ вряд ли подходит.
Разделяемая память (shared memory)
Этот способ взаимодействия реализуется не совсем напрямую, а через технологию File Mapping - отображения файлов на оперативную память. Вкраце, этот механизм позволяет осуществлять доступ к файлу таким образом, как будто это обыкновенный массив, хранящийся в памяти (не загружая файл в память явно). "Побочным эффектом" этой технологии является возможность работать с таким отображенным файлом сразу нескольким процессам. Таким образом, можно создать объект file mapping, но не ассоциировать его с каким-то конкретным файлом. Получаемая область памяти как раз и будет общей между процессами. Работая с этой памятью, потоки обязательно должны согласовывать свои действия с помощью объектов синхронизации.
Библиотеки динамической компоновки (DLL)
Библиотеки динамической компоновки также имеют способность обеспечивать обмен данными между процессами. Когда в рамках DLL объявляется переменная, ее можно сделать разделяемой (shared). Все процессы, обращающиеся к библиотеке, для таких переменных будут использовать одно и то же место в физической памяти. (Здесь также важно не забыть о синхронизации.)
Протокол динамического обмена данными (Dynamic Data Exchange, DDE)
Этот протокол выполняет все основные функции для обмена данными между приложениями. Он очень широко использовался до тех пор, пока для этих целей не стали применять OLE (впоследствии ActiveX). На данный момент DDE используется достаточно редко, в основном для обратной совместимости.

Больше всего этот протокол подходит для задач, не требующих продолжительного взаимодействия с пользователем. Пользователю в некоторых случаях нужно только установить соединение между программами, а обмен данными происходит без его участия. Замечу, что все это в равной степени относится и к технологии OLE/ActiveX.

OLE/ActiveX
Это действительно универсальная технология, и одно из многих ее применений - межпроцессный обмен данными. Хотя cтоит думаю отметить, что OLE как раз для этой цели и создавалась (на смену DDE), и только потом была расширена настолько, что пришлось поменять название ;-). Специально для обмена данными существует интерфейс IDataObject. А для обмена данными по сети используется DCOM, которую под некоторым углом можно рассматривать как объединение ActiveX и RPC.
Каналы (pipes)
Каналы - это очень мощная технология обмена данными. Наверное, именно поэтому в полной мере они поддерживаются только в Windows NT/2000. В общем случае канал можно представить в виде трубы, соединяющей два процесса. Что попадает в трубу на одном конце, мгновенно появляется на другом. Чаще всего каналы используются для передачи непрерывного потока данных.

Каналы делятся на анонимные (anonymous pipes) и именованные (named pipes).
Анонимные каналы используются достаточно редко, они просто передают поток вывода одного процесса на поток ввода другого.
Именованные каналы передают произвольные данные и могут работать через сеть. (Именованные каналы поддерживаются только в WinNT/2000.)

Сокеты (sockets)
Это очень важная технология, т.к. именно она отвечает за обмен данными в Интернет. Сокеты также часто используются в крупных ЛВС. Взаимодействие происходит через т.н. разъемы-"сокеты", которые представляют собой абстракцию конечных точек коммуникационной линии, соединяющей два приложения. С этими объектами программа и должна работать, например, ждать соединения, посылать данные и т.д. В Windows входит достаточно мощный API для работы с сокетами.
Почтовые слоты (mailslots)
Почтовые слоты - это механизм однонаправленного IPC. Если приложению известно имя слота, оно может помещать туда сообщения, а приложение-хозяин этого слота (приемник) может их оттуда извлекать и соответствующим образом обрабатывать. Основное преимущество этого способа - возможность передавать сообщения по локальной сети сразу нескольким компьютерам за одну операцию. Для этого приложения-приемники создают почтовые слоты с одним и тем же именем. Когда в дальнейшем какое-либо приложение помещает сообщение в этот слот, приложения-приемники получают его одновременно.
Объекты синхронизации
Как ни странно, объекты синхронизации тоже можно отнести к механизмам IPC. Конечно, объем передаваемых данных в данном случае очень невелик ;) Но именно эти объекты следует использовать, если одному процессу нужно передать другому что-то вроде "я закончил работу" или "я начинаю работать с общей памятью".
Microsoft Message Queue (MSMQ)

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

Удаленный вызов процедур (Remote Procedure Call, RPC)
Строго говоря, это не совсем технология IPC, а скорее способ значительно расширить возможности других механизмов IPC. С помощью этой технологии общение через сеть становится совешенно прозрачным как для сервера, так и для клиента. Им обоим начинает казаться, что их "собеседник" расположен локально по отношению к ним .
Резюме
Конечно, я перечислил далеко не все способы обмена данными. Если бы это было так, то это было бы не так интересно ;-) За рамками данной статьи остались такие вещи, как глобальная таблица атомов, хуки и некоторые другие технологии, которые с некоторой натяжкой можно признать механизмами IPC. Но главное, как я считаю, сделано: теперь вы знаете, что это за непонятные аббревиатуры и как из всего многообразия методов IPC выбрать наиболее подходящий.

/ / / / ВОПРОС-ОТВЕТ / / / / / / / / / / / / / / / /

Q| Можно ли из моей программы управлять окном которое создано другим приложением (закрывать, сворачивать, нажимать в нем кнопки и т.д.), если да то как? - Alhim

|A Выполнение этой задачи распадается на два этапа.

Сначала нужно каким-то образом определить хэндл окна, которым мы собираемся манипулировать. Основным инструментом здесь являются функции FindWindow(Ex), которые ищут окно по заданному классу и/или заголовку. В определении и того, и другого сильно помогает программа Spy++. Рассмотрим пример поиска HWND стандартной кнопки "Пуск". Сначала используем Spy++, чтобы определить классы панели задач и самой кнопки; оказывается, их имена "Shell_TrayWnd" и "Button" соответственно. Затем используем FindWindow(Ex).


 HWND hWnd;
 hWnd = FindWindow("Shell_TrayWnd", NULL);
 hWnd = FindWindowEx(hWnd, NULL, "Button", NULL);

 if (IsWindow(hWnd))
 {
  // Кнопка найдена, работаем с ней
 }
Ещё один набор функций, которые могут помочь в поиске хэндла чужого окна - это EnumChildWindows, EnumThreadWindows и EnumWindows, перечисляющие все окна, принадлежащие заданному окну, все окна заданного потока и все окна в системе соответственно. За описанием этих функций следует обратиться к документации. Кроме перечисленного можно упомянуть случай, когда приложение специально проектируется для взаимодействие с другим посредством обмена сообщениями. Например, одно приложение запускает другое, а затем обменивается с ним данными посредством WM_COPYDATA. В этом случае вполне уместно передать хэндл окна (это 4-хбайтовое целое) как параметр командной строки. После того, как хэндл окна определён, можно переходить ко второму этапу - управлению окном. Многие функции позволяют работать с окном, вне зависимости от того, какому процессу оно принадлежит. Характерные примеры таких функций - ShowWindow и SetForegroundWindow. Для примера рассмотрим, как спрятать кнопку "Пуск", получать хэндл которой мы уже научились.


 HWND hWnd;
 hWnd = FindWindow("Shell_TrayWnd", NULL);
 hWnd = FindWindowEx(hWnd, NULL, "Button", NULL);

 if (IsWindow(hWnd))
 {
  ShowWindow(hWnd, SW_HIDE);
  Sleep(5000);
  ShowWindow(hWnd, SW_SHOW); // Показываем обратно
 }

Кроме использования подобных функций, можно посылать окну сообщения. Например, послав кнопке BM_CLICK (с помощью PostMessage), мы как бы нажимаем на неё.

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

Существует несколько способов внедрить DLL в чужой процесс. Я покажу один из них; он достаточно прост и работает на всех Win32-платформах (Windows 9x, Windows NT), но в некоторых случаях недостаточно точен. Этот способ подразумевает установку хука на поток, создавший интересующее нас окно. При этом DLL, содержащая функцию хука, загружается системой в адресное пространство чужого процесса. Это как раз то, что нам нужно. А функцию хука вполне можно оставить пустой.

Рассмотрим пример DLL, которая уничтожает окно чужого процесса. (Такие вещи нужно делать, только полностью отдавая себе отчёт о возможных последствиях. Процесс, оставленный без окна, имеет хорошие шансы "рухнуть").


// _KillDll.cpp : Defines the entry point for the DLL application.
//

#include <windows.h>

// Создаём переменную в разделяемом сегменте,
// чтобы передать HWND из программы в DLL в чужом процессе.
#pragma comment(linker, "/SECTION:SHARED,RWS")
#pragma data_seg("SHARED")
__declspec(allocate("SHARED")) HWND hWndToKill = NULL;
#pragma data_seg()

BOOL APIENTRY DllMain( HANDLE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved )
{
 if (ul_reason_for_call == DLL_PROCESS_ATTACH &&
  IsWindow(hWndToKill) &&
  GetWindowThreadProcessId(hWndToKill, NULL) == GetCurrentThreadId())
 {
  // Если окно существует и принадлежит текущему потоку, убиваем его.
  HANDLE hEvent = OpenEvent(NULL, FALSE,
"{1F6C5480-155E-11d5-93A8-444553540000}");
  DestroyWindow(hWndToKill);
  SetEvent(hEvent);
  CloseHandle(hEvent);
 }

  return TRUE;
}

// Пустая функция хука.
LRESULT CALLBACK GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
{
 return 1;
}

extern "C" __declspec(dllexport) void KillWndNow(HWND hWnd)
{
 if (!IsWindow(hWnd))
  return;

 hWndToKill = hWnd;

 HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE,
"{1F6C5480-155E-11d5-93A8-444553540000}");

 DWORD dwThread = GetWindowThreadProcessId(hWnd, NULL);
 HHOOK hHook = SetWindowsHookEx(
  WH_GETMESSAGE,
  GetMsgProc,
  GetModuleHandle("_KillDll.dll"),
  dwThread);
 PostThreadMessage(dwThread, WM_NULL, 0, 0);

 WaitForSingleObject(hEvent, INFINITE);
 CloseHandle(hEvent);

 UnhookWindowsHookEx(hHook);
}

Чтобы использовать эту DLL, просто подключите её к программе (проще всего сделать это неявным методом), а затем выполните код:


extern "C" void KillWndNow(HWND hWnd);
...
HWND hWnd;
// Ищем окно
KillWndNow(hWnd);

Хотя код DLL сам по себе и небольшой, в нём есть несколько тонкостей, на которые я хотел бы обратить ваше внимание. Во-первых, я поместил переменную hWndToKill в разделяемый сегмент. Поскольку функция DestroyWindow вызывается в потоке чужого процесса, необходимо предусмотреть некоторый способ передачи хэндла окна через границы процессов. Разделяемая переменная - наиболее простое средство достичь цели. Во-вторых, DLL, содержащая функцию хука, не будет спроектирована на адресное пространство чужого процесса, пока функция хука реально не понадобится. В нашем случае хук имеет тип WH_GETMESSAGE, а значит DLL не загрузится, пока поток не получит какое-либо сообщение. Поэтому я посылаю ему сообщение WM_NULL (с кодом 0), чтобы вынудить ОС загрузить DLL. В-третьих, обратите внимание на применение события для синхронизации потоков в нашем и целевом процессах. Разумеется, для этой цели можно использовать и любой другой механизм синхронизации потоков. - Александр Шаргин (rudankort@mail.ru)

/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / /

Q| Хотелось бы побольше узнать о предварительном просмотре. В русской программе он смотрится инородным телом на своем иностранном языке. Можно ли его как-то настраивать под себя?
В этой же связи: не могу решить проблему.
В программе 3 меню и, соответственно, 3 панели инструментов, которые создал в Create. Переключая меню, вызываю ShowControlBar - прячу ненужные панели и показываю необходимую. Но после вызова PRINT PREVIEW, в окне появляются сразу все 3 панели инструментов.
Попутно: что означает AFX_IDS_PREVIEW_CLOSE в String Table? - Serg Petukhov

   Ответить на вопрос

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Успехов!

Алекс Jenter   jenter@mail.ru
Красноярск, 2001.

Предыдущие выпуски     Статистика рассылки