Введение

Ядро, предоставляющее меньше сервисов, чем традиционное ядро.

В информатике микроядро (часто сокращаемое как μ-ядро) представляет собой минимально необходимый объем программного обеспечения, способный предоставить механизмы, нужные для реализации операционной системы (ОС). Эти механизмы включают в себя управление адресным пространством нижнего уровня, управление потоками и межпроцессное взаимодействие (IPC). Если аппаратное обеспечение предоставляет несколько уровней защиты или режимов процессора, микроядро может быть единственным программным обеспечением, выполняющимся на наиболее привилегированном уровне, который обычно называют режимом супервизора или режимом ядра. Традиционные функции операционной системы, такие как драйверы устройств, стеки протоколов и файловые системы, обычно исключаются из самого микроядра и вместо этого выполняются в пользовательском пространстве. С точки зрения размера исходного кода, микроядра часто меньше монолитных ядер. Например, микроядро MINIX 3 содержит всего около 12 000 строк кода.

История

Микроядра уходят корнями к датскому пионеру в области компьютерных технологий Перу Бринчу Хансену и его работе в датской компьютерной компании Regnecentralen, где он руководил разработкой программного обеспечения для компьютера RC 4000. В 1967 году Regnecentralen устанавливал прототип RC 4000 на заводе по производству удобрений Zakłady Azotowe Puławy в Польше. Компьютер использовал небольшую операционную систему реального времени, разработанную для нужд завода. Бринч Хансен и его команда обеспокоились отсутствием универсальности и возможности повторного использования системы RC 4000. Они опасались, что для каждой установки потребуется отдельная операционная система, поэтому начали изучать новые и более общие способы создания программного обеспечения для RC 4000. В 1969 году их усилия увенчались завершением многопрограммной системы RC 4000. Её ядро обеспечивало межпроцессное взаимодействие на основе передачи сообщений для до 23 непривилегированных процессов, из которых 8 могли одновременно быть защищены друг от друга. Оно также реализовало планирование временных интервалов выполнения программ, выполняемых параллельно, запуск и управление выполнением программ по запросу других работающих программ, а также запуск передачи данных к или от периферийных устройств. Помимо этих элементарных механизмов, в нём не было встроенной стратегии выполнения программ и распределения ресурсов. Эта стратегия должна была быть реализована иерархией запущенных программ, в которой родительские процессы имели полный контроль над дочерними процессами и действовали как их операционные системы. После работы Бринча Хансена микроядра разрабатывались с 1970-х годов. Сам термин «микроядро» впервые появился не позднее 1981 года. Микроядра были задуманы как ответ на изменения в мире компьютеров и на ряд проблем, связанных с адаптацией существующих «монолитных ядер» к этим новым системам. Постоянно разрабатывались новые драйверы устройств, стеки протоколов, файловые системы и другие низкоуровневые системы. Этот код обычно располагался в монолитном ядре, и поэтому требовал значительной работы и тщательного управления кодом. Микроядра были разработаны с идеей, что все эти сервисы будут реализованы как программы пользовательского пространства, как и любые другие, что позволит работать над ними комплексно и запускать и останавливать их как любую другую программу. Это не только позволило бы упростить работу с этими сервисами, но и отделило код ядра, чтобы его можно было точно настроить, не опасаясь непредвиденных побочных эффектов. Кроме того, это позволило бы «создавать» совершенно новые операционные системы на основе общего ядра, что способствовало бы исследованиям в области ОС. Микроядра были очень актуальной темой в 1980-х годах, когда были представлены первые работоспособные локальные сети. Ядро AmigaOS Exec было ранним примером, представленным в 1986 году и использовавшимся в ПК с относительным коммерческим успехом. Отсутствие защиты памяти, которое в других аспектах считалось недостатком, позволило этому ядру достичь очень высокой производительности передачи сообщений, поскольку не требовалось копировать данные при обмене сообщениями между программами пользовательского пространства. Те же механизмы, которые позволили ядру распределяться в пользовательском пространстве, также позволили системе распределяться по сетевым соединениям. Первые микроядра, в частности Mach, созданные Ричардом Рашидом, показали разочаровывающую производительность, но их потенциальные преимущества казались настолько велики, что это было важным направлением исследований до конца 1990-х годов. Однако в это время скорость компьютеров значительно возросла по сравнению со скоростью сетевых систем, и недостатки в производительности стали перевешивать преимущества в плане разработки. Было предпринято много попыток адаптировать существующие системы для повышения производительности, но накладные расходы всегда были значительными, и большинство этих усилий требовали возврата программ пользовательского пространства обратно в ядро. К 2000 году большинство масштабных проектов по созданию ядра Mach были завершены, хотя macOS от Apple, выпущенная в 2001 году, по-прежнему использует гибридное ядро под названием XNU, которое сочетает в себе сильно модифицированное (гибридное) ядро OSF/1 (ядро OSFMK 7.3) с кодом BSD UNIX, и это ядро также используется в iOS, tvOS и watchOS. Windows NT, начиная с NT 3.1 и продолжая с Windows 11, использует гибридную архитектуру ядра. По состоянию на 2012 год GNU Hurd на базе Mach также функционирует и включен в тестовые версии Arch Linux и Debian. Хотя основные работы над микроядрами в значительной степени завершились, экспериментаторы продолжали разработки. Впоследствии было показано, что многие проблемы с производительностью более ранних конструкций не были фундаментальным ограничением концепции, а скорее были связаны с желанием разработчиков использовать системы специального назначения для реализации как можно большего числа этих сервисов. Применение более прагматичного подхода к проблеме, включая ассемблерный код и использование возможностей процессора для обеспечения концепций, обычно поддерживаемых программным обеспечением, привело к созданию новой серии микроядер с значительно улучшенной производительностью. Микроядра тесно связаны с экзоядрами. Они также имеют много общего с гипервизорами, но последние не претендуют на минимальность и специализируются на поддержке виртуальных машин; микроядро L4 часто используется в качестве гипервизора.

