Рефераты
 

Побудова надійних операційних систем, що допускають наявність ненадійних драйверів пристроїв

Побудова надійних операційних систем, що допускають наявність ненадійних драйверів пристроїв

Реферат

  • На тему: «Побудова надійних операційних систем, що допускають наявність ненадійних драйверів пристроїв»
  • Введення
  • Найбільш гострою проблемою багатьох користувачів є ненадійність комп'ютерів.
  • Дослідники у галузі комп'ютерної науки звикли до регулярних збоїв комп'ютерів і до необхідності через кожні кілька місяців встановлювати патчі програмного забезпечення. Проте переважна більшість користувачів вважає це відсутність надійності неприйнятним. Їхня внутрішня модель роботи електронного пристрою ґрунтується на досвіді використання телевізорів і відеомагнітофонів: ви купуєте пристрій, підключаєте його до мережі, і воно бездоганно працює протягом 10 років. Ніяких відмов, ніяких регулярних оновлень програмного забезпечення, ніяких газетних історій про виявлення новітніх представників нескінченної низки вірусів. Щоб зробити комп'ютерні системи більш схожими на телевізори, ми ставимо за мету свого дослідження вдосконалення надійності комп'ютерних систем, і починаємо з операційних систем.
  • 1. Чому у систем трапляються відмови?
  • Основна причина аварійних відмов операційних систем криється у двох принципових дефекти розробки, властивих всім цим системам: наявність занадто великого числа привілеїв і відсутність адекватної ізоляції збоїв. Практично всі операційні системи складаються з численних модулів, скомпонованих в одному адресному просторі і утворюють єдину бінарну програму, яка виконується в режимі ядра. Помилка в будь-якому модулі може легко призвести до руйнування структур даних в будь-якому іншому, не пов'язаним з ним модулі і до миттєвого виходу системи з ладу. Причиною, за якою всі модулі компонуються в єдиний адресний простір без підтримки будь-якої захисту між модулями, є Фаустова угода розробників: покращена продуктивність за ціну більшого числа відмов системи. Нижче ми оцінимо вартість цього компромісу.
  • Тісно пов'язаний питання відноситься до першопричину аварійних відмов. Адже якби кожен модуль був бездоганним, то не виникала б потреба в ізоляції збоїв між модулями, оскільки не було б самих збоїв. Ми стверджуємо, що більша частина збоїв виникає через помилки програмування, внаслідок надмірної складності і використання чужого коду. Дослідження показують, що в програмному забезпеченні в середньому міститься від однієї до шістнадцяти помилок на тисячу рядків коду [27, 22, 2], і що верхня межа цього діапазону явно занижена, оскільки враховувалися тільки ті помилки, які, врешті-решт, вдавалося виявити. Очевидним висновком є те, що в більшому обсязі коду міститься більша кількість помилок. У міру розвитку програмного забезпечення в кожній його новій версії з'являється все більше можливостей (і, відповідно, більший об'єм коду), і часто нова версія є менш надійною, ніж попередня. У [22] показано, що число помилок на тисячу рядків коду прагне до стабілізації у міру зростання числа випущених версій, але асимптотично цей показник відрізняється від нуля.
  • Наявність деяких з цих помилок дозволяє зловмисникам застосовувати віруси і черв'яки для зараження і пошкодження системи. Так що деякі нібито наявні проблеми «безпеки» в принципі не мають нічого спільного з порушеннями заходів безпеки (наприклад, дефектними криптографічними алгоритмами або нестійкими протоколами авторизації), а викликаються лише помилками в коді програм (наприклад, переповнення буферів дозволяють виконувати впроваджений код). Коли в цій статті ми говоримо про «надійності», ми маємо на увазі й те, що часто називають «безпекою», - неавторизований доступ внаслідок помилки в коді програми.
  • Друга проблема полягає в привнесення в операційну систему чужого коду. Найбільш досвідчені користувачі ніколи б не дозволили сторонньої організації вставити незнайомий код в ядро операційної системи, хоча, коли вони купують нове периферійне пристрій і інсталюють відповідний драйвер, вони саме це й роблять. Драйвери пристроїв звичайно пишуться програмістами, що працюють на виробників периферійних пристроїв, і контроль якості їх продукції звичайно нижче, ніж у постачальників операційних систем. У тих випадках, коли драйвер відноситься до open-source, його часто пише благонамірений, але не обов'язково досвідчений доброволець, і контроль якості забезпечується на ще більш низькому рівні. Наприклад, в Linux частота появи помилок в драйверах пристроїв від трьох до семи разів вище, ніж в інших частинах ядра [7]. Навіть компанія Microsoft, у якої є стимули та ресурси для застосування більш щільного контролю якості, не може добитися набагато кращих результатів: 85% всіх аварійних відмов Windows XP обумовлюється наявністю помилок у коді драйверів.
  • Останнім часом з'явилися публікації про родинні роботах, присвячених ізоляції драйверів пристроїв з використанням апаратури MMU [26] і віртуальних машин [19]. Ці методи концентруються на вирішенні проблем у успадкованих операційних системах; ми обговоримо їх у розд. 6. На відміну від цього, при застосуванні нашого підходу надійність досягається шляхом розробки нової полегшеної операційної системи.
  • 2. Рішення: правильна ізоляція збоїв
  • Протягом десятиліть як перевірений методу оперування кодом, що не заслуговує довіри, використовувалося розміщення його в окремому процесі та виконання в режимі користувача. Одним з ключових спостережень, отриманих у дослідженні, якому присвячена ця стаття, є те, що потужним засобом підвищення надійності операційної системи є виконання кожного драйвера у вигляді окремого процесу в режимі користувача з мінімальними необхідними привілеями. Таким чином, код, потенційно містить помилки, ізолю, і помилка, скажімо, в драйвері принтера може призвести до припинення друку, але не до запису перекручених даних у будь-які важливі структури даних ядра і виходу системи з ладу.
  • У цій статті ми проводимо ретельне відмінність між крахом операційної системи, після якого потрібне перезавантаження комп'ютера, і збоєм або відмовою сервера або драйвера, після якого в нашій системі перезавантаження не потрібно. У багатьох випадках дефектний драйвер, що виконується в режимі користувача, може бути вилучений і замінений без потреби в перезапуску інших частин операційної системи, які виконуються в режимі користувача.
  • Ми не розраховуємо на те, що незабаром з'явиться код, вільний від помилок, а якщо і з'явиться, то, звичайно, не в операційних системах, які зазвичай пишуться на C або C + +. На жаль, у програмах, написаних на цих мовах, інтенсивно використовуються покажчики, рясний джерело помилок. Тому наш підхід заснований на ідеях модульності та ізоляції збоїв. Шляхом розбиття системи на велику кількість ізольованих модулів, кожен з яких виконується в окремому процесі в режимі користувача, нам вдалося скоротити частину системи, виконувану в режимі ядра, до абсолютного мінімуму і запобігти поширенню збоїв, що виникають в інших модулях. Зменшення розмірів ядра значно скорочує число помилок, які воно, ймовірно, має містити. Малий розмір також дозволяє знизити рівень складності ядра і полегшити його розуміння, що також сприяє надійності. Тому ми пішли максими Сент-Екзюпері і зробили ядро настільки невеликим, наскільки це дозволяють людські можливості: менше 3800 рядків коду.
  • Одне із зауважень, постійно виникає з приводу таких розробок мінімального ядра, стосується уповільнення роботи системи через додаткові перемикань контексту і копіювання даних, яке потрібно для забезпечення комунікацій різних моделей, які виконуються в користувацькому адресному просторі. Це побоювання, в основному, існує з історичних причин, і ми стверджуємо, що ці причини, більшою частиною, наразі відсутні. По-перше, результати нових досліджень показують, що розробка мінімального ядра не обов'язково завдає шкоди ефективності [3, 23, 15]. Зменшення розмірів ядра при наявності розумних протоколів взаємодії серверів допомагає обмежити масштабність проблеми ефективності. По-друге, значне зростання потужності комп'ютерів в останнє десятиліття істотно послаблює проблему гарантованої продуктивності, що виникає при модульної розробці. По-третє, ми вважаємо, що настає час, коли велика частина користувачів з задоволенням пожертвує деякої ефективністю задля поліпшеної надійності.
  • Детальний обговорення ефективності нашої системи ми представляємо в розд. 5. Однак тут ми коротко згадаємо три попередніх показника ефективності на підтримку нашого доводу про те, що системи з мінімальним ядром не обов'язково повинні бути повільними. По-перше, виміряний час виконання найпростішого системного виклику getpid складає 1.01 мсек на процесорі Athlon з частотою 2.2 Ггц. Це означає, що програма, яка виробляє 10000 системних викликів в секунду, витрачає на перемикання контексту всього 1% часу ЦП, а 10000 системних викликів в секунду виробляють лише деякі програми. По-друге, наша система здатна протягом 4 секунд повністю провести свою компоновку, включаючи ядро і всі частини, що виконуються в режимі користувача (при цьому компілюються 123 файлу і відбувається 11 редагувань зв'язків). По-третє, час початкового завантаження системи з моменту виходу з монітора багатоваріантної завантаження до видачі запрошення до входу в систему становить менше 5 секунд. Після цього операційна система, повністю сумісна з POSIX, готова до використання.
  • 3. Вклад цієї статті
  • Дослідження, результати якого описуються в цій статті, було направлено на вироблення відповіді на таке запитання: як уникнути ситуацій, в яких серйозна помилка в драйвері пристрою (наприклад, використання невірного покажчика або наявність нескінченного циклу) призводить до аварійного відмови або зависання всієї операційної системи?
  • Наш підхід полягав у розробці надійної мультисерверного операційної системи поверх крихітного ядра, що не містить будь-якого зовнішнього, ненадійного коду. Для забезпечення належної ізоляції збоїв кожен сервер і драйвер виконується в режимі користувача в рамках окремого процесу. Крім того, ми додали механізми для відновлення після виникнення поширених збоїв. Ми детально описуємо засоби підтримки надійності і пояснюємо, чому вони відсутні у традиційних монолітних операційних системах. Ми також обговорюємо отримані показники ефективності системи і показуємо, що кошти підтримки надійності сповільнюють систему на 5-10%, але роблять її стійкою до наявності невірних покажчиків, нескінченних циклів і інших помилок, які призвели б до аварійного відмови або зависання традиційних операційних систем.
  • Хоча ні один з окремих аспектів нашого підходу (ядра невеликого розміру, драйвери пристроїв, які виконуються в режимі користувача, або мультисерверного системи) не є новим, ніхто раніше не збирав до купи всі ці частини для побудови невеликий, гнучкою, модульної UNIX-подібної системи, що є набагато більш відмовостійкої, ніж звичайні системи сімейства UNIX, і втрачає тільки 5-10% ефективності порівняно з нашою базовою системою, яка містить драйвери в ядрі.
  • Крім того, наш підхід у корені відрізняється від інших аналогічних робіт, оскільки ми не фокусуємося на масових операційних системах. Замість цього ми отримуємо надійність на основі нової, полегшеною архітектури. Замість того щоб додавати допоміжний код, який підвищує надійність ненадійних систем, ми розщеплює операційну систему на невеликі компоненти й досягаємо надійності за рахунок модульності системи. Хоча наші методи незастосовні до успадкованим операційним системам, ми сподіваємося, що вони допоможуть зробити більш надійними майбутні операційні системи.
  • Ми починаємо статтю з порівняння нашої розробки зі структурами інших операційних систем (розд. 2) і далі переходимо до спільному обговоренню засобів підтримки надійності нашої системи (розд. 3). Потім ми аналізуємо надійність (розд. 4) і ефективність (розд. 5) системи на основі реальних вимірів. У кінці статті ми аналізуємо деякі суміжні роботи (розд. 6) і представляємо свої висновки (розд. 7).
  • 4. Розробка операційної системи
  • Цей проект присвячений побудові більш надійної операційної системи. Перш ніж докладно описувати свою розробку, ми коротко обговоримо, яким чином вибір структури операційної системи може безпосередньо впливати на її надійність. У своїх цілях ми будемо проводити розходження між двома структурами операційних систем: монолітними системами і системами з мінімальним ядром. Існують і інші типи операційних систем, такі як екзоядра [10] і віртуальні машини [24]. Вони не мають безпосереднього відношення до даної статті, але ми повернемося до них у розд. 6.
  • Проблеми монолітних систем
  • Як показано на рис. 1, у стандартній монолітної системі ядро містить всі операційну систему, скомпоновану в єдиному адресному просторі і виконувану в режимі ядра. Ядро може бути структуровано на компоненти, або модулі, показані на малюнку у вигляді прямокутників з пунктирними сторонами, але між компонентами відсутні захисні кордону. На відміну від цього, прямокутники із суцільними сторонами відповідають окремим процесам, що виконуються в режимі користувача; кожен з цих процесів виконується в окремому адресному просторі, що захищається апаратурою MMU (Memory Management Unit, пристрій управління пам'яттю).
  • З монолітними операційними системами пов'язана низка проблем, властивих їх архітектурі. Хоча деякі з цих проблем вже згадувалися у введенні, ми наведемо тут їх зведення:

1. Відсутня належна ізоляція збоїв.

2. Весь код виконується на найвищому рівні привілейованості.

3. Величезний розмір коду припускає наявність численних помилок.

4. У ядрі присутній ненадійний сторонній код.

5. Складність систем утрудняє їх супровід.

Цей список властивостей ставить під сумнів надійність монолітних систем. Важливо розуміти, що ці властивості виникають не унаслідок поганої реалізації, а являють собою фундаментальні проблеми, пов'язані з архітектурою операційної системи.

Передбачається коректність ядра, у той час, як тільки лише його розмір означає, що воно має містити численні помилки [27, 22, 2]. Більш того, для всіх операційних систем, в яких код виконується на найвищому рівні привілейованості, і не забезпечується належне стримування поширення збоїв, будь-яка помилка може стати фатальною. Наприклад, неправильно працюючий драйвер пристрою, наданий стороннім розробником, може легко зруйнувати ключові структури даних і вивести з ладу всю систему. Реальність такої загрози випливає з того спостереження, що аварійні відмови більшості операційних систем трапляються з вини драйверів пристроїв [7, 25]. Додатковою проблемою є те, що величезний розмір монолітних ядер робить їх дуже складними і важко розуміти. Без загального розуміння ядра навіть досвідчений програміст може легко внести помилки за рахунок недостатньої поінформованості про побічні ефекти своїх дій.

Системи з мінімальним ядром

На іншому полюсі знаходиться мінімальне ядро, що містить лише чистий механізм і ніякої політики. Мінімальна ядро включає обробники переривань, механізм для запуску та зупинки процесів (шляхом завантаження регістрів MMU і ЦП), планувальник і механізм підтримки міжпроцесної комунікацій; в ідеальному випадку більше в ядро не входить нічого. Підтримка функціональних можливостей стандартної операційної системи, представлених у монолітному ядрі, переміщається в користувальницьке адресний простір, і відповідний код більше не виконується на найбільш привілейованому рівні.

Поверх мінімального ядра можливі різні організації операційної системи. Одним з варіантів є виконання всієї операційної системи в одному сервері в режимі користувача, але в такій архітектурі існують ті ж проблеми, що і в монолітній системі, і помилки, як і раніше можуть призвести до аварійного відмови всієї операційної системи, що виконується в режимі користувача. У розд. 6 ми обговоримо деякі роботи в цій області.

Кращим рішенням є виконання кожного ненадійного модуля в режимі користувача в окремому процесі, ізольованому від інших процесів. Ми до крайності захопилися цією ідеєю і повністю роздрібнили свою систему, як показано на рис. 2. Усі функціональні компоненти операційної системи, такі як драйвери пристроїв, файлова система, сервер мережі та високорівневе управління пам'яттю, виконуються як окремі процеси в режимі користувача у власному адресному просторі. Цю модель можна визначити, як мультисерверного операційну систему.

З логічної точки зору наші користувальницькі процеси можна розбити на три рівні, хоча з точки зору ядра всі вони є всього лише процесами. Найнижчий рівень процесів, які виконуються в режимі користувача, займають драйвери пристроїв, кожен з яких керує деякими пристроєм. Ми реалізували драйвери для інтерфейсу IDE, гнучких і жорстких дисків, клавіатури, дисплеїв, аудіо-пристроїв, принтерів і різних карт Ethernet. Вище рівня драйверів знаходяться серверні процеси. У їх число входять файловий сервер, сервер процесів, мережевий сервер, інформаційний сервер, сервер реінкарнації та інші. Над рівнем серверів виконуються звичайні користувальницькі процеси, включаючи різні інтерпретатори shell, компілятори, утиліти та прикладні програми. Не рахуючи невеликого числа виключень, сервери і драйвери є нормальними для користувача процесами.

Щоб уникнути будь-якої неясності ще раз зауважимо, що кожний сервер або драйвер виконується у вигляді окремого користувача процесу з власним адресним простором, повністю відокремленим від адресного простору ядра і інших серверів, драйверів і процесів користувачів. У нашій архітектурі процеси не поділяють будь-яке адресний простір і можуть спілкуватися один з одним лише з використанням механізму IPC, забезпечуваного ядром. Цей аспект є критичним для надійності, оскільки він запобігає поширенню збоїв одного сервера або драйвера на інші сервери або драйвери подібно до того, як помилка при компіляції програми, що виникає в одному процесі, не впливає на те, що робить браузер в іншому процесі.

Під час роботи в режимі користувача можливості процесів операційної системи обмежені. Тому для підтримки виконання необхідних від них завдань серверами і драйверами ядро експортує ряд системних викликів, які можуть вироблятися авторизованими процесами. Наприклад, драйвери пристроїв більше не мають привілеїв на безпосереднє виконання вводу-виводу, але можуть вимагати від ядра виконання відповідних дій від свого імені. Крім того, сервери та драйвери можуть запитувати сервіси один в одного. Всі такі IPC проводяться шляхом обміну невеликими повідомленнями фіксованого розміру. Цей обмін повідомленнями реалізується шляхом звернень до ядра, яке до виконання запитуваної дії перевіряє, авторизований чи відповідним чином викликає процес.

Розглянемо типовий виклик ядра. Компоненту операційної системи, що виконується в режимі користувача в деякому процесі, може знадобитися скопіювати дані в інше адресний простір чи з нього, але йому неможливо довірити можливість доступу до фізичної пам'яті. Натомість цього забезпечуються виклики ядра для копіювання з допустимих віртуальних адрес або в ці адреси сегмента даних цільового процесу. Цей виклик надає набагато більш слабкі можливості, ніж запис в будь-яке слово фізичної пам'яті, але все-таки ці можливості досить потужні, і тому можливість такого виклику надається тільки процесам операційної системи, яким потрібно копіювання блоків даних з одного адресного простору в інше. Для звичайних користувальницьких процесів подібні виклики заборонені.

Після приведення цього опису структури операційної системи ми можемо тепер пояснити, яким чином користувальницькі процеси отримують сервіси операційної системи, визначені в стандарті POSIX. Користувальницький процес, який бажає виконати, наприклад, виклик READ, формує повідомлення, що містить номер системного виклику і (покажчики на) параметри, і звертається до ядра із запитом посилки цього невеликого запитної повідомлення файлового сервера, що є іншим призначеним для користувача процесом. Ядро забезпечує блокування викликає процесу до тих пір, поки його запит не буде опрацьовано файловим сервером. За замовчуванням усі комунікації між процесами забороняються з міркувань безпеки, але цей запит досягає мети, оскільки комунікації з файловим сервером явно вирішуються звичайним користувальницьким процесам.

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

Ця схема проста і елегантна, вона дозволяє відокремити сервери і драйвери від ядра і дозволяє замінювати їх простим чином, що сприяє модульності системи. Хоча тут потрібно до чотирьох повідомлень, вони передаються дуже швидко (в межах 500 наносекунд на повідомлення в залежності від ЦП). Якщо і відправник, і одержувач готові до комунікації, те ядро копіює повідомлення прямо з буфера відправник у буфер одержувача без його переміщення в адресний простір ядра. Крім того, число копіювань даних є точно таким же, як в монолітній системі: диск поміщає дані прямо в буферний кеш файлового сервера, та є одне копіювання з цього кеша в адресний простір користувацького процесу.

Принципи розробки

Перш ніж перейти до докладного розгляду властивостей надійності нашої системи, коротко обговоримо принципи розробки, якими ми керувалися у прагненні до надійності:

1. Простота.

2. Модульність.

3. Найменша авторизація.

4. Відмовостійкість.

По-перше, ми зберігаємо свою систему настільки простий, наскільки це можливо, так що її легко зрозуміти, і можна з більшою вірогідністю підтримувати її в коректному стані. Це відноситься як до високорівневих проектування, так і до реалізації. Наша розробка дозволяє структурно уникнути відомих проблем, таких як вичерпання ресурсів. При потребі ми явно обмінюємо ресурси та ефективність на надійність. Наприклад, в ядрі статично оголошуються всі структури даних замість того, щоб динамічно виділяти пам'ять при необхідності. Хоча ми можемо недоіспользовать деяку пам'ять, цей підхід є дуже простим і ніколи не призводить до помилок. Іншим прикладом є те, що ми навмисне не реалізували нитки. Може бути, ми заплатили за це деякою втратою ефективності (а може бути, і ні), але зате не повинні турбуватися про потенційних «станах гонок» (race condition) і синхронізації, що істотно полегшує життя програмістам.

По-друге, ми розділили свою систему на набір невеликих незалежних модулів. Використання властивостей модульності, таких як обмеження розповсюдження збоїв, є ключовим елементом розробки нашої системи. Шляхом повного поділу операційної системи на модулі ми можемо встановити «брандмаери», крізь які не можуть розповсюджуватися помилки, що призводить до більш надійної системи. Для запобігання непрямого впливу збоїв в одному модулі на який-небудь інший модуль ми структурним чином зменшуємо їх взаємозалежність, наскільки це можливо. У тих випадках, коли це неможливо через природи модулів, ми застосовуємо додаткові засоби підтримки безпеки. Наприклад, файлова система залежить від драйверів пристроїв, але вона розробляється таким чином, щоб бути готовою до обробки збоїв драйвера.

По-третє, ми забезпечуємо дотримання принципу найменшої авторизації. Хоча ізоляція збоїв допомагає стримувати їх поширення, збій у повноважному модулі все ще може викликати значний збиток. Тому ми знижуємо рівень привілеїв всіх користувальницьких процесів до гранично припустимого мінімуму. У ядрі підтримуються бітові масиви і списки, які визначають можливості процесів. Зокрема, є шкала допустимих викликів ядра і список допустимих адрес призначення повідомлень. Ця інформація зберігається в елементах таблиці процесів, і тому її можна строго контролювати, і нею просто керувати. Інформація про авторизацію ініціюється під час завантаження системи, головним чином, на основі конфігураційних таблиць, створюваних системним адміністратором.

По-четверте, при розробці системи ми явним чином враховуємо можливість до стійкості до деяких збоїв. Всі сервери та драйвери управляються і відслідковуються спеціальним сервером, званим сервером реінкарнації, який може справлятися з двома видами проблем. Якщо системний процес завершується непередбачуваним чином, це негайно розпізнається, і процес перезапускається. Крім того, періодично перевіряється стан кожного системного процесу для перевірки його правильного функціонування. Якщо процес функціонує неправильно, він примусово завершується і перезапускається. Так працює механізм відмовостійкості: зіпсований компонент замінюється, але система весь час продовжує працювати.

5. Властивості надійності

Ми вважаємо, що в нашій розробці надійність системи підвищується в порівнянні з усіма іншими існуючими операційними системами за рахунок застосування трьох важливих підходів:

Зменшується кількість критичних збоїв.

Скорочується обсяг шкоди, яка може бути заподіяна будь-який помилкою.

Можна відновити після поширених збоїв.

У наступних підрозділах ми пояснимо, чому застосування цих підходів дозволяє підвищити надійність. Ми також порівняємо вплив деяких класів помилок на нашу систему з тим, як вони впливають на монолітні системи, такі як Windows, Linux і BSD. У розд. 6 ми порівняємо наш підхід до підвищення надійності з іншими ідеями, пропонованими в літературних джерелах.

Скорочення числа помилок в ядрі

Нашої першою лінією захисту є дуже невелике ядро. Добре відомо, що в більшому за обсягом коді міститься більша кількість помилок, і тому чим менше ядро, тим менше в ньому помилок. Якщо в якості нижньої оцінки використати 6 помилок на 1000 рядків виконуваного коду [27], то за наявності 3800 рядків виконуваного коду в ядрі буде присутній, як мінімум, 22 помилки. Крім того, 3800 рядків коду (менше 100 сторінок лістингу, включаючи заголовки та коментарі) - це досить мало, щоб весь цей код міг зрозуміти один чоловік; це істотно підвищує шанси на те, що з часом всі помилки вдасться знайти.

На відміну від цього, в ядрі монолітної системи, такий як Linux, розміром в 2.5 мільйона рядків виконуваного коду, ймовірно, повинно міститися не менше 6 * 2500 = 15,000 помилок. Крім того, за наявності системи з декількох мільйонів рядків ні одна людина не може прочитати весь вихідний код і повністю зрозуміти, як він працює, що зменшує шанси на знаходження всіх помилок.

Зниження потенційного впливу помилок

Звичайно, зменшення розміру ядра не призводить до скорочення обсягу всього коду системи. При цьому всього лише велика частина системи починає працювати в режимі користувача. Однак саме це зміна надає глибоке вплив на надійність. У коду ядра є можливість повного доступу до всього, що може робити машина. Помилки в ядрі можуть призводити до випадкової ініціалізації введення-виведення, виконання неправильного вводу-виводу, пошкодження таблиць розподілу пам'яті та іншим речам, які не можуть зробити непривілейованих програми, які виконуються в режимі користувача.

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

Для порівняння розглянемо вплив помилки в аудіо-драйвері, що виконуються в режимі ядра. Цей драйвер може ненавмисно перезаписати в стеку адресу повернення зі своєї процедури і зробити при виконанні повернення довільний перехід в монолітне ядро. Цей перехід може привести до коду управління пам'яттю, викликаючи руйнування ключових структур даних, таких як таблиці сторінок і списки вільних і зайнятих ділянок пам'яті. Монолітні системи в цьому відношенні є дуже крихкими і легко руйнуються при прояві помилки.

Відновлення після збоїв

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

Збій драйвера має наслідки і для файлової системи. Можуть бути втрачені невиконані запити вводу-виводу, і в деяких випадках інформація про помилку вводу-виводу доводиться до відома програми. Однак у багатьох випадках можливе повне відновлення. Більш докладне обговорення сервера реінкарнації і надійності на рівні додатків наводиться в розд. 4.

У монолітних системах зазвичай відсутня можливість виявлення збійних драйверів «на льоту», хоча є дані про деякі дослідження в цій області [25]. Тим не менше, заміна на льоту ядерного драйвера є складною справою, оскільки до часу заміни він може утримувати ядерні блокування або знаходитися в критичному ділянці.

Обмеження зловживань переповнювання буфера

Відомо, що переповнення буферів є рясним джерелом помилок, наявністю яких інтенсивно користуються віруси і черв'яки. Хоча наша розробка спрямована радше на боротьбу з помилками, а не із зловмисними кодом, деякі засоби нашої системи надають захист від певних видів зловживань. Оскільки наше ядро є мінімальним, і в ньому використовується тільки статичне розміщення даних, виникнення проблеми малоймовірно в найбільш чутливої частини системи. Якщо переповнення буферу трапляється в одному з користувацьких процесів, то проблема не є надто серйозною, оскільки сервери і драйвери, що виконуються в режимі користувача, володіють обмеженими можливостями.

Крім того, в нашій системі виконується тільки код, розташований в сегментах тексту, які доступні тільки з читання. Хоча це не запобігає можливість переповнення буфера, ускладнюється можливість зловживання, оскільки надлишкові дані, що знаходяться в стеці або купі, неможливо виконати як код. Цей захисний механізм є виключно важливим, оскільки він запобігає зараження вірусами і черв'яками та виконання їх власного коду. Сценарій найгіршого випадку змінюється від взяття безпосереднього управління до перезапису адреси повернення в стеку та виконання деякої існуючої бібліотечної процедури. Найбільш відомий приклад такої ситуації часто називають атакою шляхом «повернення в libc» («return-to-libc»), і цей спосіб атаки вважається набагато більш складним, ніж виконання коду в стеці або купі.

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

Забезпечення надійного IPC

Добре відомою проблемою механізмів обміну повідомленнями є управління буферами, але в нашому варіанті комунікаційних примітивів ми повністю уникаємо цієї проблеми. У нашому механізмі синхронної передачі повідомлень використовуються рандеву, в результаті чого усувається потреба в буферизації і управлінні буферами, а також відсутня проблема вичерпання ресурсів. Якщо одержувач не очікує повідомлення, то примітив SEND блокує відправника. Аналогічно, примітив RECEIVE блокує процес, якщо немає повідомлення, що очікує свого отримання. Це означає, що для заданого процесу в таблиці процесів у будь-який час повинен зберігатися єдиний вказівник на буфер повідомлення.

На додаток до цього, у нас є механізм асинхронної передачі повідомлень NOTIFY, який також не є чутливим до вичерпання ресурсів. Повідомлення є типізовані, і для кожного процесу зберігається тільки один біт для кожного типу. Хоча обсяг інформації, яку можна передати таким чином, обмежений, цей підхід був обраний з-за своєї надійності.

До речі, зауважимо, що у своєму IPC ми уникаємо переповнювання буфера шляхом обмеження засобів комунікації короткими повідомленнями фіксованої довжини. Повідомлення є об'єднанням декількох типізованих форматів повідомлень, так що розмір автоматично вибирається компілятором, як розмір найбільшого допустимого типу повідомлень, який залежить від розміру цілих чисел і покажчиків. Цей механізм передачі повідомлень використовується для всіх запитів і відповідей.

Обмеження IPC

IPC - це потужний механізмом, який потребує строгого контролі. Оскільки наш механізм передачі повідомлень є синхронним, процес, що виконує примітив IPC, блокується, поки обидва учасника не стануть готовими. Користувальницький процес може легко зловживати цим властивістю для завішування системних процесів шляхом посилки запиту без очікування відповіді. Тому є інший примітив IPC SENDREC, що комбінує в одному виклик SEND і RECEIVE. Він блокує відправника до отримання відповіді на запит. З метою захисту операційної системи цей примітив є єдиним, який можна використовувати звичайним користувачам. Насправді, в ядрі для кожного процесу підтримується бітовий масив для обмеження примітивів IPC, які дозволяється використовувати даному процесу.

Крім того, в ядрі підтримується бітовий масив, що визначає, з якими драйверами і серверами може взаємодіяти даний процес. Ця маска посилки повідомлень являє собою механізм, що запобігає безпосередню посилку повідомлень драйверам від користувацьких процесів. Натомість цього, їм дозволяється спілкуватися тільки з серверами, що забезпечують POSIX-дзвінки. Однак маска посилки повідомлень використовується також і для запобігання посилки (непередбаченого) повідомлення, скажімо, від драйвера клавіатури аудіо-драйверу. Знову шляхом суворої інкапсуляції можливостей кожного процесу ми можемо в значній мірі запобігти поширенню неминучих помилок в драйверах і їх вплив на інші частини системи.

На відміну від цього, в монолітній системі будь-який драйвер може викликати будь-який шматок коду в ядрі, використовуючи машинну інструкцію виклику підпрограми (або, ще гірше, інструкцію повернення з підпрограми, якщо стек був перезаписаний через переповнювання буфера), що дозволяє проблем, що виникають в одній підсистемі, поширюватися в інші підсистеми.

Уникання тупиків

Оскільки за замовчуванням для IPC використовуються синхронні виклики SEND і RECEIVE, можуть виникати тупики, коли два або більше число процесів одночасно намагаються обмінюватися повідомленнями, і всі процеси блокуються в очікуванні один одного. Тому ми ретельно розробляли протокол уникнення тупиків, що приписує часткове, що сходить впорядкування повідомлень.

Впорядкування повідомлень приблизно відповідає розбивка на рівні, описаного в розд. 2.2. Наприклад, звичайним користувальницьким процесам дозволяється тільки посилати повідомлення з використанням примітиву SENDREC серверів, які реалізують інтерфейс POSIX, а ці сервери можуть запитувати сервіси від драйверів, які, у свою чергу, можуть виробляти виклики ядра. Однак для асинхронних подій, таких як переривання і таймери, потрібні повідомлення, що посилаються в протилежному напрямку, від ядра сервера або драйверу. Використання синхронних викликів SEND для передачі цих подій може легко призвести до глухого кута. Ми уникаємо цієї проблеми шляхом використання для асинхронних подій механізму NOTIFY, який ніколи не блокує викликає бік. Якщо оповестітельное повідомлення не може бути доставлено процесу-адресату, воно зберігається в його елементі таблиці процесів до тих пір, поки він не виконає RECEIVE.

Хоча протокол уникнення тупиків підтримується обговорювалося вище механізмом масок посилки повідомлень, ми також реалізували в ядрі розпізнавання тупиків. Якщо виклик примітиву в деякому процесі непередбачуваних чином привів би до виникнення безвиході, то виконання примітиву не проводиться, і закликають учасника повертається повідомлення про помилку.

Уніфікація переривань і повідомлень

Базовим механізмом IPC є передача повідомлень на основі рандеву, але потрібні й асинхронні повідомлення, наприклад, для надання інформації про переривання, що є потенційним джерелом помилок в операційних системах. Ми суттєво зменшили тут шанси на появу помилок, уніфікувавши асинхронні сигнали та повідомлення. Зазвичай, коли деякий процес посилає повідомлення іншому процесу і одержувач не є готовим, відправник блокується. Ця схема не працює для переривань, оскільки обробник переривань не може дозволити собі блокування. Замість цього використовується асинхронний механізм сповіщень, при використанні якого обробник переривань виробляє виклик NOTIFY для драйвера. Якщо драйвер очікує повідомлення, то сповіщення доставляється безпосередньо. Якщо він його не очікує, то сповіщення зберігається в бітові масиви до тих пір, поки згодом драйвер не виконає виклик RECEIVE.

Обмеження функціональних можливостей драйвера

Ядро експортує обмежений набір функцій, які можна викликати ззовні. Цей ядерний API представляє собою єдиний спосіб взаємодії драйвера з ядром. Однак не кожному драйверу дозволяється використовувати будь-який виклик ядра. Для кожного драйвера в ядрі (в таблиці процесів) підтримується бітовий масив, який показує, які виклики ядра може виробляти цей драйвер. Гранулярні викликів ядра є досить дрібною. Відсутній мультиплексування викликів в один і той же номер функції. Кожен виклик індивідуально захищається власним бітом в бітові масиви. Проте на внутрішньому рівні кілька викликів може оброблятися однієї і тієї ж ядерної функцією. Цей метод дозволяє реалізувати детальне керування доступом до ядра.

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

Страницы: 1, 2


© 2010 BANKS OF РЕФЕРАТ