UNIX и постановка вопроса о переносимости программного обеспечения

UNIX и постановка вопроса о переносимости программного обеспечения

Интерес к опыту развития операционной системы UNIX обычно связывается с ее переносимостью, а расхожее толкование переносимости таково: «UNIX – это переносимая операционная система, потому что она написана на машинно-независимом языке «С». Если при переходе на новую машину вначале построить транслятор с языка «С» в ее коды и пропустить через этот транслятор прежние исходные тексты операционной системы, то мы и получим операционную систему UNIX для новой машины».

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

При переходе на другую архитектуру или же при попытках повышения эффективности системы значительную часть ядра операционной системы приходится переписывать заново, и большей частью на языке «С». Таким образом, язык «С» не обязательно служит для записи машинно-независимой части системы; более того, в текстах ядра, написанных на языке «С», попадаются даже ссылки на то, что конкретный транслятор должен разместить такую-то переменную в конкретном регистре машины, как это требуется для выполнения специфических аппаратурных команд.

Таким образом, с одной стороны, концепция абстрактной переносимости на базе языка «С» оказывается иллюзорной, с другой стороны, сам язык «С» подтверждает свою живучесть в качестве средства написания системного программного обеспечения при переходе на другую архитектуру (здесь мы не говорим о машинах нетрадиционной архитектуры). В этом случае на новое оборудование переносятся не столько тексты программ, сколько навыки программистов, владеющих языком «С».

Язык «С» – не единственный пример языка, с которым связывается переносимость программ. В принципе это относится к любому распространенному языку программирования. Например, существует обширная литература прикладных программ на Фортране, не привязываемых к конкретной машине, и именно с ней и с ее традициями связана живучесть этого языка. Вместе с тем конкретные пользователи Фортрана могут энергично возражать против утверждений о переносимости программ на нем, ссылаясь, например, на то, что вычислительный метод становится неработоспособным при изменении точности выполнения машинных операций с вещественными числами.

При постановке задачи о переносимости программ будем исходить из того, что любая программа – это выражение определенных знаний о задаче, способе ее решения и особенностях конкретной обстановки (оборудования, программного окружения), в которых эта программа будет выполняться. Предполагается, что знание оборудования и деталей программного окружения заключено в трансляторе с используемого языка программирования, и тем самым транслятор образует тот уровень общей структуры программного обеспечения, над которым надстраивается рассматриваемая программа и который скрывает от нее особенности конкретной машины. Если бы удалось обеспечить полное сохранение свойств транслятора при переходе на другую машину, то действительно одни и те же исходные тексты программ сохраняли бы пригодность без всяких изменений. Такой подход примерно соответствует аксиоматическому подходу в математике: если характеристики языка принять за аксиомы, то выполнение их на любой машине будет обеспечивать правильность работы программ, написанных в соответствии с этими аксиомами.

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

Поэтому для обеспечения переносимости программы мы не можем полностью полагаться на переносимость транслятора. Один из путей состоит в том, чтобы сознательно избегать использования особенностей транслятора, сохранение которых на другой машине маловероятно. Таким образом, переносимой будет не та программа, которая написана на «переносимом» языке, а та, которая явно была задумана как переносимая и составлялась с учетом этого требования. Вместе с тем и здесь нельзя говорить об абсолютной переносимости, поскольку можно лишь предположительно судить о том, какие черты существующей реализации языка программирования останутся неизменными при переходе на другие машины. Например, для машин, близких по архитектуре к РDР-11, можно было рассчитывать на то, что из двух байтов, входящих в состав 16-битового слова, младший байт будет всегда иметь меньший адрес, но, как известно, на многих машинах действует противоположное правило (и в результате при переносе системы UNIX на миникомпьютер фирмы IВМ слово «UNIX» напечаталось как «nUхi» [1]).

