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) і синхронізації, що істотно полегшує життя програмістам.
По-друге, ми розділили свою систему на набір невеликих незалежних модулів. Використання властивостей модульності, таких як обмеження розповсюдження збоїв, є ключовим елементом розробки нашої системи. Шляхом повного поділу операційної системи на модулі ми можемо встановити «брандмаери», крізь які не можуть розповсюджуватися помилки, що призводить до більш надійної системи. Для запобігання непрямого впливу збоїв в одному модулі на який-небудь інший модуль ми структурним чином зменшуємо їх взаємозалежність, наскільки це можливо. У тих випадках, коли це неможливо через природи модулів, ми застосовуємо додаткові засоби підтримки безпеки. Наприклад, файлова система залежить від драйверів пристроїв, але вона розробляється таким чином, щоб бути готовою до обробки збоїв драйвера.
По-третє, ми забезпечуємо дотримання принципу найменшої авторизації. Хоча ізоляція збоїв допомагає стримувати їх поширення, збій у повноважному модулі все ще може викликати значний збиток. Тому ми знижуємо рівень привілеїв всіх користувальницьких процесів до гранично припустимого мінімуму. У ядрі підтримуються бітові масиви і списки, які визначають можливості процесів. Зокрема, є шкала допустимих викликів ядра і список допустимих адрес призначення повідомлень. Ця інформація зберігається в елементах таблиці процесів, і тому її можна строго контролювати, і нею просто керувати. Інформація про авторизацію ініціюється під час завантаження системи, головним чином, на основі конфігураційних таблиць, створюваних системним адміністратором.
По-четверте, при розробці системи ми явним чином враховуємо можливість до стійкості до деяких збоїв. Всі сервери та драйвери управляються і відслідковуються спеціальним сервером, званим сервером реінкарнації, який може справлятися з двома видами проблем. Якщо системний процес завершується непередбачуваним чином, це негайно розпізнається, і процес перезапускається. Крім того, періодично перевіряється стан кожного системного процесу для перевірки його правильного функціонування. Якщо процес функціонує неправильно, він примусово завершується і перезапускається. Так працює механізм відмовостійкості: зіпсований компонент замінюється, але система весь час продовжує працювати.
0 комментариев