developing.ru - клуб программистов Здесь может быть и ваша реклама.
developing.ru >технология COM >
Михаил Безверхов
aspid@developing.ru

Соблюдай дистанцию!

В этой статье мы поговорим о решении второго затруднения в организации связи разделённых двоичных компонент. Первое затруднение очень понятно - изолированность адресных пространств процессов не позволяет "так просто" взять, да и передать данные. Второе понять труднее - та же изолированность не позволяет передать и управление.

Для того, чтобы всё было абсолютно прозрачно обозначим сначала философию взаимодействия компонент, а потом подберём для этой философии техническое решение. Итак, существует очень давняя, интуитивно понятная и всем привычная абстракция, называемая "вызов процедуры". Состоит она в том, что некий фрагмент процессорного кода оформляется по особым правилам "командами пролога" и "командами эпилога" и именуется "процедурой" (подпрограммой). После чего весь этот фрагмент программист может считать "одной большой командой" - он должен подготовить список аргументов этой подпрограммы, поместить его в то место, которое оговорено соглашением о передаче параметров, и ... передать управление внутрь этой последовательности команд. Мудрёный термин "передача управления" физически выражается в том, что в регистр IP процессора заносится адрес первой из последовательности команд подпрограммы. Сама же команда передачи управления (call) при таком переходе запоминает адрес следующей за ней самой команды процессора, так, что когда в самом конце своей работы процедура обязательно исполнит парную call "команду передачи управления обратно" (ret) в регистр IP будет восстановлен адрес команды, следующей за call. Именно поэтому в вызывающей программе call будет казаться "одной большой командой", семантика которой описывается той процедурой, управление в которую она передаёт. Вызов не "свободной процедуры", а "метода класса" ничего не меняет в данном сценарии, разве что будет передаваться и ещё один скрытый параметр - указатель this. Но сами действия предпринимаемые процессором останутся теми же самыми.

"Вызов процедуры" - очень простая, концептуально очень ёмкая и очень удобная абстракция. Все "видимые накладные расходы" на это - подготовка специального "списка аргументов", да запоминание "адреса возврата". Синхронность (т.е. прекращение выполнения вызывающей программы до тех пор, пока вызываемая не возвратит управление) здесь обеспечивается автоматически. А, например, абстракция "передача сообщения" - значительно сложнее и программисту тяжелее в её терминах мыслить. Поэтому неудивительно, что компонентная технология постаралась абстракцию вызова процедуры и сохранить, т.е. я хочу сказать - эмулировать средствами того слоя, который обеспечивает взаимодействие компонент. Ведь очевидно понятно, что просто "передать управление", т.е. заставить процессор выбирать последовательность команд из другого адресного пространства так же невозможно, как невозможно вообще прочитать и данные из него же!

Поговорим о существе возможного решения. Как преодолевается изолированность адресных пространств в отношении "разделяемых данных" мы рассмотрели ранее, в предыдущих статьях. Более подробное знание о том как это можно сделать в своей программе, вплоть до последовательности конкретных действий (безотносительно именно к COM) можно получить и здесь (с примерами кода!). Но эта технология не годится для того, чтобы на её основе можно было организовать передачу управления. И главное препятствие здесь не технического, а философского порядка - в "том" адресном пространстве "живёт" свой поток. Поэтому, даже если мы и устроим в "том" адресном пространстве разделение страниц памяти содержащих код, т.е. позволим к ним доступ из "нашего" процесса, код этот всё равно будет исполняться не "тем", а нашим собственным потоком.

Поэтому и способ реализации передачи управления будет принципиально другим. Что нужно для того, чтобы у программиста осталась иллюзия "вызова процедуры"? Выше мы об этом сказали - нужно приостановить свой поток в месте вызова "процедуры", позволить ей исполниться, и возобновить исполнение потока, как только "процедура" просигналит о своём завершении (будем пока считать, что данные мы передаём посредством "разделяемой области" - как это реализуется технически мы уже знаем). Но! "Процедура"-то должна выполняться не в нашем, а в "том" потоке! Как это сделать? Поскольку поток "передать" так же, как данные нельзя, то выход видится только один - "тот" поток должен... стоять у входа в эту самую процедуру. Т.е. "быть готовым" по"первому сигналу" начать её исполнять. А это - для нас теперь задача вполне даже реализуемая. В предыдущей статье говорилось о синхронизаторах - специальных объектах, которые умеют "усыплять" потоки, т.е. останавливать поток так, что тот совсем не получает процессорного времени и не нагружает во время своей спячки процессор.

Похоже, что принципиальные контуры технического решения начинают вырисовываться. Далее по тексту вызывающая программа естественно называется "клиент", а вызываемая не менее естественно называется "сервер". Они оба размещены в разных процессах, при этом вызываемую программу исполняет "поток сервера", а вызывающую - "поток клиента". Как мы на протяжении всего повествования говорили - инициативу (т.е. "вызывать" или "не вызывать") имеет клиент. А сервер... сервер стоит, т.е. его поток остановлен в специальном синхронизаторе C, который загодя был захвачен потоком клиента, когда клиент загружал (сам, или по его инициативе это делала операционная система) сервер. При этом "сразу за синхронизатором C" и начинается код нашей "процедуры сервера". Поэтому, для того, чтобы поток сервера получил развитие и начал исполнять процедуру не хватает только одного - чтобы клиент "отпустил" синхронизатор C. Я напомню, что синхронизатор - глобальный объект, т.е. операционная система "видит" его из любого потока, какому бы процессу он ни принадлежал - вот и вся передача сигнала на "получение управления".