Введение

Ранние ядра операционных систем были относительно небольшими, отчасти из-за ограниченного объема компьютерной памяти. По мере увеличения вычислительной мощности компьютеров росло и количество устройств, которыми требовалось управлять ядру. На протяжении большей части ранней истории Unix, ядра оставались небольшими, несмотря на то, что включали в себя различные драйверы устройств и реализации файловых систем. Когда адресные пространства увеличились с 16 до 32 бит, проектирование ядра перестало быть ограниченным аппаратной архитектурой, и ядра начали увеличиваться в размерах. Распространение Berkeley Software Distribution (BSD) Unix положило начало эпохе больших ядер. Помимо управления базовой системой, состоящей из процессора, дисков и принтеров, BSD добавила полноценную сетевую систему TCP/IP и ряд "виртуальных" устройств, позволявших существующим программам работать через сеть "незаметно". Этот рост продолжался на протяжении многих лет, что привело к созданию ядер, содержащих миллионы строк исходного кода. В результате этого роста ядра стали подвержены ошибкам и их обслуживание становилось все более сложным. Микроядро было разработано для решения проблемы роста ядер и связанных с этим трудностей. Теоретически, архитектура микроядра обеспечивает более простое управление кодом благодаря его разделению на сервисы, работающие в пользовательском пространстве. Это также повышает безопасность и стабильность за счет уменьшения объема кода, выполняющегося в режиме ядра. Например, если сетевой сервис аварийно завершил работу из-за переполнения буфера, будет повреждена только память этого сервиса, а остальная часть системы продолжит функционировать.

Межпроцессная связь

