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

Обмен данными между процессами

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

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

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

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

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

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

Но, что определённо сбивает с толку, так это упоминание в его названии слова "файл". Файлы - это очень громоздкие сущности, требующие для своего обслуживания огромной скрытой от программиста работы. Это верно, но, если внимательно читать соответствующий раздел, можно заподозрить одно обстоятельство - вполне возможно, что в реализации данного механизма от "файла" в нём используется, может быть, и не более, чем одно название. Понеже это не файл проецируется в память, это, скорее, страницы оперативной памяти отображаются на пространство файла. Так что подстрочный перевод термина memory-mapped files не совсем семантически и верен. Предположение это вполне подтверждается при внимательном чтении MSDN - параметр hFile функции CreateFileMapping не обязан всегда быть корректным дескриптором файла. Если его значение - INVALID_HANDLE_VALUE, то система всё равно создаст проекцию "файла", но уже - проекцию, отображаемую в системный страничный файл подкачки! Если уж и этот файл считать "файлом", а не какой-то особо выделенной областью на диске с организованным к ней сверхбыстрым доступом, то, наверное, надо признать, что проекция файлов всё-таки основана на "файле".

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

Но, для полноты картины, нужно также сказать и о том, для чего был придуман "механизм проекции файлов", без этого представление обо всём вместе будет явно неполным. Основное системное предназначение механизма проекции - отобразить в память образ исполнимого файла, содержащего программу. В системах DOS и Windows 3.x загрузка происходила просто - система перегоняла файл целиком с диска в буфер основной памяти. Памяти при этом часто не хватало, загрузка происходила долго и образ файла не мог превысить размера свободной физической памяти. А поскольку загрузка файла на исполнение - одна из основных операций, то это самым неблагоприятным образом отражалось на потребительских свойствах системы. Механизм проекции разом решил все эти проблемы - и исполнимый файл в формате PE (Portable Executable) можно загрузить в необходимое число регионов (в каждый - по сегменту), и доступ к страницам образа организуется по мере надобности, а не тогда, когда именно весь большой файл будет загружен, и размер физической памяти для размещения всего комплекса модулей, составляющих задачу перестал иметь какое-либо значение. А, главное, - если в несколько изолированных адресных пространств загружается один и тот же образ DLL теперь это не означает, что пространство им занимаемое должно несколько раз дублироваться. Его страницы могут быть спроецированы в несколько адресных пространств "непосредственно" - вот откуда берёт начало и к чему приводит именно механизм проекции физических страниц (и знание этого обстоятельства для нашего изложения очень важно - ниже мы его используем). Из этого, впрочем, имеются и другие следствия, не совсем следующие потоку изложения данной статьи. Поэтому интересующийся именно ими читатель приглашается сюда.

Мы же следуем теме - если подобный механизм встроен в систему как одно из основных звеньев - и для организации "коммунальных" физических страниц и для быстрой проекции исполнимых файлов, то его функциональность (с какими-то ограничениями - в частности, PE система умеет загружать в несколько несмежных регионов, а вот "обычный" файл - нет) можно предоставить и пользователю. В конце-концов, и перед ним возникают те же проблемы, что и перед самой операционной системой.

Именно особенности описанного выше базового механизма определяют и существующие в системе возможности организовать "разделяемые между процессами данные в памяти". Самый простой и самый эффективный (физически - он не эффективнее других, но не требует от программиста каких-то специальных программных действий "руками") из них - способ, основанный на том, что в составе собственного исполняемого модуля создаётся особый, видимый всем копиям данного модуля во всех процессах, сегмент данных. Видимый именно в одной и той же копии, естественно. Это возможно потому, что: 1)исполнимый файл проецируется; 2)всякий сегмент загружается в свой регион адресного пространства; 3)файл или его часть можно спроецировать в несколько адресных пространств когерентно; и 4)в составе атрибутов сегментов в PE есть соответствующее значение флажка. По сути, здесь вся работа программиста - организовать такой сегмент, а собственно проекцию выполняет системный загрузчик.

Делается это следующим образом. Программист должен объявить в своей программе сегмент с каким-то именем, отличным от имени системных сегментов (они начинаются с символа "точка"), разместить в этом сегменте требуемые для "обобществления" переменные и закрыть сегмент. В программе эти переменные адресуются точно так же, как и все другие переменные - создание дополнительного сегмента данных это только указание компилятору, что он должен при кодогенерации перечисленные переменные в указанном сегменте разместить. Делает объявление отдельного сегмента директива #pragma data_seg("<имя сегмента>"). Далее, при сборке модуля,нужно указать линкеру, чтобы он присвоил сегменту с назначенным именем атрибут shared. Это указывается опцией линкера -SECTION: <имя сегмента>, RWS. Всё! После загрузки модуля на исполнение сегмент с данным именем будет содержать единственную на все такие исполняющиеся модули копию данных. Правда, это справедливо только для копий "своего модуля", а если требуется разделять данные не среди нескольких копий одного и того же модуля, но среди нескольких разных модулей? Тогда способ этот оказывается неприменим...

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

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

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

Хотя это и кажется "крайне маловероятным" (что такое 64K в адресном пространстве 2G? Пылинка в космосе!), тем не менее это событие будет всё-таки наблюдаться. И, как показывает практика, "редко" оно наблюдается только для программиста, разрабатывающего данную программу. А вот пользователю оно наблюдается вполне достаточно "часто", чтобы можно было сделать вывод о "ненадёжности" такой программы. Поэтому более надёжным в этом отношении является использование функции MapViewOfFile и проецирование региона куда получится - место для 64K в двухгигабайтном адресном пространстве действительно всегда можно найти.

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

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

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

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

форум

технология COM

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


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