Ориентация лишь на неизменные черты оборудования и обстановки не исчерпывает содержания понятия переносимости. В идеале переносимый программный модуль должен заключать в себе не готовые решения о структуре программы, ориентированные на некоторую (хотя бы и абстрактно заданную) программную обстановку, а возможность развертывать конкретную программу с учетом особенностей любой программной обстановки, доступности тех или иных ресурсов и эффективности их использования, конкретных способов использования этих ресурсов, доступности тех или иных операций и т.п. Желательно, чтобы программа, способная работать в различных окружениях, не игнорировала различия между ними, а наоборот, каждый раз использовала особенности конкретного окружения. Это относится прежде всего к таким характеристикам обстановки, как доступный набор операций при обмене с терминалом или разрядность машинных слов и точность представления вещественных чисел. Для того, чтобы был возможен учет этих особенностей, используемый язык программирования не должен быть абсолютно непрозрачен для информации о соответствующих технических особенностях оборудования или операционной системы. Он должен передавать на уровень программы такую информацию, но, желательно, не в конкретных машинных терминах, а в обобщенной форме, согласованной с общим уровнем языка. В качестве примера можно назвать предоставляемую языком Ада возможность задавать диапазон и точность для целых и вещественных величин, а в Алголе 68 – запросы к обстановке (сведения о разрядности и точности) и характеристики свойств каналов обмена. Конечно, такой подход к переносимости является еще менее определенным, чем ориентация на неизменность языкового уровня, но он также необходим для обеспечения долговечности разрабатываемых программных средств.

Обращаясь к опыту развития системы UNIX, отметим, что в ней переносимость не является ни самоцелью, ни формальным требованием. Определяющей в ее разработке была ориентация на выявление и удовлетворение потребностей пользователей, в частности, на обеспечение гибкости использования каждого разрабатываемого программного средства в различных условиях. Переносимость при переходе на новое оборудование – это частный случай описанного требования. Поэтому интересно рассмотреть, как система UNIX справляется с более широкой задачей, а не сводить весь вопрос о переносимости к использованию языка «С» и программы lint, сигнализирующей об использовании непереносимых возможностей этого языка. Достаточно широкая, неформальная постановка этой задачи предполагает, что и средства, которыми она решается, не обязательно носят формальный характер. Действительно, они отражены не только в конкретных программах, но и в общих принципах, традициях, афоризмах, встречающихся в литературе по системе UNIX [2].

Среди таких принципов нужно прежде всего отметить ориентацию на разработку сравнительно небольших и относительно самостоятельных программ, а не больших многокомпонентных многоцелевых комплексов. Система UNIX развивалась как раз в тот период, когда утверждался «научный» подход к разработке больших программных систем, требующий предварительной разработки полных спецификаций и планирования всего жизненного цикла системы в целом. (Само название системы, «UNIX» вместо «МULТIСS», звучит, как вызов подобным «масштабным» проектам.) Сегодня уже признается неудовлетворительность этого громоздкого подхода, из-за которого система становится негибкой и уже к моменту завершения разработки безнадежно отстает от требований пользователей [3]. Преимущество подхода системы UNIX в том, что при изменении потребностей или обстановки достаточно изменить не все элементы системы, а лишь небольшую часть, непосредственно затронутую изменившимися условиями. Если же потребовалось изменение функций старой программы, то рекомендуется разрабатывать программу заново, а не вносить «заплаты» в прежний текст и загромождать старые программы новыми возможностями. Этот принцип относится и к документации системы, которая столь же модульна, как и описываемый ею продукт. При этом подходе по существу вместо одного большого жизненного цикла крупной системы мы имеем дело с отдельными жизненными циклами сравнительно небольших программ; переносимость в элементарном смысле слова означает независимость жизненного цикла конкретной программы от жизненного цикла нижележащих уровней – оборудования и операционной системы.

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

Еще одним путем обеспечения гибкости, используемым в системе UNIX, является автоматическое порождение текстов программ с учетом текущего окружения, равно как и учет окружения при выполнении программ. Например, из одного и того же исходного текста препроцессор может построить разные транслируемые тексты в зависимости от установки текущего каталога, из которого читаются тексты по оператору #inсludе (макроопределения, содержащиеся в прочитанных фрагментах, могут существенно повлиять и на дальнейшую обработку). Язык shеll предусматривает операции автоматического развертывания команд в зависимости от текущего каталога. Работа такой программы, как mаkе, тоже полностью определяется текущим каталогом, откуда под стандартным именем читается сценарий работы.