Итак, поток клиента подошёл к тому месту, откуда он, по логике программы должен выполнить call для вызова процедуры. Но вместо call в данном случае поток клиента должен сделать вызов системной функции освобождения синхронизатора C. Он это и делает - "передача управления в процедуру" состоялась и процедура на серверной стороне исполняется. А что поток клиента? Он, естественно, продолжает исполняться тоже... его-то ведь никто не останавливал! Это вызов локальной процедуры автоматически синхронен, а здесь надо поток останавливать принудительно. Если этого не сделать, то можно считать, что процедуру мы и не вызывали - её результатов поток клиента не получит, так как за время выполнения процедуры сервера он просто уйдёт вперёд.

Остановку потока клиента мы сделаем по той же самой методике - заведём синхронизатор S, который загодя будет захватыватся уже сервером в момент своей загрузки. И в месте "вызова" серверной процедуры в клиенте разместим код вида "освобождение синхронизатора C - захват синхронизатора S". Тогда поток клиента, едва он освободит C, тут же и остановится в ранее захваченном сервером синхронизаторе S! Остановится... и будет в нём стоять вечно. А чтобы этого не случилось, в том месте, где серверная процедура "возвращает управление" необходимо разместить код вида "освобождение синхронизатора S - захват синхронизатора C". Нетрудно видеть, что в таком случае и "возврат управления" произойдёт корректно - едва сервер "отпустит" S, поток клиента получит развитие - его исполнение продолжится в точности тогда, когда закончит своё исполнение процедура сервера.

Но и это ещё не всё. Ведь после того, как поток клиента получил развитие поток сервера никто так и не останавливал - он так и "молотит" "процедуру сервера", хотя его никто об этом в этот раз не просил. Поэтому, после того места, где в клиенте должен бы стоять call нам нужно вновь захватить синхронизатор C - тогда поток сервера остановится в нём и будет стоять до следующего "вызова" со стороны клиента. Теперь соберём всё вместе и посмотрим - обеспечивает ли придуманный нами механизм эмуляцию "вызова процедуры".

Конструкция сервера - поток сервера в бесконечном цикле, содержащем "процедуру сервера". Эта процедура оформлена следующим образом:

  • захватить C
  • захватить S
  • <команды тела процедуры>
  • отпустить S
  • отпустить C

При этом начальное состояние синхронизатора S - захвачен потоком сервера.

Конструкция клиента - в месте вызова "процедуры сервера" находится следующая последовательность команд:

  • приготовить список аргументов, передаваемый процедуре
  • отпустить C
  • захватить S
  • отпустить S
  • захватить C

При этом начальное состояние синхронизатора C - захвачен потоком клиента.

Итак, что мы увидим при одновременном запуске обоих процессов на исполнение (это не совсем верно - процесс клиента должен всё-таки запускаться чуть-чуть раньше сервера, чтобы он успел захватить синхронизатор C)? Исходное состояние - поток сервера стоит в синхронизаторе C, поскольку тот изначально захвачен потоком клиента. Поток же клиента развивается как он хочет - захват синхронизатора C его самого ни к чему не обязывает, а синхронизатор S он и не пытался ещё захватить. Но вот поток клиента подошёл "к месту вызова", приготовил список аргументов, отпустил синхронизатор C и попытался захватить синхронизатор S. А синхронизатор S - захвачен потоком сервера, поэтому поток клиента в нём и остановится. Зато, перед этой остановкой поток клиента отпустил синхронизатор C и тем самым дал возможность потоку сервера начать исполнять процедуру... При завершении процедуры поток сервера отпустит синхронизатор S, тем самым давая возможность продолжить развитие потоку клиента. Что он и сделает - немедленно захватит синхронизатор C, который также попытается захватить и поток сервера. Но клиент должен захватить C раньше - тогда поток сервера вновь остановится "на исходной" и будет ожидать следующего "вызова" со стороны клиента...

Вот, собственно, и вся принципиальная схема эмуляции "вызова процедуры" в тех случаях, когда клиент и сервер располагаются в разных процессах. Это только схема, пока не стоит в ней искать точного отображения на механизмы COM, хотя в одном частном случае - когда клиент и сервер исполняются в разных процессах на одной и той же машине именно описанный механизм лежит в основе функционирования системного слоя COM. Чего в ней не хватает для того, чтобы это работало? Не хватает "точной передачи параметров". Это мы допустили, что посредством разделяемой области данных значения данных из одного процесса становятся видимы процессу другому. Но на самом-то деле параметры передаются через стек. Поэтому, чтобы эта схема действительно работала надо внести в неё совершение дополнительных действий - копирование данных из разделяемой области в стек перед вызовом собственно процедуры и копирование данных из стека в разделяемую область после вызова процедуры, перед "возвратом управления" клиенту.

Естественно, что один лишь вид уже описанной последовательности действий навевает смертную тоску - а если всё это в своей программе придётся реализовывать? Я не думаю, что если бы это было так, то технологии типа COM и CORBA имели бы хоть какую-то популярность. Но выполнение всех этих действий единообразно берет на себя системный слой поддержки COM, хотя, как мы видим, скрыть факта своего существования от программиста в данном случае он не может: операции с синхронизаторами и выполнение команды call - уж больно разномасштабные вещи. Взамен, COM предлагает совершенно особую модель реализации отношений "клиент-сервер" посредством внедрения в собственный процесс "заместителей", которые и клиент и сервер могут рассматривать на правах "вызова локальной процедуры из DLL". А вот о том, что это такое - в следующий раз...

предыдущий выпускархив и оглавлениеследующий выпуск
Авторские права © 2001 - 2004, Михаил Безверхов
Публикация требует разрешения автора

разделы сайта

форум

технология COM

оптимизация сайтов


© 2000-2004 Клуб программистов developing.ru