![]() |
Технология Клиент-Сервер 2005'4 |
||||||
|
http://research.microsoft.com/os/singularity
Microsoft Research Technical Report MSR-TR-2005-135
ПО исполняется на платформе, которая эволюционировала последние 40 лет и все чаще демонстрирует свой возраст. Эта платформа представляет собой огромное собрание кода – операционных систем, языков программирования, компиляторов, библиотек и т.д. – и аппаратного обеспечения, на котором исполняются программы. С одной стороны, эта платформа – пример огромного успеха как с финансовой, так и с практической точки зрения. Она лежит в основе 179-миллиардной программной индустрии, и вызвала к жизни такие революционные новинки как Internet. С другой стороны, платформа и работающее на ней ПО куда менее надежны и безопасны, чем хотелось бы большинству пользователей (и разработчиков!).
Отчасти проблема заключается в том, что современная платформа недалеко ушла от компьютерных архитектур, операционных систем и языков программирования 1960-70 годов. Среда вычислений тех времен крайне отличалась от современной. Компьютеры были весьма ограничены в скорости и объеме памяти; они использовались только малыми группами технически грамотных и не злонамеренных пользователей; они редко объединялись в сети или общались с физическими устройствами. Сейчас все не так, но современные архитектуры компьютеров, операционные системы и языки программирования недостаточно изменились для того, чтобы отражать фундаментальные изменения в компьютерах и их использовании.
Singularity – исследовательский проект Microsoft Research, который начался с вопроса: на что была бы похожа программная платформа, если спроектировать ее на пустом месте, и во главу угла поставить не производительность, а надежность? Singularity пытается ответить на этот вопрос, опираясь на усовершенствования в языках и средствах программирования. Несмотря на то, что о надежности трудно судить по исследовательскому прототипу, Singularity показывает практичность новых технологий и архитектурных решений, ведущих к созданию множества устойчивых и надежных систем в будущем.
Развивающиеся по экспоненте аппаратные средства обычно являются движущей силой фундаментальных изменений систем и приложений. Программное обеспечение, развивающееся медленнее, редко создает возможности для фундаментальных усовершенствований. Однако программное обеспечение действительно развивается, и его изменение делает возможным – и необходимым – пересмотр старых подходов. Усовершенствования в языках программирования, исполняющих подсистемах и инструментальных средствах анализа программ дают материал для создания архитектур и систем, более надежных и устойчивых, чем существующие в настоящее время:
Языки и средства, поддерживающие перечисленные возможности, в процессе работы выявляют и предотвращают ошибки программирования. Менее исследовано, как эти механизмы способствуют глубоким изменениям в системной архитектуре, которая, в свою очередь, могла бы приблизить достижение такой цели, как устранение дефектов ПО и смягчение их последствий [28].
Эта статья в деталях описывает систему Singularity. Раздел 2 содержит обзор системы и ее новинок. Раздел 3 описывает архитектуру системы Singularity, фокусируясь на ядре, процессах и исполняющей системе языка. Раздел 4 описывает системную поддержку языков программирования. В разделе 5 описывается I/O и система безопасности. Раздел 6 содержит замеры производительности. Раздел 7 описывает работы, связанные с данной. Приложение А содержит список вызовов ABI ядра.
Singularity – это новая операционная система, разрабатываемая как основа для более надежного системного и прикладного ПО. Singularity использует достижения в языках и средствах программирования для создания среды, в которой ПО будет с большей вероятностью создаваться корректно, поведение программ будет проще контролировать, а сбои во время исполнения можно изолировать.
Ключевой аспект Singularity – модель расширения, построенная на программно-изолированных процессах (Software-Isolated Process, SIP), которые инкапсулируют части приложения или системы и обеспечивают сокрытие информации, изоляцию сбоев и строгое типизированный интерфейс. SIP используются по всей ОС и прикладном ПО. Мы верим, что создание системы на этой абстракции приведет к созданию более надежного ПО.
SIP в Singularity – это процессы ОС. Весь код за пределами ядра исполняется в SIP-ах. SIP отличаются от обычных процессов ОС следующим:
SIP-ы не просто используются для инкапсуляции расширений приложений. Вместо обычного применения разных механизмов для процессов и динамической загрузки кода Singularity использует единый механизм для расширяемости и защиты. Как следствие, Singularity нужна только одна модель восстановления после сбоев, один механизм коммуникаций, одна политика безопасности и одна модель программирования вместо слоев или частично избыточных механизмов и политик, применяемых в современных системах. Сутью эксперимента с Singularity является создание целой ОС с использованием SIP и демонстрация того, что результирующая система более надежна, чем традиционные.
Ядро Singularity практически полностью состоит из безопасного (safe) кода, а остальная часть системы, исполняемая в SIP, состоит только из проверяемого безопасного (verifiably safe) кода, включая все драйверы устройств, системные процессы и приложения. В то время, как весь untrusted-код должен быть проверяем и безопасен, части ядра Singularity и исполняющей системы, именуемые доверенной основой (trusted base) не контролируемо безопасны. Безопасность языков защищает эту проверенную основу от непроверенного кода.
Целостность SIP-ов зависит от безопасности языков и от распространяющегося на всю систему инварианта, говорящего, что процесс не может содержать ссылок на объектное пространство другого процесса.
Очевидно, что обеспечение безопасности кода крайне существенно. В настоящее время Singularity полагается на проверку исходного и промежуточного кода компилятором. В будущем типизированный ассемблер (Typed Assembly Language, TAL) позволит Singularity проверять безопасность скомпилированного кода. TAL требует, чтобы исполняемый модуль программы предоставлял доказательства своей типобезопасности (что в случае безопасных языков может делаться компилятором автоматически). Проверка корректности таких доказательств и того, что они применимы для инструкций в исполняемом модуле – прямолинейная задача для простого верификатора из нескольких тысяч строк кода. Такая стратегия сквозной проверки исключает компилятор – большую, сложную программу – из проверенной основы Singularity. Верификатор должен быть тщательно продуман, реализован и проверен, но эти задачи вполне выполнимы благодаря его простоте и размеру.
Инвариант независимости памяти (memory independence invariant), запрещающий использование указателей между объектными пространствами, служит нескольким целям. Во-первых, он улучшает абстракцию данных и изоляцию процесса, скрывая детали реализации и предотвращая появление висячих указателей (dangling pointers) на выгруженные процессы. Во-вторых, он ослабляет ограничения при реализации, позволяя процессам использовать разные исполняющие системы и их сборщики мусора. В-третьих, он делает прозрачными учет и восстановление ресурсов, благодаря однозначно монопольному использованию памяти процессом. Наконец, это упрощает интерфейс ядра, устраняя потребность управлять множеством типов указателей и адресных пространств.
Главное возражение против такой архитектуры – сложность коммуникаций посредством передачи сообщений по сравнению с гибкостью совместного использования данных. В Singularity эта проблема решается использованием эффективной системы сообщений, расширений языков программирования, сжато описывающих коммуникацию через каналы, и средств верификации.
Разработчики ПО редко представляют во всей полноте функциональные возможности, нужные пользователям их системы или приложения. Вместо того, чтобы пробовать удовлетворить все запросы пользователей с помощью монолитной системы, наиболее нетривиальное программное обеспечение обеспечивает механизмы загрузки дополнительного кода. Например, Microsoft Windows поддерживает более чем 100 000 драйверов устройства сторонних производителей, позволяющих управлять почти любым аппаратным устройством. Точно так же бесчисленные расширения браузера улучшают интерфейс браузера. Даже проекты с открытым исходным кодом – теоретически поддающиеся изменению – предполагают использование расширений, так как это проще, чем разработка и распространение новых версий программного обеспечения.
Расширение, как правило, состоит из кода, динамически загружаемого в адресное пространство родительского приложения. Обладая прямым доступом к внутренним интерфейсам и структурам данных родительского приложения, расширение может предоставлять богатую функциональность. Но гибкость дорого стоит. Расширения – одна из главных проблем надежности, безопасности и обратной совместимости ПО. Несмотря на то, что код расширений зачастую может быть не проверенным или некачественным, поступить из непроверенного источника, а то и просто быть вредоносным, он грузится напрямую в адресное пространство программы без четкого разграничения между кодом родительской программы и расширения. Результат часто бывает плачевным. Например, согласно отчету Swift, 85% диагностированных падений Windows вызвано плохими драйверами устройств. Более того, поскольку четкой границы нет, расширение может использовать нераскрываемые внутренние аспекты реализации родительского приложения, что может сдерживать эволюцию программы и требовать экстенсивного тестирования для исключения несовместимости.
Динамическая загрузка кода взимает еще один, менее очевидный налог на производительность и корректность. ПО, способное загружать код, это открытая среда, в которой невозможно сделать четкие предположения о состоянии системы или правильности переходов. Рассмотрим виртуальную машину Java (JVM). Прерывание, исключение или переключение потоков могут вызвать код, который загружает новый файл, переписывает тело класса или метода и модифицирует глобальное состояние. В общем, единственный реальный способ анализировать программу, исполняющуюся в таких условиях, – начать с неявного предположения, что среда не может произвольно измениться между двумя любыми операциями.
Альтернативой является запрет загрузки кода и изоляция динамически создаваемого кода в его собственной среде. Предыдущие попытки реализации этого не завоевали широкой популярности, так как механизмы изоляции имели проблемы с программируемостью и производительностью, делавшие их менее привлекательными, чем выполнение без изоляции. Наиболее распространенный механизм – это традиционный процесс ОС, но его высокая стоимость ограничивает его применимость. Аппаратное управления памятью дает жесткие границы и защищает состояние процессора, но также удорожает передачу управления и данных между процессами. На процессорах х86 переключение между процессами может стоить от сотен до тысяч циклов, не включая TLB и промахов мимо кэша [25].
Более новые системы, такие, как JVM или Microsoft Common Language Runtime (CLR), проектировались с учетом использования расширяемости и языковой безопасности, а не аппаратного обеспечения, в качестве механизма изоляции вычислений, выполняемых в одном адресном пространстве. Сами по себе безопасные языки не гарантируют изоляции. Совместно используемые данные могут проложить широкую тропу между объектными пространствами, при этом механизмы рефлексии могут разрушить абстракцию данных и сокрытие информации. Как следствие, такие системы включают сложные механизмы и политики безопасности, такие как контроль доступа в Java или безопасность доступа к коду в CLR, ограничивающие доступ к механизмам и интерфейсам системы [40]. Эти механизмы сложны в использовании и привносят существенные накладные расходы.
Не менее важно то, что вычисления, использующие одну исполняющую систему и работающие в одном процессе, не изолируются при сбое. Если сбой происходит в вычислении, производимом JVM, обычно перезапускается весь процесс JVM, поскольку сложно изолировать и отбросить поврежденные данные, и найти точку, с которой нужно перезапустить неудавшееся вычисление [11].
Singularity использует для инкапсуляции SIP. Драйверы устройств, системные процессы, приложения или расширения работают в собственных SIP и общаются через каналы, предоставляющие функциональность в необходимых пределах. Если внутри SIP происходит сбой, его работа завершается, что позволяет системе освободить ресурсы и оповестить партнеров этого процесса. Поскольку эти партнеры не разделяют состояние с расширением, а их связь с процессом четко определена протоколом канала, восстановление после сбоя производится локально.
Еще один источник нового кода, появляющегося во время исполнения – это динамическая кодогенерация, как правило, инкапсулируемая в интерфейсе рефлексии. Рефлексия позволяет исполняемой программе проверять исполняемый код и данные, а также порождать и выполнять новые методы. Рефлексия обычно используется для создания кода маршалинга для объектов или XML-парсеров для конкретных схем. Закрытые SIP в Singularity не позволяют использовать кодогенерацию во время исполнения.
Вместо этого Singularity предоставляет рефлексию времени компиляции (compile-time reflection, CTR), обладающую сходной функциональностью, но выполняемую при компиляции. Обычная рефлексия, имеющая доступ к данным времени выполнения, является более общим средством, чем CTR. Однако во многих случаях класс, подлежащий маршалингу, или разбираемая схема известны еще до исполнения. В таких случаях CTR порождает код в процессе компиляции. В других случаях Singularity позволит генерировать код и запускать его в отдельном SIP.
Сегодня ОС не расценивают программы или приложения как абстракции верхнего уровня. Современные приложения – это коллекции файлов, содержащих код, данные и метаданные, которые непроверенный агент устанавливает, копируя по частям в файловую систему и регистрируя их в пространствах имен. Система многого не знает о связях между частями и не имеет достаточного контроля над процессом установки. Хорошо известное последствие этого – нарушение работы одного программного продукта при установке другого, не связанного с ним.
В Singularity приложение состоит из манифеста и коллекции ресурсов. Манифест описывает приложение в терминах ресурсов и их зависимостей. Несмотря на то, что многие современные сценарии установки ПО комбинируют декларативные и императивные аспекты, манифесты Singularity содержат только декларативные выражения, описывающие желаемое состояние приложения после установки или обновления.
За реализацию этого состояния отвечает Singularity. Манифест должен предоставить достаточно информации для того, чтобы инсталлятор Singularity смог вычислить соответствующие шаги инсталляции, выявить конфликты с существующими приложениями и решить, удачна ли инсталляция. Singularity может пресечь инсталляцию, способную повредить системе.
Другие аспекты Singularity также используют информацию из манифеста. Например, модель безопасности Singularity рассматривает приложения как принципалы безопасности, что позволяет внести приложение в список контроля доступа (access control lists, ACL) файла. Такое отношение к приложениям требует сведений о компонентах приложения и их зависимостях, а также строгой идентификации, и все эти сведения должны содержаться в манифесте.
Главные достоинства Singularity – это:
На рисунке 1 приведена архитектура ОС Singularity, построенная вокруг трех ключевых абстракций: ядра, SIP и каналов. Ядро содержит основную функциональность системы, включая управление памятью, создание и завершение процессов, работу каналов, планирование (scheduling) и ввод/вывод. Как и в случае других микроядер, большая часть функциональности системы находится в процессах вне ядра.
Код в Singularity бывает проверяемым (verified) или доверенным (trusted). Безопасность типов и памяти проверяемого кода проверяется компилятором. Непроверяемый код обязан быть доверенным для системы и ограничен HAL, ядром и исполняющей системой. Большая часть ядра проверяемо безопасна, но часть написана на ассемблере, C++ и небезопасном C#.
Весь остальной код написан на безопасном языке, транслирован в безопасный Microsoft Intermediate Language (MSIL) и затем скомпилирован в х86 компилятором Bartok (оптимизирующий компилятор, написанный на C# для трансляции MCIL в x86-инструкции – прим.ред.) [20]. Сейчас мы полагаемся на то, что Bartok корректно проверяет и генерирует безопасный код. В долгосрочном плане это, очевидно, неудовлетворительно, и мы планируем использовать типизированный ассемблер, чтобы проверять результаты, выдаваемые компилятором, и сократить эту часть доверенной базы до небольшого верификатора [36].
Разделительная линия между двумя типами кода проводится исполняющей системой. Доверенный, но непроверяемый код эффективно изолирован от остального, верифицируемого. Компилятор Singularity способен «инлайнить» многие методы ядра, перенося их в пользовательские процессы.
Ядро Singularity – это привилегированный компонент системы, управляющий доступом к аппаратным ресурсам, выделяющий и освобождающий память, создающий потоки и управляющий ими, осуществляющий внутрипроцессную синхронизацию потоков и управляющий вводом-выводом. Он написан на смеси безопасного и небезопасного C#-кода и работает в собственном объектном пространстве с собственным сборщиком мусора.
Кроме обычных каналов передачи сообщений, процессы связываются с ядром через строго версионный бинарный интерфейс приложений (application binary interface, ABI), вызывающий статические методы в коде ядра. Этот интерфейс следует дизайну остальной системы и изолирует объектные пространства ядра и процесса. Все параметры ABI – значения, а не указатели, поэтому координация сборщиков мусора ядра и процессов не нужна. Единственным исключением является расположение методов ABI. Наши сборщики мусора в настоящее время не перемещают код, но ели бы они делали это, пришлось бы поддерживать постоянство адресов этих методов.
ABI обеспечивает неизменность изоляции состояния: с помощью ABI процесс не может изменить состояние другого процесса. За двумя исключениями вызовы ABI влияют только на состояние вызывающего процесса. Эти два исключения – изменение состояния дочернего процесса до или после его исполнения, но не во время исполнения. Первое – это вызов, создающий дочерний процесс, и указывающий код, загружаемый в дочерний процесс до его исполнения. Второе – это вызов, останавливающий дочерний процесс, что освобождает ресурсы после завершения исполнения всех потоков. Изоляция состояния обеспечивает единоличный контроль процесса в Singularity над своим состоянием.
Ядро экспортирует конструкции синхронизации – мьютексы, события с автоматическим и ручным сбросом – для координации потоков в процессе. Поток манипулирует этими конструкциями через строго типизированные непрозрачные дескрипторы, указывающие на таблицу дескрипторов ядра. Строгая типизация не позволяет процессу изменять дескрипторы. Кроме того, слоты в таблице дескрипторов освобождаются только после завершения процесса, чтобы не дать процессу освободить мьютекс, удерживая его дескриптор, и использовать его для манипуляций объектами другого процесса. Singularity повторно использует в процессе вхождения таблицы. В данном случае удерживание дескриптора может вызвать более мягкие, но все же болезненные ошибки внутри процесса.
ABI ядра строго версионно. Явно предоставляя информацию о версии ABI для каждой программы, Singularity обеспечивает развитие системы и обратную совместимость.
Код процесса компилируется с соответствующей скомпилированной сборкой ABI, пространства имен которой явно указывает версию. Например, Microsoft.Singularity.V1.Threads – это пространство имен, содержащее функциональность, относящуюся к работе с потоками, для первой версии ABI. Исходный код процесса содержит указание на пространство имен, содержащее требуемую версию ABI. Двоичный код процесса содержит в метаданных явные ссылки на нужную версию ABI.
Программа устанавливается только в том случае, если нужная ей версия ABI поддерживается на целевой машине. В этом случае интерфейсная ABI-сборка заменяется сборкой, которая предоставляет реализацию указанной версии ABI. Простейшая реализация во время исполнения превращает вызовы ABI в прямые вызовы статических методов ядра. Однако более новая версия Singularity может поместить в пространство имен из предыдущей версии библиотеку функций совместимости. Иначе код совместимости может исполняться в ядре, так как ядро может поддерживать множественные реализации ABI в их собственных пространствах имен.
Версия 1 ABI ядра содержит 126 вхождений.
Singularity поддерживает заменяемый во время компиляции планировщик. Мы реализовали четыре планировщика – планировщик Риальто (Rialto [32]), мультиресурсный laxity-based-планировщик, циклический (round-robin) планировщик (реализованный как вырожденный случай планировщика Риальто), и циклический планировщик минимального времени ожидания (minimum latency round-robin).
Циклический планировщик минимального времени ожидания оптимизирован под большое количество часто общающихся потоков. Планировщик поддерживает два списка запущенных потоков. Первый, неблокированный (unblocked) список, содержит потоки, только недавно ставшие исполнимыми. Второй, список выгруженных (preempted) потоков, содержит исполнимые, но приостановленные потоки. При выборе следующего потока для исполнения планировщик удаляет потоки из списка неблокированных потоков в порядке FIFO. Если список неблокированных потоков пуст, планировщик удаляет следующий поток из списка выгруженных потоков. При прерывании таймера планировщика все потоки из неблокированного списка перемещаются в конец списка выгруженных потоков, а за ними помещается поток, исполнявшийся в момент срабатывания таймера. Запускается первый поток из списка неблокированных потоков, таймер планировщика сбрасывается.
Политика использования двух списков в планировщике нацелена на работу с потоками, пробуждающимися по сообщению, выполняющими небольшой объем работы, отправляющими одно или неколько сообщений другим процессам и затем блокирующимся в ожидании сообщения. Это обычное поведение потоков, исполняющих циклы обработки сообщений. Чтобы исключить дорогостоящий сброс таймера планировщика, потоки из списка неблокированных потоков наследуют квант потока, разблокировавшего их. В соединении с использованием двух списков квантовое наследование достаточно эффективно, так как Singularity может переключиться с одного потока на другой всего за 394 цикла.
Система Singularity живет в одном виртуальном адресном пространстве. Аппаратная поддержка виртуальной памяти используется для защиты страниц, например, отображение первых 16К адресного пространства для отлавливания ссылок на нулевые указатели. Внутри системы Singularity адресное пространство логически разделяется между объектным пространством ядра, объектными пространствами каждого процесса и Обменной Кучей (Exchange Heap) для данных каналов.
Главное решение в дизайне – инвариант независимости памяти: указатели между объектными пространствами указывают только на Exchange Heap. В частности, ядро не имеет указателей на объектное пространство процесса, и ни один процесс не имеет указателя на объекты другого процесса. Это гарантирует, что каждый процесс может подвергнуться сборке мусора и быть выгруженным без взаимодействия с другими процессами.
Ядро создает процесс, выделяя достаточный объем памяти для загрузки исполняемого образа из файла, хранящегося в РЕ-формате Microsoft. Затем Singularity выполняет перемещения и адресную привязку, включая связывание с ABI-функциями ядра. Ядро запускает новый процесс, создавая поток, стартующий с точки входа исполняемого образа. Это доверенный код запуска потока, вызывающий менеджеры стека и страниц для инициализации процесса.
Процесс получает дополнительное адресное пространство, вызывая менеджер страниц ядра. Эти страницы не обязательно прилегают к существующему адресному пространству процесса, так как сборщики мусора не требуют непрерывности адресного пространства, хотя и могут нуждаться в непрерывных областях для больших объектов или массивов. В дополнение к памяти, в которой содержится код процесса и данные кучи, процесс располагает стеком для каждого потока и может обращаться к Exchange Heap.
Singularity использует связанные (составные) стеки для уменьшения количества памяти, используемой потоком. Этот стек по запросу увеличивается добавлением не смежных сегментов (4К и более). Компилятор Singularity выполняет статический меж-процедурный анализ для оптимизации размещения тестов переполнения. Каждая из этих вставляемых компилятором проверок состоит из доверенного кода, обращающегося к системным структурам данных, находящимся в объектном пространстве процесса, для определения места, оставшегося в данном сегменте стека.
Прежде, чем исполняющийся поток разместит новый стековый фрейм, потенциально способный вызвать переполнение текущего сегмента стека, доверенный код вызывает метод ядра, отключающий прерывания, и вызывает менеджер страниц, который выделяет новый сегмент стека. Этот код также инициализирует первый стековый фрейм в сегменте – между исполняемой и вызываемой процедурой – чтобы вызвать процедуру отцепления сегмента, которая освободит сегмент при выталкивании стека. Поскольку все процессы на x86 работают в кольце 0, текущий сегмент стека должен всегда оставлять достаточный участок памяти для процессора, чтобы сохранить прерывание или фрейм исключения перед тем, как обработчик переключится на специализированный стеку прерывания.
Exchange Heap отвечает в Singularity за эффективность коммуникаций, хранит данные, передаваемые между процессами (рисунок 2). Exchange Heap не подвергается сборке мусора, вместо этого используется подсчет ссылок для отслеживания использования блоков памяти, называемых регионами (regions). Процессы имеют доступ к региону через структуру allocation.
Структуры allocation также находятся в Exchange Heap, что позволяет передавать их между процессами, но каждая из них принадлежит в любой момент времени только одному процессу, который и может обращаться к ней. Нижележащий регион доступен только для чтения через несколько allocation. Кроме того, allocation могут иметь различную основу и границы, обеспечивающие различные представления нижележащих данных. Например, протокол, обрабатывающий код в сетевом стеке, может удалить из пакета инкапсулированные заголовки протокола, не копируя их. Регион отслеживает число allocation, указывающих на него, и освобождается, когда количество ссылок падает до нуля. Компилятор Singularity скрывает лишний уровень косвенности, предоставляя строго типизированную ссылку на регион и автоматически генерируя код, скрывающий подробности.
Процесс может создавать дополнительные потоки. Untrusted (но проверяемый) код, исполняемый внутри процесса, создает объект потока, инициализирует его некоторой функцией и сохраняет объект в неиспользованном слоте таблицы потоков системы. Затем этот код вызывает ThreadHandle.Create, предавая потоку индекс из таблицы. Этот метод ядра создает контекст потока для хранения регистров, занимает начальный стековый фрейм и обновляет его структуры данных. Затем он возвращает управление процессу, где runtime вызывает ThreadHandle.Start для управления потоком. Когда поток запускается, он исполняется в ядре и выполняет код, который вызывает точку входа процесса, передавая индекс потока в runtime-таблице потоков. Стартовый код процесса вызывает в объекте потока функцию, которая начинает исполнение потока.
При создании потока или процесса ядро знает только один адрес в процессе – адрес кода запуска потока в точке входа процесса, который, как и ABI ядра, не может быть перемещен.
Сборка мусора – существенный компонент большинства безопасных языков, поскольку предотвращает ошибки освобождения памяти, способные нарушить гарантии безопасности. В Singularity объектные пространства ядра и процессов подлежат сборке мусора.
Существование множества подходов и алгоритмов сборки мусора заставляет предположить, что ни один сборщик мусора не подходит для всей системы [21]. Архитектура Singularity рассматривает раздельно алгоритм, структуры данных и исполнение сборщика мусора для каждого процесса, так что можно выбрать наиболее соответствующий поведению кода процесса и исполнять его без глобальной координации. Это возможно благодаря четырем аспектам Singularity:
Исполняющие системы Singularity сейчас поддерживают пять типов коллекторов – generational semi-space, generational sliding compacting, адаптивную комбинацию двух предыдущих, mark-sweep, и параллельный (concurrent) mark-sweep. Сейчас мы используем последний для системного кода, так как он делает очень короткие паузы при сборке. При использовании этого сборщика каждый поток имеет отделенный свободный список, в нормальном случае исключающий синхронизацию потоков. Сборка мусора запускается при достижении порога размещения и выполняется в независимом потоке, помещающем достижимые объекты. В процессе сборки сборщик останавливает каждый поток для сканирования его стека, что для типичного стека означает паузу менее чем в 100 микросекунд. Накладные расходы при использовании этого сборщика выше, чем у не параллельных сборщиков, поэтому в приложениях мы используем более простой не параллельный mark-sweep сборщик.
У каждого SIP есть собственный сборщик, полностью отвечающий за сборку объектов в объектном пространстве. С точки зрения сборщика мусора, когда поток управления приходит или оставляет приложение (или ядро), это обрабатывается подобно вызову или обратному вызову от native-кода в обычных средах со сборкой мусора. Поэтому сборка мусора для различных объектных пространств может планироваться и выполняться совершенно независимо. Если приложение использует «останавливающий мир» (stop-the-world) сборщик, поток считается остановленным относительно объектного пространства приложения, даже если он работает в объектном пространстве ядра из-за вызова ядра. Поток, тем не менее, останавливается по возвращению в объектное пространство приложения на время сборки.
В среде со сборкой мусора стек потока содержит объектные ссылки, служащие потенциальными корнями для сборщика. Вызовы методов ядра исполняются в стеке пользовательского потока, и могут хранить в этом стеке указатели на ядро. На первый взгляд это кажется нарушением инварианта независимости памяти, создавая кросс-процессные указатели, или, по крайней мере, переплетает сборку мусора в пользовательском процессе и в ядре.
Чтобы устранить эту проблему, Singularity проводит границу между фреймами стека каждого пространства, так что GC не нужно видеть ссылки на другое пространство. При вызове между доменами (т.е. процесс > ядро или ядро > процесс) Singularity сохраняет регистры вызываемой стороны в специальной структуре в стеке, которая заодно разграничивает междоменный вызов.
Эти разделители также способствуют чистому завершению процессов. При уничтожении процесса его потоки останавливаются и ядро выдает каждому исключение, которое просматривает и освобождает фреймы стека процесса.
Процессы Singularity общаются исключительно с помощью отправки сообщений через каналы, которые являются двунаправленными и типизированными. Сообщения – это размеченные коллекции значений или блоки сообщений в Exchange Heap, которые передаются от посылающего к принимающему процессу. Канал типизируется контрактом, в котором указываются формат сообщений и корректные последовательности сообщений для этого канала.
Процесс создает канал, вызывая статический метод контракта NewChannel, который возвращает в выходных параметрах две конечные точки канала, асимметрично типизированные как экспортер и импортер:
C1.Exp importCh; C1.Imp exportCh; C1.NewChannel(out importCh, out exportCh); |
Процесс может передать любую из конечных точек (или обе) другим процессам по существующим каналам. Процесс, получающий конечную точку, имеет канал к процессу, владеющему другой конечной точкой. Например, если процесс приложения хочет связаться с системной службой, приложение создает две конечных точки и отправляет запрос, содержащий одну из точек, серверу имен системы, который перенаправляет конечную точку службе, тем самым создавая канал между процессом и службой.
Отправка по каналу производится асинхронно. Получение синхронно блокирует канал до получения оговоренного сообщения. Используя возможности языка, поток может ожидать первого из набора сообщений, поступающих по каналу, или ждать указанного набора сообщений из разных каналов. При отправке данных по каналу владение ими передается от посылающего процесса получающему процессу. Этот инвариант владения обеспечивается языком и исполняющей системой, и служит трем целям. Первая – предотвратить совместное использование сообщений разными процессами. Второе – способствовать статическому анализу программы, предотвращая множественные ссылки (pointer aliasing) на сообщение. Третья – обеспечить гибкость реализаций, предоставив семантику передачи сообщений, которая может быть реализована копированием или передачей указателя.
Конечные точки канала и значения, передаваемые по каналам, располагаются в Exchange Heap. Конечные точки не могут располагаться в объектном пространстве процесса, так как они передаются по каналам. Аналогично, передаваемые по каналам данные не могут располагаться в объектном пространстве, так как это нарушит инвариант независимости памяти. Отправитель сообщения передает владение им, сохраняя указатель на сообщение в конечной точке получателя, в месте, определяемом текущим состоянием протокола обмена сообщениями. Такой подход естественным образом разрешает реализации стека ввода/вывода с «нулевым копированием». Например, дисковый буфер и сетевые пакеты могут передаваться через множество каналам, стек протоколов в процесс приложения, без копирования.
Архитектура Singularity разрешает SIP использовать совершенно разные исполняющие системы, что позволяет подгонять исполняющую систему под каждый процесс. Например, SIP, исполняющий однопоточный код, может не нуждаться в поддержке синхронизации потоков, необходимой многопоточным SIP. SIP без объектов, требующих финализации (или имеющих финализаторы, не обращающиеся к совместно используемым потоками данным), могут не нуждаться в отдельном финализаторе для соблюдения семантики, диктуемой языком для финализаторов. SIP с определенными стратегиями размещения могут уметь предварительно занимать память или занимать память в стеке для всех используемых объектов, устраняя потребность в GC при исполнении SIP.
Безопасные языки программирования имеют много преимуществ при создании надежного, поддающегося анализу ПО, невосприимчивого к низкоуровневым прорехам в безопасности, от которых страдает код на С и C++. Благодаря этим практическим преимуществам популярность безопасных языков расте. Обычные операционные системы не содержат никакой особой поддержки безопасных программ, и не могут получить выигрыша от их достоинств. Singularity, наоборот, исходит из предположения о безопасности языка и создает архитектуру системы, поддерживающую и усиливающую предоставляемые языком гарантии.
Singularity объединяет исполняющую систему языка и процессы операционной системы. В безопасной системе эта поддержка нужна всем процессам, так что отдельная виртуальная машина, такая, как JVM или CLR, является излишеством. Однако простой подход к созданию гомогенных исполняющих систем, например, CLR, для всех процессов накладывает ненужные ограничения на службы и программы, чье поведение на соответствует свойствам исполняющей системы. Исполняющие системы языков предоставляют сервисы – особенно GC – которые могут непосредственно взаимодействовать с программами. Например, основанный на поколениях GC может создать секундную паузу в исполнении программы, что нарушит работу, например, медиаплеера или операционной системы. С другой стороны, работающий в реальном времени сборщик, пригодный для медиаплеера, может замедлить вычислительные задачи. Кроме всего прочего, гомогенные среды эволюционируют в большие, сложные и дорогие системы, поскольку вынуждены поддерживать всевозможные требования различных зависящих от них приложений.
Singularity поддерживает гетерогенные среды исполнения. Каждый процесс имеет собственную исполняющую систему, со своей раскладкой памяти, алгоритмом сборки мусора и библиотеками. Благодаря независимости памяти исполняющая система может быть настроена для соответствия потребностям вычислений. В частности, GC процесса можно выбирать за его алгоритм, и не заботиться о его координации с коллегой в другом процессе.
Гетерогенные среды также предоставляют новый механизм осуществления политики. Содержание среды процесса ограничивает его поведение. Например, драйверы устройства выполняются в разреженной среде, которая содержит прежде всего определенные для драйвера абстракции, типа IoPorts, приспособленные для этого класса программ. Ненужные или неподходящие для драйверов абстракции можно вынести из этой среды. Другая политика может состоять в том, что не заслуживающие доверия приложения могут выполняться только в среде, в которой автоматы безопасности проверяют и контролируют поведение программы [45].
Singularity предлагает новую модель для безопасного расширения функциональности системы или приложения. В этой модели расширения не могут обратиться к коду или структурам данных родительской программы. Они являются замкнутыми программами, выполняемыми независимо. Этот подход увеличивает сложность создания расширения, поскольку разработчик родительской программы должен определить надлежащий интерфейс, который не полагается на общие структуры данных, а разработчик расширения должен обращаться к этому интерфейсу и, возможно, повторно реализовать функциональные возможности, имеющиеся в родительском приложении. Однако широко распространенные проблемы, присущие динамической загрузке кода, являются доводом в пользу альтернатив, увеличивающих изоляцию расширений. Механизм Singularity подходит как для приложений, так и для системного кода; он не зависит от семантики API, в отличие от таких подходов как Nooks [49]; и обеспечивает простые семантические гарантии, которые могут быть поняты программистами и использоваться инструментальными средствами.
Принципиальные аргументы против модели расширения Singularity группируются вокруг сложности написания кода передачи сообщений. Мы надеемся, что лучшие модели и языки программирования упростят создание, проверку и модификацию таких программ. Продвижение в этой области даст общий выигрыш, так как коммуникация посредством передачи сообщений фундаментальна и незаменима в распределенных вычислениях и Web-сервисах. По мере роста популярности передачи сообщений, возражений против этого способа программирования будет все меньше.
Наконец, Singularity не использует для защиты аппаратного управления памятью в процессорах, что дает возможность переоценки этого аппаратного обеспечения. В общем, многие программы используют только часть функциональности аппаратного управления памятью. Встраиваемые системы (или удовлетворительно оснащенные рабочие станции и серверы) редко работают со страницами, поскольку память недорога и ее много. Большие (64-битные) адресные пространства снижают потребность в использовании множественных адресных пространств для обхода ограничений 32-битности. Singularity показывает, как безопасные языки и консервативная политика совместного использования могут занять место границ процессов и колец защиты при более низкой цене. Современное аппаратное обеспечение, если оно не полностью используется, можно заменить более простыми механизмами с меньшим количеством таких узких мест в производительности, как TLB (Translation Lookaside Buffer, буфер быстрого преобразования адреса, специальная кэш-память, используемая для ускорения страничного преобразования).
Singularity выиграет от защиты памяти при использовании доверенного (непроверяемого) кода. Например, DMA сейчас по сути своей небезопасен и, из-за различий в интерфейсах устройств, не может быть инкапсулирован или виртуализован системой. Защита памяти при DMA может защитить систему от неверного использования DMA. Аппаратная поддержка сегментированных стеков может снизить сложность компилятора и накладные расходы на этот механизм во время исполнения.
Singularity написана на Sing#, который является расширением языка Spec#, разработанного в Microsoft Research. Сам Spec# является расширением Microsoft C#, предоставляющего конструкции (пре- и пост-условия, и инварианты объектов) для описания поведения программы [7]. Спецификации могут быть статически проверены верификатором Boogie или вставленными компилятором тестами времени исполнения. Sing# вносит в этот язык поддержку каналов и низкоуровневых конструкций, необходимых для системного кода.
Для разработки и реализации расширения языков программирования было две причины. Во-первых, немногие языки поддерживают связь посредством передачи сообщений. В большинстве случаев передача сообщений отдается на откуп библиотекам, что и синтаксически, и семантически неуклюжий способ переводить асинхронные операции на синхронный язык типа C#. Sing# предоставляет поддержку коммуникаций посредством передачи сообщений, что делает этот стиль коммуникаций и абстракцию SIP более эффективными в реализации и более удобными для программистов. Во-вторых, интеграция возможности в язык позволяет проверить больше аспектов программы. Конструкции Singularity позволяют статически проверять коммуникации.
Контракты каналов занимают центральное место в архитектуре изоляции Singularity и напрямую поддерживаются в Sing#. Вот контракт, описывающий простое взаимодействие с каналом:
contract C1 { in message Request(int x) requires x>0; out message Reply(int y); out message Error(); state Start: Request? -> (Reply! or Error!) -> Start; } |
В контракте C1 объявлено 3 сообщения: Request, Reply и Error. Для каждого сообщения указаны типы аргументов, содержащихся в сообщении. Например, и Request, и Reply содержат одно целое значение, а Error не содержит никаких значений. Кроме того, каждое сообщение может указывать оператор Spec#, дополнительно ограничивающий аргументы.
Сообщения могут быть также помечены направлением. Контракт всегда пишется с точки зрения экспортера. Так, в примере, Request – это сообщение, которое может быть отправлено импортером экспортеру, тогда как Reply и Error отправляются экспортером импортеру. Сообщения без уточнения сообщения могут перемещаться в обоих направлениях.
После объявления сообщения в контракте описывается допустимый протокол сообщений в виде конечного автомата, управляемого событиями отправки и получения. Первое объявленное состояние считается исходным состоянием взаимодействия. В примере контракта С1 объявлено единственное состояние Start. После имени состояния действие Request? указывает, что в состоянии Start экспортная сторона канала желает получить (?) сообщение Request. Идущая следом конструкция (Reply! или Error!) указывает, что экспортер посылает (!) сообщение Reply или Error. Последняя часть (-> Start) указывает, что затем взаимодействие возвращается в состояние Start, и этот цикл продолжается бесконечно.
Несколько более сложный пример – часть контракта для сетевого стека:
public contract TcpConnectionContract { in message Connect(uint dstIP, ushort dstPort); out message Ready(); // Initial state state Start : Ready! -> ReadyState; state ReadyState : one { Connect? -> ConnectResult; BindLocalEndPoint? -> BindResult; Close? -> Closed; } state BindResult : one { OK! -> Bound; InvalidEndPoint! -> ReadyState; } in message Listen(); state Bound : one { Listen? -> ListenResult; Connect? -> ConnectResult; Close? -> Closed; } ... |
Спецификация протокола в контракте служит нескольким целям. Она может помочь обнаружить ошибки программирования во время исполнения или с помощью средств статического анализа. Мониторинг во время исполнения управляет конечным автоматом контракта в соответствии с обменом сообщениями через канал и отслеживает ошибочные переходы. Эта техника проста в реализации, но отслеживает ошибки в работе только одной программы. Более того, она неспособна отловить такие живучие ошибки, как взаимоблокировки. Статический анализ программ может дать большие гарантии того, что процесс корректен и не содержит повторяющихся ошибок при исполнении всех программ.
В текущее время Singularity использует комбинацию run-time мониторинга и статической проверки. Все сообщения канала проверяются на соответствие контракту канала, что выявляет проблемы корректности, но не живучести. У нас также есть блок статического контроля, проверяющий свойства безопасности. Чтобы статически убедиться в отсутствии взаимоблокировок, мы планируем проверять контракты при помощи более общего статического анализа, основанного на проверке соответствий [42].
В дополнение, компилятор Singularity использует контракт для определения максимального числа сообщений, которые могут ожидать в канале, что позволяет компилятору статически занимать буферы в конечных точках канала. Статически размещенные буферы увеличивают производительность связи.
Каналы в Singularity представлены парами конечных точек, представляющих экспортирующую и импортирующую стороны канала. У каждой конечной точки есть тип, который указывает контракт канала. Типы конечных точек неявно объявляются в каждом контракте. Контракт С1 представляет собой класс, а конечные точки – это вложенные типы внутри этого класса:
Каждый класс контракта содержит методы отправки и получения сообщений, объявленных в контракте. В примере имеются следующие методы:
C1.Imp { void SendRequest(int x); void RecvReply(out int y); void RecvError(); } C1.Exp { void RecvRequest(out int x); void SendReply(int y); void SendError(); } |
Семантика методов Send состоит в асинхронной отправке сообщений. Методы Receive блокируются до прихода сообщения. Если сперва прибывает другое сообщение, происходит ошибка. Такие ошибки никогда не должны случаться, если программа проходит тест, проверяющий контракт. Пока получатель не знает точно, какое сообщение должно быть следующим, эти методы использовать нельзя. Вместо них Sing# предоставляет конструкцию switch receive.
Рассмотрим следующий код, ожидающий сообщений Reply или Error в импортирующей конечной точке типа C1.Imp.
void M( C1.Imp a) { switch receive { case a.Reply(x): Console.WriteLine(“Reply {0}”, x); break; case a.Error(): Console.WriteLine(“Error”); break; } } |
Выражение switch receive выполняется в два шага:
В приведенном выше примере у switch receive есть два паттерна – приход Reply в конечную точку «а» и приход Error в ту же самую конечную точку. В первом случае целый аргумент сообщения Reply автоматически связывается с локальной переменной x.
Однако конструкция switch receive является более общей, так как паттерны могут работать с множественными конечными точками. Следующий пример содержит две конечные точки a и b, которые могут получать сообщения Reply или Error.
void M (C1.Imp a, C1.Imp b) { switch receive { case a.Reply(x) && b.Reply(y): Console.WriteLine(“Both replies {0} and{1}”, x, y); break; case a.Error(): Console.WriteLine(“Error reply on a”); break; case b.Error(): Console.WriteLine(“Error reply on b”); break; case a.ChannelClosed(): Console.WriteLine(“Channel a is closed”); break; } } |
Пример показывает, как ждать конкретной комбинации сообщений, используя выражение switch receive. Первая ветвь используется только если сообщение Reply приходит на обе конечные точки a и b. Последний случай содержит паттерн ChannelClosed(), особый паттерн, который срабатывает, если канал закрыт (с другой стороны) и никаких других сообщений не ожидается.
Для гарантии изоляции памяти конечных точек и других данных, передаваемых по каналу, все блоки в Exchange Heap являются ресурсами, которые нужно отслеживать при компиляции. В частности, статические проверки обеспечивают, что доступ к этим ресурсам производится только в точках программы, где она владеет ресурсом, и методами, не нарушающими владение ресурсами. Существует строгая модель владения отслеживаемыми ресурсами. Каждым ресурсом в любой момент времени может владеть не более одного потока (или структуры данных в потоке). Например, если конечная точка передается в сообщении от потока Т1 потоку Т2, ее собственник меняется: от Т1 она переходит к сообщению, а затем, после получения сообщения, к Т2.
Для упрощения статического отслеживания ресурсов указатели на ресурсы могут содержаться только в статических переменных, сообщениях и структурах данных, которые сами отслеживаются. Эти ограничения иногда могут быть обременительными. Поэтому Sing# предоставляет возможность обойти их, сохраняя отслеживаемые ресурсы косвенно, внутри структур данных, через абстракцию TRef.
TRef – это ячейка хранения типа TRef<T>, содержащая отслеживаемую структуру данных типа Т. Сигнатура TRef такова:
class TRef<T> where T: ITracked { public TRef([Claims] T i_obj); public T Acquire(); public void Release([Claims] T newObj); } |
При создании TRef<T> конструктор требует в качестве аргумента объект типа Т. Вызывающая сторона должна владеть объектом на стороне создания. После создания владение передается созданному TRef. Для получения содержимого TRef используется метод Acquire. Если TRef заполнен, он возвращает свое содержимое и передает владение стороне, вызвавшей Acquire. После этого TRef считается пустым. Release передает владение объектом T от вызывающей стороны TRef-у. После этого TRef является заполненным. TRef-ы потокобезопасны. Операция Acquire блокируется, пока TRef заполнен.
TRef – это компромисс между статическими и динамическими проверками. При использовании TRef множественные некорректные запросы превращаются во взаимоблокировки, и освобождением ресурса занимается механизм финализации сборщика мусора.
Поскольку владение блоками памяти передается при обмене сообщениями от одного потока или процесса другому, Singularity необходим способ выделения и отслеживания таких блоков. Система каналов требует, чтобы параметры сообщения были или скалярами, или блоками в Exchange Heap. Есть два вида блоков Exchange Heap: индивидуальные блоки и векторы. Их типы записываются, соответственно, так:
using Microsoft.Singularity.Channels; R* in ExHeap pr; R[] in ExHeap pv; |
ПРИМЕЧАНИЕ Конечные точки в Exchange heap представлены как отдельные блоки. |
Тип указателя pr говорит, что он указывает на структуру R в Exchange Heap. ExHeap – это тип, определенный исполняющей системой, обеспечивающей выделение, освобождение и другую поддержку данной кучи. Тип pv – это вектор R в Exchange Heap.
Инвариант Exchange Heap в том, что она не содержит никаких указателей на GC-кучу какого либо процесса. Таким образом, тип R должен быть обмениваемым типом, то есть примитивным value-типом (int, char и т.д.), перечислением или простой структурой с полями обмениваемых типов.
Проверка того, что код, исполняемый в Singularity, типобезопасен и удовлетворяет инвариантам независимости памяти – трехступенчатый процесс. Компилятор Sing# при компиляции проверяет типобезопасность, правила владения и соответствие протоколам. Верификатор Singularity проверяет эти же свойства в сгенерированном MSIL-коде. Наконец, back-end компилятор должен – но пока не делает – выдавать типизированный ассемблер, который позволит проверять эти свойства операционной системе. Кто-то скажет, что для безопасности действительно необходим только последний этап. Конечно, в буквальном смысле это верно, но на практике программистам лучше находить ошибки как можно раньше, и иметь их полное объяснение. Более того, избыточная верификация предотвращает ошибки самой верификации.
Закрытый мир SIP несовместим со средствами рефлексии, встроенными в среды Java или CLR, способными генерировать и запускать код во время исполнения. Как следствие, Singularity не поддерживает служб рефлексии времени исполнения.
Рефлексия времени компиляции (Compile-Time Reflection, CTR) – это частичная замена возможностей рефлексии CLR. CTR похоже на такие методики как макросы, бинарную перезапись кода, аспекты, метапрограммирование и multistage-языки. Основная идея в том, что программы могут содержать элементы-заглушки (классов, методов, полей и т.д.), последовательно разворачиваемые генератором.
Способность создавать повторяющийся код по шаблонам под управлением существующих структур программы является очень мощной возможностью. Например, в Singularity приложения и драйверы устройств декларативно описывают свои требования к ресурсам, таким, как I/O и служебные каналы. Код загрузки (startup code) этих процессов может быть сгенерирован по этим описаниям автоматически.
Генераторы пишутся на Sing# как преобразования. Преобразование содержит программную структуру сопоставления с образцом и шаблон кода для создания новых элементов кода. Такая комбинация позволяет анализировать и проверять преобразование независимо от кода, к которому оно должно быть применено. Например, такие ошибки, как генерация вызова несуществующего метода или вызов с неверным типом объекта, могут быть отловлены еще в преобразовании. В этом отношении CTR похожа на многоступенчатые языки. Заметьте, что преобразования CTR могут быть частью доверенной основы, что позволяет встраивать доверенный код в другие процессы (к которым нет полного доверия).
transform DriverTransform where $IoRangeType: IoRange { class $DriverCategory: DriverCategoryDeclaration { [$IoRangeAttribute(*)] $IoRangeType $$ioranges; public readonly static $DriverCategory Values; generate static $DriverCategory() { Values = new $DriverCategory(); } implement private $DriverCategory() { IoConfig config = IoConfig.GetConfig(); Tracing.Log(Tracing.Debug, "Config: {0}", config.ToPrint()); forall ($cindex = 0; $f in $$ioranges; $cindex++) { $f = ($f.$IoRangeType) config.DynamicRanges[$cindex]; } } } } |
Приведенное выше преобразование DriverTransform генерирует код запуска для драйвера устройства по декларативному объявлению ресурсов, нужных этому драйверу. Например, следующая декларация в драйвере SB16 описывает его требования IoPorts:
internal class Sb16Resources: DriverCategoryDeclaration { [IoPortRange(0, Default = 0x0220,Length = 0x10)] internal readonly IoPortRange basePorts; [IoPortRange(1, Default = 0x0380,Length = 0x10)] internal readonly IoPortRange gamePorts; internal readonly static Sb16Resources Values; reflective private Sb16Resources(); } |
С этим классом будет сопоставлен DriverTransform, так как класс унаследован от DriverCategoryDeclaration и содержит указанные элементы, такие, как поле Values подходящего типа и заглушка для private-конструктора. Ключевым словом reflective помечена заглушка, чье определение будет сгенерировано преобразованием с использованием модификатора implement. Заглушки – это опережающие ссылки, позволяющие коду программы ссылаться на код, который будет сгенерирован преобразованием позже.
Переменные паттерна в преобразовании начинаются со знака $. В примере $DriverCategory связывается (bound) к SB16Resources. Переменные, соответствующие более, чем одному элементу, начинаются с $$. Например, $$ioranges представляет список полей типа $IoRangeType, унаследованного от IoRange (типы разных полей не обязаны совпадать). Чтобы генерировать код для каждого элемента коллекции (такой, как коллекция полей $$ioranges), шаблоны могут содержать ключевое слово forall, создающее копию шаблона для каждого связывания в коллекции. Результирующий код, порожденный приведенным выше преобразованием, эквивалентен следующему:
class SB16Resources { ... static Sb16Resources() { Values = new Sb16Resources(); } private SB16Resources() { IoConfig config = IoConfig.GetConfig(); Tracing.Log(Tracing.Debug, "Config: {0}", config.ToPrint()); basePorts = (IoPortRange)config.DynamicRanges[0]; gamePorts = (IoPortRange)config.DynamicRanges[1]; } } |
Пример также показывает, что типы сгенерированного преобразованием кода могут быть проверены при компиляции преобразования, вместо откладывания такой проверки до применения преобразования, как в случае макросов. В примере присваивание Values проверяемо безопасно, так как тип конструируемого объекта ($DriverCategory) соответствует типу поля Values.
Построенная на основе ядра, SIP, каналов и языковой модели, описанной выше, ОС Singularity предлагает многие обычные службы ОС.
Система ввода/вывода Singularity состоит из трех слоев: HAL, менеджера ввода/вывода и драйверов. HAL – это маленькая доверенная абстракция аппаратного обеспечения РС: абстракции IoPorts, IoDma, IoIrq и IoMemory для доступа к устройствам; интерфейсы к таймерам, контроллер прерываний, часы реального времени и отладочная консоль; заглушка для отладки ядра; регистратор событий, векторы прерываний и исключений; обнаружение ресурсов BIOS и код связывания стека. HAL написан на C#, C++ и ассемблере. Доля ассемблера и C++ в HAL составляет примерно 5% от доверенного кода системы (35 из 561 файла).
Ядро Singularity использует манифест для создания и привязки драйверов устройств. При запуске ядро производит «plug and play»-конфигурирование системы. Ядро использует информацию, полученную загрузчиком из BIOS и от шин, например, PCI, для перечисления устройств, запуска подходящих драйверов и передачи этим драйверам объектов, инкапсулирующих доступ к аппаратному обеспечению.
Все драйверы написаны на безопасном коде и исполняются в отдельных процессах. Драйверы общаются с остальными частями системы, включая сетевой стек и файловую систему, только через каналы. При запуске драйвера ядро предоставляет ему четыре типа инициализированных объектов, позволяющих драйверу связываться с его устройством. Все эти объекты предоставляют безопасный интерфейс, проверяющий каждую ссылку перед прямым обращением к памяти, используемой для общения с аппаратурой.
IoPort предоставляет интерфейс к регистрам портов ввода/вывода устройства. Он проверяет, что ссылки на регистры не выходят за границы, и что драйвер не пишет в доступную только для чтения память. IoDma предоставляет доступ к встроенному контроллеру DMA для старого аппаратного обеспечения. IoIrqs оповещает драйвер о поступлении аппаратных прерываний. IoMemory предоставляет доступ к фиксированному региону памяти, содержащему отображение регистров или выделенному для использования DMA, производя при этом проверки границы диапазона.
Единственный небезопасный аспект интерфейса драйвер-устройство – это DMA. Существующая архитектура DMA не содержит никакой защиты памяти, поэтому неправильно работающий драйвер может заставить работающее с DMA устройство переписать любую часть памяти. Разнообразие интерфейсов DMA помешало нам найти хорошую абстракцию для их инкапсуляции. Мы надеемся, что в будущем аппаратное обеспечение будет предоставлять защиту памяти для DMA.
Прерывание от устройства обслуживается ядром, которое маскирует прерывание, а затем сигнализирует соответствующему IoIrq драйвера. Процесс каждого драйвера содержит поток, ожидающий своего события Irq, которое запускает обработку прерывания и делает возможным дальнейшее использование линии прерывания через ABI ядра. Планировщик запускается сразу после обработчика прерывания.
Singularity широко использует метаданные для описания частей системы, объяснения, как они стыкуются и описания их поведения. Метаданные в Singularity декларативно описывают компоненты, систему или приложения Singularity вместе с их зависимостями, ресурсами и экспортом. Инструменты Singularity используют эти метаданные для проверки и конфигурирования кода системы и приложений, как до, так и во время исполнения.
Системный образ Singularity – это составной артефакт. Он состоит из ядра, драйверов устройств, приложений и метаданных, описывающих эти индивидуальные артефакты. Кроме этого, он содержит манифест, описывающий системную политику. Манифест также указывает на манифесты, описывающие индивидуальные компоненты. Через эти манифесты такое ПО, как загрузчик или верификатор системы, может обнаружить любой компонент Singularity.
Системного образа и манифеста Singularity достаточно для того, чтобы обеспечить оффлайн-анализ системы. Наша цель – позволить администратору использовать только описание аппаратного обеспечения и системный манифест для ответа на такие вопросы, как: «Загрузится ли система на этом конкретном аппаратном обеспечении, какие драйверы и сервисы запустятся и какие приложения смогут работать?»
Системный образ Singularity содержит метаданные, описывающие драйверы устройств. Через метаданные Singularity поддерживает три инварианта. Во-первых, Singularity никогда не установит драйвер устройства, который не может успешно запуститься из-за конфликта ресурсов с другим драйвером или частью системы. Во-вторых, Singularity никогда не запустит драйвер устройства, который не может успешно работать из-за конфликтующего или недостающего ресурса. В-третьих, драйвер устройства не может обращаться во время исполнения к ресурсам, не объявленным в его метаданных.
Там, где это возможно, Singularity использует атрибуты C# для вставки метаданных в исходный код, чтобы поддерживать только один документ с исходным кодом. Атрибуты могут быть связаны с такими сущностями, как классы, методы или поля. Компилятор «пропускает» атрибуты в результирующий бинарный MSIL. Компиляторы, компоновщики, средства установки и верификации могут читать метаданные, закодированные в атрибутах, из бинарного MSIL без исполнения кода.
Для примера, следующий код показывает некоторые атрибуты, используемые для объявления зависимостей и потребностей в ресурсах драйвера S3Trio64:
[DriverCategory] [Signature("/pci/03/00/5333/8811")] class S3TrioConfig : DriverCategoryDeclaration { // Hardware resources from PCI config [IoMemoryRange(0, Default = 0xf8000000, Length = 0x400000)] IoMemoryRange frameBuffer; // Fixed hardware resources [IoFixedMemoryRange(Base = 0xb8000, Length = 0x8000)] IoMemoryRange textBuffer; [IoFixedMemoryRange(Base = 0xa0000, Length = 0x8000)] IoMemoryRange fontBuffer; [IoFixedPortRange(Base = 0x03c0, Length = 0x20)] IoPortRange control; [IoFixedPortRange(Base = 0x4ae8, Length = 0x02)] IoPortRange advanced; [IoFixedPortRange(Base = 0x9ae8, Length = 0x02)] IoPortRange gpstat; // Channels [ExtensionEndpoint(typeof(ExtensionContract.Exp))] TRef<ExtensionContract.Exp:Start> iosys; [ServiceEndpoint(typeof(VideoDeviceContract.Exp))] TRef<ServiceProviderContract.Exp:Start> video; ... } |
Атрибуты [DriverCategory] and [Signature] говорят, что этот модуль – драйвер устройства из класса видеоустройств PCI. DriverCategory указывает категорию приложений, реализующих драйверы устройств для определенных аппаратных средств. Другие категории включают ServiceCategory, для приложений, реализующих программные службы, и WebAppCategory для расширений Cassini, Web-сервера Singularity.
Атрибут [IoMemoryRange] говорит, что frameBuffer использует первое вхождение из пространства конфигурации PCI устройства. Это вхождение обнаруживается после конфигурирования аппаратуры, и такие параметры аппаратуры, как размер региона памяти, должны быть совместимы со значениями атрибута. Атрибуты [IoFixedMemoryRange] и [IoFixedPortRange] указывают, что драйверу необходим либо фиксированный интервал адресного пространства для доступа к памяти, либо фиксированный интервал портов ввода/вывода для доступа к регистрам устройства.
Атрибут [ExtensionEndpoint] указывает контракт канала и локальную конечную точку, используемые для связи с родительским процессом драйвера. В случае драйвера устройства, например, S3Trio64, родительским процессом является система ввода/вывода.
Атрибуты [ServiceEndpoint] объявляют контракт канала и локальную конечную точку, используемые для получения запросов от клиентов.
Во время компиляции компилятор C# передает атрибуты в бинарный файл MSIL. Используя библиотеку доступа к метаданным MSIL, средства Singularity могут разбирать инструкции и потоки метаданных в MSIL.
При компоновке (link) утилита mkmani считывает атрибуты для создания манифеста приложения. Манифест приложения – это XML-файл, перечисляющий компоненты, экспорт и зависимости приложения.
Следующий XML – это часть манифеста драйвера S3Trio64:
<manifest> <application identity="S3Trio64" /> <assemblies> <assembly filename="S3Trio64.exe" /> <assembly filename="Namespace.Contracts.dll" version="1.0.0.2299" /> <assembly filename="Io.Contracts.dll" version="1.0.0.2299" /> <assembly filename="Corlib.dll" version="1.0.0.2299" /> <assembly filename="Corlibsg.dll" version="1.0.0.2299" /> <assembly filename="System.Compiler.Runtime.dll" version="1.0.0.2299" /> <assembly filename="Microsoft.SingSharp.Runtime.dll" version="1.0.0.2299" /> <assembly filename="ILHelpers.dll" version="1.0.0.2299" /> <assembly filename="Singularity.V1.ill" version="1.0.0.2299" /> </assemblies> <driverCategory> <device signature="/pci/03/00/5333/8811" /> <ioMemoryRange index="0" baseAddress="0xf8000000" rangeLength="0x400000" /> <ioMemoryRange baseAddress="0xb8000" rangeLength="0x8000" fixed="True" /> <ioMemoryRange baseAddress="0xa0000" rangeLength="0x8000" fixed="True" /> <ioPortRange baseAddress="0x3c0" rangeLength="0x20" fixed="True" /> <ioPortRange baseAddress="0x4ae8" rangeLength="0x2" fixed="True" /> <ioPortRange baseAddress="0x9ae8" rangeLength="0x2" fixed="True" /> <extension startStateId="3" contractName="Microsoft.Singularity.Extending.ExtensionContract" endpointEnd="Exp" assembly="Namespace.Contracts" /> <serviceProvider startStateId="3" contractName="Microsoft.Singularity.Io.VideoDeviceContract" endpointEnd="Exp" assembly="Io.Contracts" /> </driverCategory> ... </manifest> |
Как говорилось в разделе «Абстракция приложений», приложение в Singularity – это абстракция верхнего уровня. Чтобы исполняться, кусок кода должен быть добавлен к системе инсталлятором Singularity.
Инсталлятор начинает с метаданных из манифеста приложения. Он проверяет наличие и безопасность каждой из сборок приложения. Он также проверяет, что все контракты каналов реализованы корректно, и что все зависимости сборок и ABI ядра могут быть корректно разрешены.
После разрешения (resolve) и проверки внутренних свойств инсталлятор пытается разрешить и проверить все внешние зависимости. Например, установка гарантирует, что любые ресурсы аппаратных средств, используемые драйвером устройства, не конфликтуют с ресурсами аппаратных средств, требуемыми любым другим драйверам. Инсталлятор также проверяет существование каждого типа канала, используемого приложением. Если приложение экспортирует канал, инсталлятор проверяет, что экспортируемый канал не конфликтует с другим приложением. При возникновении конфликтов их разрешает политика из системного манифеста. Например, в манифесте может быть объявлено, что контракт для видеоадаптера может предоставлять только один драйвер. Установка других видеодрайверов в этом случае может быть запрещена, или же при загрузке может активироваться только один видеодрайвер.
Как описано в разделе Рефлексия времени компиляции, для генерирования доверенного кода инициализации внутрипроцессных объектов, ссылающихся на системные ресурсы, используется CTR. Шаблоны CTR исполняются во время установки, используя помеченные атрибутами элементы программы, перечисленные в манифесте приложения.
Процесс установки завершается обновлением метаданных системного манифеста для включения в него нового приложения или драйвера устройства.
В текущей реализации весь процесс установки производится offline, и установка становится видимой только после следующей загрузки системы. Такую схему можно легко дополнить онлайн-установкой, но это пока не требовалось нашими сценариями использования.
Во время исполнения метаданные управляют инициализацией ядра, драйверов устройств, сервисами и приложениями. Загрузчик считывает часть системного манифеста, чтобы определить, какое ядро, драйверы и службы должны быть загружены. Порядок их загрузки и запуска не оговорен, система принимает решение на основании указанных зависимостей.
При запуске каждого приложения ядро проверяет и разрешает (resolve) все зависимости из метаданных и строит в ядре запись конфигурирования процесса. Доверенный код, встроенный в приложение с помощью CTR, разбирает эту запись, создает экземпляры локальных объектов для доступа к внешним ресурсам, и помещает локальные объекты в объект конфигурации в объектном пространстве процесса.
Возвращаясь к примеру драйвера для S3Trio64, ядро записывает в конфигурационную запись драйвера потребность в объектах IoMemoryRange для frameBuffer, textBuffer, and fontBuffer, а также IoPortRange для портов ввода вывода control, advanced и gpstat. Ядро создает один канал для подключения драйвера к подсистеме ввода/вывода и второй – для связи драйвера с пространством имен. Конечные точки каналов вписываются в конфигурационную запись драйвера.
При начале исполнения драйвера доверенный код его исполняемого модуля создает объекты IoMemoryRange и IoPortRange в объектном пространстве драйвера. Поскольку конструкторы этих объектов доступны только доверенному коду, драйвер может обращаться только к ресурсам ввода/вывода, объявленным в его метаданных, и проверенным на отсутствие конфликтов подсистемой ввода/вывода ядра.
Объявление конечных точек канала в метаданных приложения гарантирует три важных свойства. Во-первых, код процесса Singularity может статически проверяться на то, что он общается только через полностью объявленные каналы в полном соответствии с контрактами каналов. Во-вторых, приложения не содержат глобальных имен. Например, драйверу S3Trio64 неизвестно имя /dev/video из системного пространства имен. Вместо него драйвер использует при ссылках на канал по данному контракту (ServiceProviderContract) локальное имя S3Trio64Config.video. Всю раскладку пространства имен ввода – вывода можно изменить, не затрагивая ни одной строки кода драйвера. В-третьих, приложение может исполняться в «песочнице», в соответствии с принципом наименьших привилегий, с целью устранения источников ошибок и уязвимостей системы. Например, несмотря на то, что у драйвера S3Trio64 есть конечная точка, подключенная к системному пространству имен, он не может создавать новые имена или подключаться к какому-либо системному процессу.
Для облегчения доступа к метаданным они отображаются на системное пространство имен. Например, система ввода/вывода создает дерево пространства имен, описывающее отображение драйверов устройств на текущие аппаратные средства. /hardware/locations перечисляет все шины и каждое местоположение на шине. Местоположение представлено как дерево каталогов, которое содержит символическую связь с экземпляром устройства, расположенным по этому местоположению.
Аналогично, в дереве /hardware/registrations перечисляет все драйверы, зарегистрированные в системе. В пределах этого дерева есть одна символическая связь, указывающая на драйвер, зарегистрированный для соответствующего префикса сигнатуры устройства.
Дерево /hardware/devices содержит вхождения для всех физических устройств в системе. Сигнатура устройства (определенная в перечислении устройств) отражается в структуре каталога. Каждый экземпляр устройства в этом дереве – отдельное поддерево с символическими связями, указывающими на соответствующие вхождения в деревьях местоположений и драйверов, и показывающими, как экземпляр устройства был найден, с чем ассоциирован, и как активируется.
В дереве /hardware/drivers перечисляются все зарегистрированные драйверы, каждой реализации драйвера соответствует поддерево. Имена здесь основаны непосредственно на названии пространства имен класса драйвера. Поддерево отдельного драйвера содержит символическую связь, указывающую на исполняемый образ драйвера, а также поддерево для каждого экземпляра драйвера. Это поддерево содержит связи с экземпляром соответствующего устройства. Кроме того, в этом пространстве содержатся связывания для всех конечных точек ServiceProviderContract, созданных для каждого экземпляра драйвера.
Наконец, пространство имен /dev это открытый каталог, в котором содержатся символические связи с конечными точками ServiceProviderContract из поддерева /hardware/drivers. Таким образом, устройство может быть привязано к открытому имени, не зная истинного имени драйвера.
Singularity предоставляет единое однородное пространство имен для всех служб системы. Пространство имен охватывает непостоянные системные службы, типа драйверов устройства и сетевых соединений, и постоянное хранилище в файловой системе. Пространство имен реализовано как отдельный (корневой) сервер имен и службы. Сервер имен позволяет службам регистрироваться и удалять свою регистрацию в иерархическом пространстве имен, что позволяет клиентам обнаруживать их. Службы отвечают на запросы и, с помощью реализации контракта сервера имен, могут расширить пространство имен за пределы его точки монтирования (mount point).
Пространство имен является иерархическим. Клиентские программы могут получать доступ к службе, передавая путь и новый канал серверу имен. Пути могут выглядеть как "/filesystems/ntfs" или "/tcp/128.0.0.1/80. Концептуально пространство имен состоит из каталогов и служб. Каталоги – это коллекции каталогов и служб, разделяющих общий префикс пути. Службы – это активные сущности, отвечающие на запросы по зарегистрированным каналам.
Пространство имен может существовать на нескольких серверах имен. Можно зарегистрировать службу (идентичную корневому серверу имен), обрабатывающую все запросы ниже какой-то точки в иерархии. Регистрация, ее отмена, поисковые запросы к этому поддереву будут перенаправляться к вспомогательному серверу имен. Эта функциональность похожа на точки подключения (mount points) в файловых системах Unix. Однако дополнительные серверы имен не обязаны действовать так же, как корневой. Например, служба TCP может экспортировать обширное динамическое пространство IP-адресов и создавать подключения по запросу. Или же, вспомогательный сервер имен может реализовать символьные ссылки.
Слегка упрощенная форма контракта пространства имен для клиента (серверный контракт включает регистрацию) выглядит так:
public contract NamespaceContract : ServiceContract { in message Bind(char[] in path, ServiceContract.Exp:Start exp); out message AckBind(); out message NakBind(ServiceContract.Exp:Start exp); in message Notify(char[] in pathSpec, NotifyContract.Imp:Start imp); out message AckNotify(); out message NakNotify(NotifyContract.Imp:Start imp); in message Find(char[] in pathSpec); out message AckFind(FindResponse[] in results); out message NakFind(); out message Success(); override state Start: one { Success! -> Ready; } state Ready: one { Bind? -> ( AckBind! or NakBind! ) -> Ready; Find? -> ( AckFind! or NakFind! ) -> Ready; Notify? -> ( AckNotify! or NakNotify! ) -> Ready; } } |
Сообщение Bind предоставляет путь в пространстве имен и канал, который передается службе, зарегистрированной под этим именем. Сообщение Notify передается в канал, который получает оповещения об изменениях в каталоге, лежащем по этому пути. Сообщение Find возвращает пути к элементам пространства имен, соответствующим спецификации пути (сообщение Success используется в стандартном протоколе для инициализации канала).
Следующая хронология показывает, как используется сервер имен. Процессы C, S и NS представляют клиента, служба и сервис имен, соответственно. nsC и nsS – это каналы к серверу имен, которые имеются у клиента и службы
Пространство имен Singularity – удобный механизм именования и доступа к службам и объектам, но оно не предоставляет средств сохранения данных. Singularity предоставляет также службу файловой системы, являющуюся поддеревом пространства имен. Файловая система регистрирует себя как служба пространства имен в своей точке подключения (например, “/fs”) и обслуживает запросы под ее доменом. Поскольку файловая система действует как служба пространства имен, пути в файловой системе являются суффиксами путей в пространстве имен (т.е., “/fs/foo/bar”).
Файловая система Singularity поддерживает общие абстракции и операции. Она состоит из каталогов и файлов. Каталоги могут содержать файлы и/или другие каталоги, и поддерживают традиционные операции типа перечислений. Файлы – это массивы байтов переменной длины, которые клиенты могут читать или записывать с произвольным смещением.
Файлы и каталоги обладают собственными контрактами. Контракт файла разрешает операции чтения и записи. Контракт каталога предусматривает такие операции над файлами и каталогами, как создание, удаление и запрос атрибутов. Благодаря тому, что файловая система выступает в роли провайдера пространства имен, операции типа перечисления каталогов и поиска не требуют специальных сообщений в контрактах файловой системы. Сейчас мы рассматриваем пути дальнейшей интеграции контрактов файловой системы и пространства имен, поскольку их функциональность существенно пересекается.
Внутренне файловая система работает как стандартный процесс Singularity. Она включает четыре типа обработчиков (worker): управляющий, обработчик пространства имен, файлов и каталогов. Управляющий обработчик, который регистрирует себя в пространстве имен отдельно (например, “/Fsctrl”), обслуживает создание файловой системы, инициализацию и запросы на подключение. Когда файловая система смонтирована, ее обработчик пространства имен обрабатывает запросы, направленные от родительского провайдера пространства имен для файловой системы. Важнейшими из них являются запросы Bind. При получении Bind-запросов обработчик пространства имен передает конечную точку обработчику файлов или каталогов, в зависимости от типа конечной точки.
Обработчик файлов или каталогов, в свою очередь, получив конечную точку, выполняет реальные операции файловой системы, поступающие через эту конечную точку. Конечные точки, поступающие как части запросов Bind, концептуально связаны с файлом или каталогом файловой системы. Соответственно, запросы к этим конечным точкам не содержат информации о пути или дескриптора файла.
Для надежного хранения и получения данных из постоянных хранилищ (то есть дисков) Singularity использует модифицированную версию Boxwood как нижележащую систему хранения [34]. Boxwood исходно разрабатывался как распределенная система хранения, экспортирующая абстракции более высокого уровня (например, В-деревья) вместо простого блочного интерфейса, в целях демонстрации того, что более абстрактные интерфейсы упростят создание приложений и снизят накладные расходы. Как таковое, создание подобного файловой системе интерфейса поверх Boxwood несложно, поскольку Boxwood устраняет потребность в большинстве манипуляций данными, а также в коде, обеспечивающем параллелизм, непротиворечивость и восстановление. Структуры файловой системы в Singularity почти идентичны аналогичным в BoxFS [34]. Файлы хранятся и обрабатываются как В-деревья, в которых данные – это файловые блоки, а ключи – номера блоков. Каталоги хранятся и обрабатываются как В-деревья, в которых данные – это файлы или другие каталоги, чьи ключи – это строковые имена. Метаданные о сущностях файловой системы хранятся в этих В-деревьях под отдельными ключами.
Единственная значительно измененная часть Boxwood – это низкоуровневый интерфейс к дискам. В Windows запросы к диску проходят через системные вызовы, но в Singularity все взаимодействие с дисками производится через каналы. Поэтому самый нижний слой Boxwood был переработан для использования каналов.
Во избежание избыточного и дорогостоящего копирования, байтовые массивы C# в Boxwood были заменены указателями на данные в Exchange Heap.
Singularity обеспечивает строгую изоляцию процессов. На этой основе мы создаем модель защиты, которая стремится поддерживать целостность системы и управление доступом к ресурсам, соответствующее политике системы и приложений [1].
Центром модели безопасности Singularity являются приложения. Как будет показано ниже, принципалы в этой модели – это приложения и их комбинации. Как говорилось в разделе «Сервер имен», приложения именуются в иерархическом пространстве имен.
Доверие к создателям приложений может быть выражено в форме пространства имен. Например, системная политика может гласить, что часть пространства имен, отведенного Microsoft, может содержать только приложения, имеющие сертификат Microsoft. Форма пространства имен может также использоваться, чтобы дифференцировать группы программ по тому, насколько им доверяют согласно системной политике.
Некоторые меры безопасности в Singularity могут приниматься статически, во время установки приложений. Поскольку весь доступ к ресурсам в Singularity производится через каналы, инсталлятор системы может контролировать ресурсы, к которым получит доступ приложение, статически управляя его каналами. Требования любого приложения оговорены в статическом манифесте. Инсталлятор системы разрешает (resolve) все несвязанные каналы в манифесте приложения, обеспечивая конфигурацию приложения, которая создает эти каналы во время выполнения. Эта статическая проверка иногда может использоваться для обеспечения безопасности на основе наименьших возможных привилегий. Например, если приложение должно работать только локально, инсталлятор не должен предоставлять ему прямой канал к сети.
Экземпляры приложений Singularity создаются во время исполнения в форме одного или нескольких процессов. Каждый процесс обладает неизменной индивидуальностью (identity, конечно, не совсем «индивидуальность», но подобрать более точный аналог, увы, не удалось – прим.пер.) благодаря вызовам, породившим его. Все пары конечных точек каналов, созданных процессом, инициализируются этой индивидуальностью по умолчанию. При передаче конечной точки канала другому процессу получающий процесс может узнать индивидуальность процесса по переданной конечной точке. Таким образом, процесс может узнать принципала, связанного с сообщениями, поступающими по каналу. Принципал может затем использоваться при решениях по динамическому контролю доступа к ресурсам, которые могут быть разделяемыми.
Принципалы, играющие роль субъектов при решениях по контролю доступа – это составные сущности, образованные из индивидуальностей запрашивающего приложения и тех приложений в цепочке вызовов, которые привели к исполнению запрашивающего приложения. (В некоторых случаях, для серверных приложений, использующих свои собственные полномочия, цепочка вызовов может быть подавлена.) Мы отслеживаем историю вызова на уровне процесса. Индивидуальность, порождаемая при вызове процесса, не сильнее и не слабее, чем у вызывающей стороны – она просто другая. Некоторые основанные на языках системы безопасности полагаются, наоборот, на стек вызовов, а не на историю, что дает большую гранулярность, причем таким образом, что каждый стековый фрейм обычно приводит к снижению полномочий.
В этой среде пользователи представлены как роли программ. Программа, аутентифицирующая пользователя (например, проверяя пароль или сертификат), участвует в результирующей составной индивидуальности. Поэтому индивидуальность пользователя, вошедший в систему через протокол удаленного доступа, отличается от индивидуальности аутентифицированного локальным считывателем смарт-карт.
Составные принципалы Singularity – это составные сущности, представляемые текстовыми строками:
/sys/login@/users/fred + /apps/ms/word |
Эта строка может представлять системную программу проверки пароля, исполняемую от лица пользователя “fred” и затем вызывающую Microsoft Word.
Вместо списков контроля доступа (access control list, ACL) в Singularity для определения паттернов проверки принципалов используются выражения контроля доступа (access control expressions, ACE). Эти выражения могут быть весьма гибкими. Например, можно указать, что только Word может читать файлы, защищенные определенным паттерном, или что доступ получит “fred”, исполняющий любую программу Microsoft. Более того, язык паттернов поддерживает косвенность для общих подвыражений (в нашей реализации – в иерархии имен). Эта возможность эквивалентна группам в обычных системах управления доступом.
Мы рассчитываем, что сможем определить правила политики, из которых могут быть выведены ACE. Хотелось бы заменить большое количество несопоставимых АСЕ намного меньшим набором правил. Эти правила лучше всего будут работать в структурированных средах, например, в файловых системах.
Контракты каналов могут содержать подтипы для указания того, какие сообщения может отправлять владелец конечной точки. Например, подтип TcpConnectionContract из раздела 4.1 может описывать только методы, доступные принципалу, которому разрешено слушать, но не подключаться. Таким образом, подтип соответствует набору разрешений. Для многих протоколов создание канала будет запрещено при проверке доступа, определяющей, должно ли быть выдано запрашивающему разрешение на основании подтипа канала.
Являясь предметом runtime-ограничений, конечные точки могут свободно передаваться между процессами. Вместе с ними передаются полномочия по отправке сообщений, указанные в контракте канала. Процессы могут свободно выполнять действия над этими сообщениями без дальнейших проверок доступа. Однако проверки доступа могут выполняться, и в этом случае они будут основываться на индивидуальности нового владельца канала. Таким образом, владелец канала не может притвориться его создателем.
Как описано выше, по умолчанию при вызове процесса индивидуальность нового процесса – это составной принципал в форме invoker + invokee. Кроме вызова процесса, есть по крайней мере два сценария, в которых процесс может захотеть предоставить некоторые аспекты своей индивидуальности другому процессу. В первом случае процесс может хотеть предоставить партнеру возможность, которая позволяет партнеру действовать под объединенной индивидуальностью. Во втором случае системная политика может позволять новой службе быть посредником (возможно, добавляющим функциональность) при доступе к существующей службе. В этом случае посредник должен будет действовать от лица клиента. В обоих случаях мы поддерживаем наследование индивидуальности особым образом «благословляя» (blessing) конечные точки канала: возможность (конечную точку) в первом случае и канал, используемый для привязки к посреднику – во втором случае. «Благословленные» конечные точки позволяют получателю наследовать индивидуальность партнера в некотором ограниченном контексте.
Процессы, содержащие несколько индивидуальностей, могут запутаться и начать использовать эти индивидуальности неправильно. Поэтому мы хотим ограничить процессы одной индивидуальностью для большинства случаев, а также по возможности упростить работу со множественными индивидуальностями.
Если цель Singularity – надежность, почему эта статья содержит измерения производительности? Ответ прост: эти цифры демонстрируют, что предлагаемая нами архитектура не только не вредит производительности, но зачастую работает так же, как обычные системы, а то и быстрее. Другими словами, это практическая основа, на которой можно строить систему.
С другой стороны, в этой статье никак не отражена наша цель – повышение надежности. Измерить этот аспекта системы существенно сложнее, чем производительность. У нас пока нет результатов для Singularity.
Этот раздел содержит сравнение производительности Singularity с другими системами. Все системы работали на AMD Athlon 64 3000+ (1.8 GHz) с чипсетом NVIDIA nForce4 Ultra, 1GB RAM, HDD Western Digital WD2500JD 250GB 7200RPM SATA (без использования очередей команд), и nForce4 Ultra Gigabit NIC (без аппаратной акселерации TCP). Мы использовали FreeBSD 5.3, Red Hat Fedora Core 4 (версия ядра 2.6.11-1.1369_FC4) и Windows XP (SP2). Singularity исполнялась с параллельным mark-sweep GC для ядра, непараллельным mark-sweep GC для процессов и минимальным round-robin-планировщиком.
Стоимость (циклы процессора) | ||||
---|---|---|---|---|
Singularity | FreeBSD | Linux | Windows | |
Read cycle counter | 8 | 6 | 6 | 2 |
Вызов ABI | 87 | 878 | 437 | 627 |
Переключение потоков | 394 | 911 | 906 | 753 |
2 thread wait-set ping pong | 1,207 | 4,707 | 4,041 | 1,658 |
2 message ping pong | 1,452 | 13,304 | 5,797 | 6,344 |
Создание и запуск процесса | 300,000 | 1,032,000 | 719,000 | 5,376,000 |
В таблице 1 приведена стоимость примитивных операций в Singularity и трех остальных системах. На Unix-системах при вызове ABI вызывался clock_getres(), в Windows – SetFilePointer(), а на Singularity – ProcessService.GetCyclesPerSecond().Все эти вызовы работают с готовыми структурами данных в соответствующих ядрах. Тест потоков Unix проводился в пользовательском режиме с использованием библиотеки pthreads. Потоки, реализуемые ядром, имели существенно более низкую производительность. Тест “2 thread wait-set ping pong” измерял стоимость переключения между двумя потоками в одном процессе с использованием объекта синхронизации. Тест “2 message ping pong” измерял стоимость посылки 1-байтного сообщения от одного процесса другому и обратно. Под Unix использовались сокеты, под Windows – именованные каналы, в Singularity – каналы.
Singularity – это новая система, и ее производительность пока не отлажена до конца. Основные операции с потоками в Singularity, такие, как передача процессорного времени (yielding processor) или синхронизация двух потоков, сравнимы или несколько быстрее, чем на других системах. Однако, благодаря SIP-архитектуре Singularity, межпроцессные операции выполняются значительно быстрее, чем в других системах. Вызовы ядра из процесса в Singularity в 5-10 раз быстрее, поскольку вызов не пересекает границ аппаратной защиты. Простое RPC-подобное взаимодействие двух процессов в 4-9 раз быстрее. А создание процесса в 2-18 раз быстрее, чем в других системах. Это превосходство должно еще больше вырасти, когда мы улучшим реализацию потоков в Singularity.
Чтобы определить влияние архитектуры Singularity на ввод/вывод, мы измерили стоимость случайного и последовательного дискового чтения и записи на различных операционных системах.
При последовательных тестах считывалось или записывалось 512 MB данных в одинаковую область жесткого диска. Тест случайного чтения/записи выполняет 1000 операций над одними и теми же последовательностями блоков диска. Тесты однопоточны и производят синхронный неформатированный ввод/вывод. Каждый тест исполнялся семь раз, результаты усреднялись. Под Singularity тест связывался с процессом драйвера диска через канал, а под FreeBSD, Linux и XP для связи с драйверами использовались системные вызовы. Драйверы FreeBSD и Linux поддерживали ATA-7 с теоретической максимальной пропускной способностью в 133MB/с, а драйверы Singularity и Windows поддерживали ATA-5 с теоретической максимальной пропускной способностью в 66MB/s.
На рисунке 3 показана пропускная способность систем в операциях ввода/вывода в секунду. При операциях случайного чтения производительность Singularity отличалась от вариантов Unix примерно на 10% и была незначительно выше, чем у Windows. При операциях случайной записи производительность Singularity была самой высокой для большинства размеров блока. Интересно отметить, что все системы показали при случайной записи лучшую производительность, чем при случайном чтении.
При операциях последовательного чтения производительность Windows XP была существенно выше, чем у других систем при размерах блока менее 8 килобайт. При 8KB различия между системами становятся малоразличимыми. При размерах блока более 8 КВ производительность всех систем не отличалась более чем на 6% от лидера, FreeBSD. Границу в 6% мы относим на счет различных стандартов АТА, поддерживаемых разными ОС.
При операциях последовательной записи каждая из систем показала наилучшую производительность по крайней мере для одного из размеров блока менее 8КВ (FreeBSD не смогла выполнить тест при размере блока в 512 байт – производительность упала до 50 операций в секунду, и тест не закончился за приемлемый промежуток времени). При размерах блока более 8КВ FreeBSD снова показала наивысшую производительность (с разницей не более 6% между лучшим и худшим результатом).
Результаты, представленные на рисунке 3, показывают, что по производительности дисковых операций Singularity вполне конкурентоспособна на фоне современных ОС. Дисковый драйвер Singularity не слишком оптимизирован, и пока не реализует последнюю версию спецификаций ATA, но общая производительность системы уже вполне на уровне. Это показывает, что исполнение дисковых драйверов в SIP и коммуникации через каналы не влекут за собой существенных потерь в производительности.
В предыдущих тестах в Singularity использовались каналы с нулевым копированием. Чтобы определить стоимость операций канала, мы изменили дисковый драйвер для выполнения последовательных операции чтения непосредственно, без каналов. Результаты приведены в таблице 2.
I/O-операции в секунду | |||
---|---|---|---|
Размер блока (байт) | без каналов | с каналами | Снижение эффективности, % |
512 | 17658 | 15831 | 10 |
1024 | 16652 | 15003 | 10 |
2048 | 15050 | 13669 | 9 |
4096 | 12491 | 11598 | 7 |
8192 | 7652 | 7650 | 0 |
16384 | 3825 | 3823 | 0 |
32768 | 1903 | 1902 | 0 |
65536 | 950 | 950 | 0 |
При размерах блока менее 8КВ производительность последовательного чтения ограничена фиксированной стоимостью операции. Это включает создание запроса на чтение, опрос битов статуса устройства и обработку одного прерывания на запрос. Каналы накладывают дополнительную нагрузку в 2 операции посылки и приема на запрос. Сравнение производительности с использованием каналов и без них позволяет оценить стоимость операций канала в 6 µs. Микротест показал стоимость операции посылки-приема 1.1 µs (1 925 цикла), с погрешностью 1.9 µs. Однако между микротестом и дисковым тестом есть два различия. Во-первых, драйвер диска содержит тщательно проработанный паттерн посылки-приема, позволяющий обрабатывать различные сообщения, которые может получать драйвер, а эта конструкция требует больше циклов для поиска подходящего паттерна. Во-вторых, операции посылки-приема в дисковом тесте передают владение разделяемым буфером кучи от вызывающей стороны вызываемой, и это требует некоторой работы по учету в разделяемой куче.
При размерах блоков в 8КВ и более производительность последовательного чтения диктуется скоростью DMA. При измерениях стоимости канала сильное влияние оказывает погрешность изменения.
Чтобы оценить накладные расходы, вносимые механизмом расширений Singularity, в более реалистичном сценарии, мы измерили производительность по тесту SPECweb99 на Cassini, Web-сервере с открытым исходным кодом, написанном на C#. Этот тест задействует значительную часть программного обеспечения Singularity. Cassini работает на портированной версии классов Microsoft .NET, использующей каналы для коммуникаций с сетевым стеком и файловой системой. Код Cassini в основном не модифицировался, за исключением использования каналов для связи с Web-расширениями (включая код теста) исполнявшимися в отдельных SIP.
ПРИМЕЧАНИЕ Наше использование этого теста (в переводе на C#) не вполне корректно в нескольких аспектах – TCP-стек Singularity не полностью совместим с IPv4, Cassini не полностью совместим со стандартом, время прогрева, продолжительность исполнения и число итераций при тестировании были уменьшены, отсутствовало ведение лога на стороне сервера. Тем не менее, Singularity полностью реализует все динамические операции в тесте при стандартной рабочей нагрузке. |
Singularity выдала 91 операцию в секунду при взвешенной средней пропускной способности в 362 Kбит/с. Web-сервер IIS, выполняемый под управлением Windows 2003 на идентичной аппаратуре, выдает 761 операцию в секунду при взвешенной средней пропускной способности в 336 Kбит/с.
Нестабильность системы под высокой нагрузкой и узкие места файловой системы ограничивает количество подключений, которое может обслуживать Singularity на минимально приемлемой для теста скорости, и, соответственно, снижает общие показатели Singularity. Однако среднее время отклика Singularity при 23 подключениях (322 миллисекунды на операцию) сравнимо с временем, показанным Windows при 25 подключениях – 322 миллисекунды на операцию. Это предполагает, что показатели теста Singularity ограничиваются не внутренней латентностью системы или SIP.
Singularity сейчас ограничена пропускной способностью файловой системы. Файловая система основана на абстракциях Boxwood [34], чьи проблемы с производительностью присущи не только Singularity. Мы измеряли производительность файловой системы используя простой тест, читающий случайно выбранные файлы из теста SPECweb. Под Windows его пропускная способность составила 2.7 MB/сек. Под Singularity пропускная способность составила 2.6 MB/сек. При пропускной способности в 2.6MB/сек система может поддерживать не более 50–70 подключений SPECweb.
Сетевой стек Singularity, наоборот, не является узким местом, и может поддерживать пропускную способность в 48Mбит/сек.
Порождаемый SIP перерасход памяти ограничивает число и гранулярность процессов, которые могут быть созданы в системе. В таблице 3 приведены размеры файлов для минимальной программы “hello world”. На Unix-системах программы статически скомпонованы с библиотеками. В Singularity код компонуется с полной runtime-системой (включая GC), и размер замеряется после Bartok-оптимизации, удаляющей неиспользуемый код и данные. Как можно видеть, размер кода и данных в процессе Singularity грубо сравним с программой на С под Unix.
hello world | Размер файла (байт) | |||
---|---|---|---|---|
Singularity | Free BSD | Linux | Windows | |
C | 89,796 | 431,900 | 36,864 | |
C++ | 534,856 | 984,520 | 69,632 | |
Sing# | 286,208 |
В таблице 4 приведен объем адресного пространства, используемый этими программами под различными системами. Процесс Singularity занимает меньше памяти, чем процессы большинства других систем (за одним исключением). В одном случае разница составила порядок.
Использование памяти (KB) | ||||
---|---|---|---|---|
Singularity | FreeBSD | Linux | Windows | |
C | 1,200 | 1,416 | 644 | |
C (static) | 232 | 664 | 544 | |
C++ | 2,148 | 2,532 | 804 | |
C++ (static) | 704 | 1,216 | 572 | |
Sing#/C# | 408 | 4,116 |
В Singularity мы надеемся добиться совместного использования доступных только для чтения страниц, содержащих код исполняющей системы среди сходных процессов, уменьшить потребление памяти и ускорить создание процессов. В данном примере примерно 280КВ из 408КВ runtime-а находятся в исполняемом модуле, а не размещены в куче. Из них 173 КВ – это код, 26КВ – данные, доступные только для чтения, и 72КВ – данные, доступные для чтения и записи (22KB VTable, 28KB неизменяемых строк и 15KB объектов System.Type). С некоторыми изменениями – например, выносом блокировок из неизменяемых объектов – перезаписываемые данные тоже могут разделяться. В этом случае разделяемым становится 58% объектного пространства.
Большая часть связанных работ может быть разделена на четыре основные части: архитектура ОС, расширяемость системы, языковая безопасность и выявление дефектов.
Singularity – это ОС на основе микроядра, во многом отличающаяся от предшествующих микроядерных систем, таких, как Mach, L4, SPIN, Vino и Exokernel [2, 8, 17, 25, 46]. В микроядерных ОС монолитное ядро разделено на компоненты, исполняемые в отдельных процессах. Предыдущие системы, за исключением расширений ядра в SPIN, были написаны на не безопасных языках программирования и использовали в качестве механизма изоляции аппаратное управление памятью и кольца защиты процессора. Singularity использует для изоляции процессов и предотвращения доступа к аппаратным ресурсам языковую безопасность и коммуникации посредством передачи сообщений.
Процессы, поддерживаемые аппаратным обеспечением, влекут существенные накладные расходы. Поэтому микроядерные архитектуры эволюционировали в сторону расширений ядра, в то же время пытаясь сохранить целостность системы. В SPIN расширения реализуются на безопасном языке с использованием возможностей языка программирования для запрета доступа к интерфейсам ядра [9]. Vino использует «песочницу» для того, чтобы предотвратить доступ небезопасных расширений к коду и данным ядра, и легковесные транзакции для контроля над использованием ресурсов [46]. Обе системы позволяют расширениям напрямую манипулировать данными ядра, что оставляет возможность повреждений из-за некорректных или злонамеренных операций и несогласованности данных после сбоев расширения. Более строгая модель расширений Singularity запрещает совместное использование данных ядром и расширением. Кроме того, Singularity использует единый общий для всей системы механизм расширений, от драйверов устройств до приложений, вместо специализированного механизма расширений ядра. В Engler Exokernel определялись расширения ядра на языке предметной области и в ядре генерировался код для этого безопасного анализируемого языка[22]. Такой подход привлекателен благодаря хорошо определенным предметным областям, например, фильтрации пакетов, но его сложно обобщить.
Ранние образцы ОС, написанных на безопасных языках программирования, были «открытыми» системами [33], которые исполнялись в едином адресном пространстве и поддерживали потоки (под сбивающим с толку названием «процессы»). Они рассматривались как «однопользовательские» системы и, соответственно, в них уделялось мало внимания безопасности, изоляции и отказоустойчивости. Smalltalk-80 и «Lisp Machine Lisp» (диалект Lisp использовавшийся для Lisp-машин MIT) использовали обеспечения языковой безопасности динамическую типизацию и проверку времени исполнения, но изоляция зависела от дисциплинированности программиста и могла быть нарушена вследствие интроспективных и системных операций [23, 54]. Pilot и Cedar/Mesa были однопользовательскими системами с единым адресным пространством, реализованными на Mesa, статически типизированном безопасном языке [43, 50].
Inferno – это ОС с единым адресным пространством, исполняющая только программы на безопасном языке программирования (Limbo) [15]. В отличие от Singularity она поддерживает только один образ виртуальной машины, зависит от динамической загрузки кода и не обеспечивает изоляции памяти или сбоев.
RMoX – ОС, частично написанная на Occam [6]. Ее архитектура похожа на архитектуру Singularity, и основывается на обмене сообщениями между процессами. Однако RMoX использует ядро из OSKit, написанное на С, а на безопасном языке написаны только драйверы устройств и системный процесс.
Несколько операционных систем было написано на Java. JavaOS – это перенос виртуальной машины Java на голое железо [44]. Она замещает исходную ОС микроядро, написанным на небезопасном языке и библиотеками Java. В отличие от Singularity она поддерживает только один процесс и одно объектное пространство.
Система JX во многих отношениях похожа на Singularity. Это система на основе микроядра, практически полностью написанная на безопасном языке (Java) [24]. Процессы в JX на разделяют память и связываются через синхронный RPC с глубоким копированием параметров. Процессы исполняются в едином аппаратном адресном пространстве и используют для изоляции языковую безопасность. Singularity использует асинхронную передачу сообщений по строго типизированным каналам, являющимся более общими (RPC – это частный случай) и обеспечивающим проверку поведения связи и свойств живучести системы. В Singularity реализована передача по каналам с нулевым копированием, с сохранением независимости памяти. В JX используется модель расширений Java с динамической загрузкой. SIP-ы в Singularity закрыты, что обеспечивает изоляцию сбоев и более точный анализ программ, тем самым способствуя оптимизации кода и выявлению дефектов.
Драйверы устройств – это одновременно и наиболее распространенное расширение ОС, и крупнейший источник дефектов [12, 37, 49]. Nooks предоставляет защищенную среду для исполнения существующих драйверов устройств в ядре Linux [48, 49]. Он использует аппаратное управление памятью для изоляции драйвера от структур данных и кода ядра. Вызовы, пересекающие границу защиты, проходят через runtime Nooks, проверяющий параметры и отслеживающий использование памяти. Singularity, не испытывая давления требований обратной совместимости, исполняет драйверы в SIP. В процессе побочных разработок были созданы средства поиска дефектов в драйверах. Средства анализа ПО, такие, как Static Driver Verifier от Microsoft, для поиска ошибок в драйверах выполняют анализ, специфичный для предметной области [5]. Безопасный язык программирования может сделать эти средства более точными и уменьшить число непроверенных предположений о соответствии семантике языка.
Заслуживает интереса и разработка лучших механизмов изоляции расширений в прикладном ПО. Изоляция сбоев ПО (software fault isolation, SFI, методика, получившая название «песочница») изолирует подозрительный код в его собственном домене, вставляя тесты времени исполнения для проверки ссылок на память и косвенной передачи управления. Использование песочницы привносит значительные накладные расходы и обеспечивает безопасность памяти, но не типобезопасность. Песочница не предоставляет также никаких механизмов управления данными, совместно используемыми приложением и его расширением. Наконец, при использовании песочницы ошибки обнаруживаются только при исполнении, а не при компиляции.
Небольшие изменения в аппаратном обеспечении для управления памяти могут обеспечить более тонкое управление границами защиты в адресном пространстве. Например, защита памяти Mondrian обеспечивает произвольное управление доступом (по границе слова) с приемлемыми накладными расходами [55].
Java, кроме всего прочего, стимулировала использование динамической загрузки кода (т.н. апплеты) и потребовала новой модели безопасностей для защиты от непроверенных расширений. JVM комбинирует проверяемый типобезопасный код и тонкое управление доступом во время исполнения для создания среды, в которой система может ограничивать исполнение непроверенных расширений [35]. Singularity Исполняет расширения в отдельных процессах, что дает лучшие гарантии изоляции и облегчает решение проблем безопасности.
Другие проекты реализовали функциональность, подобную ОС, например, механизмы процессов и планировки, в рантайме Java. При исполнении нескольких приложений в процессе JVM эти механизмы контролируют распределение ресурсов и обеспечивают очистку при сбоях. В J-Kernel реализованы домены защиты для процесса JVM, созданы отменяемые (revocable) возможности управления разделяемыми объектами, а также разработана четкая семантика завершения работы (выгрузки) домена [26]. Luna усовершенствовала runtime-механизмы J-Kernel расширением системы типов Java, распознающим разделяемые данные и позволяющим управлять их совместным использованием [27]. KaffeOS предоставляет абстракцию процесса в JVM наряду с механизмами управления использованием ресурсов группой процессов [4]. Многие из этих идей использованы в Java в новой возможности, изолятах (isolates [41]), подобных существующим AppDomains в Microsoft CLR. Singularity устраняет дублирование механизмов управления ресурсами и изоляции между операционной системой и runtime-ом языка, предоставляя механизм для всех уровней системы. SIP-ы Singularity закрыты и не расширяемы, что обеспечивает больший уровень изолированности и устойчивости к сбоям, чем Java или CLR, использующие общую исполняющую систему.
Безопасные языки программирования – это не недавняя выдумка. Pascal и Ada – это безопасные, статически проверяемые императивные языки. Modula-3, Dylan и Java – это безопасные объектно-ориентированные языки. Безопасные языки стали более популярны с появлением более быстрых процессоров, улучшенных систем типов и усовершенствованных исполняющих систем. Тем не менее, они не слишком широко применяются при реализации систем, поскольку их накладные расходы по времени и месту превышают таковые для низкоуровневых языков наподобие С/C++, и предоставляют ограниченный контроль над раскладкой данных. В Java некоторые из этих накладных расходов можно отнести на счет открытой среды исполнения языка, где рефлексия и динамическая загрузка классов ограничивают возможности компилятора по глобальному анализу и оптимизации кода. В Singularity этих возможностей нет, и глобально оптимизирующий компилятор может производить объектный код, конкурирующий с обычными, небезопасными языками [20].
Другая линия исследований привела к типобезопасным диалектам С (но не C++). CCured – это компилятор и исполняющая система, экстенсивно анализирующая С-код для определения его статической безопасности [39]. Для свойств, которые не могут быть проверены статически, он вставляет тесты времени исполнения. Cyclone – еще один безопасный диалект C [30]. Он менее агрессивен в отношении вставки runtime-тестов, чем CCured, которому может понадобиться изменить раскладку структур для вставки информации о типах. Однако Cyclone может отвергнуть программу на С как небезопасную по своей сути. Vault – более агрессивная переработка С, вводящая новые безопасные языковые конструкции и язык спецификаций для явного управления ресурсами и представления низкоуровневых данных. Он сохраняет некоторую бинарную совместимость с С и не использует сборку мусора [14].
Системы, зависящие от языковой безопасности, не могут доверять компилятору, и должны проверять безопасность кода перед его исполнением. Если исполняемые модули представляются в виде промежуточных типизированных языков, наподобие байткода Java или Microsoft MSIL, проверка является относительно прямолинейным процессом. Этот подход сейчас используется в Singularity для проверки типобезопасности кода системы и приложений. Подобную верификацию можно выполнить и на ассемблере, если компилятор дополнит его аннотациями типов [36, 38]. Низкоуровневый непроверяемый небезопасный код – это потенциальное слабое место любой системы, но в особенности – систем, не полагающихся на защиту памяти. Singularity содержит небезопасный код на нижних уровнях рантайма языка и операционной системы. Проверка безопасности этого кода может помочь в обеспечении надежности системы. Оним из направлений активных исследований является создание безопасного сборщика мусора [53].
Дизайн Singularity рассчитан на облегчение деятельности статических средств поиска дефектов. Анализ систем, написанных на таких небезопасных языках, как С или C++, поскольку слабые гарантии этих языков не предоставляют инструментам четкой семантики и сложны для анализа. Инструментальные средства для этих языков являются либо эвристическими [10, 16, 18, 31] либо делают гарантии согласно предположению, что программы не нарушают семантику языка и не используют дыры, типа приведения указателей на целых числа [5, 13]. Singularity компилируется в MSIL, безопасный промежуточный язык с четкой, хотя и неформальной, семантикой, служащий надежной основой для анализа программ.
Еще одна сложность для средств поиска дефектов – открытость среды, в которой исполняется код. Такая открытость является следствием наличия публичных интерфейсов, которые могут вызываться в разнообразных контекстах, и динамической модификации кода, проистекающей из рефлексии и загрузки кода. Singularity помечает свои интерфейсы описывающими их функциональное поведение спецификациями, которые могут проверяться как статически, так и во время исполнения. В настоящее время каналы, публичные интерфейсы процессов, содержат описание поведения протокола канала, которое может быть проверено с помошью методики проверки соответствия [42]. Кроме того, процессы Singularity закрыты, так что компилятор или средство статического анализа может видеть весь их код, и считать, что он не изменится во время исполнения.
Singularity – это ОС на основе микроядра, использующая прогресс языков программирования и компиляторов для создания легковесных, программно изолированных процессов, которые обеспечивают защиту и изоляцию сбоев с меньшими накладными расходами, чем традиционные процессы, поддерживаемые аппаратно. Singularity обеспечивает изоляцию, исполняя проверяемо безопасные программы и запрещая передачу указателей на объекты между объектными пространствами процессов.
SIP-ы, в свою очередь, дают новое решение проблемы расширения кода системы и приложений. В модели Singularity расширения не загружаются в родительские процессы, а исполняются в собственных процессах, связанных строго типизированными каналами. Эта модель решает некоторые проблемы расширений, так как в Singularity они не могут напрямую обращаться к данным и интерфейсам родителей, а в случае сбоя могут быть легко завершены с помощью выгрузки родительскими процессами.
Singularity – это прежде всего лаборатория для исследования взаимодействий между архитектурой системы, языками программирования, компиляторами, спецификациями и проверками. Прогресс в любой из этих областей порождает продвижение во всех остальных, что ограничивает ценность исследования этих областей по отдельности. Singularity невелика и хорошо структурирована, что позволяет производить изменения, охватывающие несколько областей сразу. В то же время она достаточно велика и реалистична, чтобы продемонстрировать практические преимущества новых методик.
Copyright © 1994-2016 ООО "К-Пресс"