Здоровья всем читателям!
Вашему вниманию предлагается описание системных вызовов микроядерной операционной системы Хамелеон aka Xameleon. Мой хамелеончик ещё не вылупился из своего яйца и пока набирается сил в виртуальной машине. Но ему очень одиноко и маленькая ящерица желает поближе познакомиться с жителями Хабра.
Спор «микроядро vs монолит» ведётся много лет, но представляют ли обе спорящие стороны об архитектуре системы, построенной на микроядре? Возможно, этот топик немного прольёт свет на архитектуру микроядерных систем.
Документ имеет длительную историю — однажды я осознал, что отдал слишком много времени Хамелеону, от чего получил проблемы на основной работе. Поэтому было решено убить двух зайцев и написать статью, рекламирующую генератор отчётов и, наконец, создать документацию. Сперва она была на ломаном английском, но в какой-то момент было решено не позориться и переписать на чуть более лучший русский.
Собственно, история правок документа:
Перейдём к делу. Операционную систему Хамелеон можно отобразить следующим образом:
В основе лежит микроядро L4 Pistachio. Микроядро состоит из двух модулей: собственно микроядра L4Ka Pistachio и базового менеджера памяти L4 Sigma0. Узнать больше о Pistachio лучше всего из этого документа: L4 Version X.2 Reference Manual (Latest snapshot, July 19 2010) .
Операционная система Хамелеон — то, что находится выше микроядра. Система организована в виде нескольких процессов, работающих поверх микроядра и взаимодействующих между собой и с микроядром посредством IPC (Inter Process Communication) — специфичные системные вызовы, реализующие синхронную передачу сообщений. Основное правило системы — любое взаимодействие между прикладной задачей и системой осуществляется (за несколькими исключениями) на основе IPC. Запрашивающая задача специальным образом формирует сообщение, затем вызывает IPC, которое передаёт сообщение принимающей задаче. Некоторые IPC блокируются до получения ответа, другие IPC блокируются до тех пор, пока не будут приняты принимающей стороной. Синхронность накладывает требование для успешной передачи сообщения — их должна слушать принимающая сторона. Отсюда следует вывод, что некоторые задачи должны постоянно слушать сообщения. Такими задачами в Хамелеоне являются Супервизор, Файловая система, Сетевой стек, драйвера устройств. Хорошей новостью для нас является факт, что задача в состоянии ожидания сообщения не потребляет вычислительные ресурсы процессора и отдаёт своё время другим задачам. Самое интересно для нас, что концепция L4 использует один примитив для передачи и приёма сообщений — это сообщение IPC. Сообщение на уровне ядра организовано одним системным вызовом, могущим иметь несколько сочетаний из фаз передачи и приёма.
Спецификация описывает следующие типы данных, из которых первые пять типов представлены микроядром:
А теперь рассмотрим внимательно базовый сервис Хамелеона — Супервизор. С точки зрения микроядра, Супервизор не что иное, как первая пользовательская задача, поэтому любая другая задача, будь то прикладная программа, сервис или драйвер, могут через KIP (Kernel Interface Page — базовое понятие микроядра L4) получить ThreadID супервизора и с его помощью узнать ThreadID других подсистем (используя системный вызов GetDeviceHandle).
Двадцать два системных вызова для управления ресурсами. Этого вполне достаточно, чтобы выделить и освободить виртуальную память, запустить на выполнение заранее подготовленный в памяти процесс, запустить драйвер/реализацию протокола, послать сигнал процессу, создать программный поток (нить исполнения), ожидать завершения процесса, установить таймер процесса и выполнить несколько других функций.
Рассмотрим некоторые из системных вызовов подробнее. Поскольку Супервизор умеет запустить новый процесс на исполнение, память этого процесса должна быть подготовлена заранее. Соответственно, нам понадобится блок памяти, куда мы помещаем новый процесс перед его исполнением. Запросить память для процесса можно с помощью следующего системного вызова:
Как я уже говорил выше, первая версия документации была на ломаном английском. К сожалению, я не успел русифицировать всю спецификацию, поэтому описание тела сообщения осталось на «руглише».
Симметричный системный вызов для работы с выделенной памятью называется ReferencingSegment. Для того, чтобы освободить ранее выделенный сегмент памяти, необходимо с помощью вызова ReferencingSegment сбросить счётчик ссылок на этот сегмент. При этом сам сегмент и занятые им страницы виртуальной памяти доступны для последующей аллокации с помощью AllocateSegment:
Процесс также может завершить себя используя вызов ExitProcess:
В отличие от предыдущих сообщений, это сообщение не имеет фазы приёма. Это обусловлено тем, что процесс после передачи этого сообщения завершится, поэтому Супервизор не посылает подтверждение. Сообщение передаёт один параметр — код возврата. Этот код будет передан задаче, ожидающей завершения процесса при помощи системного вызова ProcessWait:
Поскольку микроядро IPC использует синхронные вызовы, то в многих случаях ограничения, накладываемые синхронностью, можно обойти с помощью многопоточности. Любой процесс может создать программный поток (нить исполнения) в своём адресном пространстве. При этом все потоки разделяют общие ресурсы. Следующий системный вызов создаёт новый программный поток:
Если потоки создаются, то должен быть и способ их останова и освобождения ресурсов. Для этого служит системный вызов ExitThread:
Немного юмора: «Что общего у процесса системы Хамелеон и амёбы — они размножаются делением». Если серьёзно, то в данном случае велосипед не изобретался, а за основу был взят POSIX fork():
Реализация функции fork() на основе примитивов L4 — нетривиальная, но выполнимая, задача. Фишка этого вызова в том, что вызывает его один процесс, а ответ получают два процесса — вызывающий и вновь созданная копия вызывающего процесса. Разумеется оба процесса, инициатор и его копия, будут выполнятся в разных адресных пространствах.
Процесс может клонировать себя и мы подошли к интересному вызову — создание новой задачи из подготовленного образа. Обратите внимание на MR2 (Message register 2) — в нём передаётся хэндл на блок памяти запускаемого процесса. Этот блок памяти должен быть создан с помощью описанного выше системного вызова AllocateSegment и в нём должен хранится подготовленный образ стартуемого процесса. Также системный вызов задаёт базовые параметры процесса — точка входа, начало и размер сегментов кода, данных и BSS.
Обратите внимание на два последних параметра, передаваемых в сообщении ExecProcess. Вы видите ещё не рассмотренные выше типы данных — Compound. В теле сообщения передаются не сами элементы, их дескрипторы. В случае сообщения ExecProcess, эти дескрипторы указывают на два блока памяти — первый блок памяти передаёт char ** argv, второй блок памяти передаёт char ** envp — аналоги аргументов функции int main(int argc, char ** argv, char **envp).
Супервизор — базовый сервис и он предоставляет интерфейс для доступа к другим зарегистрированным сервисам. Например, некая задача желает напрямую работать с драйвером последовательного порта, без использования файловой системы. Узнать ThreadId зарегистрированного драйвера серийного порта можно при помощи системного вызова GetDeviceHandle:
Используя полученный вызовом GetDeviceHandle идентификатор процесса ThreadID, мы можем посылать ему сообщения и получать ответы от него. Формат сообщений зависит от типа устройства и может быть описан одним из следующих протоколов:
Скачать полную (пре-альфа) версию спецификации в формате PDF можно по ссылке: Xameleon спецификация семейства протоколов. Документ ещё очень сырой, но я надеюсь, что ваши комментарии придадут силы для его дальнейшего исправления.
Обсудить системный вызов, уточнить, указать на ошибку или задать вопрос можно не только на Хабре, но и на этой странице: fotki.yandex.ru/users/almandrykin/album/164974 — выберите картинку и в комментариях задайте свой вопрос.
Так получилось, что описание даже нескольких системных вызовов Супервизора слишком масштабно. Если будет замечен интерес, то возможно описание остальных системных вызовов Супервизора, а также описание системных вызовов Фаловой системы, Сетевой подсистемы, а также системных вызовов драйверов блочных, символьных и сетевых устройств.
Наконец, несколько слов о том, какие средства разработки можно использовать для написания программ для системы Хамелеон. Для разработки можно использовать любой язык программирования, умеющий создавать исполняемые файлы формата Elf или PE — gcc, g++, Visual C++, различные компиляторы Паскаля. В общем случае процесс компиляции аналогичен компиляции в любой другой системе, но главное правило — не используйте стандартные библиотеки, поставляемые с компилятором. Текущая реализация Хамелеона использует оригинальную Elf-based libc и оригинальную crt0, обеспечивающих подмножество POSIX функций. Почти ничто не мешает собрать эту libc компилятором, генерирующим объектный код в формате PE и собирать программы MS Visual C++ компилятором. Также возможна реализация системных вызовов на языке Pascal и тогда больше никто не скажет, что этот язык не создан для системного программирования.
p.s. При создании документации использовался генератор отчётов FastReport.
p.p.s. Мой английский действительно плох. Прошу отнестись снисходительно. Обещаю перевести на русский язык весь документ.
Вашему вниманию предлагается описание системных вызовов микроядерной операционной системы Хамелеон aka Xameleon. Мой хамелеончик ещё не вылупился из своего яйца и пока набирается сил в виртуальной машине. Но ему очень одиноко и маленькая ящерица желает поближе познакомиться с жителями Хабра.
Спор «микроядро vs монолит» ведётся много лет, но представляют ли обе спорящие стороны об архитектуре системы, построенной на микроядре? Возможно, этот топик немного прольёт свет на архитектуру микроядерных систем.
Документ имеет длительную историю — однажды я осознал, что отдал слишком много времени Хамелеону, от чего получил проблемы на основной работе. Поэтому было решено убить двух зайцев и написать статью, рекламирующую генератор отчётов и, наконец, создать документацию. Сперва она была на ломаном английском, но в какой-то момент было решено не позориться и переписать на чуть более лучший русский.
Собственно, история правок документа:
Перейдём к делу. Операционную систему Хамелеон можно отобразить следующим образом:
В основе лежит микроядро L4 Pistachio. Микроядро состоит из двух модулей: собственно микроядра L4Ka Pistachio и базового менеджера памяти L4 Sigma0. Узнать больше о Pistachio лучше всего из этого документа: L4 Version X.2 Reference Manual (Latest snapshot, July 19 2010) .
Операционная система Хамелеон — то, что находится выше микроядра. Система организована в виде нескольких процессов, работающих поверх микроядра и взаимодействующих между собой и с микроядром посредством IPC (Inter Process Communication) — специфичные системные вызовы, реализующие синхронную передачу сообщений. Основное правило системы — любое взаимодействие между прикладной задачей и системой осуществляется (за несколькими исключениями) на основе IPC. Запрашивающая задача специальным образом формирует сообщение, затем вызывает IPC, которое передаёт сообщение принимающей задаче. Некоторые IPC блокируются до получения ответа, другие IPC блокируются до тех пор, пока не будут приняты принимающей стороной. Синхронность накладывает требование для успешной передачи сообщения — их должна слушать принимающая сторона. Отсюда следует вывод, что некоторые задачи должны постоянно слушать сообщения. Такими задачами в Хамелеоне являются Супервизор, Файловая система, Сетевой стек, драйвера устройств. Хорошей новостью для нас является факт, что задача в состоянии ожидания сообщения не потребляет вычислительные ресурсы процессора и отдаёт своё время другим задачам. Самое интересно для нас, что концепция L4 использует один примитив для передачи и приёма сообщений — это сообщение IPC. Сообщение на уровне ядра организовано одним системным вызовом, могущим иметь несколько сочетаний из фаз передачи и приёма.
Спецификация описывает следующие типы данных, из которых первые пять типов представлены микроядром:
А теперь рассмотрим внимательно базовый сервис Хамелеона — Супервизор. С точки зрения микроядра, Супервизор не что иное, как первая пользовательская задача, поэтому любая другая задача, будь то прикладная программа, сервис или драйвер, могут через KIP (Kernel Interface Page — базовое понятие микроядра L4) получить ThreadID супервизора и с его помощью узнать ThreadID других подсистем (используя системный вызов GetDeviceHandle).
Двадцать два системных вызова для управления ресурсами. Этого вполне достаточно, чтобы выделить и освободить виртуальную память, запустить на выполнение заранее подготовленный в памяти процесс, запустить драйвер/реализацию протокола, послать сигнал процессу, создать программный поток (нить исполнения), ожидать завершения процесса, установить таймер процесса и выполнить несколько других функций.
Рассмотрим некоторые из системных вызовов подробнее. Поскольку Супервизор умеет запустить новый процесс на исполнение, память этого процесса должна быть подготовлена заранее. Соответственно, нам понадобится блок памяти, куда мы помещаем новый процесс перед его исполнением. Запросить память для процесса можно с помощью следующего системного вызова:
Как я уже говорил выше, первая версия документации была на ломаном английском. К сожалению, я не успел русифицировать всю спецификацию, поэтому описание тела сообщения осталось на «руглише».
Симметричный системный вызов для работы с выделенной памятью называется ReferencingSegment. Для того, чтобы освободить ранее выделенный сегмент памяти, необходимо с помощью вызова ReferencingSegment сбросить счётчик ссылок на этот сегмент. При этом сам сегмент и занятые им страницы виртуальной памяти доступны для последующей аллокации с помощью AllocateSegment:
Процесс также может завершить себя используя вызов ExitProcess:
В отличие от предыдущих сообщений, это сообщение не имеет фазы приёма. Это обусловлено тем, что процесс после передачи этого сообщения завершится, поэтому Супервизор не посылает подтверждение. Сообщение передаёт один параметр — код возврата. Этот код будет передан задаче, ожидающей завершения процесса при помощи системного вызова ProcessWait:
Поскольку микроядро IPC использует синхронные вызовы, то в многих случаях ограничения, накладываемые синхронностью, можно обойти с помощью многопоточности. Любой процесс может создать программный поток (нить исполнения) в своём адресном пространстве. При этом все потоки разделяют общие ресурсы. Следующий системный вызов создаёт новый программный поток:
Если потоки создаются, то должен быть и способ их останова и освобождения ресурсов. Для этого служит системный вызов ExitThread:
Немного юмора: «Что общего у процесса системы Хамелеон и амёбы — они размножаются делением». Если серьёзно, то в данном случае велосипед не изобретался, а за основу был взят POSIX fork():
Реализация функции fork() на основе примитивов L4 — нетривиальная, но выполнимая, задача. Фишка этого вызова в том, что вызывает его один процесс, а ответ получают два процесса — вызывающий и вновь созданная копия вызывающего процесса. Разумеется оба процесса, инициатор и его копия, будут выполнятся в разных адресных пространствах.
Процесс может клонировать себя и мы подошли к интересному вызову — создание новой задачи из подготовленного образа. Обратите внимание на MR2 (Message register 2) — в нём передаётся хэндл на блок памяти запускаемого процесса. Этот блок памяти должен быть создан с помощью описанного выше системного вызова AllocateSegment и в нём должен хранится подготовленный образ стартуемого процесса. Также системный вызов задаёт базовые параметры процесса — точка входа, начало и размер сегментов кода, данных и BSS.
Обратите внимание на два последних параметра, передаваемых в сообщении ExecProcess. Вы видите ещё не рассмотренные выше типы данных — Compound. В теле сообщения передаются не сами элементы, их дескрипторы. В случае сообщения ExecProcess, эти дескрипторы указывают на два блока памяти — первый блок памяти передаёт char ** argv, второй блок памяти передаёт char ** envp — аналоги аргументов функции int main(int argc, char ** argv, char **envp).
Супервизор — базовый сервис и он предоставляет интерфейс для доступа к другим зарегистрированным сервисам. Например, некая задача желает напрямую работать с драйвером последовательного порта, без использования файловой системы. Узнать ThreadId зарегистрированного драйвера серийного порта можно при помощи системного вызова GetDeviceHandle:
Используя полученный вызовом GetDeviceHandle идентификатор процесса ThreadID, мы можем посылать ему сообщения и получать ответы от него. Формат сообщений зависит от типа устройства и может быть описан одним из следующих протоколов:
- формате обмена с сервисом;
- формате обмена с блочным устройством;
- формате обмена с символьным устройством;
- формате обмена с сетевым устройством.
Скачать полную (пре-альфа) версию спецификации в формате PDF можно по ссылке: Xameleon спецификация семейства протоколов. Документ ещё очень сырой, но я надеюсь, что ваши комментарии придадут силы для его дальнейшего исправления.
Обсудить системный вызов, уточнить, указать на ошибку или задать вопрос можно не только на Хабре, но и на этой странице: fotki.yandex.ru/users/almandrykin/album/164974 — выберите картинку и в комментариях задайте свой вопрос.
Так получилось, что описание даже нескольких системных вызовов Супервизора слишком масштабно. Если будет замечен интерес, то возможно описание остальных системных вызовов Супервизора, а также описание системных вызовов Фаловой системы, Сетевой подсистемы, а также системных вызовов драйверов блочных, символьных и сетевых устройств.
Наконец, несколько слов о том, какие средства разработки можно использовать для написания программ для системы Хамелеон. Для разработки можно использовать любой язык программирования, умеющий создавать исполняемые файлы формата Elf или PE — gcc, g++, Visual C++, различные компиляторы Паскаля. В общем случае процесс компиляции аналогичен компиляции в любой другой системе, но главное правило — не используйте стандартные библиотеки, поставляемые с компилятором. Текущая реализация Хамелеона использует оригинальную Elf-based libc и оригинальную crt0, обеспечивающих подмножество POSIX функций. Почти ничто не мешает собрать эту libc компилятором, генерирующим объектный код в формате PE и собирать программы MS Visual C++ компилятором. Также возможна реализация системных вызовов на языке Pascal и тогда больше никто не скажет, что этот язык не создан для системного программирования.
p.s. При создании документации использовался генератор отчётов FastReport.
p.p.s. Мой английский действительно плох. Прошу отнестись снисходительно. Обещаю перевести на русский язык весь документ.