Pull to refresh

Проект «Оберон 2013»

Reading time 14 min
Views 36K
Мне тут коллеги одну полезную нагрузочку дали. Я быстренько с вами проведу беседу о вреде избирательной слепоты, поскольку с нею, проклятою, в нашей индустрии не совсем благополучно. Значит, перво-наперво, прежде чем с нею бороться, с нашим общим врагом – слепотой проклятой, давайте мы как следует обсудим и изучим примеры того, что не видит сообщество, чтобы наверняка знать, как оно может быть на самом деле.

История


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



Через почти 30 лет, наблюдая ситуацию неконтролируемого усложнения продуктов в ИТ, Н. Вирт решил вновь преподнести людям свое видение того, как строить маленькие, мощные и надежные системы. Сама работа автора будет служить доказательством того, что такие системы существуют и работают.

Все течет, все меняется


Как оказалось, та модель процессора, которая использовалась в первоначальной системе, уже пропала с рынка. Как и его архитектурные аналоги. Вместо использования готовых коммерческих решений, Н. Вирт решил дополнить свою систему еще одним компонентом — своим вариантом языка для программирования FPGA, который был назван Lola. Следовательно, процессор для новой машины был разработан самим Виртом на базе RISC. Его мы рассмотрим в отдельном разделе статьи. Вся система работала на девборде Xilinx Spartan-3 с 1 МБ памяти и 25 МГц процессором.

То есть, мы имеем проект, в котором реализован полный цикл разработки вычислительного комплекса from scratch, и реализованы все доступные инженеру/программисту этапы этого проекта.

Процессор


RISC-архитектура отличается особой последовательностью работы процессора с данными. Все вычисляющие инструкции процессора работают с данными, расположенными в регистрах, а данные в память/из памяти переносятся особыми инструкциями процессора, SAVE и LOAD. Таким образом достигается монотонность работы, когда количество тактов на одну инструкцию предсказуемо, и зачастую большинство инструкций умещается в один такт. Особенностью процессора Вирта так же является параллельная работа конвейера по записи данных в память, правда, для этого пришлось снизить частоту получившегося процессора в два раза, с 50 до 25 МГц.

Система команд опирается на 32-х битное слово, в котором старшие четыре бита отданы под определение формата команды. Формат команды определяет класс операции, количество операндов и их местоположение.
Команды АЛУ определяют набор арифметических и логических операций с тремя регистрами или с двумя регистрами и константой. Команды АЛУ оставляют после себя дополнительные значения знака результата в специальных однобитных регистрах.
Команды памяти определяют инструкции загрузки в память и чтения из памяти одного слова или одного байта в регистр.
Команды управления потоком исполнения позволяют управлять значением регистра PC, описывать условные и безусловные переходы с опорой на значение регистра или константы.

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

Выглядит это примерно так:



Подробнее о наборе инструкций можно прочитать в обобщенном и конкретном документах. В них приводится схема процессора и периферических устройств на языке Verilog, но в последующих редакциях Вирт предоставил схемы на языке Lola вместе с описанием языка, после дополнительной отладки и проверки.

Так же Вирт описывает схему и реализацию работы с некоторыми стандартными интерфейсами, типа RS232 и так же рассказывает о том, как подключал SD карту в качестве хранилища данных. Как реализованы периферийные устройства рассказано в документации к проекту, так как я в основном программист, а не электронщик, описывать деталей не стану, чтобы не допустить совсем уж детских ошибок. Возможно, более опытные товарищи помогут восполнить этот пробел.

Система


Основная цель ОС состоит в том, чтобы предоставить пользователю и программисту определенный уровень абстракции. Устройства ввода/вывода, устройства хранения и интерфейсы взаимодействия с этими устройствами, а так же ограничения железа. Гибкость абстракций, выбранных в системе Оберон позволяет бесшовно запускать ее в качестве отдельной программы на различных софтверных платформах/ОС, при этом эмулирован может быть уровень процессора или же уровень программных реализаций абстракций.

Язык


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

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

Модульность


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

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

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

Базовая метаинформация


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

Динамическая модульность