Принцип организации интерфейса программного модуля с окружением, согласно которому программный модуль задает некоторое символьное имя, а соответствующий этому имени конкретный объект ищется в окружении, является не просто удачной находкой авторов системы UNIX. Этот принцип, по-видимому, имеет фундаментальное значение для общей задачи представления знаний. Он используется в системах искусственного интеллекта и постепенно проникает в практические языки программирования (поиск реакции на нестандартную ситуацию по имени этой ситуации в языках ПЛ-1 и Ада, выбор реакции на внешнее сообщение по имени этого сообщения в языке Smаlltаlk-80, широкое использование внешних ссылок по именам в операционных системах для комплексирования программ). В системе UNIX эти средства используются как при порождении текстов программ (макрогенерация), так и динамически, во время работы этих программ. Использование одних и тех же имен стандартных процедур (например, ввода/вывода) в реализациях системы UNIX на разных типах машин служит примером применения того же принципа. Естественно, поиск по имени на фазе макрогенерации или трансляции экономнее с точки зрения затрат на выполнение программы, но динамическое использование этих средств также бывает необходимым.

С неожиданным подтверждением основных принципов системы UNIX автор этих строк встретился, сопоставив их со своей прежней работой [4], написанной еще до знакомства с системой UNIX. В этой работе рассматриваются принципы организации естественного языка, обеспечивающие ему гибкость и применимость в самых различных областях человеческой деятельности. В отличие от формальных моделей языка, в основу которых кладется единый общий план, охватывающий всю создаваемую модель, естественный язык можно рассматривать как совокупность относительно независимых подсистем, а единство языка обеспечивается тем, что различные подсистемы опираются на общий набор средств нижнего уровня. В то же время этот набор средств нижнего уровня не обязателен ни для какой конкретной подсистемы. Запас подсистем на всех уровнях открыт для расширения, вновь возникающие или изменяющиеся подсистемы могут основываться как на уже имеющихся средствах нижнего уровня, так и создавать себе новые. Общая база, необходимая для совместного функционирования всех этих средств, невелика по объему и тоже может быть заменена на другую при сохранении почти всех взаимоотношений между системами более высоких уровней (такая замена происходит, например, при переходе от устной формы языка к письменной). Наконец, при взаимодействии между подсистемами широко применяется рассмотренная выше идентификация по имени, при которой одно и то же имя может использоваться в различных подсистемах, выделяя в них различные, но функционально аналогичные объекты. Все это удивительно согласуется с принципами, практически выработанными в процессе развития системы UNIX.

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

Список литературы

1. Р.J.Jаliсs, Т.S.Неinеs. Тrаnsроrting а роrtаblе ореrаting sуstеm UNIX tо аn IВМ mini-соmрutеr. Соmmuniсаtiоns оf thе АСМ, v. 26, nо. 12 (1983), 1066-1072.

2. М.D.Мсilrоу, Е.N.Рinsоn, В.А.Таguе. UNIX timе-shаring sуstеm: Fоrеwоrd. Веll Sуstеm Тесhniсаl Jоurnаl, v. 57, nо. 6, раrt 2 (Julу–Аugust, 1978), 1899-1904.

3. J.Martin. Applicatiоn devеlорmеnt withоut рrоgrаmmers. Sаvаnt institutе, 1981.

4. Г.С.Цейтин. О соотношении естественного языка и формальной модели. Вопросы кибернетики. Общение с ЭВМ на естественном языке. Под ред. В.Ю.Розенцвейга. М., 1982, 20-34.

Материалы международной конференции SORUCOM 2011 (12–16 сентября 2011 года)
Помещена в музей с разрешения автора 16 декабря 2013