Основні функції і компоненти ядра ОС UNIX

[ виправити ] текст може містити помилки, будь ласка перевіряйте перш ніж використовувати.

скачати

У цій частині курсу ми більш докладно зупинимося на базових функціях ядра ОС UNIX. Основна мета цієї частини - ввести слухача курсу (і читача цього документа) в основні ідеї ядра ОС UNIX, тобто показати, чим керувалися розробники системи при виборі базових проектних рішень. При цьому ми не прагнемо викладати технічні деталі організації ядра, оскільки (як це зазвичай буває при спробах спільного ідейно-технічного викладу) ми втратили б явне розходження між принциповими і технічними рішеннями.

Можливо, вибір тем цієї частини досить суб'єктивний. Не виключено, що хтось інший звернув би більшу увагу на інші питання, пов'язані з функціями ядра операційної системи. Проте підкреслимо, що ми дотримуємося класичному уявленню про функції ядра ОС, введеного ще професором Дейкстри. Відповідно з цим поданням, ядро ​​будь-якої ОС насамперед відповідає за управління основною пам'яттю комп'ютера і віртуальною пам'яттю виконуваних процесів, за керування процесором і планування розподілу процесорних ресурсів між спільно виконуваними процесами, за управління зовнішніми пристроями і, нарешті, за забезпечення базових засобів синхронізації та взаємодії процесів. Саме ці питання ми розглянемо в даній частині курсу стосовно до ОС UNIX (іноді до UNIX взагалі, а іноді до UNIX System V).

Управління пам'яттю

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

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

Операційна система UNIX починала своє існування з застосування дуже простих методів управління пам'яттю (простий своппінг), але в сучасних варіантах системи для управління пам'яттю застосовується досить витончена техніка.

Віртуальна пам'ять

Ідея віртуальної пам'яті далеко не нова. Зараз багато хто вважає, що в основі цієї ідеї лежить необхідність забезпечення (за підтримки операційної системи) видимості практично необмеженої (32 - або 64-розрядної) адресується пам'яті користувача при наявності основної пам'яті суттєво менших розмірів. Звичайно, цей аспект дуже важливий. Але також важливо розуміти, що віртуальна пам'ять підтримувалась і на комп'ютерах з 16-розрядної адресацією, в яких обсяг основної пам'яті найчастіше істотно перевищував 64 Кбайта.

Згадайте хоча б 16-розрядний комп'ютер PDP-11/70, до якого можна було підключити до 2 Мбайт основної пам'яті. Іншим прикладом може служити видатна вітчизняна ЕОМ БЕСМ-6, в якій при 15-розрядної адресації 6-байтових (48-розрядних) машинних слів обсяг основної пам'яті був доведений до 256 Кбайт. Операційні системи цих комп'ютерів тим не менш підтримували віртуальну пам'ять, основним змістом якої було забезпечення захисту користувальницьких програм однієї від іншої і надання операційній системі можливості динамічно гнучко перерозподіляти основну пам'ять між одночасно підтримуваними користувацькими процесами.

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

У найбільш простому і найбільш часто використовуваному випадку сторінкової віртуальної пам'яті кожна віртуальна пам'ять (віртуальна пам'ять кожного процесу) і фізична основна пам'ять представляються складаються з наборів блоків або сторінок однакового розміру. Для зручності реалізації розмір сторінки завжди вибирається рівним числу, що є ступенем 2. Тоді, якщо загальна довжина ВА є N (в останні роки це теж завжди деяка ступінь 2 - 16, 32, 64), а розмір сторінки є 2M), то віртуальний адреса розглядається як структура, що складається з двох полів: перше поле займає ( N-M +1) розрядів адреси і задає номер сторінки віртуальної пам'яті, друге поле займає (M-1) розрядів і задає зміщення всередині сторінки до адресується елемента пам'яті (у більшості випадків - байти). Апаратна інтерпретація віртуальної адреси показана на малюнку 3.1.

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

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

Основні функції і компоненти ядра ОС UNIX

Рис. 3.1. Схема сторінкової організації віртуальної пам'яті

При сегментно-сторінкової організації віртуальної пам'яті відбувається дворівнева трансляція ВА у фізичний. У цьому випадку віртуальний адреса складається з трьох полів: номера сегмента віртуальної пам'яті, номера сторінки всередині сегмента і зміщення всередині сторінки. Відповідно, використовуються дві таблиці відображення - таблиця сегментів, що зв'язує номер сегмента з таблицею сторінок, і окрема таблиця сторінок для кожного сегмента (рисунок 3.2).

Основні функції і компоненти ядра ОС UNIX

Рис. 3.2. Схема сегментно-сторінкової організації віртуальної пам'яті