Необходимость такой передачи управления вытекает из необходимости наличия в ОС не просто модульности, а динамической модульности, так как ОС это инфраструктурный продукт, и сама по себе не имеет конкретный применений у пользователей, пользователи должны иметь возможность в процессе работы программы загрузить в память нужные им модули. При этом у модуля есть точка входа и тело, которое выполнится один раз, при этом важным моментом является тот факт, что модуль останется загруженным в память, и может предоставлять свои типы и процедуры для дальнейшей работы других модулей. Так вот, для того чтобы в процессе работы ОС управление можно было передавать уже загруженным модулям, а заодно и загружать модули и выполнять их процедуры, понадобилось иметь возможность выполнять процедуры по имени. Конечно, у данной фичи есть невообразимо сложные аналоги в мейнстриме, но на данный момент такой фичи достаточно. Все равно, она реализуется библиотечно, а в язык добавлять ничего не надо.

Сборка мусора


В операционной системе Оберон сборка мусора выполняется на уровне ядра ОС, которое управляет размещением динамических объектов в куче. Для компонентной среды сборщик мусора это очевидная необходимость, так как компонент, предоставляющий динамические объекты клиентам не может решать, когда их удалить из памяти.
Вот цитата самого Вирта по этому поводу:
The Oberon System does not provide an explicit deallocation procedure allowing the programmer to signal that a variable will no longer be referenced. The first reason for this omission is that usually a programmer would not know when to call for deallocation. And secondly, this «hint» could
not be taken as trustworthy. An erroneous deallocation, i.e. one occurring when there still exist references to the object in question, could lead to a multiple allocation of the same space with disastrous consequences. Hence, it appears wise to fully rely on system management to determine which areas of the store are truly reusable.


Итак, когда требования к языку и среде его исполнения (она же ОС, она же рантайм) определены, можно приступить к написанию кода. По сути, ОС по набору функций может быть описана всего несколькими модулями. Остальные функции могут взять на себя сторонние компоненты. Компилятор не обязан быть частью ОС, но так как без него не создать ни единой команды процессору, мы рассмотрим его в первую очередь.

Компилятор


Язык программирования — это бесконечный набор символов из набора ключевых слов, которые связаны ограниченным набором выражений, составляющих синтаксис. Каждое выражение представляет собой синтаксическую конструкцию с правилами ее написания. Смысл написанного определяется набором семантических правил, сопоставленных с каждой конструкцией.

Оберон


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

Как можно видеть в этом проекте, Оберон и его концепции подходят для реализации кода на любом уровне, низкоуровневый, middleware, высокоуровневые редакторы схем, почтовики и т.д. Все эти приложения Вирт реализовал как proof of concept в своей ОС.

Пример низкоуровневого кода на языке Оберон.
Модуль Kernel, сборщик мусора. github.com/ilovb/ProjectOberon2013/blob/master/Sources/Kernel.Mod#L82
(* ---------- Garbage collector ----------*)

  PROCEDURE Mark*(pref: LONGINT);
    VAR pvadr, offadr, offset, tag, p, q, r: LONGINT;
  BEGIN SYSTEM.GET(pref, pvadr); (*pointers < heapOrg considered NIL*)
    WHILE pvadr # 0 DO
      SYSTEM.GET(pvadr, p); SYSTEM.GET(p-4, offadr);
      IF (p >= heapOrg) & (offadr = 0) THEN q := p;   (*mark elements in data structure with root p*)
        REPEAT SYSTEM.GET(p-4, offadr);
          IF offadr = 0 THEN SYSTEM.GET(p-8, tag); offadr := tag + 16 ELSE INC(offadr, 4) END ;
          SYSTEM.PUT(p-4, offadr); SYSTEM.GET(offadr, offset);
          IF offset # -1 THEN (*down*)
            SYSTEM.GET(p+offset, r); SYSTEM.GET(r-4, offadr);
            IF (r >= heapOrg) & (offadr = 0) THEN SYSTEM.PUT(p+offset, q); q := p; p := r END
          ELSE (*up*) SYSTEM.GET(q-4, offadr); SYSTEM.GET(offadr, offset);
            IF p # q THEN SYSTEM.GET(q+offset, r); SYSTEM.PUT(q+offset, p); p := q; q := r END
          END
        UNTIL (p = q) & (offset = -1)
      END ;
      INC(pref, 4); SYSTEM.GET(pref, pvadr)
    END
  END Mark;

  PROCEDURE Scan*;
    VAR p, q, mark, tag, size: LONGINT;
  BEGIN p := heapOrg;
    REPEAT SYSTEM.GET(p+4, mark); q := p;
      WHILE mark = 0 DO
        SYSTEM.GET(p, tag); SYSTEM.GET(tag, size); INC(p, size); SYSTEM.GET(p+4, mark)
      END ;
      size := p - q; DEC(allocated, size);  (*size of free block*)
      IF size > 0 THEN
        IF size MOD 64 # 0 THEN
          SYSTEM.PUT(q, 32); SYSTEM.PUT(q+4, -1); SYSTEM.PUT(q+8, list3); list3 := q; INC(q, 32); DEC(size, 32)
        END ;
        IF size MOD 128 # 0 THEN
          SYSTEM.PUT(q, 64); SYSTEM.PUT(q+4, -1); SYSTEM.PUT(q+8, list2); list2 := q; INC(q, 64); DEC(size, 64)
        END ;
        IF size MOD 256 # 0 THEN
          SYSTEM.PUT(q, 128); SYSTEM.PUT(q+4, -1); SYSTEM.PUT(q+8,  list1); list1 := q; INC(q, 128); DEC(size, 128)
        END ;
        IF size > 0 THEN
          SYSTEM.PUT(q, size); SYSTEM.PUT(q+4, -1); SYSTEM.PUT(q+8, list0); list0 := q; INC(q, size)
        END
      END ;
      IF mark > 0 THEN SYSTEM.GET(p, tag); SYSTEM.GET(tag, size); SYSTEM.PUT(p+4, 0); INC(p, size)
      ELSE (*free*) SYSTEM.GET(p, size); INC(p, size)
      END
    UNTIL p >= heapLim
  END Scan;