Межпроцессное взаимодействие (МПК) — это любой механизм, позволяющий отдельным процессам обмениваться данными, как правило, посредством отправки сообщений. Общая память, в строгом определении, также является механизмом межпроцессного взаимодействия, но аббревиатура IPC обычно относится исключительно к передаче сообщений, и именно последний аспект особенно важен для микроядер. IPC позволяет строить операционную систему из ряда небольших программ, называемых серверами, которые используются другими программами в системе и вызываются через IPC. Поддержка большей части или всего периферийного оборудования осуществляется таким образом, с серверами для драйверов устройств, стеков сетевых протоколов, файловых систем, графики и т. д. IPC может быть синхронным или асинхронным. Асинхронное IPC аналогично сетевой коммуникации: отправитель отправляет сообщение и продолжает выполнение. Получатель проверяет наличие сообщения (опрашивает) или получает уведомление о нем через некоторый механизм уведомления. Асинхронное IPC требует, чтобы ядро поддерживало буферы и очереди для сообщений и обрабатывало переполнение буфера; оно также требует двойного копирования сообщений (от отправителя в ядро и из ядра к получателю). В синхронном IPC первая сторона (отправитель или получатель) блокируется до тех пор, пока другая сторона не будет готова выполнить IPC. Оно не требует буферизации или множественного копирования, но неявная синхронизация может усложнить программирование. Большинство программистов предпочитают асинхронную отправку и синхронный прием. Микроядра первого поколения обычно поддерживали как синхронное, так и асинхронное IPC и страдали от низкой производительности IPC. Йохен Лидтке считал, что проектирование и реализация механизмов IPC являются основной причиной этой низкой производительности. В своем микроядре L4 он разработал методы, которые снизили стоимость IPC на порядок. К ним относятся системный вызов IPC, поддерживающий операции отправки и приема, делая все IPC синхронным, и передачу максимально возможного объема данных в регистрах. Кроме того, Лидтке ввел концепцию прямого переключения процесса, при котором во время выполнения IPC выполняется (неполное) переключение контекста от отправителя непосредственно к получателю. Если, как в L4, часть или все сообщение передаются в регистрах, это передает часть сообщения в регистре без какого-либо копирования. Кроме того, избегаются накладные расходы на вызов планировщика; это особенно полезно в распространенном случае, когда IPC используется в стиле удаленного вызова процедур (RPC) клиентом, вызывающим сервер. Другая оптимизация, называемая ленивым планированием, позволяет избежать обхода очередей планирования во время IPC, оставляя потоки, блокирующиеся во время IPC, в очереди готовности. После вызова планировщика он перемещает такие потоки в соответствующую очередь ожидания. Поскольку во многих случаях поток разблокируется до следующего вызова планировщика, этот подход значительно экономит ресурсы. Позднее аналогичные подходы были приняты QNX и MINIX 3. В серии экспериментов Чен и Бершад сравнили циклы памяти на инструкцию (MCPI) монолитной Ultrix с циклами микроядра Mach в сочетании с сервером 4.3BSD Unix, работающим в пользовательском пространстве. Их результаты объяснили более низкую производительность Mach более высоким MCPI и показали, что IPC само по себе не несет ответственности за большую часть системных накладных расходов, что позволяет предположить, что оптимизации, ориентированные исключительно на IPC, будут иметь ограниченный эффект. Лидтке позже уточнил результаты Чена и Бершада, отметив, что основная часть разницы между MCPI Ultrix и Mach была вызвана промахами кэша и придя к выводу, что резкое сокращение рабочего набора кэша микроядра решит проблему. В клиент-серверной системе большая часть коммуникации по сути синхронна, даже если используются асинхронные примитивы, поскольку типичная операция — это клиент, вызывающий сервер, а затем ожидающий ответа. Поскольку это также способствует более эффективной реализации, большинство микроядер обычно последовали примеру L4 и предоставили только синхронный IPC-примитив. Асинхронное IPC можно реализовать поверх с использованием вспомогательных потоков. Однако опыт показал, что полезность синхронного IPC сомнительна: синхронное IPC навязывает многопоточную структуру в противном случае простым системам, что приводит к сложностям синхронизации. Кроме того, вызов сервера в стиле RPC последовательно распределяет клиент и сервер, чего следует избегать, если они работают на разных ядрах. Поэтому версии L4, развернутые в коммерческих продуктах, сочли необходимым добавить механизм асинхронного уведомления для лучшей поддержки асинхронной коммуникации. Этот механизм, подобный сигналу, не передает данные и, следовательно, не требует буферизации ядром. Имея две формы IPC, они, тем не менее, нарушили принцип минимальности. Другие версии L4 перешли на полностью асинхронное IPC. Поскольку синхронное IPC блокирует первую сторону до готовности другой, неограниченное использование может легко привести к взаимоблокировкам. Кроме того, клиент может легко организовать атаку типа «отказ в обслуживании» на сервер, отправив запрос и не пытаясь получить ответ. Поэтому синхронное IPC должно предоставлять средства для предотвращения неопределенной блокировки. Многие микроядра предоставляют тайм-ауты для вызовов IPC, которые ограничивают время блокировки. На практике выбор разумных значений тайм-аута затруднителен, и системы почти неизбежно используют бесконечные тайм-ауты для клиентов и нулевые тайм-ауты для серверов. В результате тенденция заключается в том, чтобы не предоставлять произвольные тайм-ауты, а только флаг, указывающий, что IPC должно завершиться неудачей, если партнер не готов. Этот подход фактически предоставляет выбор двух значений тайм-аута: нуля и бесконечности. Недавние версии L4 и MINIX пошли по этому пути (более старые версии L4 использовали тайм-ауты). QNX избегает этой проблемы, требуя от клиента указать буфер ответа как часть вызова отправки сообщения. Когда сервер отвечает, ядро копирует данные в буфер клиента, не дожидаясь, пока клиент явно получит ответ.

Серверы