Сегментно-сторінкова організація віртуальної пам'яті дозволяла спільно використовувати одні й ті ж сегменти даних і програмного коду у віртуальній пам'яті різних завдань (для кожної віртуальної пам'яті існувала окрема таблиця сегментів, але для спільно використовуваних сегментів підтримувалися загальні таблиці сторінок).

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

Як же досягається можливість наявності віртуальної пам'яті з розміром, істотно перевищує розмір оперативної пам'яті? В елементі таблиці сторінок може бути встановлений спеціальний прапор (що означає відсутність сторінки), наявність якого змушує апаратуру замість нормального відображення ВА у фізичний припинити виконання команд і передати управління відповідного компоненту операційної системи. Англійський термін "demand paging" (перегортання на вимогу) досить точно характеризує функції, виконувані цим компонентом. Коли програма звертається до віртуальної сторінці, відсутньої в основній пам'яті, тобто "Вимагає" доступу до даних або програмного коду, операційна система задовольняє цю вимогу шляхом виділення сторінки основної пам'яті, переміщення в неї копії сторінки, що знаходиться у зовнішній пам'яті, та відповідної модифікації елемента таблиці сторінок. Після цього відбувається "повернення з переривання", і команда, на вимогу "якою виконувалися ці дії, продовжує своє виконання.

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

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

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

Найбільш поширеними традиційними алгоритмами (як в глобальному, так у локальному варіантах) є алгоритми FIFO (First In First Out) і LRU (Least Recently Used). При використанні алгоритму FIFO для заміщення вибирається сторінка, яка найдовше залишається приписаної до віртуальної пам'яті. Алгоритм LRU припускає, що заміщувати слід ту сторінку, до якої найдовше не відбувалися звернення. Хоча інтуїтивно здається, що критерій алгоритму LRU є більш правильним, відомі ситуації, в яких алгоритм FIFO працює краще (і, крім того, він набагато дешевше реалізується).

Зауважимо ще, що при використанні глобальних алгоритмів, незалежно від конкретного вживаного алгоритму, можливі й теоретично неминучі критичні ситуації, які називаються по-англійськи thrashing (незважаючи на безліч спроб, хорошого російського еквівалента так і не вдалося придумати). Розглянемо простий приклад. Нехай на комп'ютері в мультипрограмному режимі виконуються два процеси - П1 у віртуальній пам'яті ВП1 і П2 у віртуальній пам'яті ВП2, причому сумарний розмір ВП1 і ВП2 більше розмірів основної пам'яті. Припустимо, що в момент часу t1 в процесі П1 виникає вимога віртуальної сторінки ВС1. Операційна система обробляє відповідне переривання і вибирає для заміщення сторінку основної пам'яті С2, приписаних до віртуальної сторінці ВС2 віртуальної пам'яті ВП2 (тобто в елементі таблиці сторінок, відповідному ВС2, проставляється прапор відсутності сторінки). Для повної обробки вимоги доступу до ВС1 в загальному випадку буде потрібно два обміну із зовнішньою пам'яттю (перший, щоб записати поточний зміст С2, другий - щоб прочитати копію ВС1). Оскільки операційна система підтримує мультипрограмний режим роботи, то під час виконання обмінів доступ до процесора отримає процес П2, і він, цілком імовірно, може вимагати доступу до своєї віртуальної сторінці ВС2 (яку в нього щойно забрали). Знову буде оброблятися переривання, і ОС може замінити деяку сторінку основної пам'яті С3, яка приписана до віртуальній сторінці ВС3 в ВП1. Коли закінчаться обміни, пов'язані з обробкою вимоги доступу до ВС1, відновиться процес П1, і він, цілком імовірно, зажадає доступу до своєї віртуальної сторінці ВС3 (яку в нього тільки що відібрали). І так далі. Загальний ефект полягає в тому, що безперервно працює операційна система, виконуючи незліченні і безглузді обміни із зовнішньою пам'яттю, а для користувача процеси П1 і П2 практично не просуваються.

Зрозуміло, що при використанні локальних алгоритмів ситуація thrashing, яка зачіпає кілька процесів, неможлива. Однак у принципі можлива аналогічна ситуація всередині однієї віртуальної пам'яті: ОС може кожен раз заміщати ту сторінку, до якої процес звернеться в наступний момент часу.

Єдиним алгоритмом, теоретично гарантує відсутність thrashing, є так званий "оптимальний алгоритм Біледі" (на ім'я придумав його угорського математика). Алгоритм полягає в тому, що для заміщення слід вибирати сторінку, до якої в майбутньому найбільш довго не буде звернень. Зрозуміло, що в динамічному середовищі операційної системи точне знання майбутнього неможливо, і в цьому контексті алгоритм Біледі представляє тільки теоретичний інтерес (хоча він з успіхом застосовується практично, наприклад, в компіляторах для планування використання регістрів).

У 1968 році американський дослідник Пітер Деннінг сформулював принцип локальності посилань (званий принципом Деннінг) і висунув ідею алгоритму підкачки, заснованого на понятті робочого набору. У певному сенсі запропонований ним підхід є практично реалізується апроксимацією оптимального алгоритму Біледі. Принцип локальності посилань (недоведені, але підтверджується на практиці) полягає в тому, що якщо в період часу (Tt, T) програма зверталася до сторінок (С1, С2, ..., Сn), то при належному виборі t з великою ймовірністю ця програма буде звертатися до тих же сторінок в період часу (T, T + t). Іншими словами, принцип локальності стверджує, що якщо не надто далеко заглядати в майбутнє, то можна добре його прогнозувати виходячи з минулого. Набір сторінок (С1, С2, ..., Сn) називається робочим набором програми (або, правильніше, відповідного процесу) у момент часу T. Зрозуміло, що з плином часу робочий набір процесу може змінюватися (як за складом сторінок, так і за їх кількістю). Ідея алгоритму підкачки Деннінг (іноді званого алгоритмом робочих наборів) полягає в тому, що операційна система в кожний момент часу повинна забезпечувати наявність в основній пам'яті поточних робочих наборів всіх процесів, яким дозволено конкуренція за доступ до процесора. Ми не будемо вдаватися в технічні деталі алгоритму, а лише зазначимо таке. По-перше, повна реалізація алгоритму Деннінг практично гарантує відсутність thrashing. По-друге, алгоритм реалізуємо (відома, щонайменше, одна його повна реалізація, яка проте зажадала спеціальної апаратної підтримки). По-третє, повна реалізація алгоритму Деннінг викликає дуже великі накладні витрати.

Тому на практиці застосовуються полегшені варіанти алгоритмів підкачки, заснованих на ідеї робочого набору. Один з таких варіантів застосовується і в ОС UNIX (наскільки нам відомо, у всіх версіях системи, що відносяться до галузі System V). Ми коротко опишемо цей варіант в п. 3.1.3.

Апаратно-незалежний рівень управління пам'яттю

Матеріал, наведений у даному розділі, хоча і не відображає в повному обсязі всі проблеми і рішення, пов'язані з управлінням віртуальною пам'яттю, достатній для того, щоб усвідомити важливість і складність відповідних компонентів операційної системи. У будь-якій операційній системі управління віртуальною пам'яттю займає центральне місце. Колись Ігор Сілін (основний розробник відомої операційної системи Дубна для БЕСМ-6) висунув тезу, відомий в народі як "Теза Силіна": "Витрати, витрачені на управління віртуальною пам'яттю, окупаються". Я думаю, що будь-який фахівець в області операційних систем погодиться з істинністю цієї тези.

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

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

По-перше, віртуальна пам'ять кожного процесу представляється у вигляді набору сегментів (рисунок 3.3).

Основні функції і компоненти ядра ОС UNIX

Рис. 3.3. Сегментна структура віртуального адресного простору

Як видно з малюнка, віртуальна пам'ять процесу ОС UNIX розбивається на сегменти п'яти різних типів. Три типи сегментів обов'язкові для кожної віртуальної пам'яті, і сегменти цих типів присутні у віртуальній пам'яті в одному примірнику для кожного типу. Сегмент програмного коду містить тільки команди. Реально в нього поміщається відповідний сегмент виконуваного файлу, який зазначався в якості параметра системного виклику exec для даного процесу. Сегмент програмного коду не може модифікуватися в ході виконання процесу і тому можливе використання одного примірника коду для різних процесів.

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

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

Розділяється сегмент віртуальної пам'яті утворюється при підключенні до неї сегмента пам'яті, що розділяється (див. п. 3.4.1). За визначенням, такі сегменти призначені для координованого спільного використання декількома процесами. Тому розділяється сегмент повинен допускати доступ по читанню і по запису і може розподілятися кількома процесами.

Сегменти файлів, що відображаються у віртуальну пам'ять (див. п. 2.4.5), являють собою різновид поділюваних сегментів. Різниця полягає в тому, що якщо при необхідності звільнити оперативну пам'ять сторінки поділюваних сегментів копіюються ("відкачуються") у спеціальну системну область підкачки (swapping space) на диску, то сторінки сегментів файлів, що відображаються у віртуальну пам'ять, у разі необхідності відкачуються прямо на своє місце в області зовнішньої пам'яті, займаної файлом. Такі сегменти також допускають доступ і з читання, і за записом і є потенційно спільно використовуваними.

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

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

У будь-якому описувач фізичної сторінки зберігаються копії ознак обігу та модифікації сторінки, вироблюваних конкретної використовуваної апаратурою.

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

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

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

Сторінкове заміщення основної пам'яті і swapping

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

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

Відкачування сторінок, що не входять в робочі набори процесів, виробляє спеціальний системний процес-stealer. Він починає працювати, коли кількість сторінок у списку вільних сторінок досягає встановленого нижнього порогу. Функцією цього процесу є аналіз необхідності відкачування сторінки (на основі ознаки зміни) і запис копії сторінки (якщо це потрібно) у відповідну область зовнішньої пам'яті (тобто або в системну область підкачки - swapping space для анонімних сторінок, або в деякий блок файлової системи для сторінки, що входить в сегмент відображуваного файлу).

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

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

Зауважимо, що ми описали найбільш складний алгоритм, коли б то не було використовувався в ОС UNIX. В останній "фактично, стандартною" версії ОС UNIX (System V Release 4) використовується більш спрощений алгоритм. Це глобальний алгоритм, в якому ймовірність thrashing погашається за рахунок своппінга. Використовуваний алгоритм називається NRU (Not Recently Used) або clock. Сенс алгоритму полягає в тому, що процес-stealer періодично очищає ознаки звернення всіх сторінок основної пам'яті, що входять у віртуальну пам'ять процесів (звідси назва "clock"). Якщо виникає потреба в відкачці (тобто досягнуть нижня межа розміру списку описувачів вільних сторінок), то stealer вибирає в якості кандидатів на відкачування перш за все ті сторінки, до яких не було звернень по запису після останньої "очищення" і у яких немає ознаки модифікації (тобто ті, які можна дешевше звільнити). У другу чергу вибираються сторінки, які дійсно потрібно відкачувати. Паралельно з цим працює описаний вище алгоритм своппінга, тобто якщо виникає вимога сторінки, а вільних сторінок немає, то відповідний процес стає кандидатом на своппінг.

На закінчення торкнемося ще однієї важливої ​​теми, безпосередньо пов'язану з керуванням віртуальною пам'яттю - копіювання сторінок при спробі запису (copy on write). Як ми відзначали в п. 2.1.7, при виконанні системного виклику fork () ОС UNIX утворює процес-нащадок, що є повною копією свого предка. Тим не менш, у нащадка своя власна віртуальна пам'ять, і ті сегменти, які повинні бути його приватними сегментами, в принципі повинні були б повністю скопіювати. Однак, незважаючи на те, що приватні сегменти допускають доступ і з читання, і за записом, ОС не знає, чи буде предок або нащадок реально зробити запис у кожну сторінку таких сегментів. Тому було б нерозумно проводити повне копіювання приватних сегментів під час виконання системного виклику fork ().

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

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

Управління процесами і нитками

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

Звичайно, не менш важливо (а насправді, суттєво більш важливо) захищати саму операційну систему від можливості її пошкодження яким би то не було для користувача процесом. В ОС UNIX це досягається за рахунок того, що ядро ​​системи працює у власному "ядерне" віртуальному просторі, до якого не може мати доступу ні один призначений для користувача процес.

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

В останні роки у зв'язку з широким поширенням так званих симетричних мультипроцесорних архітектур (Symmetric Multiprocessor Architectures - SMP) в ОС UNIX був впроваджений механізм легковагих процесів (light-weight processes), або ниток, або потоків управління (threads). Говорячи по-простому, нитка - це процес, що виконується у віртуальній пам'яті, використовуваною спільно з іншими нитками того ж "великовагового" (тобто володіє окремої віртуальною пам'яттю) процесу. У принципі, легковагі процеси використовувалися в операційних системах багато років тому. Вже тоді стало ясно, що програмування з неконтрольованим використанням загальної пам'яті приносить більше клопоту і неприємностей, ніж користі, через необхідність використання явних примітивів синхронізації.

Однак, до теперішнього часу в практику програмістів так і не були впроваджені більш безпечні методи паралельного програмування, а реальні можливості мультипроцесорних архітектур для забезпечення розпаралелювання потрібно було якось використати. Тому знову в ужитку увійшли легковагі процеси, які тепер отримали назву threads (нитки). Найбільш важливо (з нашої точки зору) те, що для впровадження механізму ниток потрібна істотна переробка ядра. Різні виробники апаратури і програмного забезпечення прагнули якомога швидше виставити на ринок продукт, придатний для ефективного використання на SMP-платформах. Тому версії ОС UNIX знову дещо розійшлися.

Всі ці питання ми обговоримо більш детально в даному розділі.

Користувацька і ядерна складові процесів

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

Статична частина контексту процесу системного рівня включає наступне:

Описувач процесу, тобто елемент таблиці описувачів існуючих в системі процесів. Описувач процесу включає, зокрема, наступну інформацію: стан процесу; фізичну адресу в основний або зовнішньої пам'яті u-області процесу; ідентифікатори користувача, від імені якого запущений процес; ідентифікатор процесу; іншу інформацію, пов'язану з управлінням процесом. U-область (u-area) - індивідуальна для кожного процесу область простору ядра, має таку властивість, що хоча u-область кожного процесу розташовується в окремому місці фізичної пам'яті, u-області всіх процесів мають один і той же віртуальний адреса в адресному просторі ядра. Саме це означає, що яка б програма ядра не виконувалася, вона завжди виконується як ядерна частина деякого користувача процесу, і саме того процесу, u-область якого є "видимої" для ядра в даний момент часу. U-область процесу містить: вказівник на описувач процесу; ідентифікатори користувача; лічильник часу, протягом якого процес реально виконувався (тобто займав процесор) в режимі користувача та режимі ядра; параметри системного виклику; результати системного виклику; таблиця дескрипторів відкритих файлів ; граничні розміри адресного простору процесу; граничні розміри файлу, в який процес може писати;

і т.д.

Динамічна частина контексту процесу - це один або кілька стеків, які використовуються процесом при його виконанні у режимі ядра. Кількість ядерних стеків процесу відповідає числу рівнів переривання, підтримуваних конкретною апаратурою.

Принципи організації багато режиму

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

Історично ОС UNIX є системою поділу часу, тобто система повинна перш за все "справедливо" розділяти ресурси процесора (ів) між процесами, що відносяться до різних користувачам, причому таким чином, щоб час реакції кожного дії інтерактивного користувача знаходилося в допустимих межах. Проте останнім часом зростає тенденція до використання ОС UNIX в додатках реального часу, що вплинуло і на алгоритми планування. Нижче ми опишемо загальну (без технічних деталей) схему планування поділу ресурсів процесора (ів) між процесами в UNIX System V Release 4.

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

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

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

Традиційне рішення ОС UNIX полягає у використанні динамічно змінюються пріоритетів. Кожен процес при своєму утворенні отримує певний встановлений системою статичний пріоритет, який в подальшому може бути змінений за допомогою системного виклику nice (див. п. 3.1.3). Цей статичний пріоритет є основою початкового значення динамічного пріоритету процесу, що є реальним критерієм планування. Всі процеси з динамічним пріоритетом не нижче порогового беруть участь в конкуренції за процесор (за схемою, описаною вище). Проте кожного разу, коли процес успішно відпрацьовує свій квант на процесорі, його динамічний пріоритет зменшується (величина зменшення залежить від статичного пріоритету процесу). Якщо значення динамічного пріоритету процесу досягає деякого нижньої межі, він стає кандидатом на відкачування (своппінг) і більше не конкурує за процесор.

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

Як вписуються в цю схему процеси реального часу? Перш за все, потрібно розібратися, що розуміється під концепцією "реального часу" в ОС UNIX. Відомо, що існують принаймні два розуміння терміну - "м'яке реальний час (soft realtime)" і "жорстке реальний час (hard realtime)".

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

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

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

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

Тим самим своєрідним, але логічним чином в сучасних варіантах ОС UNIX одночасно реалізована як можливість поділу часу для інтерактивних процесів, так і можливість м'якого реального часу для процесів, пов'язаних з реальним управлінням поведінкою об'єктів в реальному часі.

Традиційний механізм управління процесами на рівні користувача

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

Перш за все змалюємо загальну схему можливостей користувача, пов'язаних з управлінням процесами. Кожен процес може утворити повністю ідентичний підлеглий процес за допомогою системного виклику fork () і чекати закінчення виконання своїх підлеглих процесів за допомогою системного виклику wait. Кожен процес у будь-який момент часу може повністю змінити вміст свого образу пам'яті за допомогою однієї з різновидів системного виклику exec (змінити образ пам'яті згідно з вмістом зазначеного файлу, що зберігає образ процесу (виконуваного файлу)). Кожен процес може встановити свою власну реакцію на "сигнали", вироблені операційною системою у відповідність із зовнішніми або внутрішніми подіями. Нарешті, кожен процес може вплинути на значення свого статичного (а тим самим і динамічного) пріоритету за допомогою системного виклику nice.

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

Ось що робить ядро ​​системи при виконанні системного виклику fork:

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

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

Щоб процес-предок міг синхронізувати своє виконання з виконанням своїх процесів-нащадків, існує системний виклик wait. Виконання цього системного виклику призводить до призупинення виконання процесу до тих пір, поки не завершиться виконання будь-якого з процесів, які є його нащадками. В якості прямого параметра системного виклику wait вказується адреса пам'яті (покажчик), за яким має бути повернена інформація, що описує статус завершення чергового процесу-нащадка, а у відповідь (поворотним) параметром є PID (ідентифікатор процесу) завершився процесу-нащадка.

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

Прикладами сигналів (не вичерпними всі можливості) є наступні:

закінчення процесу-нащадка (з причини виконання системного виклику exit або системного виклику signal з параметром "death of child (смерть нащадка)"; виникнення виняткової ситуації в поведінці процесу (вихід за допустимі межі віртуальної пам'яті, спроба запису в область віртуальної пам'яті, яка доступна тільки для читання і т.д.); перевищення верхньої межі системних ресурсів; сповіщення про помилки в системних виклики (неіснуючий системний виклик, помилки в параметрах системного виклику, невідповідність системного виклику поточному стану процесу і т.д.); сигнали, які посилає іншим процесом у режимі користувача (див. нижче); сигнали, що надходять внаслідок натискання користувачем певних клавішею на клавіатурі терміналу, пов'язаного з процесом (наприклад, Ctrl-C або Ctrl-D); сигнали, що служать для трасування процесу.

Для установки реакції на надходження певного сигналу використовується системний виклик

oldfunction = signal (signum, function),

де signum - це номер сигналу, на надходження якого встановлюється реакція (всі можливі сигнали пронумеровані, кожному номеру відповідає власне символічне ім'я; відповідна інформація міститься в документації до кожної конкретної системі UNIX), а function - адреса вказується користувачем функції, яка повинна бути викликана системою при вступі зазначеного сигналу даному процесу. Значення, що повертається oldfunction - це адреса функції для реагування на надходження сигналу signum, встановлений у попередньому виклику signal. Замість адреси функції у вхідних параметрах можна задати 1 або 0. Завдання одиниці призводить до того, що далі для даного процесу надходження сигналу з номером signum буде ігноруватися (це допускається не для всіх сигналів). Якщо в якості значення параметра function вказаний нуль, то після виконання системного виклику signal перше ж надходження даному процесу сигналу з номером signum призведе до завершення процесу (буде проінтерпретувати аналогічно виконання системного виклику exit, див. нижче).

Система надає можливість для користувача процесів явно генерувати сигнали, що направляються іншим процесам. Для цього використовується системний виклик

kill (pid, signum)

(Цей системний виклик називається "kill", тому що найбільш часто застосовується для того, щоб примусово закінчити зазначений процес.) Параметр signum задає номер генерованого сигналу (у системному виклику kill можна вказувати не всі номери сигналів). Параметр pid має наступні значення:

якщо в якості значення pid вказано ціле позитивне число, то ядро ​​пошле вказаний сигнал процесу, ідентифікатор якого дорівнює pid; якщо значення pid дорівнює нулю, то зазначений сигнал посилається всім процесам, які належать до тієї ж групи процесів, що і посилає процес (поняття групи процесів аналогічно поняттю групи користувачів; повний ідентифікатор процесу складається з двох частин - ідентифікатора групи процесів та індивідуального ідентифікатора процесу; в одну групу автоматично включаються всі процеси, що мають загального предка; ідентифікатор групи процесу може бути змінений за допомогою системного виклику setpgrp); якщо значення pid одно -1, то ядро ​​посилає вказаний сигнал процесам, дійсний ідентифікатор користувача яких дорівнює ідентифікатору поточного виконання процесу, що посилає сигнал (див. п. 2.5.1).

Для завершення процесу за його власною ініціативою використовується системний виклик

exit (status),

де status - це ціле число, що повертається процесу-предка для його інформування про причини завершення процесу-нащадка (як описувалося вище, для отримання інформації про статус завершився процесу-нащадка у процесі-предка використовується системний виклик wait). Системний виклик називається exit (тобто "вихід", оскільки вважається, що будь-який призначений для користувача процес запускається ядром системи (власне, так воно і є), і завершення користувацького процесу - це остаточний вихід з нього в ядро.

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

Можливості управління процесами, які ми обговорили до цього моменту, дозволяють утворити новий процес, що є повною копією процесу-предка (системний виклик fork); чекати завершення будь-якого утвореного таким чином процесу (системний виклик wait); породжувати сигнали і реагувати на них (системні виклики kill і signal); завершувати процес за його власною ініціативою (системний виклик exit). Проте поки залишається незрозумілим, яким чином можна виконати в утвореному процесі довільну програму. Зрозуміло, що в принципі цього можна було б домогтися за допомогою системного виклику fork, якщо образ пам'яті процесу-предка заздалегідь побудований так, що містить всі потенційно необхідні програми. Але, звичайно, цей спосіб не є раціональним (хоча б тому, що свідомо призводить до перевитрати пам'яті).

Для виконання довільної програми в поточному процесі використовуються системні виклики сімейства exec. Різні варіанти exec трохи розрізняються способом завдання параметрів. Тут ми не будемо вдаватися в деталі (за ними краще звертатися до документації по конкретній реалізації). Розглянемо деякий узагальнений випадок системного виклику

exec (filename, argv, argc, envp)

Ось що відбувається при виконанні цього системного виклику. Береться файл з ім'ям filename (може бути вказано повне або скорочене ім'я файлу). Цей файл повинен бути виконуваним файлом, тобто являти собою закінчений образ віртуальної пам'яті. Якщо це насправді так, то ядро ​​ОС UNIX виробляє реорганізацію віртуальної пам'яті процесу, який звернувся до системного виклику exec, знищуючи в ньому старі сегменти і утворюючи нові сегменти, у які завантажуються відповідні розділи файлу filename. Після цього у новоутвореному користувальницькому контексті викликається функція main, якої, як і годиться, передаються параметри argv і argc, тобто деякий набір текстових рядків, заданих в якості параметра системного виклику exec. Через параметр envp оновлений процес може звертатися до змінних свого оточення.

Слід зауважити, що при виконанні системного виклику exec не утворюється новий процес, а лише змінюється вміст віртуальної пам'яті існуючого процесу. Іншими словами, змінюється тільки користувальницький контекст процесу.

Корисні можливості ОС UNIX для спілкування споріднених або незалежно утворених процесів розглядаються нижче в розділі 3.4.

Поняття нитки (threads)

Поняття "легковісного процесу" (light-weight process), або, як прийнято називати його в сучасних варіантах ОС UNIX, "thread" (нитка, потік управління) давно відомо в області операційних систем. Інтуїтивно зрозуміло, що концепції віртуальної пам'яті й потоку команд, що виконується в цій віртуальній пам'яті, в принципі, ортогональні. Ні з чого не випливає, що однією віртуальної пам'яті повинен відповідати один і тільки один потік управління. Тому, наприклад, в ОС Multics (розділ 1.1) допускалося (і було прийнятої практики) мати довільну кількість процесів, які виконуються в загальною (що розділяється) віртуальної пам'яті.

Зрозуміло, що якщо кілька процесів спільно користуються деякими ресурсами, то при доступі до цих ресурсів вони повинні синхронізувати (наприклад, з використанням семафорів, див. п. 3.4.2). Багаторічний досвід програмування з використанням явних примітивів синхронізації показав, що цей стиль "паралельного" програмування породжує серйозні проблеми при написанні, налагодженні і супроводі програм (найбільш важко виявляються помилки в програмах зазвичай пов'язані з синхронізацією). Це стало однією з причин того, що в традиційних варіантах ОС UNIX поняття процесу жорстко пов'язувалося з поняттям окремою і недоступною для інших процесів віртуальної пам'яті. Кожен процес був захищений ядром операційної системи від неконтрольованого втручання інших процесів. Багато років автори ОС UNIX вважали це одним з основних достоїнств системи (утім, це думка існує і сьогодні).

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

Другий (і може бути більш суттєвою) проблемою стала поява так званих симетричних мультипроцесорних комп'ютерних архітектур (SMP - Symmetric Multiprocessor Architectures). У цих комп'ютерах фізично присутні кілька процесорів, які мають однакові за швидкістю можливості доступу до спільно використовуваної основної пам'яті. Поява подібних машин на світовому ринку, природно, поставило проблему їх ефективного використання. Зрозуміло, що при застосуванні традиційного підходу ОС UNIX до організації процесів від наявності загальної пам'яті не дуже багато толку (хоча при наявності можливостей пам'яті, що розділяється (див. п. 3.4.1) про це можна сперечатися). До моменту появи SMP з'ясувалося, що технологія програмування все ще не може запропонувати ефективного і безпечного способу реального паралельного програмування. Тому довелося повернутися до явного паралельного програмування з використанням паралельних процесів у загальній віртуальній (а тим самим, і основний) пам'яті з явною синхронізацією.

Що ж розуміється під "ниткою" (thread)? Це незалежний потік керування, що виконується в контексті деякого процесу. Фактично, поняття контексту процесу, яке ми обговорювали в п. 3.1.1, змінюється наступним чином. Все, що не відноситься до потоку управління (віртуальна пам'ять, дескриптори відкритих файлів і т.д.), залишається в загальному контексті процесу. Речі, які характерні для потоку керування (регістровий контекст, стеки різного рівня і т.д.), переходять з контексту процесу в контекст нитки. Загальна картина показана на малюнку 3.4.

Основні функції і компоненти ядра ОС UNIX

Рис. 3.4. Співвідношення контексту процесу і контекстів ниток

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

Наведене коротке обговорення поняття нитки здається достатнім для того, щоб зрозуміти, що впровадження в ОС UNIX механізму легковагих процесів вимагає істотних переробок ядра системи. (Завжди важко впровадити в програму кошти, для підтримки яких вона не була спочатку пристосована.)

Підходи до організації ниток і управління ними у різних варіантах ОС UNIX

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

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

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

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

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

У системі Solaris (правильніше говорити SunOS 4.x, оскільки Solaris в термінології Sun представляє собою не операційну систему, а розширену операційного середовища) прийнятий наступний підхід. При запуску будь-якого процесу можна вимагати резервування одного або декількох процесорів мультипроцессорной системи. Це означає, що операційна система не надасть ніякому іншому процесу можливості виконання на зарезервованому (их) процесорі (ах). Незалежно від того, чи готова до виконання хоча б одна нитка такого процесу, зарезервовані процесори не будуть використовуватися ні для чого іншого.

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

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

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

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

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

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

Управління вводом / виводом

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

Традиційно в ОС UNIX виділяються три типи організації введення / виводу і, відповідно, три типи драйверів. Блоковий введення / висновок головним чином призначений для роботи з каталогами та звичайними файлами файлової системи, які на базовому рівні мають блочну структуру. У пп. 2.4.5 і 3.1.2 вказувалося, що на рівні користувача тепер можливо працювати з файлами, прямо відображаючи їх у сегменти віртуальної пам'яті. Ця можливість розглядається як верхній рівень блокового вводу / виводу. На нижньому рівні блочний введення / висновок підтримується блочними драйверами. Блоковий введення / висновок, крім того, підтримується системної буферизацією (див. п. 3.3.1).

Символьний введення / висновок служить для прямого (без буферизації) виконання обмінів між адресним простором користувача та відповідним пристроєм. Спільною для всіх символьних драйверів підтримкою ядра є забезпечення функцій пересилання даних між призначеними для користувача і ядерним адресними просторами.

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

Принципи системної буферизації вводу / виводу

Традиційним способом зниження накладних витрат при виконанні обмінів з пристроями зовнішньої пам'яті, що мають блокову структуру, є буферизація блокового вводу / виводу. Це означає, що будь-який блок пристрої зовнішньої пам'яті зчитується насамперед у певний буфер області основної пам'яті, називаної в ОС UNIX системним кешем, і вже звідти повністю або частково (залежно від виду обміну) копіюється у відповідне користувальницьке простір.

Принципами організації традиційного механізму буферизації є, по-перше, те, що копія вмісту блоку утримується в системному буфері до тих пір, поки не виникне необхідність її заміщення унаслідок браку буферів (для організації політики заміщення використовується різновид алгоритму LRU, див. п. 3.1 .1). По-друге, при виконанні запису будь-якого блоку пристрої зовнішньої пам'яті реально виконується лише оновлення (або освіту і наповнення) буфера кешу. Дійсний обмін з пристроєм виконується або при виштовхуванні буфера внаслідок заміщення його вмісту, або при виконанні спеціального системного виклику sync (або fsync), підтримуваного спеціально для насильницького виштовхування в зовнішню пам'ять оновлених буферів кеша.

Ця традиційна схема буферизації увійшла в суперечність з розвинутими в сучасних варіантах ОС UNIX засобами управління віртуальною пам'яттю і особливо з механізмом відображення файлів у сегменти віртуальної пам'яті (див. пп. 2.4.5 та 3.1.2). (Ми не будемо докладно пояснювати тут суть цих протиріч і запропонуємо читачам поміркувати над цим.) Тому в System V Release 4 з'явилася нова схема буферизації, поки що використовується паралельно зі старою схемою.

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

Нова схема буферизації в ядрі ОС UNIX головним чином грунтується на тому, що для організації буферизації можна не робити майже нічого спеціального. Коли один з користувацьких процесів відкриває не відкритий до цього часу файл, ядро ​​утворює новий сегмент і підключає до цього сегменту файл, що відкривається. Після цього (незалежно від того, чи буде для користувача процес працювати з файлом у традиційному режимі з використанням системних викликів read і write або підключить файл до сегмента своєї віртуальної пам'яті) на рівні ядра робота буде проводитися з тим ядерним сегментом, до якого підключений файл на рівні ядра. Основна ідея нового підходу полягає в тому, що усувається розрив між управлінням віртуальною пам'яттю і загальносистемної буферизацією (це потрібно було б зробити давно, оскільки очевидно, що основну буферизацію в операційній системі повинен виробляти компонент управління віртуальною пам'яттю).

Чому ж не можна відмовитися від старого механізму буферизації? Вся справа в тому, що нова схема припускає наявність деякої безперервної адресації усередині об'єкта зовнішньої пам'яті (повинен існувати ізоморфізм між відображуваним і відображеним об'єктами). Однак, при організації файлових систем ОС UNIX досить складно розподіляє зовнішню пам'ять, що особливо стосується i-вузлів. Тому деякі блоки зовнішньої пам'яті доводиться вважати ізольованими, і для них виявляється вигідніше використовувати стару схему буферизації (хоча, можливо, у завтрашніх варіантах UNIX і вдасться повністю перейти до уніфікованої новою схемою).

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

Для доступу (тобто для отримання можливості подальшого виконання операцій вводу / виводу) до файлу будь-якого виду (включаючи спеціальні файли) призначений для користувача процес повинен виконати попереднє підключення до файлу за допомогою одного із системних викликів open, creat, dup або pipe. Програмні канали та відповідні системні виклики ми розглянемо в п. 3.4.3, а поки трохи більш докладно, ніж у п. 2.3.3, розглянемо інші "ініціалізували" системні виклики.

Послідовність дій системного виклику open (pathname, mode) наступна:

аналізується несуперечність вхідних параметрів (головним чином, відносяться до прапорів режиму доступу до файлу); виділяється або перебуває простір для описувача файлу в системній області даних процесу (u-області); в загальносистемної області виділяється або перебуває існуюче простір для розміщення системного описувача файлу (структури file); проводиться пошук в архіві файлової системи об'єкта з ім'ям "pathname" і утворюється або виявляється описувач файлу рівня файлової системи (vnode в термінах UNIX V System 4); виконується зв'язування vnode з раніше утвореної структурою file.

Системні виклики open і creat (майже) функціонально еквівалентні. Будь-який існуючий файл можна відкрити за допомогою системного виклику creat, і будь-який новий файл можна створити за допомогою системного виклику open. Проте, стосовно до системного виклику creat ми повинні підкреслити, що в разі свого природного застосування (для створення файлу) цей системний виклик створює новий елемент відповідного каталогу (відповідно до заданого значенням pathname), а також створює і відповідним чином ініціалізує новий i-вузол .

Нарешті, системний виклик dup (duplicate - скопіювати) призводить до утворення нового дескриптора вже відкритого файлу. Цей специфічний для ОС UNIX системний виклик служить виключно для цілей перенаправлення вводу / виводу (див. п. 2.1.8). Його виконання полягає в тому, що в u-області системного простору для користувача процесу утворюється новий описувач відкритого файлу, що містить знову утворений дескриптор файлу (ціле число), але посилається на вже існуючу загальносистемну структуру file і містить ті ж самі ознаки і прапори, які відповідають відкритому файлу-зразку.

Іншими важливими системними викликами є системні виклики read і write. Системний виклик read виконується таким чином:

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

Операція запису виконується аналогічним чином, але змінює вміст буфера буферного пулу.

Системний виклик close призводить до того, що драйвер обриває зв'язок з відповідним користувальницьким процесом і (у випадку останнього за часом закриття пристрою) встановлює загальносистемний прапор "драйвер вільний".

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

Природно, що оскільки драйвери головним чином призначені для управління зовнішніми пристроями, програмний код драйвера повинен містити відповідні засоби для обробки переривань від пристрою. Виклик індивідуальної програми обробки переривань в драйвері відбувається з ядра операційної системи. Подібним же чином у драйвері може бути оголошений вхід "timeout", до якого звертається ядро ​​при закінченні раніше замовленого драйвером часу (такий часовий контроль є необхідним при управлінні не надто інтелектуальними пристроями).

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

Основні функції і компоненти ядра ОС UNIX

Рис. 3.5. Інтерфейси та вхідні точки драйверів

Блокові драйвери

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

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

Символьні драйвери

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

Символьні драйвери не використовують системну буферизацію. Вони безпосередньо копіюють дані з пам'яті користувача процесу при виконанні операцій запису або в пам'ять користувача процесу при виконанні операцій читання, використовуючи власні буфера.

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

Потокові драйвери

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

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

Взаємодія процесів

Кожен процес в ОС UNIX виконується у власній віртуальної пам'яті, тобто якщо не вживати додаткових зусиль, то навіть процеси-близнюки, утворені в результаті виконання системного виклику fork (), насправді повністю ізольовані один від одного (якщо не рахувати того, що процес-нащадок успадковує від процесу-предка всі відкриті файли). Тим самим, у ранніх варіантах ОС UNIX підтримувалися дуже слабкі можливості взаємодії процесів, навіть входять у загальну ієрархію породження (тобто мають спільного предка).

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

Мабуть, застосування такого підходу було реакцією на надмірно складні механізми взаємодії і синхронізації паралельних процесів, що існували в історично попередньої UNIX ОС Multics. Нагадаємо (див. розділ 1.1), що в ОС Multics підтримувалася сегментно-сторінкова організація віртуальної пам'яті, і в загальній віртуальній пам'яті могло виконуватися кілька паралельних процесів, які, природно, могли взаємодіяти через спільну пам'ять. За рахунок можливості включення одного й того ж сегменту в різну віртуальну пам'ять аналогічна можливість взаємодій існувала і для процесів, які виконуються не в загальній віртуальній пам'яті.

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

Зрозуміло, що стиль ранніх варіантів ОС UNIX стимулював істотно більш просте програмування. У найбільш простих випадках процес-нащадок утворювався тільки для того, щоб асинхронно з основним процесом виконати будь-яку просту дію (наприклад, запис у файл). У більш складних випадках процеси, пов'язані ієрархією споріднення, створювали обробні "конвеєри" з використанням техніки програмних каналів (pipes). Ця техніка особливо часто застосовується при програмуванні на командних мовами (див. розділ 5.2).

Довгий час батьки-засновники ОС UNIX вважали, що в тій області, для якої призначався UNIX (розробка програмного забезпечення, підготовка та супровід технічної документації і т.д.) цих можливостей цілком достатньо. Однак поступове поширення системи в інших областях і порівняльна простота нарощування її можливостей призвели до того, що з часом в різних варіантах ОС UNIX в сукупності з'явився явно надлишковий набір системних засобів, призначених для забезпечення можливості взаємодії та синхронізації процесів, які не обов'язково пов'язані ставленням споріднення ( у світі ОС UNIX ці кошти зазвичай називають IPC від Inter-Process Communication Facilities). З появою UNIX System V Release 4.0 (і більш старшої версії 4.2) всі ці кошти були узаконені і увійшли до фактичний стандарт ОС UNIX сучасного зразка.

Не можна сказати, що кошти IPC ОС UNIX ідеальні хоча б в якому-небудь відношенні. При розробці складних асинхронних програмних комплексів (наприклад, систем реального часу) найбільше незручностей завдає надмірність засобів IPC. Завжди можливі кілька варіантів реалізації, і дуже часто неможливо знайти критерії вибору. Додаткову проблему створює той факт, що в різних варіантах системи кошти IPC реалізуються по-різному, найчастіше одні кошти реалізовані на основі використання інших засобів. Тому ефективність реалізації розрізняється, через що ускладнюється розробка мобільних асинхронних програмних комплексів.

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

Порядок розгляду не відображає якусь особливу ступінь важливості або перевагу конкретного засобу. Ми починаємо з пакету засобів IPC, які з'явилися в UNIX System V Release 3.0. Цей пакет включає:

засоби, що забезпечують можливість наявності спільної для процесів пам'яті (сегменти пам'яті, що розділяється - shared memory segments); засоби, що забезпечують можливість синхронізації процесів при доступі до спільно використовуваних ресурсів, наприклад, до пам'яті, що розділяється (семафори - semaphores); засоби, що забезпечують можливість посилки процесом повідомлень іншому безпідставного процесу (черги повідомлень - message queues).

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

Для кожного механізму підтримується загальносистемна таблиця, елементи якої описують всіх існуючих в даний момент представників механізму (конкретні сегменти, семафори або черги повідомлень). Елемент таблиці містить деякий числовий ключ, який є вибраним користувачем ім'ям представника відповідного механізму. Іншими словами, щоб два чи більше процесів могли використовувати певний механізм, вони повинні заздалегідь домовитися про іменуванні використовуваного представника цього механізму і добитися того, щоб той же представник не використовувався іншими процесами. Процес, який бажає почати користуватися одним з механізмів, звертається до системи з системним викликом з сімейства "get", прямими параметрами якого є ключ об'єкта і додаткові прапори, а у відповідь параметром є числовою дескриптор, який використовується в подальших системних викликах подібно до того, як використовується дескриптор файлу при роботі з файловою системою. Допускається використання спеціального значення ключа з символічним ім'ям IPC_PRIVATE, що зобов'язує систему виділити новий елемент у таблиці відповідного механізму незалежно від наявності або відсутності в ній елементу, що містить те ж значення ключа. При вказівці інших значень ключа завдання прапора IPC_CREAT призводить до утворення нового елемента таблиці, якщо в таблиці відсутній елемент з вказаним значенням ключа, або знаходження елемента з цим значенням ключа. Комбінація прапорів IPC_CREAT і IPC_EXCL призводить до видачі діагностики про помилкову ситуації, якщо в таблиці вже міститься елемент з вказаним значенням ключа. Захист доступу до раніше створеним елементам таблиці кожного механізму грунтується на тих же принципах, що й захист доступу до файлів.

Перейдемо до більш детального вивчення конкретних механізмів цього сімейства.

Колективна пам'ять

Для роботи з пам'яттю, що використовуються чотири системні виклики:

shmget створює новий сегмент пам'яті, що розділяється або знаходить існуючий сегмент з тим же ключем; shmat підключає сегмент із зазначеним дескриптором до віртуальної пам'яті звертається процесу; shmdt відключає від віртуальної пам'яті раніше підключений до неї сегмент із зазначеним віртуальним адресою початку; нарешті, системний виклик shmctl служить для управління різноманітними параметрами, пов'язаними з існуючим сегментом.

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

Синтаксис системного виклику shmget виглядає наступним чином:

shmid = shmget (key, size, flag);

Параметр size визначає бажаний розмір сегмента в байтах. Далі робота відбувається за загальними правилами. Якщо в таблиці пам'яті, що розділяється знаходиться елемент, що містить заданий ключ, і права доступу не суперечать поточним характеристикам звертається процесу, то значенням системного виклику є дескриптор існуючого сегменту (і звернувся процес так і не дізнається реального розміру сегменту, хоча згодом його все-таки можна дізнатися за допомогою системного виклику shmctl). В іншому випадку створюється новий сегмент з розміром не менше встановленого в системі мінімального розміру сегмента пам'яті, що розділяється і не більше встановленого максимального розміру. Створення сегмента не означає негайного виділення під нього основної пам'яті. Ця дія відкладається до виконання першого системного виклику підключення сегменту до віртуальної пам'яті деякого процесу. Аналогічно, при виконанні останнього системного виклику відключення сегмента від віртуальної пам'яті відповідна основна пам'ять звільняється.

Підключення сегмента до віртуальної пам'яті виконується шляхом звернення до системного виклику shmat:

virtaddr = shmat (id, addr, flags);

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

Для відключення сегмента від віртуальної пам'яті використовується системний виклик shmdt:

shmdt (addr);

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

Системний виклик shmctl:

shmctl (id, cmd, shsstatbuf);

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

Семафори

Механізм семафорів, реалізований в ОС UNIX, є узагальненням класичного механізму семафорів загального вигляду, запропонованого більше 25 років тому відомим голландським фахівцем професором Дейкстри. Зауважимо, що доцільність введення такого узагальнення достатньо сумнівна. Зазвичай навпаки використовувався полегшений варіант семафорів Дейкстри - так звані виконавчі семафори. Ми не будемо тут заглиблюватися в загальну теорію синхронізації на основі семафорів, але зауважимо, що достатність у загальному випадку двійкових семафорів доведена (відомий алгоритм реалізації семафорів загального вигляду на основі двійкових). Звичайно, аналогічні міркування можна було б застосувати і до варіанту семафорів, застосованому в ОС UNIX.

Семафор в ОС UNIX складається з наступних елементів:

значення семафора; ідентифікатор процесу, який хронологічно останнім працював з семафором; число процесів, які чекають збільшення значення семафора, а число процесів, які очікують нульового значення семафора.

Для роботи з семафорами підтримуються три системні виклики:

semget для створення та отримання доступу до набору семафорів; semop для маніпулювання значеннями семафорів (це саме той системний виклик, який дозволяє процесам синхронізувати на основі використання семафорів); semctl для виконання різноманітних керуючих операцій над набором семафорів.

Системний виклик semget має наступний синтаксис:

id = semget (key, count, flag);

де прямі параметри key і flag і повертається значення системного виклику мають той же зміст, що для інших системних викликів сімейства "get", а параметр count задає число семафорів в наборі семафорів, що володіють одним і тим же ключем. Після цього індивідуальний семафор ідентифікується дескриптором набору семафорів і номером семафора в цьому наборі. Якщо до моменту виконання системного виклику semget набір семафорів із зазначеним ключем вже існує, то звертається процес отримає відповідний дескриптор, але так і не дізнається про реальну кількість семафорів в групі (хоча пізніше це все-таки можна дізнатися за допомогою системного виклику semctl).

Основним системним викликом для маніпулювання семафором є semop:

oldval = semop (id, oplist, count);

де id - це раніше отриманий дескриптор групи семафорів, oplist - масив описувачів операцій над семафорами групи, а count - розмір цього масиву. Значення, що повертається системним викликом, є значенням останнього обробленого семафора. Кожен елемент масиву oplist має наступну структуру:

номер семафора у вказаному наборі семафорів; операція; прапори.

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

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

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

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

Системний виклик semctl має формат

semctl (id, number, cmd, arg);

де id - це дескриптор групи семафорів, number - номер семафора в групі, cmd - код операції, а arg - покажчик на структуру, вміст якої інтерпретується по-різному, в залежності від операції. Зокрема, за допомогою semctl можна знищити індивідуальний семафор у зазначеній групі. Проте деталі цього системного виклику настільки громіздкі, що ми рекомендуємо у разі необхідності звертатися до технічної документації використовуваного варіанта операційної системи.

Черги повідомлень

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

msgget для утворення нової черги повідомлень або отримання дескриптора існуючої черги; msgsnd для посилки повідомлення (вірніше, для його постановки в зазначену чергу повідомлень); msgrcv для прийому повідомлення (вірніше, для вибірки повідомлення з черги повідомлень); msgctl для виконання ряду керуючих дій.

Системний виклик msgget володіє стандартним для сімейства "get" системних викликів синтаксисом:

msgqid = msgget (key, flag);

Ядро зберігає повідомлення у вигляді зв'язного списку (черги), а дескриптор черги повідомлень є індексом у масиві заголовків черг повідомлень. На додаток до інформації, спільної для всіх механізмів IPC у UNIX System V, в заголовку черги зберігаються також:

покажчики на перше і останнє повідомлення в даній черги; число повідомлень і загальна кількість байтів даних у всіх них разом узятих; ідентифікатори процесів, які останніми послали або прийняли повідомлення через цю чергу; тимчасові мітки останніх виконаних операцій msgsnd, msgrsv і msgctl.

Як звичайно, при виконанні системного виклику msgget ядро ​​ОС UNIX або створює нову чергу повідомлень, поміщаючи її заголовок у таблицю черг повідомлень і повертаючи користувачеві дескриптор новоствореної черги, або знаходить елемент таблиці черг повідомлень, що містить вказаний ключ, і повертає відповідний дескриптор черги. На малюнку 3.6 показані структури даних, що використовуються для організації черг повідомлень.

Основні функції і компоненти ядра ОС UNIX

Рис. 3.6. Структури даних, що використовуються для організації черг повідомлень

Для посилки повідомлення використовується системний виклик msgsnd:

msgsnd (msgqid, msg, count, flag);

де msg - це покажчик на структуру, що містить визначений користувачем цілочисельний тип повідомлення і символьний масив - власне повідомлення; count задає розмір повідомлення в байтах, а flag визначає дії ядра при виході за межі допустимих розмірів внутрішньої буферної пам'яті.

Для того, щоб ядро ​​успішно поставило зазначене повідомлення у вказану чергу повідомлень, повинні бути виконані наступні умови: звертається процес повинен мати відповідні права за записом у дану чергу повідомлень; довжина повідомлення не повинна перевищувати встановлений в системі верхня межа; загальна довжина повідомлень (включаючи знову посилається) не повинна перевищувати встановлену межу; зазначений у повідомленні тип повідомлення повинен бути позитивним цілим числом. У цьому випадку звернувся процес успішно продовжує своє виконання, залишивши відправлене повідомлення в буфері черги повідомлень. Тоді ядро ​​активізує (пробуджує) всі процеси, які очікують надходження повідомлень з даної черги.

Якщо ж виявляється, що нове повідомлення неможливо буферизованная в ядрі через перевищення верхньої межі сумарної довжини повідомлень, що знаходяться в одній черзі повідомлень, то звернувся процес відкладається (присипляє) до тих пір, поки черга повідомлень не розвантажиться процесами, які очікували одержання повідомлень. Щоб уникнути такого відкладання, який звертається процес повинен вказати в числі параметрів системного виклику msgsnd значення прапора з символічним ім'ям IPC_NOWAIT (як у випадку використання семафорів), щоб ядро ​​видало свідчить про помилку код повернення системного виклику mgdsng у разі неможливості включити повідомлення у вказану чергу.

Для прийому повідомлення використовується системний виклик msgrcv:

count = msgrcv (id, msg, maxcount, type, flag);

Тут msg - це покажчик на структуру даних в адресному просторі користувача, призначену для розміщення отримане повідомлення; maxcount задає розмір області даних (масиву байтів) у структурі msg; значення type специфікує тип повідомлення, яке бажано прийняти; значення параметра flag вказує ядру, що слід зробити, якщо у зазначеній черги повідомлень відсутнє сполучення з вказаним типом. Значення, що повертається системного виклику задає реальне число байтів, переданих користувачеві.

Виконання системного виклику, як зазвичай, починається з перевірки правомочності доступу звертається процесу до зазначеної черги. Далі, якщо значенням параметра type є нуль, ядро ​​обирає перше повідомлення з вказаної черги повідомлень і копіює його в задану користувача структуру даних. Після цього коригується інформація, що міститься в заголовку черги (число повідомлень, сумарний розмір і т.д.). Якщо будь-які процеси були відкладені через переповнення черги повідомлень, то всі вони активізуються. У випадку, якщо значення параметра maxcount виявляється менше реального розміру повідомлення, ядро ​​не видаляє повідомлення з черги і повертає код помилки. Однак, якщо задано прапор MSG_NOERROR, то вибірка повідомлення виробляється, і в буфер користувача переписуються перший maxcount байтів повідомлення.

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

У всіх випадках, якщо у зазначеній черги відсутні повідомлення, що відповідають специфікації параметра type, ядро ​​відкладає (присипляє) звернувся процес до появи в черзі необхідного повідомлення. Однак, якщо в параметрі flag задано значення прапора IPC_NOWAIT, то процес негайно сповіщається про відсутність повідомлення в черзі шляхом повернення коду помилки.

Системний виклик

msgctl (id, cmd, mstatbuf);

служить для опитування стану описувача черги повідомлень, зміни його стану (наприклад, зміни прав доступу до черги) і для знищення зазначеної черги повідомлень (деталі ми опускаємо).

Програмні канали

Як ми вже неодноразово зазначали, традиційним засобом взаємодії і синхронізації процесів в ОС UNIX є програмні канали (pipes). Теоретично програмний канал дозволяє взаємодіяти будь-якому числу процесів, забезпечуючи дисципліну FIFO (first-in-first-out). Іншими словами, процес, який читає з програмного каналу, прочитає ті дані, які були записані в програмний канал найбільш давно. У традиційній реалізації програмних каналів для зберігання даних використовувались файли. У сучасних версіях ОС UNIX для реалізації програмних каналів застосовуються інші засоби IPC (зокрема, черги повідомлень).

Розрізняються два види програмних каналів - іменовані і неіменовані. Іменований програмний канал може слугувати для спілкування та синхронізації довільних процесів, які знають ім'я даного програмного каналу і які мають відповідні права доступу. Неіменованим програмним каналом можуть користуватися тільки створив його процес і його нащадки (необов'язково прямі).

Для створення іменованого програмного каналу (або одержання до нього доступу) використовується звичайний файловий системний виклик open. Для створення ж неіменованого програмного каналу існує спеціальний системний виклик pipe (історично більш ранній). Проте після одержання відповідних дескрипторів обидва види програмних каналів використовуються одноманітно за допомогою стандартних файлових системних викликів read, write і close.

Системний виклик pipe має наступний синтаксис:

pipe (fdptr);

де fdptr - це покажчик на масив з двох цілих чисел, в який після створення неіменованого програмного каналу будуть поміщені дескриптори, призначені для читання з програмного каналу (за допомогою системного виклику read) та запису в програмний канал (за допомогою системного виклику write). Дескриптори неіменованого програмного каналу - це звичайні дескриптори файлів, тобто такого програмного каналу відповідають два елементи таблиці відкритих файлів процесу. Тому при подальшому використанні системних викликів read і write процес абсолютно не зобов'язаний відрізняти випадок використання програмних каналів від випадку використання звичайних файлів (власне, на цьому й грунтується ідея перенаправлення вводу / виводу та організації конвеєрів).

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

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

Закінчення роботи процесу з програмним каналом (незалежно від того, іменований він чи неіменованим) проводиться за допомогою системного виклику close. В основному, дії ядра при закритті програмного каналу аналогічні діям при закритті звичайного файлу. Проте є відмінність в тому, що при виконанні останнього закриття каналу по запису всі процеси, які очікують читання з програмного каналу (тобто процеси, які звернулися до ядра з системним викликом read і відкладені через брак даних в каналі), активізуються з поверненням коду помилки з системного виклику. (Це абсолютно виправдано у разі неіменованим програмних каналів: якщо достовірно відомо, що більше нічого читати, то навіщо змушувати далі чекати читання. Для іменованих програмних каналів це рішення не є очевидним, але відповідає загальній політиці ОС UNIX про раннє попередження процесів.)

Програмні гнізда (sockets)

Операційна система UNIX з самого початку проектувалася як мережева ОС в тому сенсі, що повинна була забезпечувати явну можливість взаємодії процесів, що виконуються на різних комп'ютерах, з'єднаних мережею передачі даних. Головним чином, ця можливість базувалася на забезпеченні файлового інтерфейсу для пристроїв (включаючи мережеві адаптери) на основі поняття спеціального файлу. Іншими словами, два або більше процесів, що розташовуються на різних комп'ютерах, могли домовитися про спосіб взаємодії на основі використання можливостей відповідних мережевих драйверів.

Ці базові можливості були в принципі достатніми для створення мережевих утиліт; зокрема, на їх основі був створений вихідний в ОС UNIX механізм мережевих взаємодій uucp. Проте організація мережевих взаємодій користувача процесів була скрутна головним чином тому, що при використанні конкретної мережної апаратури і конкретного мережевого протоколу потрібно було виконувати безліч системних викликів ioctl, що робило програми залежними від специфічної мережевого середовища. Був потрібен підтримуваний ядром механізм, що дозволяє приховати особливості цього середовища і дозволити одноманітно взаємодіяти процесам, що виконується на одному комп'ютері, в межах однієї локальної мережі або рознесеним на різні комп'ютери територіально розподіленої мережі. Перше рішення цієї проблеми було запропоновано і реалізовано в UNIX BSD 4.1 у 1982 році (вступну інформацію див в п. 2.7.3).

На рівні ядра механізм програмних гнізд підтримується трьома складовими: компонентом рівня програмних гнізд (незалежних від мережевого протоколу і середовища передачі даних), компонентом протокольного рівня (незалежних від середовища передачі даних) і компонентом рівня управління мережним пристроєм (див. малюнок 3.7).

Основні функції і компоненти ядра ОС UNIX

Рис. 3.7. Одна з можливих конфігурацій програмних гнізд

Допустимі комбінації протоколів і драйверів задаються при конфігурації системи, і під час роботи системи їх міняти не можна. Легко бачити, що за своїм духом організація програмних гнізд близька до ідеї потоків (див. пп. 2.7.1 та 3.4.6), оскільки заснована на поділі функцій фізичного управління пристроєм, протокольних функцій і функцій інтерфейсу з користувачами. Однак це менш гнучка схема, оскільки не допускає зміни конфігурації "на ходу".

Взаємодія процесів на основі програмних гнізд засноване на моделі "клієнт-сервер". Процес-сервер "слухає (listens)" своє програмне гніздо, одну з кінцевих точок двонаправленого шляхи комунікацій, а процес-клієнт намагається спілкуватися з процесом-сервером через інше програмне гніздо, яка є другою кінцевою точкою комунікаційного шляху і, можливо, що розташоване на іншому комп'ютері . Ядро підтримує внутрішні з'єднання і маршрутизацію даних від клієнта до сервера.

Програмні гнізда з загальними комунікаційними властивостями, такими як спосіб іменування та протокольний формат адреси, групуються у домени. Найбільш часто використовуваними є "домен системи UNIX" для процесів, які взаємодіють через програмні гнізда в межах одного комп'ютера, і "домен Internet" для процесів, які взаємодіють в мережі відповідно до сімейством протоколів TCP / IP (див. п. 2.7.2 ).

Виділяються два типи програмних гнізд - гнізда з віртуальним з'єднанням (у початковій термінології stream sockets) і датаграмною гнізда (datagram sockets). При використанні програмних гнізд з віртуальним з'єднанням забезпечується передача даних від клієнта до сервера у вигляді безперервного потоку байтів з гарантією доставки. При цьому до початку передачі даних повинне бути встановлене з'єднання, яке підтримується до кінця комунікаційної сесії. Датаграмною програмні гнізда не гарантують абсолютної надійної, послідовної доставки повідомлень і відсутності дублікатів пакетів даних - датаграм. Але для використання датаграмною режиму не потрібно попереднє дороге встановлення з'єднань, і тому цей режим в багатьох випадках є кращим. Система за замовчуванням сама забезпечує відповідний протокол для кожної допустимої комбінації "домен-гніздо". Наприклад, протокол TCP використовується за умовчанням для віртуальних з'єднань, а протокол UDP - для датаграмною способу комунікацій (інформація про ці протоколи представлена ​​у п. 2.7.2).

Для роботи з програмними гніздами підтримується набір спеціальних бібліотечних функцій (в UNIX BSD це системні виклики, проте, як ми зазначали у п. 2.7.3, в UNIX System V вони реалізовані на основі потокового інтерфейсу TLI). Розглянемо коротко інтерфейси і семантику цих функцій.

Для створення нового програмного гнізда використовується функція socket:

sd = socket (domain, type, protocol);

де значення параметра domain визначає домен даного гнізда, параметр type вказує тип створюваного програмного гнізда (з віртуальним з'єднанням або датаграмною), а значення параметра protocol визначає бажаний мережевий протокол. Зауважимо, що якщо значенням параметра protocol є нуль, то система сама обирає відповідний протокол для комбінації значень параметрів domain і type, це найбільш поширений спосіб використання функції socket. Повертається функцією значення є дескриптором програмного гнізда і використовується у всіх наступних функціях. Виклик функції close (sd) призводить до закриття (знищення) зазначеного програмного гнізда.

Для зв'язування раніше створеного програмного гнізда з ім'ям використовується функція bind:

bind (sd, socknm, socknlen);

Тут sd - дескриптор раніше створеного програмного гнізда, socknm - адреса структури, яка містить ім'я (ідентифікатор) гнізда, що відповідає вимогам домену даного гнізда і використовуваного протоколу (зокрема, для домену системи UNIX ім'я є ім'ям об'єкта у файловій системі, і при створенні програмного гнізда дійсно створюється файл), параметр socknlen містить довжину в байтах структури socknm (цей параметр необхідний, оскільки довжина імені може дуже відрізнятися для різних комбінацій "домен-протокол").

За допомогою функції connect процес-клієнт запитує систему зв'язатися з існуючим програмним гніздом (у процесу-сервера):

connect (sd, socknm, socknlen);

Сенс параметрів такої ж, як у функції bind, проте в якості імені вказується ім'я програмного гнізда, яке повинне знаходитися на іншій стороні комунікаційного каналу. Для нормального виконання функції необхідно, щоб у гнізда з дескриптором sd і у гнізда з ім'ям socknm були однакові домен і протокол. Якщо тип гнізда з дескриптором sd є датаграмною, то функція connect служить тільки для інформування системи про адресу призначення пакетів, які в подальшому будуть надсилатися за допомогою функції send; ніякі дії по встановленню з'єднання в цьому випадку не виробляються.

Функція listen призначена для інформування системи про те, що процес-сервер планує встановлення віртуальних з'єднань через вказане гніздо:

listen (sd, qlength);

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

Щоб відібрати процесом-сервером чергового запиту на встановлення з'єднання з вказаним програмним гніздом служить функція accept:

nsd = accept (sd, address, addrlen);

Параметр sd задає дескриптор існуючого програмного гнізда, для якого раніше була виконана функція listen; address вказує на масив даних, до якого має бути поміщена інформація, що характеризує ім'я програмного гнізда клієнта, з боку якого надходить запит на встановлення з'єднання; addrlen - адреса, по якому знаходиться довжина масиву address. Якщо до моменту виконання функції accept чергу запитів на встановлення з'єднань порожня, то процес-сервер відкладається до надходження запиту. Виконання функції призводить до встановлення віртуального з'єднання, а її значенням є новий дескриптор програмного гнізда, який повинен використовуватися при роботі через дане з'єднання. За адресою addrlen поміщається реальний розмір масиву даних, які записані за адресою address. Процес-сервер може продовжувати "слухати" наступні запити на встановлення з'єднання, користуючись встановленими з'єднанням.

Для передачі і прийому даних через програмні гнізда з встановленим віртуальним з'єднанням використовуються функції send і recv:

count = send (sd, msg, length, flags);

count = recv (sd, buf, length, flags);

У функції send параметр sd задає дескриптор існуючого програмного гнізда з встановленим з'єднанням; msg вказує на буфер з даними, які потрібно послати; length задає довжину цього буфера. Найбільш корисним допустимим значенням параметра flags є значення з символічним ім'ям MSG_OOB, завдання якого означає потребу у позачерговій посилці даних. "Позачергові" повідомлення надсилаються крім нормального для даного з'єднання потоку даних, обганяючи всі непрочитані повідомлення. Потенційний одержувач даних може отримати спеціальний сигнал і в ході його обробки негайно прочитати позачергові дані. Значення, що повертається функції дорівнює числу реально посланих байтів і в нормальних ситуаціях збігається зі значенням параметра length.

У функції recv параметр sd задає дескриптор існуючого програмного гнізда з встановленим з'єднанням; buf вказує на буфер, в який слід помістити прийняті дані; length задає максимальну довжину цього буфера. Найбільш корисним допустимим значенням параметра flags є значення з символічним ім'ям MSG_PEEK, завдання якого призводить до перепису повідомлення в користувальницький буфер без його видалення з системних буферів. Значення, що повертається функції є числом байтів, реально поміщених в buf.

Зауважимо, що у разі використання програмних гнізд з віртуальним з'єднанням замість функцій send і recv можна використовувати звичайні файлові системні виклики read і write. Для програмних гнізд вони виконуються абсолютно аналогічно функціям send і recv. Це дозволяє створювати програми, які не залежать від того, чи працюють вони зі звичайними файлами, програмними каналами або програмними гніздами.

Для здійснення та отримання повідомлень в датаграмною режимі використовуються функції sendto і recvfrom:

count = sendto (sd, msg, length, flags, socknm, socknlen);

count = recvfrom (sd, buf, length, flags, socknm, socknlen);

Сенс параметрів sd, msg, buf і lenght аналогічний змістом однойменних параметрів функцій send і recv. Параметри socknm і socknlen функції sendto задають ім'я програмного гнізда, в яке надсилається повідомлення, і можуть бути опущені, якщо до цього викликалася функція connect. Параметри socknm і socknlen функції recvfrom дозволяють серверу отримати ім'я Отця, що послав повідомлення процесу-клієнта.

Нарешті, для негайної ліквідації встановленого з'єднання використовується системний виклик shutdown:

shutdown (sd, mode);

Виклик цієї функції означає, що потрібно негайно зупинити комунікації або з боку посилає процесу, або з боку приймаючої процесу, або з обох сторін (в залежності від значення параметра mode). Дії функції shutdown відрізняються від дій функції close тим, що, по-перше, виконання останньої "пригальмовується" до закінчення спроб системи доставити вже надіслані повідомлення. По-друге, функція shutdown розриває з'єднання, але не ліквідує дескриптори раніше з'єднаних гнізд. Для остаточної їх ліквідації все одно потрібно виклик функції close.

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

Потоки (streams)

Тут нам майже нічого додати до матеріалу, наведеному в п. 2.7.1. На основі використання механізму потокових мережевих драйверів в UNIX System V створена бібліотека TLI (Transport Layer Interface), що забезпечує транспортний сервіс на основі стеку протоколів TCP / IP. Можливості цього пакету перевищують описані вище можливості програмних гнізд і, звичайно, дозволяють організовувати різноманітні види комунікації процесів. Проте різноманіття і складність набору функцій бібліотеки TLI не дозволяють нам в подробицях описати їх в рамках цього курсу. Крім того, TLI відноситься, скоріше, не до теми ОС UNIX, а до теми реалізацій семиуровневой моделі ISO / OSI. Тому в разі потреби ми рекомендуємо користуватися технічною документацією по використовуваному варіанту ОС UNIX або читати спеціальну літературу, присвячену мережним можливостям сучасних версій UNIX.


Додати в блог або на сайт

Цей текст може містити помилки.

Програмування, комп'ютери, інформатика і кібернетика | Реферат
233.2кб. | скачати


Схожі роботи:
Основні роботи операційної системи UNIX Підтримка мережі UNIX
Основні ядра мосту
Основні компоненти НД
Основні компоненти їжі
Національна культура і е основні компоненти
Загальні вимоги та основні компоненти СКУД
Архітектура та основні компоненти персонального комп`ютера
Основні компоненти педагогічної характеристики соціалізації школяра
Основні компоненти системи управління документообігом СУД
© Усі права захищені
написати до нас