Пример высокоуровневого кода
Модуль Texts, сохранение текстовых файлов редактором. github.com/ilovb/ProjectOberon2013/blob/master/Sources/Texts.Mod#L127
  PROCEDURE Store* (VAR W: Files.Rider; T: Text);
    VAR p, q: Piece;
      R: Files.Rider;
      off, rlen, pos: LONGINT;
      N, n: INTEGER;
      ch: CHAR;
      Dict: ARRAY 32, 32 OF CHAR;
  BEGIN pos := Files.Pos(W); Files.WriteInt(W, 0); (*place holder*)
    N := 1; p := T.trailer.next;
    WHILE p # T.trailer DO
      rlen := p.len; q := p.next;
      WHILE (q # T.trailer) & (q.fnt = p.fnt) & (q.col = p.col) & (q.voff = p.voff) DO
        rlen := rlen + q.len; q := q.next
      END;
      Dict[N] := p.fnt.name;
      n := 1;
      WHILE Dict[n] # p.fnt.name DO INC(n) END;
      Files.WriteByte(W, n);
      IF n = N THEN Files.WriteString(W, p.fnt.name); INC(N) END;
      Files.WriteByte(W, p.col); Files.WriteByte(W, p.voff); Files.WriteInt(W, rlen);
      p := q
    END;
    Files.WriteByte(W, 0); Files.WriteInt(W, T.len);
    off := Files.Pos(W); p := T.trailer.next;
    WHILE p # T.trailer DO
      rlen := p.len; Files.Set(R, p.f, p.off);
      WHILE rlen > 0 DO Files.Read(R, ch); Files.Write(W, ch); DEC(rlen) END ;
      p := p.next
    END ;
    Files.Set(W, Files.Base(W), pos); Files.WriteInt(W, off); (*fixup*)
    T.changed := FALSE;
    IF T.notify # NIL THEN T.notify(T, unmark, 0, 0) END
  END Store;


Язык послужил прямым предком для языка параллельного программирования (Active Oberon), различных модификаций языка Оберон под другие среды исполнения (Component Pascal, Zonnon), был реализован на нескольких платформах (JVM, CLR, JS), послужил прообразом языка Java. Сама система Оберон послужила прообразом для проекта Microsoft Singularity. Такое заимствование нельзя назвать однозначно негативным фактором, я считаю, главный негативный фактор это забытие, которому бизнес предал источник своих успешных концепций.

Парсер


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

Архитектура компилятора Оберона может быть описана этой схемой:



Так как в Обероне symbol в общем случае не равен character, необходим модуль, сканирующий цепочки букв и возвращающий парсеру цепочки абстрактных символов языка.

Парсер однопроходный и не хранит очень много информации о программе в процессе работы. Это достигается по причине простоты и монотонности инструкций языка и процессора. После формирования полной информации о той или иной команде она передается Генератору кода, который сразу производит формирование готового к исполнению кода для процессора RISC. Слежение за состоянием регистров так же возложено на генератор.

Так же сканер занимается графическим отображением места ошибки в исходном коде, и поэтому взаимодействует с модулями ОС.

Кодогенерация


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

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

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

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

Организация памяти


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

Компилятор так же управляет передачей запроса на выделение памяти под динамические объекты ядром, но на этом функции компилятора по управлению такими объектами заканчиваются, далее они рассматриваются просто как смещение по значению указателя.

Ядро и системные модули


Ядро системы Оберон представляет собой модуль, который в основном занимается управлением памятью, выделением памяти под новый экземпляр, сборкой мусора и отловом исключений. Так же, из соображений простоты в ядро был встроен набор процедур по доступу к SD карте по протоколу SPI. Это сделано для того, чтобы минимизировать число модулей в статически слинкованной прошивке и в процессе загрузки сразу перейти к обычному методу динамической загрузки файлов-модулей с диска.

Загрузкой и динамической линковкой модулей занимается модуль Modules. Этот модуль так же занимается поиском команд (экспортированных процедур) по имени, и содержит внутри себя список модулей. Список модулей это немного магический тип, который с виду выглядит как связный список указателей на структуры, но на самом деле указывает на начальную позицию расположения самого модуля в таблице модулей. Очередная низкоуровневая возможность Оберона.

Файловая подсистема представлена модулем Files, который внутри себя скрывает реализацию обращения к файловой системе SD-карты. Таким образом клиенты модуля Files уже ничего не знают о низкоуровневых особенностях. Это, как очевидно, высокоуровневые возможности языка Оберон.

Графическая и текстовая подсистемы представлены набором модулей, о которых можно подробнее почитать в документации проекта. Здесь лишь отмечу, что система Оберон изначально графическая по своей сути, но имеет развитые возможности текстовых команд. Пример работы с текстовыми командами в графическом интерфейсе можно понаблюдать на известной гифке.

гифка
image

А вот как выглядит графический интерфейс в процессе работы:



Концепция текстовых команд непосредственно связана с понятием команды, как экспортированной процедуры модуля. Тут мы можем увидеть, насколько тонка грань между реальным кодом и пользовательским интерфейсом в системе Оберон, при сохранении платформонезависимости самой концепции.

Сам графический интерфейс тайловый, монохромный. Это связано с ограничением видеоподсистемы девборды. Графические примитивы базируются на понятии Фрейма, в которых различные модули могут отрисовывать текстовое или графическое содержимое. Идея фреймов была реализована в виде фреймворка BlackBox под Windows, где являлась одной из центральных абстракций графического приложения.

В системе Оберон реализован механизм шрифтов, хоть и не такой мощный, как современные аналоги.

Прикладные модули


Прикладные модули, или, проще, приложения — это прямой аналог приложений в популярных ОС. Так как после загрузки в память модули остаются в памяти, взаимодействие с юзером основано на асинхронной обработке событий от Фреймов.

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

Из прикладных модулей, представленных в стандартной поставке можно выделить некоторое количество тестовых модулей, рисовалку фракталов, и тому подобную мелочь. Из больших приложений можно выделить инструменты разработчика, которые реализованы в тесной связке текстовой подсистемы и компилятора.



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

Можно так же отметить приложение почтового клиента и сервера.

Заключение


Как мы уже поняли, в составе системы Оберон имеются в наличии разнородные компоненты, приложения высокого и низкого уровня, приближенные к железу или к абстрактным вещам, типа электронных схем, объединенные общей концепцией простоты и написанные на языке Оберон.

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

Никлаус Вирт в своем почтенном возрасте продолжает следовать принципам, которые заложил в начале эпохи ИТ. Следует ли этим принципам сама индустрия? Мне кажется — не следует, но это уже философский вопрос, вопрос целеполагания, внутренней непротиворечивости и самоограничения, недоступного рынку.

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

С другой стороны, просто непонятно, почему при наличии ресурсов и ниши те же господа из МЦСТ потратили сотни усилий на создание продуктов, которые позволят встроиться в существующую экосистему Си и GNU/Linux вместо выстраивания собственной экосистемы, пусть даже и не на основе Оберона (хотя хотелось бы, чего скрывать).

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

Потому что трудно проводить беседу о борьбе с избирательной слепотой. Может быть так, – беседу закончим. Прямо приступим к борьбы с ею. Ведь у нас…(заплетающимся языком), одно должны помнить – все мы, как один, должны мы все бороться. Должны с ею, как один. Должны мы все … бороться, один должны как. Мы все…

Ссылки


Tags:
Hubs:
+8
Comments 388
Comments Comments 388

Articles