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

Мне давно не приходилось получать такого удовольствия от чтения статей в журнале Computer. Статья Эндрю Таненбаума и его молодых коллег написана свойственным Таненбауму легким стилем, хотя посвящена очень серьезным вопросам (надеюсь, что мне удалось хотя бы частично сохранить этот стиль в своем пересказе). Лично меня очень обрадовало то, что давно любимый мной микроядерный подход к построению операционных систем, как кажется, получает новую жизнь. Я постарался сохранить в своем пересказе все основные моменты статьи.

Кстати, именно эта статья вызвала новый виток дискуссии Эндрю Таненбаума с Линусом Торвальдсом (первый спор о микроядерном подходе возник еще в 1992 г.).

Покупатели компьютеров рассчитывают на такую же их надежность, как, например, у телевизоров: купил, подключил к сети, и компьютер надежно работает в течение 10 лет. Наибольшие нарушения надежности и безопасности компьютеров вызывает операционная система. Хотя в прикладных программах может содержаться много дефектов, если бы операционные системы были свободны от ошибок, ошибки в приложениях могли бы причинять только ограниченный ущерб. Характеристики надежности и безопасности операционных систем являются взаимосвязанными. Например, ошибка переполнения буфера может привести к краху системы (проблема надежности), но та же ошибка может позволить проникнуть в компьютер умело написанному вирусу или червю (проблема безопасности). Повышение уровня надежности способствует повышению уровня безопасности.

В современных операционных системах имеются две характеристики, делающие их ненадежными и небезопасными: они огромны и обладают очень плохой изоляцией сбоев. В ядре ОС Linux содержится более 2,5 миллионов строк кода, в ядре Windows XP — в два с лишним раза больше. В одном из исследований надежности программного обеспечения показано, что в программах имеется от шести до шестнадцати ошибок на 1000 строк исполняемого кода; в другом исследовании насчитывается от двух до 75 ошибок на 1000 строк исполняемого кода в зависимости от размера модуля. При таких оценках ядро Linux содержит около 15000 ошибок, а Windows XP — больше 30000 ошибок. Еще хуже то, что около 70% кода операционных систем занимает код драйверов устройств, в которых ошибки встречаются в 3-7 раз чаще, чем в обычном коде. Понятно, что просто невозможно найти и исправить все ошибки; более того, при исправлении ошибок часто привносятся новые.

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

Проект Nooks

К счастью, ситуация не является безнадежной. Исследователи прилагают усилия к созданию более надежных операционных систем. Авторы статьи рассматривают четыре подхода, используемые исследователями для придания будущим операционным системам более высокого уровня надежности и безопасности. Наиболее консервативным является подход проекта Nooks, направленного на повышение надежности существующих операционных систем (таких как Windows и Linux). В Nooks поддерживается монолитная структура ядра, но ядро защищается от содержащих ошибки драйверов устройств путем обертывания каждого драйвера уровнем защитного программного обеспечения. Оболочка драйвера тщательно отслеживает все взаимодействия между драйвером и ядром. Целями проекта Nooks являются защита ядра от сбоев драйверов, автоматическое восстановление после отказа драйвера и достижение всего этого путем небольших изменений в существующих драйверах и ядре. Первая реализация была выполнена для Linux, но идеи применимы и к другим унаследованным операционным системам.

Хотя эксперименты показали, что Nooks может перехватить 99% фатальных ошибок драйверов и 55% их прочих ошибок, система не является совершенной. Например, в драйверах могут выполняться привилегированные команды, которым в них выполняться не полагается; в них может производиться запись в неверные порты; в них могут возникать бесконечные циклы. Кроме того, группе Nooks пришлось вручную написать большое число оболочек драйверов, и в них самих могут содержаться ошибки. Наконец, не предотвращается полностью возможность доступа по записи ко всей памяти. Тем не менее, этот проект является потенциально полезным шагом к повышению надежности унаследованных операционных систем.

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

Паравиртуализация

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

