|
Побудова надійних операційних систем, що допускають наявність ненадійних драйверів пристроїв
p align="left">Порівняємо цю ситуацію з можливим поведінкою драйвера у монолітному ядрі. Помилка в коді може призвести до запису в адресний простір користувацького процесу замість читання з нього, що зруйнує процес. Крім того, ядерний драйвер може викликати будь-яку функцію в усьому ядрі, включаючи функції, які не повинні викликатися драйверами. Оскільки відсутня будь-яка внутрішньоядерні захист, це практично неможливо запобігти. У нашій розробці жоден драйвер не може викликати ядерну функцію, яка не була явно експортована як частина інтерфейсу між ядром і цим драйвером.Заборона доступу до портів введення-виведення Для кожного драйвера в ядрі підтримується список портів введення-виведення, з яких він може читати, а також тих, у які він може писати. Читання і запис захищаються по окремо, так що процес, у якого є право на тільки читання з деякого порту вводу-виводу, не може писати в нього. Будь-яка спроба порушення цих правил призводить до вироблення коду помилки, що повертається закликають учасника. Таким чином, драйвер принтера може бути обмежений доступом тільки до портів введення-виведення принтера, аудіо-драйвер може бути обмежений доступом тільки до портів введення-виведення звукової карти і т.д. На відміну від цього, в монолітних системах відсутній спосіб обмеження доступу внутрішньоядерної драйвера тільки до невеликого числа портів введення-виведення. Ядерний драйвер може випадково виконати запис в будь-який порт вводу-виводу і завдати істотної шкоди. У деяких випадках в адресний простір драйвера можуть відображатися реальні регістри пристрої введення-виведення, щоб уникнути якого б то не було взаємодії з ядром при здійсненні введення-виведення. Однак, оскільки не в усіх архітектурах допускається відображення регістрів введення-виведення в призначені для користувача процеси із забезпеченням необхідного рівня захисту, ми вибрали модель, в якій реальні операції введення-виведення виконуються тільки ядром. Це проектне рішення є ще одним прикладом того, що ми віддаємо перевагу надійності на шкоду ефективності. Хоча в даний час таблиці, що дозволяють доступ до портів введення-виведення, ініціалізувалися з конфігураційного файлу, ми плануємо реалізувати сервер шини PCI, який буде робити це автоматично. Сервер шини PCI може отримати з BIOS порти введення-виведення, необхідні кожному драйверу, і використовувати цю інформацію для ініціалізації таблиць ядра. Перевірка параметрів Оскільки всі виклики ядра проводяться шляхом генерації внутрішнього переривання, ядро може виконати обмежену валідацію параметрів до диспетчеризації дзвінка. Ця валідація включає перевірки як справності (sanity), так і прав доступу (permission). Наприклад, якщо драйвер просить ядро записати блок даних з використанням фізичної адресації, то цей виклик може бути відхилений, оскільки не в усіх драйверів є право на такі дії. Використовуючи віртуальну адресацію, ядро, мабуть, не зможе сказати, чи є ця адреса записи правильним, але воно, принаймні, зможе перевірити, що ця адреса дійсно є допустимим адресою в сегменті даних або стека користувацького процесу, а не відноситься до сегменту тексту і не є якимось випадковим недійсним адресою. Хоча такі перевірки справності є грубими, це краще, ніж нічого. У монолітних системах ніщо не перешкоджає драйверу виконувати запис за адресами, за якими не можна писати не за яких умов, таким як адреси в сегменті тексту ядра. Відлов поганих покажчиків У програмах на мовах C і C + + використовується безліч покажчиків, і ці програми весь час схильні помилок, пов'язаних з використанням поганих покажчиків. Разменованіе невірного покажчика часто призводить до виявлення апаратурою помилки сегментації. У нашій розробці сервер або драйвер, що намагаються разименовать поганий покажчик, примусово завершуються, і видається дамп пам'яті для майбутньої налагодження, точно так само, як і для інших користувальницьких процесів. Якщо поганий покажчик виявляється в частині операційної системи, що виконується в режимі користувача, то сервер реінкарнації негайно помічає наявність збійної ситуації і замінює примусово завершений процес його свіжою копією. Приборкання нескінченних циклів Коли драйвер впадає в нескінченний цикл, це створює загрозу споживання нескінченного часу ЦП. Планувальник зауважує наявність такої поведінки і поступово знижує пріоритет несправного процесу, поки він не стає непрацюючим процесом. Проте інші процеси можуть продовжувати нормально працювати. Після вичерпання зумовленого інтервалу часу сервер реінкарняціі помітить, що даний драйвер не відповідає на запити, примусово завершить і перезапустить його. На відміну від цього, коли в нескінченний цикл впадає ядерний драйвер, він споживає весь час ЦП і фактично завішують всю систему. Перевірка DMA Однією з речей, яку ми не можемо забезпечити, є запобігання заподіяння шкоди системі через невірного DMA (Direct Memory Access, прямий доступ до пам'яті). Для запобігання перезапису драйвером через DMA довільної частини реальної пам'яті потрібно апаратна захист. Проте ми можемо виявити деякі помилки DMA наступним чином. DMA зазвичай запускається шляхом запису адреси DMA в деякий порт вводу-виводу. Ми можемо надати бібліотечну процедуру, яка викликається для запису в деякий порт вводу-виводу з попередніми декодуванням (способом, що залежить від пристрою) записів у цей порт вводу-виводу з метою знаходження використовуваних адрес DMA і перевірки їх допустимості. У зловмисних драйверах така перевірка може обходитися, але в добропорядних драйверах цей спосіб дозволяє виловити хоча б деякі помилки при помірних накладних витратах. Залежно від апаратури ми можемо надійти ще краще. Якщо б на периферійної шині малося MMU (Memory Management Unit, пристрій управління пам'яттю) введення-виведення, ми могли б точно обмежити доступ до пам'яті для кожного драйвера [16]. Для систем з шиною PCI-X ми збираємося покласти на свій сервер шини PCI відповідальність за ініціалізацію таблиць MMU введення-виведення. Це частина нашої майбутньої роботи. 6. Аналіз надійності Для перевірки надійності системи ми вручну внесли деякі ретельно підібрані помилки в деякі з своїх серверів і драйверів, щоб побачити, що в результаті відбудеться. Як описувалося в розд. 3.3, наша система розробляється для виявлення та виправлення багатьох помилок, і саме це ми і спостерігали. Якщо з якої б то не було причини відбувався збій деякого компонента, це розпізнавалася сервером реінкарнації, який застосовував усі необхідні кошти для пожвавлення збійного компонента. Нижче це описується більш детально. Для розуміння роботи нашої системи потрібно розрізняти два класи помилок. Перший клас складають логічні помилки, що означають, що сервер або драйвер дотримується протоколу межмодульних взаємодій і нормально відповідає на запити, як якщо б він успішно виконав роботу, чого насправді не відбувається. Прикладом є драйвер принтера, який друкує безглузду інформацію, але виробляє нормальні повернення. Для будь-якої системи дуже важко, якщо не неможливо, відловлювати помилки такого роду. Логічні помилки перебувають за межами цього дослідження. Другий клас складається з протокольних помилок, за наявності яких порушуються правила, що визначають поведінку серверів і драйверів. Наприклад, в нашій системі від серверів і драйверів потрібно відповідати на періодичні запити стану, що надходять від сервера реінкарнації. Якщо вони не підкоряються цьому правилу, робиться коригуючий дію. Наша система розробляється для боротьби з протокольними помилками. Сервер реінкарнації Сервер реінкарнації - це центральний сервер, керуючий усіма серверами і драйверами операційної системи. Він дозволяє істотно підвищити надійність, забезпечуючи: 1. Негайне розпізнавання фатальних збоїв. 2. Періодичний моніторинг стану. Таким чином, він допомагає відловлювати два поширених виду збоїв: померлі або погано себе провідні системні процеси і негайно береться за вирішення найбільш гострої проблеми. Якщо системний процес завершується, то сервер реінкарнації безпосередньо оповіщається про це і перевіряє свої таблиці, щоб зрозуміти, чи слід перезапустити сервіс. Цей механізм, наприклад, забезпечує негайну заміну драйвера, примусово завершеного через використання поганого покажчика. Крім того, періодичний моніторинг стану допомагає дисциплінувати погано себе провідні системні сервіси. Наприклад, драйвер, який впадає в нескінченний цикл і не може відповісти на запит стану від сервера реінкарнації, буде примусово завершений і перезапущений. Заміна драйвера пристрою складається з суворо контрольованій послідовності дій. По-перше, сервер реінкарнації породжує новий процес, виконання якого затримується, оскільки для нього ще не призначено привілеї. Потім сервер реінкарнації повідомляє про новий драйвері файлової системи і, нарешті, призначає необхідні привілеї. Коли всі ці кроки успішно виконуються, новий процес починає працювати і виконує код драйвера, що береться з файлової системи. В якості додаткової обережності двійковий код деяких драйверів може дублюватися в основній пам'яті, щоб, наприклад, драйвер для диска кореневої файлової системи можна було завантажити без потреби в обміні з диском. Надійність рівня додатків Наявність збійного драйвера може приводити до наслідків для файлової системи і додатків, що виробляють введення-виведення. Якщо у файлової системи був невиконаний запит вводу-виводу, їй буде повернуто код помилки, що говорить про збій драйвера. У цей момент можуть бути зроблені різні дії. Необхідно проводити відмінність між блоковими і символьними пристроями, тому що введення-виведення для блокових пристроїв буферізуется в буферному кеші файлової системи. На рис. 3 наводиться огляд різних сценаріїв відновлення на рівні програми. При фатальному збої блокового драйвера можливо повне відновлення без втрати даних, прозоре для програми. Коли розпізнається збій, сервер реінкарнації запускає нову копію драйвера і скидає кеш файлової системи для синхронізації. Таким чином, буферний кеш не тільки підвищує продуктивність, але також є важливим і для надійності. Прозоре відновлення іноді є можливим і при збоях драйверів символьних пристроїв. Оскільки запит вводу-виводу не буферізуется в кеші блоків файлової системи, інформація про помилку вводу-виводу повинна бути доведена до програми. Якщо програма не може призвести відновлення, про проблему буде сповіщений користувач. Фактично, збої драйверів проштовхуються нагору, що призводить до різних сценаріїв відновлення. Наприклад, якщо відбувається збій драйвера Ethernet, то мережевий сервер помітить відсутність пакетів і зробить прозоре відновлення, якщо додаток використовує надійний транспортний протокол, такий як TCP. З іншого боку, якщо відбувається збій драйвера принтера, то користувач, звичайно, помітить, що його виведення на друк не вдався і повторить команду друку. Таким чином, у багатьох випадках наша система може забезпечити повне відновлення на прикладному рівні. В решті випадках інформація про збої введення-виведення доводиться до користувача. Можна було б пом'якшити цю незручність шляхом використання тіньового драйвера для відновлення додатків, який використовували зіпсований драйвер в момент його фатального збою, застосовуючи методи, продемонстровані в [25]. Нам не дає зробити це брак робочої сіли. Результати перевірки надійності Для перевірки надійності своєї системи ми вручну внесли збої в деякі з своїх драйверів, щоб протестувати деякі види помилок і подивитися на те, що вийде. У найпростішому випадку ми завершували драйвер з застосуванням сигналу SIGKILL. Більш серйозні тестові випадки змушували драйвери разименовивать погані покажчики або впадати в нескінченний цикл. У всіх випадках сервер реінкарнації розпізнавав проблему і заміняв несправний драйвер свіжої копією. З тестуванні надійності, ми витягли кілька уроків, важливих для розробки нашої системи. По-перше, оскільки сервер реінкарнації перезапускає несправні сервери і драйвери, потрібно, щоб у них не зберігалося стан, і вони могли б бути належним чином повторно ініціалізували при повторному запуску. Компоненти, що зберігають стан, такі як файлова система і сервер процесів, неможливо вилікувати таким чином, оскільки вони дуже багато втрачають при перезапуску. Наші можливості обмежені. Інше спостереження полягає в тому, що деякі драйвери були реалізовані таким чином, що ініціалізація відбувається тільки при першому виклику OPEN. Однак для прозорого відновлення після збою драйвера на рівні додатків не повинен турбуватися повторний виклик OPEN. Замість цього, виконання виклику READ або WRITE у відновленому драйвері має змусити драйвер призвести повторну ініціалізацію. Крім того, хоча ми визнаємо наявність залежностей між файловою системою і драйверами, наші тести виявили деякі інші взаємозалежності. Наприклад, наш інформаційний сервер, що видає на екран налагодження дампи при натисканні функціональних клавіш, втрачає своє відображення клавіш після перезапуску. В якості загального правила, залежності слід запобігати, і всі компоненти повинні бути підготовлені для боротьби з непередбачуваними збоями. Нарешті, щоб ще більше підвищити надійність, слід змінити і користувальницькі додатки. За історичними причинами в більшості програм передбачається, що будь-який збій драйвера є фатальним, і вони негайно здаються, хоча іноді можливо відновлення. Прикладом, в якому можливе відновлення на рівні програми, є печатка. Якщо демон лінійного принтера сповіщається про тимчасове збій драйвера, він може автоматично повторно видати команду друку без втручання користувача. Подальші експерименти з поновленням на рівні додатків є частиною нашої майбутньої роботи. 7. Вимірювання продуктивності Продуктивність є проблемою, супутньої мінімальним ядер протягом десятиліть. Тому негайно постає питання: у що обходяться що обговорювалися вище зміни? Щоб розібратися в цьому, ми створили прототип, що складається з невеликого ядра і підтримуваного їм набору драйверів пристроїв і серверів, що працюють в режимі користувача. В якості основи прототипу ми почали з використання системи MINIX 2 з-за її невеликого розміру і довгої історії. Код системи вивчався багатьма десятками тисяч студентів в сотнях університетів протягом 18 років, і в останні 10 років майже не надходили повідомлення про помилки, що мають відношення до ядра; мабуть, відсутність помилок пов'язано з малими розмірами ядра. Потім ми значно змінили код, видаливши з ядра драйвери пристроїв і додавши засоби підвищення надійності, що обговорювалися в розд. 3. Таким чином, ми отримали практично нову систему MINIX 3 без потреби у написанні великого обсягу коду, не істотного для даного проекту, такого як драйвери і файлова система. Оскільки нас цікавить вартість змін, що обговорювалися в даній статті, ми порівнюємо свою систему з базовою системою, в якій драйвери пристроїв є частиною ядра, шляхом запуску одних і тих же тестів на обох системах. Це набагато більш чистий перевірка, ніж порівняння нашої системи з Linux або Windows, яке нагадувало б порівняння яблук з ананасами. Таким порівнянь часто заважають відмінності в якості компіляторів, в стратегіях управління пам'яттю, у файлових системах, в обсязі виконаної оптимізації, в зрілості систем і в багатьох інших факторах, які можуть повністю затінити все інше. Тестовою системою був 2.2 GHz Athlon (більш точно, AMD64 3200) з 1 Гб основної пам'яті і 40 гігабайтним диском IDE. Жоден з драйверів не був оптимізований для роботи в режимі користувача. Наприклад, ми очікуємо, що на Pentium зможемо забезпечити захищеним чином прямий доступ драйверів пристроїв до необхідних їм портів введення-виведення, усуваючи, таким чином, багато викликів ядра. Однак для підтримки переносимості інтерфейс не буде змінюватися. Крім того, в даний час в драйверах використовується програмований введення-виведення, що набагато повільніше використання DMA. Після реалізації цих оптимізацій ми очікуємо істотного підвищення ефективності. Тим не менше, навіть при використанні існуючої системи погіршення продуктивності виявилося цілком розумним. Результати тестування системних викликів Перший пакет тестів містив тести чистих POSIX-сумісних системних викликів. Користувацька програма повинна була зафіксувати реальний час у тактах системних годин (на частоті 60 Гц), потім мільйони раз зробити системний виклик, після чого знову зафіксувати реальний час. Час обробки системного виклику обчислювалося як різниця між кінцевим і початковим часом, поділена на число викликів, за вирахуванням накладних витрат на організацію циклу, які вимірювалися окремо. Число ітерацій циклу було різним для кожного тесту, оскільки тестування 100 мільйонів разів виклику getpid було розумним, але читання 100 мільйонів разів з 64-магабайтного файлу зайняв би надто багато часу. Всі тести виконувалися на незавантажених системі. Для цих тестів частоти успішних звернень до кешу ЦП і кешу файлового сервера імовірно становили 100%. Коротко проаналізуємо результати цих тестів. Виконання системного виклику getpid зайняло 0.831 мсек при використанні ядерних драйверів і 1.011 мсек при використанні драйверів, що працюють в режимі користувача. При виконанні цього виклику від користувацького процесу менеджеру пам'яті надсилається одиночне повідомлення, на яке негайно виходить відповідь. При використанні драйверів, які виконуються в режимі користувача, виклик виконується повільніше з-за наявності перевірки прав процесів на посилку таких повідомлень. При виконанні такого простого виклику істотне уповільнення викликають навіть кілька додаткових рядків коду. Хоча у відсотках різниця становить 22%, на кожен виклик витрачається лише 180 додаткових наносекунд, так що навіть при частоті 10,000 звернень в секунду втрати складають всього 2.2 мсек в секунду, набагато менше 1%. При виконанні виклику lseek проводиться набагато велика робота, і тому відносні накладні витрати знижуються до 11%. При виконанні відкриття та закриття файлу цей показник становить лише 9%. Читання і запис 64-кілобайтний ділянок даних займає менше 90 мсек, і падіння продуктивності складає 8%. При використанні драйверів, що виконуються в режимі користувача, створення файлу, запис в нього 1 кілобайт даних і видалення даних займають 13.465 мсек. Через використання буферного кешу файлового сервера в жодному з цих тестів не викликалися драйвери, і тому ми можемо укласти, що інші зміни, не пов'язані з драйверами, сповільнюють систему приблизно на 12%. Результати тестування дискового введення-виведення У другому пакеті тестів ми читали з файлу і писали в файл порції від 1 кілобайт до 64 мегабайт. Тести пропускалися багато разів, так що читається файл розміщувався у 12-мегабайтним кеші файлового сервера, крім випадку 64-мегабайтним обмінів, коли обсягу кешу не вистачало. Використання внутрішнього кеша дискового контролера не блокувалося. Як ми бачимо, різниця в продуктивності становить від 3% до 18%, у середньому - 8.4%. Однак зауважимо, що найгірший показник продуктивності отримано для 1-кілобайтний записів, але абсолютна часом зросла всього на 457 наносекунд. Це співвідношення зменшується при збільшенні обсягу введення-виведення, оскільки скорочуються відносні накладні витрати. У трьох 64-магабайтних тестах, результати яких показані на рис. 6 і 7, це співвідношення становить всього від 3% до 5%. В іншому тесті проводиться читання з безпосереднього блокового пристрою, відповідного жорсткого диска. Запис на безпосереднє пристрій зруйнувала б його вміст, тому такий тест не виконувався. При виконанні цих тестів не використовується буферний кеш файлової системи, і перевіряється тільки переміщення бітів з диска. Як ми бачимо, в цьому випадку середній показник накладних витрат становить лише 9%. Результати тестування додатків Наступний набір тестів складався з реальних програм, а не простих вимірів часу виконання системних викликів. Результати наведено на рис. 8. Перший тест полягав у побудові області початкового завантаження (boot image) у циклі, що містить виклик system («make image»); тим самим, побудова вироблялося багато разів. При кожному побудові компілятор мови C викликався 123 рази, асемблер - 4 рази і компонувальник - 11 разів. Побудова ядра, драйверів, серверів і програми init, а також збірка області початкового завантаження зайняли 3.878 секунд. Середній час компіляції становило 32 мсек на файл. Другий тест містив цикл, у якому компілювати тести відповідності стандарту POSIX. Набір з 42 тестових програм компілюватися за 1,577 секунди, або приблизно за 37 мсек на файл тесту. Тести з третього по сьомий складалися в сортуванні до 64-мегабайтной файлу та застосування до нього sed, grep, prep і uuencode відповідно. У цих тестах у різних обсягах змішувалися обчислення і обміни з диском. Кожен тест пропускався лише по одному разу, так що кеш файлової системи практично не використовувався, кожен блок брався з диска. Середнє падіння продуктивності склало в цих випадках 6%. Якщо взяти середнє значення для останнього стовпця показників 1922 тестів, відображених на рис. 6-8, ми отримаємо 1.08. Іншими словами, версія з драйверами, що виконуються в режимі користувача, виявилася приблизно на 8% повільніше версії з ядерними драйверами для операцій, які залучають обміни з дисками. Мережева продуктивність Ми тестували також і мережеву продуктивність системи з драйверами, що виконуються в режимі користувача. Тестування проводилося з використанням карти Intel Pro/100, оскільки у нас не було драйвера для карти Intel Pro/1000. Ми змогли управляти Ethernet на повній швидкості. Крім того, ми запускали тести поворотної петлі з відправником та одержувачем, що знаходяться на одній машині, і спостерігали пропускну здатність в 1.7 Гб / сек. Оскільки це еквівалентно використанню мережевого з'єднання для посилки на швидкості 1.7 Гб / сек і одночасного прийому на тій же швидкості, ми впевнені, що управління гігабітної апаратурою Ethernet з єдиним односпрямованим потоком на швидкості в 1 Гб / с не повинна створити проблему при використанні драйвера, що виконується в режимі користувача. Розмір коду Швидкість - це не єдиний показник, який представляє інтерес; дуже важливим є і кількість помилок. На жаль, ми не можемо безпосередньо перерахувати всі помилки, але розумним замінником числа помилок, ймовірно, є число рядків коду. Нагадаємо: чим більше код, тим більше помилок. Підрахувати кількість рядків коду не так просто, як може здатися на перший погляд. По-перше, порожні рядки і коментарі не додають в код складності, і тому ми їх не враховуємо. По-друге, # define й інші визначення у файлах заголовків також не додають у код складності, і тому файли заголовків теж не враховуються. Підрахунок числа рядків виконувався з використанням Perl-скрипта sclc.pl, доступного в Internet. Результати для ядра, чотирьох серверів (файлової системи, сервери процесів, сервера реінкарнації, інформаційного сервера), п'яти драйверів (жорсткого диска, флоппі-диска, RAM-диска, терміналу, пристрої журналізацію) і програми init показані на рис. 9. На малюнку можна бачити, що ядро складається з 2947 рядків на мові C і 778 рядків на мові асемблера (для програмування низькорівневих функціональних можливостей, таких як перехоплення переривань і збереження регістрів ЦП при перемиканні процесів). Всього є 3725 рядків коду. І тільки цей код виконується в режимі ядра. Іншим способом вимірювання розміру коду для C-програм є підрахунок числа точок з комою, оскільки багато операторів мови C завершуються крапкою з комою. У коді ядра є 1729 точок з комою. Нарешті, розмір скомпільованій ядра складає 21,312 байт. Це число задає тільки розмір коду (тобто сегмента тексту). Початкові дані (3800 байт) і стек в це число не входять. Цікаво, що статистика розмірів коду, показана на рис. 9, представляє мінімальну, але функціонуючу операційну систему. Загальний розмір ядерної частини і частини, що працює в режимі користувача, складає всього 18,000 рядків коду, незвичайно мало для POSIX-сумісної операційної системи. 8. Споріднені дослідження Ми є не першими дослідниками, що намагаються запобігти відмови систем з вини драйверів пристроїв, що містять помилки. І ми не перші намагаємося застосувати мінімальне ядро в якості можливого рішення. Ми навіть не є першими серед тих, що реалізовував драйвери, що працюють в режимі користувача. Тим не менш, ми вважаємо, що ми першими побудували повністю POSIX-сумісну операційну систему з відмінними властивостями ізоляції збоїв поверх мінімального ядра з 3800 рядків; в цій системі кожен драйвер виконується в режимі користувача в окремому процесі, а вся ОС виконується у вигляді декількох призначених для користувача процесів. У цьому розділі ми обговоримо проекти інших дослідницьких груп, які почасти схожі на те, що робимо ми. Ізоляція драйверів в програмному забезпеченні Одним з найважливіших дослідницьких проектів, у якому робиться спроба побудувати надійну систему в присутності ненадійних драйверів пристроїв, є Nooks [26]. Метою Nooks є підвищення надійності існуючих операційних систем. Словами авторів, «ми націлюємо існуючі розширення на масові операційні системи, а не пропонуємо нову архітектуру розширень. Ми хочемо, щоб сьогоднішні розширення виконувалися на сьогоднішніх платформах, по можливості, без їх зміни.» Ідея полягає у зворотній сумісності з існуючими системами, але невеликі зміни дозволяються. Підхід Nooks полягає в тому, щоб залишити драйвери пристроїв у ядрі, але укласти їх у свого роду полегшену захисну оболонку, щоб помилки драйвера не могли поширюватися на інші частини операційної системи. Nooks працює шляхом вставки прозорого рівня підвищення надійності між обертається драйверів пристрою й, що залишився частиною операційної системи. Весь трафік управління і даних між драйвером і залишилася частиною ядра перевіряється рівнем підвищення надійності. При запуску драйвера рівень підвищення надійності модифікує таблицю сторінок ядра таким чином, щоб заборонити доступ по запису до сторінок, які не є частиною драйвера, запобігаючи, тим самим, їхню безпосередню модифікацію. Для підтримки законного доступу по запису в структури даних ядра Nooks копіює необхідні дані в драйвер, а після модифікації переписує їх назад. Наша мета повністю відрізняється від мети Nooks. Ми не намагаємося зробити більш надійними успадковані системи. Будучи дослідниками, ми задаємо питання: як слід розробляти майбутні операційні системи, щоб із самого початку запобігти виникненню цієї проблеми? Ми вважаємо, що правильна розробка майбутніх систем полягає в побудові мультисерверного операційної системи та виконання ненадійного коду в незалежних процесах в режимі користувача, що зробить цей код набагато менш шкідливим (як обговорювалося в розд. 3). Незважаючи на різні цілі, є й технічні аспекти, у відношення яких системи можна порівнювати. Розглянемо лише кілька прикладів. Nooks не може впоратися зі складними помилками, такими як ненавмисне зміна в драйвері таблиці сторінок; в нашій системі у драйверів відсутній доступ до таблиці сторінок. Nooks не може впоратися з нескінченними циклами; ми можемо, оскільки, коли драйвер не відповідає правильним чином серверу реінкарнації, він примусово завершується і перезапускається. Хоча на практиці Nooks може в більшості випадків впоратися з неприпустимими записами в структури даних ядра, в нашій розробці такі записи не допускаються структурно. Nooks не може впоратися з драйвером принтера, який випадково намагається зробити запис в порти введення-виведення, керуючі диском; ми відловлюємо 100% таких спроб. Заслуговує на увагу й розмір коду. Nooks включає 22,000 рядків коду, майже в шість разів більше розміру всього нашого ядра і більше мінімальної конфігурації всієї нашої операційної системи. Важко відійти від цієї аксіоми: у більшому за розміром коді міститься більше помилок. Тому статистично Nooks, ймовірно, міститься в п'ять разів більше помилок, ніж у всьому нашому ядрі. Ізоляція драйверів з використанням віртуальних машин В іншому проекті з інкапсуляції драйверів це робиться з використанням поняття віртуальної машини для їх ізоляції від інших частин системи [19, 18]. Коли драйвер викликається, він запускається на другий віртуальній машині, не в тій, в якій працює основна система, так що його збій не псує основну систему. Подібно Nooks, цей підхід повністю фокусується на виконанні успадкованих драйверів для успадкованих операційних систем. Автори не стверджують, що для нових розробок хорошим підходом є включення ненадійного коду в ядро з подальшою захистом кожного драйвера шляхом його виконання на окремій віртуальній машині. Хоча цей підхід дозволяє досягти намічених цілей, з ним пов'язані деякі проблеми. По-перше, є питання, пов'язані з тим, наскільки можуть довіряти один одному основна система та віртуальна машина, на якій виконується драйвер. По-друге, запуск драйвера на віртуальній машині породжує проблеми з тимчасовими співвідношеннями і блокуваннями, оскільки всі віртуальні машини працюють у режимі поділу часу, і ядерний драйвер, що розроблявся в розрахунку на виконання без переривань, може бути непередбачуваним чином квантованих в часі з непередбачуваними наслідками. По-третє, може знадобитися спільне використання кількома віртуальними машинами деяких ресурсів, таких як конфігураційне простір шини PCI. По-четверте, механізм віртуальної машини споживає додаткові ресурси, хоча відповідні витрати сумірні з витратами нашої схеми: від 3% до 8%. Хоча для цих проблем пропонуються рішення, підхід у кращому випадку є громіздким і в основному підходить для захисту успадкованих драйверів в успадкованих операційних системах, а не для використання в нових розробках, яким присвячено наше дослідження. Засоби безпеки, засновані на мовах У попередній роботі один з авторів також торкався проблему безпечного виконання зовнішнього коду всередині ядра. У проекті Open Kernel Environment (OKE) забезпечується безпечна, що контролює ресурси середовище, що дозволяє завантажити в ядро операційної системи Linux повністю оптимізований власний код [4]. Код компілюється з використанням спеціального компілятора Cyclone, який додає до об'єктному коду інструментарій у відповідності з політикою, яка визначається привілеями користувача. Cyclone, подібно Java, є мовою з типовою безпекою, в якому більша частина помилок, пов'язаних з покажчиками, запобігається мовними засобами. Явне довірче управління (trust management) і контроль авторизації забезпечують адміністраторам можливість здійснювати суворий контроль над наданням зовнішнім модулям привілеїв, і цей контроль автоматично приводиться у виконання в коді цих модулів. Крім забезпечення авторизації, компілятор грає центральну роль в перевірці того, що код відповідає встановленої політиці. Для цього використовуються як статичні перевірки, так і динамічний інструментарій. OKE дозволяє зовнішнім модулям інтенсивно взаємодіяти з іншими частинами ядра, наприклад, шляхом спільного використання пам'яті ядра. Робоча середовище забезпечує ключові засоби безпеки. Зокрема, для даних завжди проводиться прибирання сміття, і не може відбутися звернення за вказівником до вільної пам'яті. Більш того, OKE може забезпечувати контроль над усіма ресурсами зовнішніх модулів ядра: час ЦП, купа, стек, точки входу і т.д. Середа OKE розроблялася в розрахунку на написання драйверів і розширень ядра. Проте, оскільки для забезпечення безпечного програмування в ядрі Linux потрібні процедури суворого контролю доступу і складні засоби, середу досить важко використовувати. Як відзначають автори, основна причина полягає в тому, що організація Linux просто не призначена для забезпечення можливості безпечних розширень. Віртуальні машини і екзоядра Класичні віртуальні машини [24] представляють собою потужний засіб для одночасного виконання кількох операційних систем. Екзоядра [10] схожі на віртуальні машини, але в них ресурси швидше розділяються, а не реплікуються, що призводить до більшої ефективності. Проте жоден з цих підходів не вирішує проблему, поставлену в розд. 1.3: як запобігти відмови операційних систем з вини драйверів пристроїв, що містять помилки? Драйвери, що виконуються в режимі користувача в монолітному ядрі Раннім проектом, в якому застосовувалися драйвери, що виконуються в режимі користувача, був Mach 3.0 [11]. Система складалася з мікроядра Mach, поверх якого запускалася ОС Berkeley UNIX у вигляді користувацького процесу, і драйвери пристроїв також виконувалися в призначених для користувача процесах. На жаль, у разі фатального збою драйвера Berkeley UNIX доводилося перезапускати, так що від ізоляції драйверів було мало користі. Планувалася мультисерверного система, яка повинна була виконуватися над Mach, але вона так і не була повністю реалізована. В аналогічному проекті в університеті New South Wales реалізовувалися драйвери Linux для жорсткого диска і гігабайтної апаратури Ethernet, що виконуються в режимі користувача [8]. Для блоків розміром менше 32 Кб продуктивність ядерного драйвера була значно вище, але на блоках більшого розміру вирівнювався. Під час тестування Ethernet виявилося так багато аномалій, ймовірно, пов'язаних з управлінням буферами, але не можна було зробити які-небудь висновки. Розробки мінімальних ядер Хоча витяг драйверів з ядра є великим кроком вперед, ще краще витягти з ядра операційну систему. Саме тут починають застосовуватися мінімальні ядра з надзвичайних скороченням числа реалізованих у них абстракцій. Ймовірно, першим мінімальним ядром була система RC4000 Брінка Хансена (Brinch Hansen), що датується початком 1970-х рр. [13]. З середини 1980-х рр. був написаний ряд мінімальних ядер, включаючи Amoeba [21], Chorus [5], Mach [1] і V [6]. Проте ні в одному з них не застосовувалося безпечне програмне забезпечення: у всіх було не ізольовані драйвери всередині ядра. QNX є комерційною UNIX-подібної системою реального часу з закритими кодами [17]. Хоча у неї є мінімальне ядро, зване Neutrino, з приводу системи опубліковано мало статей, і точні деталі нам невідомі. Проте на основі останніх проспектів ми робимо висновок, що Neutrino є гібридним ядром, оскільки менеджер процесів працює в адресному просторі ядра. На початку 1990 рр. покійний Йохан Лідтке (Jochen Liedtke) написав мінімальне ядро L4 мовою асемблера для архітектури x86. Швидко стало зрозуміло, що воно не є стерпним, і його важко підтримувати, і тому він переписав ядро на мові C [20]. Після цього воно продовжувало розвиватися. В даний час є дві основні гілки: L4/Fiasco, підтримуване в технічному університеті Дрездена, і L4Ka: Pistachio, підтримуване в університеті Карлсруе та університеті New South Wales. Вони написані на C + +. Ключовими ідеями в L4 є адресні простору, нитки і IPC між нитками в різних адресних просторах. Менеджер ресурсів, що виконується в режимі користувача та запускається при завантаженні системи, управляє системними ресурсами і розподіляє їх між користувацькими процесами. L4 - це одне з небагатьох дійсно мінімальних ядер з драйверами пристроїв, що працюють у режимі користувача. Проте відсутня реалізація, в якій кожен драйвер виконувався б в окремому адресному просторі, і API L4 зовсім відрізняється від нашого API, тому ми не можемо запустити на ньому будь-які тести. Однак виявилося неважко запустити скрипт підрахунку числа рядків над поточною версією ядра L4Ka: Pistachio. Результати показані на рис. 10, і їх можна порівняти з даними на рядку «Kernal». Розмір початкового коду майже у два рази перевищує розмір нашого ядра, а бінарний код у шість разів більше, проте функціональні можливості L4Ka: Pistachio є зовсім іншими, так що важко сказати що-небудь ще, крім того, що це ядро значно більше за розміром. Односерверні операційні системи Одним зі способів використання мінімальних ядер є забезпечення платформи, поверх якої, як єдиний сервер, запускається вся операційна система, можливо, в режимі користувача. Для отримання системних сервісів для користувача програми запитують їх у процесу операційної системи. Властивості такої архітектури аналогічні властивостям монолітних систем, що обговорювалося в розд. 2.1. Помилка в драйвері як і раніше може зламати всю операційну систему, а в результаті і прикладні програми. Тому, з точки зору ізоляції збоїв, виконання всієї операційної системи в одному користувача процесі нітрохи не краще її виконання в режимі ядра. Єдиним реальним перевагою є те, що перезавантаження після фатального збою сервера операційної системи, виконується в режимі користувача, і всіх додатків відбувається швидше, ніж перезавантаження комп'ютера. Одним із прикладів цієї технології є ОС Berkeley UNIX поверх Mach (перейменована в Darwin компанією Apple), яка є основою системи Apple Mac OS X [28]. Однак у цій системі UNIX виконується в ядрі, що робить його просто інакше структурованим монолітним ядром. Другий приклад - ОС MkLinux, в якій Linux виконується в єдиному користувача процесі поверх Mach. Третій приклад - L4-Linux, в якій повний варіант Linux виконується поверх L4 [15]. В останній з перерахованих систем користувальницькі процеси отримують сервіси операційної системи шляхом виклику віддалених процедур у сервері Linux з використанням механізму IPC L4. Вимірювання показують падіння продуктивності в порівнянні із звичайною ОС Linux на 5-10%, що дуже близько до нашими спостереженнями. Однак єдина рядок з помилковим кодом в драйвері Linux може призвести до збою фатального всієї операційної системи, так що єдиним перевагою цієї архітектури з точки зору надійності є більш швидке завантаження. Мультисерверного операційні системи Більш складний підхід полягає в розщепленні операційної системи на частини і виконання кожної частини у власній області захисту. Одним з таких проектів був SawMill Linux [12]. Проте в 2001 р. проект був несподівано зупинений після того, як багато хто з його основних учасників пішли з IBM. Іншим мультисерверного проектом є DROPS, в якому ОС також будується поверх мінімального ядра L4/Fiasco [14]. Цей проект орієнтована на мультимедійні додатки. Однак більшість драйверів пристроїв виконується у складі великого серверного процесу L4-Linux, і тільки мультимедійні підсистеми виконуються окремо. Після деякої настройки програш в продуктивності знизився до 2-4%. Ще однією мультисерверного операційною системою з драйверами, що виконуються в режимі користувача, є Nemesis [23]. У цій системі є єдиний адресний простір, розділяється всіма процесами, але використовується апаратна захист між процесами. Подібно DROPS ця система була орієнтована на мультимедійні додатки, але не була POSIX-сумісною і навіть UNIX-подібної. Висновок Основне досягнення роботи, описаної в цій статті, полягає в тому, що ми побудували POSIX-сумісну операційну систему, засновану на мінімальному ядрі, вихідні тексти якого складають менше 3800 рядків. Тільки цей код виконується в режимі ядра. Наскільки нам відомо, наше мінімальне ядро є найменшим серед усіх існуючих ядер, які підтримують повністю POSIX-сумісну мультисерверного операційну систему, яка функціонує у режимі користувача. Унікальність нашої системи полягає також у тому, що в ній кожен драйвер пристрою виконується в окремому користувача процесі, і є можливість реінкарнації бездіяльних або невірно функціонуючих драйверів на льоту, без перезавантаження операційної системи. Ми не стверджуємо, що можемо відловити будь-яку помилку, але ми істотно підвищили надійність операційної системи шляхом структурного усунення багатьох різних класів помилок. Для досягнення максимальної надійності у своїй розробці ми керувалися принципами простоти, модульності, найменшою авторизації і відмовостійкості. У розуміється і мінімальному ядрі міститься менша кількість помилок, і воно в меншій мірі піддається фатальним збоїв. Наприклад, у нашому коді ядра неможливі переповнення буферів, оскільки всі структури даних у ньому оголошуються статично, а не з використанням динамічного розподілу пам'яті. Крім того, шляхом переміщення більшої частини коду (і більшої частини помилок) у непривілейованих користувальницькі процеси і обмеження можливостей кожного з них ми домоглися належної ізоляції збоїв і обмежили масштаб відповідного потенційного збитку. Більш того, більшість серверів і всі драйвери в операційній системі піддаються моніторингу і автоматично відновлюються при виявленні проблеми. За це скорочення числа фатальних збоїв операційної системи ми платимо зниженням продуктивності на 5-10%. Ми вважаємо цю ціну цілком обгрунтованою. Звичайно, драйвери, файлові системи та інші компоненти не стають в нашій розробці магічним чином безпомилковим. Однак за наявності стабільного мінімального ядра сценарій найгіршого випадку змінюється від потреби в перезавантаженні комп'ютера до потреби в перезапуску операційної системи в режимі користувача. Принаймні, це відновлення відбувається набагато швидше. У кращому випадку, якщо, скажімо, в драйвері принтера виникає аварійний відмова з причини записи по невірному вказівником, сервер реінкарнації автоматично запускає свіжу копію цього драйвера. Потрібно заново виконати поточне завдання на друк, але все це ніяк не вплине на інші програми, які виконувалися до моменту фатального збою драйвера. Ситуація з блоковими пристроями справи ще краще. Якщо виявляється збій дискового драйвера, то система може зробити повне відновлення шляхом прозорої заміни драйвера і перезапису блоків з буферного кешу файлової системи. На завершення статті зазначимо, що ми показали, як можна підвищити надійність операційної системи з використанням елегантного, полегшеного підходу. Наша система в цей час є стійкою до більшості видів невірної роботи, що викликається помилками. Проте є нові проблеми, пов'язані із зловмисними серверами і драйверами. Ми продовжуємо дослідницьку роботу в цій області.
Страницы: 1, 2
|
|