Техническим журналистам свойственно "рисовать" объекты и предметы IT в идиллических тонах, превознося блага новых решений. "Картина" на тему: "Разработка программ в среде операционной платформы Inferno", которая собственно сейчас находится в ваших руках, писалась в основном в природных тонах, хотя и необычными кистями…
Предисловие
В конструкции ОС Inferno имеется немало любопытных и подчас инновационных концепций, одни из них могли бы стать предметом специального исследования (ядро, виртуальная машина, коммуникационный протокол), другие - даже отдельной книги (язык программирования). Различные аспекты ОС и ее компонент получили освещение в ряде русскоязычных статей (ссылки приведены в послесловии). Такое положение, с одной стороны, облегчало работу над обзором, а с другой - создавало трудности, потому как заставляло постоянно оглядываться на уже проделанную работу с тем, чтобы избежать непотребных повторений. Автор этих строк ставил своей целью ответить на тот круг вопросов, который наилучшим образом позволяет раскрыть данную тему.
"Ничего святого"
В начале XIV столетия - на переломе классического Средневековья и эпохи гуманизма - итальянский поэт по имени Дуранте Алигъери, сокращенно Данте, находившийся в изгнании и дважды заочно осужденный к сожжению в своей родной Флоренции, написал поэму "Комедия" (автор назвал эпический труд так согласно нормам античной поэтики, как произведение, завершающееся благополучной и радостной развязкой). В XVI как обозначение высочайшей степени совершенства книги к ее названию восхищенным потомством был добавлен эпитет "Божественная". В двух словах, "Божественная Комедия" заключает в себе в форме аллегорического видения загробной жизни нравственно-религиозную мысль с поучительной целью. Поэма эта не принадлежит ни к какому определенному жанру - это совершенно своеобразная, единственная в своем роде смесь всех элементов различных направлений поэзии.
Наше время. 1995 - 1996 годы "нирваны нестандартно мыслящих программистов" Bell Labs' CSRC (Computing Sciences Research Center) ознаменовались временной заморозкой работ над ОС Plan 9 (точнее сказать, ее исследовательской версией - Brazil) по причине проектирования новой системы. Главный архитектор проекта, Роб Пайк, под впечатлением "Божественной Комедии", а отнюдь не, скажем, очередного бестолкового фильма с участием Жан Клода Ван-Дамма (речь идет о картине "Инферно") - шутка, окрещает систему "Inferno" (Ад - первая часть книги). Бескрайнее пространство имен бестселлера мировой литературы обеспечило названиями также и важнейшие компонентные подсистемы новой ОС: Limbo (первый, внешний круг Ада, Лимб), Styx (ливень третьего круга образует поток, который в пятом круге разливается в озеро стоячей воды и образует смрадное болото Стикс) и Dis (Стикс окружает цитадель Ада, Дит, окрестности которого составляют шестой круг).
Теперь оставим в покое "бездуховное" именование и слегка окунемся в биографию мира Inferno, в которой вроде бы и нет ничего особо диковинного, тем не менее внимания она все же заслуживает.
Итак, во всеуслышанье заговорили об Inferno (a.k.a. Inferno Network Software Solutions) в мае 1996 г., именно тогда ОС была впервые анонсирована. В сентябре того же года выходит ее бета-версия. Возникает софтверный стартап Старого Света Vita Nuova (со штаб-квартирой в Йорке, Англия). С названием, заимствованным у автобиографической исповеди Данте "Новая Жизнь" (La Vita Nuova), которую, к слову, можно считать прелюдией к "Божественной Комедии", компания специализируется на технологиях распределенных вычислений, используемых в сетевых устройствах и встраиваемых (embedded) системах.
Первая и вторая версии ОС реализованы Inferno Business Unit (подразделение Lucent Technologies, также как и Bell Labs) в марте и ноябре 1997 г. соответственно. Из-за смещения интересов, человеко-годы "плановых ребят" начинают тратится по назначению, т.е. на новую версию Plan 9, что буквально толкает Inferno Business Unit на поиски организаций для развития проекта. Результаты этой процедуры не заставили себя долго ждать и вскоре подписывается стратегическое партнерское соглашение с Vita Nuova, следуя которому последняя с 1997 по 1999 гг. вовлекается в активное продвижение продукта.
В марте 2000 г. Vita Nuova приобретает исключительные права на разработку и распространение Inferno (а также физической продукции Plan 9). Под началом указанной компании уже в июле 2000 г. выходит третья версия системы. Три года спустя, в декабре 2003 г., появляется текущая, предварительная четвертая версия.
Lasciate ogni speranza voi qu'entrate
"Оставьте входящие сюда всякую надежду" - так гласит надпись над вратами в Ад по версии Данте. Что же касается "Ада" в исполнении компьютерных ученых с мировыми именами, то вход в него предваряют следующие слова (так звучит недословное переложение ответа на вопрос "Что такое Inferno?"):
"Inferno - это компактная операционная среда для построения распределенных и сетевых систем из множества разнообразных устройств и платформ. Благодаря инновационным, уникальным характеристикам Inferno вы получаете непревзойденный набор программных инструментальных средств".
Согласитесь, слова по меньшей мере громкие. Доказывать их истинность из-за сложности и многогранности сего процесса мы не станем, взамен предоставим факты, способные направить на путь подтверждения. Подобно Данте, путешествующему по кругам Ада в сопровождении Вергилия, мы будем исследовать пружины механизма программирования в Inferno, проходя ярус за ярусом ее каркас (несколько условный, взятый на вооружение для удобства повествования) сверху вниз.
На предположительном верхнем, командном уровне, разработка программ в Inferno имеет сравнительно много общего с Unix и Plan 9. Знакомые с первой и/или ее клонами найдут здесь ставшие классическими инструментальные средства: поиска (вместо одиозной find легкий pipe: "du -a | grep имя_файла"), сравнения (diff и cmp), архивирования+сжатия (tar и gzip), строкового и потокового редактирования (ed и sed), генерирования компиляторов (lex и yacc) и др. Все они были упрощены до уровня здравого минимализма и переписаны на новом языке. Plan 9-фаны обнаружат C-компиляторы (0c, 1c, 2c, 5c, 8c, kc, qc, vc) и ассемблеры (0a, 1a, 2a, 5a, 8a, ka, qa, va), утилиту mk, программируемый символьный отладчик acid, Acme IDE, rc-подобную оболочку sh (плюс ее альтернативу под названием tiny, а также mash - устаревшую альтернативу), поддержку механизма plumbing, системные вызовы управления пространством имен: bind, mount, unmount. Очевидно, что наследство богатое и знаменитое, но им созидатели Inferno не ограничились. В "боевой" комплект ОС вошли: новый, безопасный, модульный и т.д. язык программирования высокого уровня Limbo и необычная виртуальная машина Dis, модифицированная реализация подсистемы Tk (до 4-й версии Inferno вместе с ядром отвечала за управление окнами) для Limbo, интерпретатор языка Tcl.
Графические демо сценарии на языке shell
Широкое распространение в Inferno получила концепция динамически загружаемых модулей. Одним из примеров ее применения является оболочка sh, которую та действительно гальванизировала, предоставив std (эквивалент "стандартной библиотеки" для командной строки), regex (распознавание регулярных выражений), expr (простая целочисленная арифметика), file2chan (создание файла в пространстве имен с детерминированными свойствами), tk (доступ к графической подсистеме Inferno Tk, а также взаимодействие каналов, на котором собственно основан механизм событий Tk в ОС) и прочие модули. Поскольку основные навыки работы с командной строкой и, скажем, тем же Tk получить не составляет больших усилий, мы попробуем разобраться более или менее детально с конкретной работой этого… существа.
#!/dis/sh
load tk
wid:= ${tk window 'Тест окно'}
Что мы видим: стандартный заголовок для сценария sh, загрузку модуля tk, и создание нового Tk-окна с заголовком "Тест окно". Последняя операция возвращает идентификатор на окно (переменная $wid), который также является каналом (через него можно получать сообщения от окна). Определив элементарную функцию-исполнитель:
fn x {
tk $wid $*
}
можно приступать к выполнению команд Tk. Например, создать кнопку в новом окне можно следующим образом:
x button.b -text {Надпись на кнопке}
x pack.b
x update
Поскольку Inferno Tk отправляет события через каналы Limbo, модуль Tk обеспечивает доступ к простым строковым каналам. Канал создается с помощью команды chan:
chan cmd
Команда tk namechan предназначена для создания канала, известного Tk:
tk namechan $wid cmd
send отправляет строку в канал; принятое значение присваивается встроенной переменной ${recv}. Теперь можно получать события от Tk:
x.b configure -command {send cmd Текст в консоли}
while {} {
echo ${recv cmd}
}
В результате создается фоновый процесс, выводящий в консоли сообщение "Текст в консоли" каждый раз, когда нажимается кнопка с надписью "Надпись на кнопке". Взаимодействие с оконным менеджером осуществляется на схожий манер. Когда инициализируется окно, то оно автоматически ассоциируется с каналом под номером равным значению идентификатора окна. Поступающие при этом строки являются событиями оконного менеджера, наподобие изменения размеров (resize) и перемещения (move). При желании она могут интерпретироваться "по иному", или же отправляться назад оконному менеджеру для обработки по умолчанию посредством tk winctl. Полезной идиомой является ниже указанная конструкция, которая выполняет всю обычную обработку событий окна:
while {} {
tk winctl $wid ${recv $wid}
} &
И последнее замечание, операция закрытия окна вызывает останов всех процессов текущей группы, поэтому чтобы не закрывалось окно оболочки, из которой был запущен графический сценарий, следует разветвлять (fork) группу процессов с помощью раннего выполнения команды pctl newpgrp.
Limbo: коммуникация каналов и основные графические возможности
Будучи предложенным новичком "комнаты Unix" (the Unix room) Шоном Дорвардом (Sean Dorward), язык Limbo суть безусловно интегральная составляющая часть Inferno. Перед обсуждением языка не помешает ознакомится с рядом фундаментальных императивов, который был выдвинут при его проектировании:
- Малый размер - язык не должен требовать большого компилятора/runtime-системы.
- Портабельность - скомпилированный код должен выполняться на различных машинных архитектурах.
- Скорость - время исполнения должно приближаться к таковому в C.
- Динамичность - чтобы минимизировать использование памяти и увеличить гибкость, части кода и данных должны быть загружаемыми и выгружаемыми во время исполнения.
- Безопасность - проверки типов во время компиляции и исполнения должны использоваться с тем, чтобы предотвратить запуск некорректных программ.
- Многозадачность - необходима легкость в написании программ, содержащих несколько процессов (в терминологии Inferno - нитей), которые коммуникатируют между собой и/или совместно используют данные.
- Высокоуровневая сборка мусора - программисты должны сбросить с плеч бремя явного освобождения памяти.
Что же получилось в результате? Прежде всего удачный инструмент, язык простой, но мощный, и незаменимый при разработки параллельных, распределенных систем. Чтобы бегло ознакомиться с особенностями языка, рассмотрим пару примеров (замечание: номера строк введены для удобства ссылок). Полагаем, что можно безболезненно обойтись без "Hello World!" и ей подобных, поэтому приводим примеры более серьезных программ.
01 implement Targ1;
02
03 include "sys.m";
04 sys: Sys;
05 include "draw.m";
06
07 Targ1: module {
08 init: fn (nil: ref Draw->Context, argv: list of string);
09};
10
11 init (nil: ref Draw->Context, argv: list of string)
12 {
13 sys = load Sys Sys->PATH;
14 n:= len argv;
15 sync:= chan of int;
16 spawn targ (sync, n);
17 # Вывод параметров командной строки с секундной задержкой
18 for (i:= 0; i < n; i++) {
19 sys->print ("%d: %s\n", i, hd argv);
20 argv = tl argv;
21 <-sync;
22 }
23}
24
25 targ (sync: chan of int, n: int)
26 {
27 for (;;) {
28 sys->sleep (1000);
29 sync <-= 1;
30 }
31}
Первая программа записывает свои параметры, разделенные пробелами, по одному на каждую строку файла вывода. Взглянув на синтаксис языка Limbo, можно констатировать факт его попадания под отнюдь не дурное влияние C (что неудивительно) и Pascal - это выражения, операторы и соглашения первого и объявления второго. Структурная программная организация (по всей видимости, уходит корнями в Modula-2) - модули. Так, первая строка в листинге указывает на создание модуля Targ1. Имя файла не обязательно должно совпадать с именем модуля. В 3-й и 5-й строках объявляются стандартные встроенные модули sys.m (обеспечивает основные системные и В/В примитивы) и draw.m (графические возможности). В Limbo отсутствует препроцессор, как у C, следовательно, include является ключевым словом, а не директивой препроцессора, тем не менее по функциональности они эквивалентны. Имя файла, предваренное include, всегда заключается в кавычки ("модуль.m"). 4-я строка объявляет переменную sys типа Sys (Limbo регистро-зависимый язык), определенную в модуле sys.m. По соглашению, внутреннее значение sys на данном этапе равняется nil (ссылка на "ничего").
Каждый из модулей Limbo состоит из разделов интерфейса и реализации. В интерфейсе объявляются данные и методы, типы данных и константы модуля, в примере это строки с 7-й по 9-ю - модуль Targ1 и его единственная функция init с двумя аргументами: ref Draw->Context и list of string. Подобная init необходима во всех программах, вызывающихся из командной строки Inferno; она во многом аналогична функции main языка C, поскольку обозначает начало выполнения инструкций. Аргумент ref Draw->Context определен в draw.m, он используется для захвата контекста показа, потребность в нем есть даже в том случае, если программа совсем не работает с графикой, так, в нашем примере, этот аргумент может быть назван nil. list of string есть список (группа последовательных, упорядоченных элементов) параметров командной строки.
В реализации размещаются тела функций модуля, в примере это строки с 11-й по 31-ю. В 13-й посредством ключевого слова load и константы PATH, содержащей путь к модулю, создается ссылка на реализацию библиотечного модуля Sys, которая присваивается переменной sys. Происходит связывание программы с модулем, при этом первая получает доступ ко всем данным и методам последнего. В следующей строке под номером 14 переменной n присваивается значение количества элементов в списке параметров.
Limbo присуща легкость в организации упреждающей многозадачности (другими словами, созданию асинхронных, независимых нитей, совместно использующих одно общее пространство памяти) и управлении взаимодействием между ними с помощью коммуникационного канала. В 15-й строке программы объявляется канал-коммуникатор sync целого типа. Далее, строка 16, с использованием оператора spawn инициируется отдельная нить targ, служащая темпо-регулятором показа параметров командной строки; с этого момента функции init и targ будут запускаться параллельно в независимых нитях. 17-я служит комментарием - в Limbo комментарии начинаются с шарпа и распространяются на всю оставшуюся строку. В строках с 18-й по 22-ю задается цикл секундного ожидания и вывода параметров командной строки. Инкремент (фигурирующий в заголовке цикла) допускается только в постфиксной форме. Перемещение по элементам argv выполняется посредством операторов hd (первый элемент списка) и tl (часть списка без первого элемента). Последнее выражение цикла - ожидание значения, получаемого из канала. В targ же совершается секундная задержка, - это строка 27, где функция sys->sleep получает время в миллисекундах как аргумент, и отправка единицы. После n-передач выполняется выход из цикла.
01 implement Targ2;
02
03 include "sys.m";
04 sys: Sys;
05 include "draw.m";
06 draw: Draw;
07 Point, Rect, Display, Image, Font, Screen, Context: import draw;
08
09 Targ2: module {
10 init: fn (ctxt: ref Draw->Context, argv: list of string);
11};
12
13 init (ctxt: ref Draw->Context, argv: list of string)
14 {
15 sys = load Sys Sys->PATH;
16 draw = load Draw Draw->PATH;
17
18 display:= ctxt.display;
19 screen:= ctxt.screen;
20
21 black:= display.color (Draw->Black);
22 grey:= display.rgb (192,192,192);
23 font:= Font.open (display, "*default*");
24 wr:= Rect ((100,100), (325,150));
25 r:= Rect ((125,110), (300,140));
26
27 win:= screen.newwindow (wr, Draw->White);
28
29 n:= len argv;
30 for (i:= 0; i < n; i++) {
31 win.draw (r, grey, nil, r.min);
32 win.text (r.min.add ((5,8)), black, (0,0), font, string i+": "+hd argv);
33 argv = tl argv;
34 sys->sleep (1000);
35 }
36}
Одну из важнейших ролей в Inferno играют графические возможности. На втором примере, где параметры командной строки показываются в окне, попытаемся по возможности сжато разобраться с использованием модуля Draw, в котором реализованы низкие уровни графической системы.
В 6-й строке объявляется переменная draw типа Draw, в следующей за ней импортируются основные типы библиотеки. Строки с 9-й по 11-ю (интерфейс), ref Draw->Context уже не nil, а ctxt, поскольку необходима "настоящая" графика. Переходим к реализации. После строк 15 и 16, в которых загружаются модули Sys и Draw, для обращения к типам Display и Screen происходит создание локальных переменных display и screen. Первая, типа ref Draw->Display, представляет связь с физическим устройством отображения, CRT или LCD дисплей, на котором программа будет рисовать. Вторая, типа ref Draw->Screen, является структурой данных, используемой системой для управления окнами.
Следующий этап, строки 21-25, - распределение графических единиц: цветов, прямоугольников и шрифтов. Цвет black создается посредством имени, известному Draw; grey же определяется с использованием тройки красный/зеленый/синий (red/green/blue, RGB). Копия стандартного шрифта *default* загружается в переменную, удачно названную font. Модуль Draw содержит два геометрических типа: Point, точка с целочисленными координатами x и y, описывающая верхний левый угол соответствующего пикселя, и Rect, прямоугольная область, определяемая двумя точками - верхней левой (Rect.min) и нижней правой (Rect.max). Прямоугольник wr описан размером и размещением окна: верхний левый угол (100,100), нижний правый угол (325, 150). Прямоугольник r служит блоком серого цвета. В 27-й строке создается окно win; первый подразумевающийся аргумент, Screen, описывает то, где и как окно размещается и его цвет. Наконец, в теле цикла, в 31-й и 32-й строках, окно прорисовывается: вызов win.draw создает серый блок, определенный r, а win.text выводит сообщение. Выражение r.min.add ((5,8)) идентифицирует верхний левый угол текста, добавляя точку (5,8) к координатам верхнего левого угла прямоугольника r.min.
После того как программа завершит свою работу и последняя ссылка на все частные графические ресурсы исчерпается, в дело вступит сборщик мусора, т.е. заботится об удалении окна с экрана не нужно.
Dis, ассемблер
Виртуальная машина Dis представляет собой среду выполнения для программ, запущенных в Inferno. В общем, Dis - это программный процессор CISC-подобного типа, трехоперандная машина с архитектурой "память-в-память". Limbо-программы компилируются в байт-код (промежуточное представление) Dis. Когда программа загружается в память для выполнения, байт-коды транслируются в более эффективный формат для выполнения. Формат этот - поток команд ассемблера Dis. Код Dis может либо интерпретироваться посредством C библиотеки в системные вызовы ОС-хозяина, либо компилироваться опциональным on-the-fly (just-in-time, JIT) компилятором в "родной" машинный код используемого аппаратного обеспечения. Архитектура "память-в-память" по существу бесконечная регистровая машина, со множеством регистров равным количеству "слов" в памяти, что упрощает on-the-fly компиляцию на большинство современных целевых процессоров. Команды Dis великолепно соответствуют архитектурному набору процессорных команд, вследствие чего результирующий код по скоростным характеристикам не уступает скомпилированному C.
Несмотря на то что изучение нового языка, к тому же такого не тривиального как Dis, является весьма полезным занятием, именно этот язык осваивать нет особой необходимости. Есть удобный Limbo с поразительными скоростными характеристиками и прочими свойствами, которые буквально заставляют побрезговать ассемблером. Кроме того, (в основном из-за вышеуказанной причины) в системе отсутствует Limbo версия ассемблера, наличествует лишь гостевая команда, которая заставляет желать лучшего.
Все же хотя бы проиллюстрировать язык виртуальной машины мы можем (представленный далее код получен в результате дизассемблирования программы Targ1):
lea 8(mp), 56(fp)
load 0(mp), 56(fp), 36(mp)
lenl 36(fp), 48(fp)
newcw 44(fp)
frame $1, 56(fp)
movp 44(fp), 32(56(fp))
spawn 56(fp), $20
movw $0, 40(fp)
bgew 40(fp), 48(fp), $19
frame $2, 52(fp)
movp 4(mp), 32(52(fp))
movw 40(fp), 36(52(fp))
headp 36(fp), 40(52(fp))
lea 56(fp), 16(52(fp))
mcall 52(fp), $0, 36(mp)
tail 36(fp), 36(fp)
recv 44(fp), 52(fp)
addw $1, 40(fp)
jmp $8
ret
mframe 36(mp), $1, 36(fp)
movw $1000, 32(36(fp))
lea 40(fp), 16(36(fp))
mcall 36(fp), $1, 36(mp)
send $1, 32(fp)
jmp $20
Портирование ядра
Сердцем системы есть связка ядро и подчиненный ему коммуникационный протокол Styx. Нижний уровень Inferno - очень малых размеров традиционное монолитное ядро, отвечает за такие задания, как управление данными и ресурсами, сеть, защиту, создание и поддержку пространств имен.
Исходный код ядра написан на ANSI C, как результат этого - портирование Inferno на новую архитектуру, для которой доступен компилятор с C, является сравнительно простым занятием. В общем и целом, код ядра состоит из портабельной части, совместно-используемой всеми архитектурами, и процессоро-специфической части - для каждой поддерживаемой архитектуры. Портабельный код часто транслируется и размещается в библиотеке, связанной с конкретной архитектурой. Построение ядра выполняется путем трансляции архитектуро-специфического кода и загрузки его с библиотеками, содержащими портабельный код. Поддержка для новой архитектуры обеспечивается либо приобретением или построением компилятора с языка C платформы и использованием его для трансляции портабельного кода в библиотеки, либо написанием собственного архитектуро-специфического кода с последующей загрузкой его с библиотеками. Архитектуро-специфический код характеризуется несколькими простыми функциями (test-and-set, управления уровнем прерываний, управления информацией метки планировщика, устройств плавающей точки), которые необходимо реализовать на манер платформо-специфической модели. Новому порту также понадобятся драйверы устройств для соответствующей конфигурации. Они могут быть взяты из уже доступного набора драйверов и адаптированы, или же написаны с нуля. Ядро требует загрузки на целевую платформу, она обычно выполняется платформо-специфическим начальным загрузчиком, обеспечивающим также функции инициализации памяти и уровня прерываний. Загрузка ядра завершается началом инициализации интегрированных в него драйверов.
Послесловие
Существует, и по сию пору дает себя знать, некий странный взгляд на Inferno, заключающийся в том, что ОС - игрушечная. Такое понимание не только наивно - оно решительно, в самом корне, противоречит духу и строю ПО, всей его идейно-философской концепции. Inferno - не для отдохновения и не для забавы, она - для зрелого и мыслящего изучения. И, поверьте, затраченные при этом ресурсы, окупятся позднее с лихвой. Поэтому после демонстрации основ ОС и программирования в ней, спешим одарить ссылками на релевантные информационные источники.
Официальный сайт Vita Nuova,
содержит наибольшее количество информации о системе. На закаленном временем
немецком ресурсе Vorlesungen
в каталоге guide/ доступна книга Inferno User's Guide, и лекции по Inferno -
в pdf. На следующем сайте, присутствует
руководство Inferno Progammer's Guide - файл ProgGuide.pdf, не утративший
востребованности SDK Reference - Limbo_Ref20/, и прочая документация.
Анализ ОС и Limbo (вместе с обсуждением "Inferno/Limbo vs. Java OS/Java") дан
в одноименной работе. Брайаном Керниганом (Brian W. Kernighan) и Кристофером Ван Уиком (Christopher J. Van Wyk) написан документ "Timing Trials, or, the Trials of Timing: Experiments with Scripting and User-Interface Languages", в нем наличествует результаты тестов Limbo; версии HTML и Postscript вместе с самими тестами и входными данными можно скачать, посетив страницу одного из авторов.
От англоязычных ресурсов перейдем к русскоязычным (хотелось бы сказать ресурсам, но как таковых, их не существует, подавал надежды сайт plan9inferno.narod.ru, но... не получилось) информационным материалам. Их немного: великолепная обзорная статья "Inferno" А. Зубинского, журнал "Компьютерное Обозрение"; публикация издательского дома "КОМИЗДАТ" "Inferno - виртуальный пост-Unix в кармане" А. Чеботарева; перевод главы об архитектуре ОС книги Inferno User's Guide - статья "ОС для сетевых компьютеров" журнала "Мир ПК"; перевод документа Введение в rc со множеством примеров; и напоследок - произведения Данте: "Новая жизнь", "Божественная комедия".