В принципе, этот подход должен обеспечивать более высокую надежность, чем у отдельной операционной системы, поскольку при отказе виртуальной машины, содержащей один или несколько драйверов, эта виртуальная машина может быть перезапущена, а драйверы могут быть возвращены в исходное состояние. Здесь не предпринимаются попытки вернуть драйверы в состояние, предшествующее отказу, как это делается в Nooks. Измерения производительности показывают, что накладные расходы на использование паравиртуальных машин в такой манере составляют около 3-8%.

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

Проект Minix 3

В операционной системе Minix 3 микроядро обрабатывает прерывания, обеспечивает основные механизмы для управления процессами, реализует межпроцессные взаимодействия и производит планирование процессов. Оно также предоставляет небольшой набор вызовов ядра для авторизованных драйверов и серверов, например, для чтения части заданного пользовательского адресного пространства или записи в авторизованные порты ввода-вывода. В адресном пространстве микроядра работает драйвер таймера, но он планируется как отдельный процесс. Никакие другие драйверы в режиме ядра не работают.

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

Поверх уровня драйверов устройств располагается уровень серверов. Файловый сервер является небольшой (4,500 строк исполняемого кода) программой, которая принимает запросы от пользовательских процессов по обработке Posix-совместимых вызовов, относящихся к файлам (read, write, lseek и stat) и выполняет их. На этом уровне находится и менеджер процессов, который поддерживает управление процессами и памятью и выполняет Posix-совместимые и другие системные вызовы, такие как fork, exec и brk. Несколько необычным является сервер реинкарнации, который является родительским процессом всех других серверов и всех драйверов. Если драйвер или сервер аварийно или по собственной инициативе завершается, либо не отвечает на периодические запросы отклика, то сервер реинкарнации принудительно завершает его, если это требуется, и перезапускает из копии на диске или в основной памяти. В число других серверов входит сервер сети, поддерживающий весь стек TCP/IP; простой сервер имен, используемый всеми остальными серверами; и информационный сервер, способствующий отладке.

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

Межпроцессные взаимодействия (IPC) в MINIX 3 поддерживаются на основе передачи сообщений фиксированной длины с использованием принципа рандеву: система копирует сообщение напрямую от отправителя к получателю, когда оба они к этому готовы. Кроме того, поддерживается механизм асинхронного уведомления о событиях. События, о которых оказалось невозможно уведомить процесс, фиксируются в битовой шкале в таблице процессов. С системой передачи сообщений интегрирована обработка прерываний. Обработчики прерываний используют механизм уведомлений для сигнализации о завершении ввода-вывода. Этот механизм позволяет обработчику установить бит в битовой шкале «необработанных прерываний» драйвера и продолжить выполнение без блокировки. Когда драйвер становится готовым к получению прерывания, ядро преобразует его в обычное сообщение.

Надежность Minix 3 происходит из разных источников. Во-первых, размер кода, выполняемого в ядре, составляет около 4000 строк, и общее число ошибок — всего около 24. Небольшой размер ядра позволяет верифицировать его код вручную или на основе формальных методов. Особенности IPC позволяют избежать потребности управления буферами в ядре. Кроме того, для каждого процесса ограничены доступные примитивы IPC, включая адреса назначения и события, о которых происходит уведомление. Например, пользовательские процессы могут использовать только принцип рандеву и посылать сообщения только Posix-серверам. В дополнение к этому, все структуры ядра являются статическими. Все эти свойства значительно упрощают код и устраняют ошибки в ядре, связанные с переполнением буферов, «утечку памяти» (memory leak), несвоевременные прерывания и т.д. Конечно, перемещение большей части операционной системы в процессы, выполняемые в режиме пользователя, не устраняют неизбежные ошибки в драйверах и серверах, но это ограничивает их воздействие на систему в целом. Ошибочный код в ядре может испортить критичные структуры данных, записать на диск ненужные данные и т.д.; ошибочный же код в большинстве драйверов и серверов не может принести такого вреда, поскольку эти процессы строго разделены и очень ограничены в своих возможностях.

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

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

