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

Жизнь - это процесс...

Приступая к этой статье я долго думал. С одной стороны, мы проскакиваем мимо очень важных сторон COM - библиотеки типов, передачи ошибки, множественного наследования и т.д. С другой стороны - объяснить, почему их конструкция устроена именно такой, не объясняя того, что COM - междупроцессная технология, убедительно тоже невозможно. Поэтому, наверное, нужно сделать так - до какого-то предела нам следует рассмотреть междупроцессное взаимодействие компонентов в COM, а потом сделать небольшой шаг в сторону - рассмотреть пропущенное, но уже с учётом изученного. Методически, как я тут обнаружил, нам здорово повезло сравнительно с теми, кто с чистого листа изучает CORBA. В CORBA нет "внутрипроцессных серверов", соответственно, нет и возможности раздельно объяснить взаимодействие двоичных разделённых компонент и самого междупроцессного взаимодействия. Я представил себя на месте преподавателя CORBA в этом случае и как-то мне стало хорошо - пока что наши статьи касались только базовых основ и конструкций, которые мы очень успешно разобрали не выходя за пределы "только DLL" и не связываясь с проблемами многопроцессной работы. И то, я не уверен, что мне очень хорошо удалось объяснить то, что статический тип имеет два аспекта... Как в данном случае поступил бы преподаватель CORBA - не знаю. А вот нам этот самый шаг дастся значительно проще - для нас все эти категории последовательны и естественно не смешиваются. Так что, уважаемые читатели, в наших статьях открывается новое качество - принципы, на которых построена технология CORBA в данном случае те же самые, которые мы разбираем на примере COM.

Вдохновившись таким вступлением, приступим. Тема эта - сравнима по объёму со всем предыдущим материалом, поэтому здесь я только начну обозначать проблему. В последующих статьях мы всё время будем возвращаться к этой модели и добавлять в неё детали - до тех пор, пока не получим совершенно подробного представления.

Предыдущее изложение позволило нам понять философию зарождения и взаимодействия подлинно двоичных объектов - объектов, которые не знали друг друга в исходном тексте. Мы увидели, что понятие "объект" в таком случае имеет смысл только с философской и модельной стороны. Также, прямо не говорилось, но, как я надеюсь, теперь и говорить не нужно - COM или какая-нибудь аналогичная технология возможны только в объектной (а не процедурной) парадигме программирования. Сейчас вы должны ясно понимать по каким именно причинам проистекает и первое и второе. Но на протяжении всего предыдущего изложения я изредка получал и вопрос, на который ответить тогда не мог - а зачем так тщательно городить всю эту двоичную изоляцию клиента и сервера?

До тех пор, пока мы рассматривали только inproc-сервер, это действительно было необъяснимо, во всяком случае, вызов процедуры из DLL выглядел вполне конкурентоспособно с нашими конструкциями. Но, начиная с этой статьи, на этот вопрос начнёт формироваться достойный ответ. Сегодня у нас - начало прорыва... прорыва за границу процесса.

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

Если вы помните, то в статье COM - что должен знать допризывник? мы немного говорили о модели, в которой нужно понимать работу машины. В ней существует устройство, которое одну за одной, в предписанной последовательности, выбирает команды программы и их исполняет. Устройство это называется "процессор", оно составляет один из важнейших ресурсов машины. Другим столь же важным ресурсом машины является "память" - устройство, которое может просто в неизменном виде хранить то, что в неё записали и позволяет содержимое читать. Поэтому работа примитивнейшей машины состоит в том, что процессор из памяти выбирает команду, её исполняет, выбирает следующую команду... и т.д. Конечно, у нормальной машины существует побольше ресурсов - устройства ввода-вывода, какие-то процессоры специальной обработки и т.п. Распоряжается всеми ресурсами машины специальная первичная управляющая программа, она называется "операционная система" (ОС, в просторечии - "система"), а исполняемая "программа пользователя" потребляет эти ресурсы в режиме, допущенном операционной системой.

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

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

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

Поэтому вполне корректно говорить о том, что всякая программа исполняется в своём "потоке", что есть общепринятое сокращение от слов "поток управления", а фактически и представляет собой не что иное, как "внимание процессора". Ситуация не изменится, если мы будем в этих условиях рассматривать не две программы, а N - внешне ситуация будет выглядеть так, как будто бы программы одновременно исполняются все, но каждая на в N раз более медленном процессоре. Если, конечно сам квант "внимания процессора" будет не слишком большим, иначе "рывки" внешний наблюдатель всё-таки заметит. Парадоксально, но "незаметный для наблюдателя квант внимания" может быть довольно большим - в системе Windows NT потоки переключаются один раз в 200 мсек, т.е. каждый поток непрерывно работает 0.2 сек и это почти не производит на наблюдателя тягостного впечатления.

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

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

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

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

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

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

Здесь нужно отметить, что, поскольку разбираемые категории - фундаментальны ещё со времён первых компьютеров, на любом компьютере архитектуры фон Неймана и в любой операционной системе они имеют место быть. Они не везде называются так, как мы только что сказали. Но по своей природе (и связанным с ней свойствам) - они одинаковы. Так, в операционной системе OS/2 "поток" (thread, термин Win32 и NT) принято называть "нитью". А в операционной системе Unix процессы делятся на "лёгкие" и "тяжёлые". Лёгкие те, которые исполняются в одном и том же адресном пространстве. Тяжёлые те, что имеют адресное пространство своё... Что есть что в наших терминах, я думаю, уже вполне понятно. Поскольку COM - технология Windows, то и терминологию, обозначающую процессы мы будем применять "родную" - "поток" и "процесс".

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

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

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

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

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

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

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

форум

технология COM

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


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