Серверы микроядра по сути являются демонами, как и любые другие программы, за исключением того, что ядро предоставляет некоторым из них привилегии для доступа к областям физической памяти, которые обычно недоступны большинству программ. Это позволяет некоторым серверам, в частности драйверам устройств, взаимодействовать напрямую с аппаратным обеспечением. Базовый набор серверов для микроядра общего назначения включает серверы файловых систем, серверы драйверов устройств, сетевые серверы, серверы дисплея и серверы устройств пользовательского интерфейса. Этот набор серверов (взятый из QNX) предоставляет примерно тот же набор сервисов, что и монолитное ядро Unix. Необходимые серверы запускаются при загрузке системы и предоставляют сервисы, такие как доступ к файлам, сети и устройствам, обычным приложениям. Поскольку такие серверы работают в среде пользовательского приложения, разработка сервера больше похожа на обычную разработку приложений, чем на процесс сборки и загрузки, необходимый для разработки ядра. Кроме того, многие "сбои" можно устранить, просто остановив и перезапустив сервер. Однако при сбое сервера теряется часть состояния системы, поэтому этот подход требует, чтобы приложения умели обрабатывать отказы. Хорошим примером является сервер, отвечающий за TCP/IP соединения: при его перезапуске приложения будут испытывать "разрыв" соединения, что является нормальным явлением в сетевой системе. Для других сервисов отказ менее вероятен и может потребовать изменений в коде приложения. В QNX возможность перезапуска предоставляется в составе QNX High Availability Toolkit.

Драйверы устройств

Драйверы устройств часто выполняют прямой доступ к памяти (DMA) и, следовательно, могут записывать в произвольные области физической памяти, включая различные структуры данных ядра. Поэтому этим драйверам необходимо доверять. Распространено ошибочное мнение, что это означает необходимость их включения в ядро. На самом деле, доверие к драйверу не повышается и не понижается от того, является ли он частью ядра. Хотя запуск драйвера устройства в пользовательском пространстве не обязательно уменьшает ущерб, который может нанести некорректно работающий драйвер, на практике это полезно для стабильности системы при наличии драйверов с ошибками (а не злонамеренных): нарушения доступа к памяти самим кодом драйвера (а не устройством) все еще могут быть перехвачены аппаратным обеспечением управления памятью. Кроме того, многие устройства не поддерживают DMA, и их драйверы могут быть сделаны ненадежными при работе в пользовательском пространстве. В последнее время все большее число компьютеров оснащаются IOMMU, многие из которых можно использовать для ограничения доступа устройства к физической памяти. Это также позволяет считать драйверы пользовательского режима ненадежными. Драйверы пользовательского режима на самом деле появились раньше микроядер. Michigan Terminal System (MTS) в 1967 году поддерживала драйверы пользовательского пространства (включая поддержку файловой системы), став первой операционной системой, разработанной с такой возможностью. Исторически драйверы представляли меньшую проблему, поскольку количество устройств было небольшим и им доверяли, поэтому размещение их в ядре упрощало разработку и избегало потенциальных проблем с производительностью. Это привело к традиционной модели драйверов в ядре, используемой в Unix, Linux и Windows NT. С ростом количества различных периферийных устройств объем кода драйверов значительно увеличился и в современных операционных системах по размеру превосходит сам код ядра.

Безопасность

Преимущества микроядер в области безопасности часто обсуждаются. В контексте безопасности принцип минимальности микроядра, по мнению некоторых, является прямым следствием принципа наименьших привилегий, согласно которому весь код должен обладать только теми привилегиями, которые необходимы для обеспечения требуемой функциональности. Минимальность требует, чтобы доверенная вычислительная база системы (TCB) была сведена к минимуму. Поскольку ядро (код, выполняющийся в привилегированном режиме аппаратного обеспечения) имеет неконтролируемый доступ к любым данным и, следовательно, может нарушить их целостность или конфиденциальность, ядро всегда является частью TCB. Его минимизация естественна при разработке, ориентированной на безопасность. Следовательно, архитектуры микроядер использовались в системах, предназначенных для приложений с высокими требованиями к безопасности, включая KeyKOS, EROS и военные системы. Фактически, в общих критериях (CC) на самом высоком уровне заверения (Уровень гарантии оценки (EAL) 7) содержится явное требование, чтобы объект оценки был "простым", что признает практическую невозможность установления подлинной надежности для сложной системы. Вновь, термин "простой" является вводящим в заблуждение и недостаточно четко определенным. По крайней мере, в критериях оценки надежных компьютерных систем Министерства обороны были введены более точные формулировки для классов B3/A1: text="ТКБ должен [реализовать] полные, концептуально простые механизмы защиты с четко определенной семантикой. Значительные системно-инженерные усилия должны быть направлены на минимизацию сложности ТКБ, а также на исключение из ТКБ тех модулей, которые не критичны для защиты."|sign=|source=Критерии оценки надежных компьютерных систем Министерства обороны.

В 2018 году на Азиатско-Тихоокеанской конференции систем был представлен доклад, в котором утверждалось, что микроядра продемонстрированно безопаснее монолитных ядер, путем анализа всех опубликованных критических CVE для ядра Linux на тот момент. Исследование показало, что 40% проблем вообще не могли бы возникнуть в формально верифицированном микроядре, и только 4% проблем остались бы полностью не устраненными в такой системе.