Всё началось полтора месяца назад, когда я от нахлынувшего по результатам собеседований чувства свободы и вседозволенности решил написать новую 64-битную операционную систему. 10 лет назад у меня уже был подобный опыт, но тогда система была 32-х разрядной и обладала другими недостатками, которые сейчас-то я и решил исправить. Например, в той системе (DuS) не было memory management, т.е. страничной организации, поэтому память приходилось распределять до запуска программ, что доставляло неудобства.
Но главное, там не было доступа к сети, а как же можно работать в системе, если там нет Интернета?
Итак, система называется LDuS, буква L намекает на длинный (т. е. 64-битный) режим. Недавно появилась ОС Android L, где L означает то же самое, там предусмотрена возможность работы с 64-разрядной архитектурой ARM, а сами смартфоны с такой архитектурой обещают вот-вот появиться. То, что получилось к данному моменту, я назвал версией 0.1.
Загрузка
Загрузка устроена 4-х ступенчатая, если не больше: boot-сектор -> начальный загрузчик -> загрузчик LDuS -> ядро LDuS. Первые два этапа я взял без изменений из DuS, в частности, начальный загрузчик позволяет загружать другие файлы из загрузочного диска, который должен иметь мою файловую систему dusfs. Загрузчик LDuS загружает ядро, заполняет все необходимые таблицы процессора, маппирует 1-й мегабайт памяти по тождественным адресам, переходит в 64-битный режим и вызывает ядро. Загрузчик небольшой, поэтому написан на ассемблере fasm, где удобно переключаться между разной разрядностью кода.
Я принял решение после включение 64-битного режима никогда (в отличие от DuS) не переходить в реальный режим для обращения к BIOSу. Если и придётся зачем-то обращаться, то стоит сделать эмуляцию режима 8086. Поэтому карту памяти составляет загрузчик (обращаясь к прерыванию int 15h BIOS'а) ещё до перехода в 64-битный режим и потом передаёт её ядру.
Модули
Ядро и все прочие модули написаны на C и компилируются стандартным gcc. Поскольку виртуальный адрес занимает 64 бита (в реальности 48), то каждому модулю можно присвоить свой уникальный адрес, содержащий хэш-код имени модуля и некоторую другую информацию, в частности. модули делятся на категории по интересам. Значит, линковка библиотек в процессе работы системы не нужна. При обращении к библиотечной функции ядро перехватывает прерывание отсутствия страницы, по адресу находит библиотеку и подгружает её. Чтобы всё это объяснить gcc, пришлось освоить язык его линковщика и написать файл *.ld, куда заносятся адреса всех используемых модулем объектов, его статических переменных, а также самого модуля. Этот файл генерируется программой ldus из файла *.ldus, где перечислены исходные тексты, входящие в модуль, импортируемые модули, а также экспортируемые и статические объекты.
Система задумана как микроядерная. Драйвера, за исключением вывод на терминал, планируется не включать в ядро, а делать отдельными процессами. В этом случае они не будут видеть память друг друга, а тем более ядра, а значит, ничего не испортят другому драйверу. Поскольку включать в ядро другие драйверы не планируется, то откуда же загружать сами эти драйверы (к BIOS'у обращаться нельзя)? Для решения проблемы я сделал файловую систему dusfs, содержащую основные модули для загрузки, и включил её непосредственно в ядро в качестве бинарного массива данных. Далее, есть ранее написанный драйвер файловой системы dusfs, который без проблем скомпилировался с ядром и подгружает из этого массива недостающие страницы файлов при первом обращении.
Ядро
В ядре уже многое сделано, есть процессы, потоки, задачи (это всё разные вещи), их можно создавать, запускать и удалять. Есть обработка прерываний и управление памятью. Страницы памяти автоматически подключаются при первом обращении и удаляются при освобождении виртуальной памяти с помощью функции free(). Есть многозадачность и даже планировщик задач, которого не было в DuS, там задачи выполнялись по кругу. Планировщик устроен просто: выполняется та задача, которая раньше выполнялась меньшее время (выраженное в тиках процессора, поделённых на вес задачи) по отношению к общему времени работы и ожидания, время сна не учитывается.
Задачам можно назначать два типа приоритетов: относительный и абсолютный. Если есть задача с более высоким абсолютным приоритетом, то задачи с более низким не выполняются вообще, это актуально для обработчиков прерываний. Относительный приоритет позволяет одним задачам выполняться в заданное число раз быстрее других. Исходя из относительных приоритетов всех выполняющихся и ожидающих в данный момент задач вычисляются веса, на которые и делятся тики процессора. А именно, вес задачи — это доля её относительного приоритета в сумме всех относительных приоритетов.
Планировщик задач не прикреплён к таймеру, а вызывается всякий раз, когда ядро получает управление, это может возникнуть при любом прерывании, сбое в программе, или вызове системной функции. Так же, как и в DuS, системные функции не сосредоточены в ядре, а распределены по разным процессам. Например, драйверы клавиатуры и таймера — это отдельные процессы, работающие на уровне привилегий 3 (самом низком) как и все остальные процессы, только абсолютный приоритет у них выше.
Шлюзы
Аналогом системных вызовов являются шлюзы. Шлюз это такой адрес памяти, как будто адрес функции, но там ничего не записано (дыра, нет памяти), а если приложение туда перейдёт, то вызов перехватит ядро и перенаправит вызов или себе или другому процессу, в зависимости от того, куда ведёт шлюз. Если шлюз ведёт в другой процесс, то при вызове шлюза будет или создан новый поток этого процесса или взят один из свободных его потоков.
Итак, поток одного процесса может через шлюз вызвать системную функцию, которую реализует другой процесс, при этом изначальный поток приостановится, а управление перейдёт к потоку второго процесса. Этот поток может, в свою очередь, вызвать функцию в третьем процессе и т. д. В итоге потоки выстраиваются в цепочку, которая и называется заданием. Именно для заданий назначаются приоритеты, при этом задание может перетекать из одного процесса в другой. Можно представить, что процессы расположены вдоль, задания поперёк, а их пересечения — это потоки.
Отображение памяти
Для увеличения скорости обмена данными между процессами предусмотрен ещё один механизм — отображение части памяти одного процесса в виртуальную память другого. Например, так устроен драйвер таймера. Поток этого драйвера висит на прерывании от таймера (спит в ядре до возникновения прерывания), а при возникновении прерывания изменяет текущее значение времени у себя в памяти. Эта область его памяти отображена в другие процессы по фиксированному адресу, так что, чтобы узнать время, другому процессу достаточно прочитать его из своей памяти, функцию через шлюз вызывать не нужно.
Так же планируется организовать работу с файлами. В настоящее время есть драйвер файловой системы в памяти (ramfs), в котором, во-первых, доступны в виде файлов модули, встроенные в ядро, а во-вторых, можно создавать новые файлы и каталоги, длина каждого файла ограничена половиной размера оперативной памяти. Кроме того, есть общая концепция файловой подсистемы и написаны соответствующие функции стандратной библиотеки [f]open/[f]read/[f]write.
Если в классическом понимании буферизованный ввод-вывод (т. е. функции fopen/fread/fwrite...) является надстройкой на базовым (open/read/write), то в LDuS они находятся на одном уровне. Дело в том, что файлы в LDuS не читаются/пишутся, а только отображаются, т. е. память, в которой эаписан файл, вклинивается в память процесса, желающего с ним работать. А значит, с открытым файлом всегда ассоциирован буфер, под которым понимается отображение этого файла в память.
Например, макрос getc() читает байт из буфера без вызова функции (чтобы чтение байтов в цикле происходило быстро). Поскольку буфер это образ файла в памяти, то чтение происходит непосредственно из памяти драйвера файловой системы. Если при этом файловая система хорошая, т. е. блоки файла выровнены по границам страниц (4K), то чтение происходит из памяти драйвера диска. Осталось только инженерам реализовать отображение диска в память, и тогда данные будут читаться прямо с диска без посредников. Аналогичное дело обстоит и с записью файла с помощью putc().
Ввод-вывод
Есть драйвер клавиатуры и отдельный драйвер видеосистемы VGA, пока только текстовой её части, зато с цветами, целых 16 классических цветов VGA! Я не стал вмонтировать драйвер VGA в ядро, оставив там только необходимую для ядра терминальную часть, а сделал отдельный процесс, всё же у нас микроядерная архитектура. Кроме того, драйвер VGA в отличие от встроенного в ядро позволяет работать с видеопамятью без переключения задач. А именно, страница видеопамяти отображена в виртуальное пространство процесса, поэтому он может самостоятельно изменять содержимое своего экрана, что значительно быстрее, чем вызывать функцию для вывода каждого символа, как это сделано в ядерном драйвере. Следует уточнить, что отображена не сама аппаратная видеопамять, поскольку операция записи в неё небыстрая, а её образ, т. е. обычная память. Переносом данных из образа активного экрана в настоящую видеопамять периодически занимается драйвер VGA.
В этом же драйвере сделана поддержка 8 экранов, между которыми можно переключаться нажатиями Ctrl-Alt-F1...F8. На экранах можно запускать независимые параллельно работающие задания, даже чересчур независимые, например, с одного экрана нельзя убить программу на другом, поскольку ядро не понимает и не собирается понимать права доступа в Unix-стиле (пользователи, root и т. д.), а понимает только, что кто породил процесс, тот его и может убить, или отец его, или дед и т. д. Так что получается 8 независимых операционных подсистем, имеющих, правда, общую файловую систему.
Оболочка
В настоящее время в LDuS есть простейший интерпретатор командной строки (shell) и классический набор программ в духе Norton Commander, а именно, оболочка (DuS Commander), возможности которой задаются в конфигурации и определяются набором сопутствующих программ, и две сопутствующие программы — просмотрщик и редактор текстов. Эти программы были написаны давно для моей же 32-разрядной системы DuS, так что нововведением являются не сами они, а только их встраивание в новую систему. Для встранивания был реализован режим совместимости с 32-битными программами и написан модуль, реализующий некоторые функции DuS.
Что дальше
Система достигла некоего функционального и презентативного минимума, поэтому можно общую часть её написания закрыть и перейти к специальной части. А именно, сейчас упор в разработке будет делаться на работу в сети (даже при том, что ещё нет драйвера обычного диска). Цель состоит в том, чтобы сделать в LDuS минимальный веб-сервер и разместить на нём мой сайт, где есть не только блог и поисковая система, но и файлы, полезные для студентов, и даже онлайн-тестер для подготовки к зачёту. Если не приспособить новую систему к какой-нибудь полезной нагрузке, то, чувствую, она так и останется игрушкой, как DuS.
Исходные тексты системы можно найти здесь. Доступен также образ загрузочной дискеты, который можно легко подключить к эмулятору и воспроизвести показанное на видео. Для запуска DuS Commander нужно выполнить команду tools/dc.
Автор: dcherukhin