В течение десятилетий мультисерверные архитектуры на основе микроядра критиковались за недостаточную эффективность. Однако различные проекты показали, что при модульной разработке можно достичь приемлемой производительности. Несмотря на то, что система Minix 3 не оптимизировалась для достижения высокой производительности, она работает достаточно быстро. Вынос с ядра драйверов привел к снижению производительности на 10%, и система собирается, включая ядро, общие драйверы и все серверы (112 компиляций и 11 редактирований связей) менее чем за 6 секунд на процессоре Athlon с 2,2 Ггц. Тот факт, что мультисерверная архитектура смогла обеспечить высоконадежную Unix-подобную среду за счет небольшого снижения производительности, показывает практичность этого подхода. Minix 3 для Pentium свободно распространяется под лицензией Беркли на сайте www.minix3.org.

Проект Singularity

Наиболее радикальный подход предложен в Microsoft Research. По существу, этот подход отвергает понятие операционной системы как единой программы, выполняемой в режиме ядра, плюс некоторый набор пользовательских процессов, выполняемых в режиме пользователя, и заменяет его системой, написанной на новом типизированном языке, в котором отсутствуют проблемы с указателями и пр., свойственные C и C++. Подобно двум предыдущим, этот подход существует десятилетия. Он использовался в компьютере Burroughs B5000. Тогда единственным доступным языком был Algol, и защита поддерживалась не устройством управления памятью, а за счет невозможности генерации «опасного» кода компилятором языка Algol.

Система, называемая Singularity (http://research.microsoft.com/os/singularity), пишется почти целиком на новом типизированном языке Sing#. Этот язык основан на C#, но дополнен примитивами передачи сообщений, семантика которых определяется формальными, письменными контрактами. Поскольку безопасность языка строго ограничивает поведение системы и процессов, все процессы выполняются в едином виртуальном адресном пространстве. Эта схема ведет как к безопасности (поскольку компилятор не позволяет процессу обращаться к данным любого другого процесса), так и к эффективности (поскольку устраняется потребность в обращениях к ядру и переключениях контекста).

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

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

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

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

В Singularity поддерживается совместно используемая процессами «куча» объектов, но в каждый момент времени каждый объект из кучи принадлежит только одному процессу. Однако владение объектом может быть передано другому процессу через канал. Например, когда драйвер диска считывает блок, он помещает этот блок в кучу. Потом система передает дескриптор (handle) этого блока процессу, запросившему данные, поддерживая принцип единственного владельца, но позволяя передавать данные без копирования.

Для всех служб в Singularity поддерживается единое иерархическое пространство имен. Сервер корневого имени управляет верхней частью дерева, но к его узлам могут монтироваться другие серверы имен. В частности, файловая система, которая является всего лишь процессом, монтируется к узлу /fs, так что /fs/users/linda/foo может быть именем пользовательского файла. Файлы реализуются в виде B-деревьев, ключами которых являются номера блоков. При обработке системных вызовов файловая система требует от дискового драйвера поместить в кучу нужные блоки. Затем владение блоком передается по цепочке пользовательскому процессу.

Для каждого компонента системы имеются метаданные, описывающие его зависимости, экспортирование, ресурсы и поведение. Эти метаданные используются для верификации. Образ системы состоит из микроядра, драйверов и приложений, требуемых для выполнения системы, и их метаданных. Внешние верификаторы могут выполнить многочисленные проверки образа до запуска системы; в частности, таким образом можно убедиться в том, что драйверы не конфликтуют по ресурсам. Верификация состоит из трех шагов. Сначала компилятор контролирует типы, владение объектами, протоколы каналов и т.д. Затем компилятор генерирует внутреннее представление на языке MSIL (Microsoft Intermediate Language), переносимый байт-код, который может проверяться верификатором. Наконец, представление системы на MSIL компилируется в командный код x86 компилятором постобработки, который может вставлять в код проверки времени выполнения. Такая избыточная верификация служит для отлавливания ошибок в самих верификаторах.

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

Пересказ Сергея Кузнецова статьи Эндрю Таненбаума, Джоррита Хердера и Херберта Боса «Можем ли мы сделать операционные системы надежными и безопасными».

Can We Make Operating Systems Reliable and Secure?
Andrew S. TanenbaumJorrit N. HerderHerbert Bos, Vrije Universiteit, Amsterdam.
Computer (IEEE Computer Society, V. 39, No 5, May 2006).

Ваша реакция?
+1
0
+1
0
+1
0
+1
0
+1
0
+1
0
+1
0
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x