Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Report.docx
Скачиваний:
12
Добавлен:
05.03.2016
Размер:
976.3 Кб
Скачать

3.1 Теоретична частина.

Питання №19. Механізми синхронізації процесів (потоків), що використовуються в ОС Linux: перерахувати, вказати загальний принцип роботи, переваги та недоліки.

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

Процес - це об'єкт ОС Linux, який складається з адресного простору пам'яті і набору структур даних. По суті, процес це запущена програма або служба.

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

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

Кожен процес ОС Linux характеризується набором атрибутів, який відрізняє даний процес від всіх інших процесів. пК таких атрибутів відносяться:

* Ідентифікатор процесу (PID) . Кожен процес в системі має унікальний ідентифікатор. Кожен новий запущений процес отримує номер на одиницю більше ппредыдущего.

* Ідентифікатор батьківського процесу (PPID). Даний атрибут процес отримує під час свого запуску і використовується для отримання статусу батьківського процесу.

* Реальний і ефективний ідентифікатори користувача (UID,EUID) і групи (GID, EGID). Дані атрибути процесу говорять про його приналежність до конкретного користувачеві і групі. Реальні ідентифікатори збігаються з ідентифікаторами користувача, який запустив процес, і групи, до якої він належить. Ефективні - від чийого імені був запущений процес. Права доступу процесу до ресурсів ОС Linux ефективними ідентифікаторами. Якщо на виконуваному файлі програми встановлено спеціальний біт SGID або SUID, то процес даної програми буде володіти правами доступу власника виконуваного файлу. Для управління процесом (наприклад, kill) використовуються реальні ідентифікатори. Всі ідентифікатори передаються від батьківського до дочірнього процесу. Для перегляду даних атрибутів можна скористатися командою ps, задавши бажаний формат відображення колонок.

* Пріоритет або динамічний пріоритет (priority) і відносний або статичний (nice) пріоритет процесу. Статичний пріоритет або nice-пріоритет лежить в діапазоні від -20 до 19, типово використовується значення 0. Значення -20 відповідає найбільш високого пріоритету, nice-пріоритет не змінюється планувальником, він успадковується від батьків або його вказує автор. Динамічний пріоритет використовується планувальником для планування виконання процесів. Цей пріоритет зберігається в полі prio структури task_struct процесу. Динамічний пріоритет обчислюється виходячи зі значення параметра пісе для даної задачі шляхом обчислення надбавки або штрафу, залежно від інтерактивності завдання. Користувач має можливість змінювати тільки статичний пріоритет процесу. При цьому підвищувати пріоритет може тільки root. В ОС Linux існують дві команди управління пріоритетом процесів: nice і renice.

* Стан процесу. В ОС Linux кожен процес обов'язково знаходиться в одному з перерахованих нижче станів і може бути переведений з одного стану в інше системою або командами користувача. Розрізняють наступне стану процесів:

TASK_RUNNING - процес готовий до виконання або виконується (runnable). Позначається символом R.

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

TASK_STOPPED - виконання процесу зупинено (stopping). Будь-який процес можна зупинити. Це може робити як система, ptak і користувач. Стан такого процесу позначається символом Т.

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

TASK_UNINTERRUPTIBLE -неперерваний процес (uninterruptible). Процеси в цьому стані очікують завершення операції введення - виведення з прямим доступом в пам'ять. Такий процес не можна завершити, поки не завершиться операція введення/виводу. Процеси з таким станом позначаються символом D. Стан аналогічно TASK_INTERRUPTIBLE, за винятком того, що процес не відновлює виконання при отриманні сигналу. Використовується у випадку, коли процес повинен чекати безперервно або коли очікується, що певна подія може виникати досить часто. Так як завдання в цьому стані не відповідає пна сигнали, TASK_UNINTERRUPTIBLE використовується менш часто, ніж TASK_INTERRUPTIBLE.

Типи процесів в Linux процеси поділяються на три типи:

  • Системні процеси - є частиною ядра і завжди розташовані в оперативній пам'яті. Системні процеси не мають псоответствующих їм програм у вигляді виконуваних файлів і запускаються при ініціалізації ядра системи. Виконувані інструкції і дані пэтих процесів знаходяться в ядрі системи, таким чином, вони можуть викликати функції і звертатися до даних, недоступним для інших ппроцессов. Системними процесами, наприклад, є: shed (диспетчер підкачки), vhand (диспетчер сторінкового заміщення), kmadaemon (диспетчер пам'яті ядра).

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

  • Прикладні (користувача) процеси. пК прикладних процесів відносяться всі інші процеси, запущені в системі. Як правило, це процеси, породжені в рамках користувальницького сеансу роботи. Наприклад, команда ls породить відповідний процес цього типу. Найважливішим прикладним процесом є командний інтерпретатор (shell), який забезпечує вашу роботу в LINUX. Він запускається відразу ж після реєстрації в системі. пПрикладные процеси linux можуть виконуватися як в інтерактивному, так і у фоновому режимі, але в будь-якому випадку час їх життя (і виконання) обмежено сеансом роботи користувача. При виході з системи всі прикладні процеси будуть знищені.

Ієрархія процесів

В Linux реалізована чітка ієрархія процесів в системі. Кожен процес в системі має всього одного з батьків і може мати один або пболее породжених процесів.

На останній фазі завантаження ядро підіймається кореневу файлову систему і формує середовище виконання нульового процесу, створюючи простір ппроцесса, инициализируя нульову точку входу в таблиці процесу і роблячи кореневий каталог поточним для процесу. Коли формування середовища виконання процесу закінчується, система виповнюється вже у вигляді нульового процесу. Нульовий процес "гілкується", запускаючи fork прямо з ядра, оскільки сам процес виконується в режимі ядра. Код, що виконується породженим процесом 1, включає в себе виклик системної функції exec, запускі на виконання програму з файлу/etc/init". На відміну від нульового процесу, який є процесом системного рівня, що виконуються в режимі ядра, процес 1 відноситься до користувача рівня. Зазвичай процес 1 іменується процесом init, оскільки він відповідає за ініціалізацію нових процесів. пНа самом деле ви можете помістити будь-яку програму в /sbin/init і ядро запустить її як тільки закінчить завантажуватися. пЗадачей init"а є запуск всього іншого потрібним чином.

Init читає файл /etc/inittab, в якому містяться інструкції для подальшої роботи. Першою інструкцією, звичайно, є запуск сценарію ініціалізації. У системах, заснованих на Debian, скриптом ініціалізації буде /etc/init.d/rcS, у Red Hat - /etc/rc.d/rc.sysinit. Це те місце, де відбувається перевірка і монтування файлових систем (/etc/fstab), установка годин системного часу, включення своп розділу, присвоєння імені хоста і т.д. пДалее буде викликаний наступний скрипт, який переведе нас на "рівень запуску" за замовчуванням. це означає просто деякий набір демонів, які повинні бути запущені.

Syslogd (/etc/init.d/syslogd) - скрипт, який відповідає за запуск і зупинка системного логгера (система журнальної реєстрації подій SYSLOG, дозволяє записувати системні повідомлення у файли журналів /var/log).

Xined-Демон Інтернет-служб, управляє сервісами для інтернету. Демон прослуховує сокети і якщо в якомусь з них є повідомлення попределяет якого сервісу належить даний сокет і викликає відповідну програму для обробки запиту.

crond - Демон cron відповідає за перегляд файлів crontab і виконання, внесених до нього команд у вказаний час для опредленного ппользователя. Програма crontab(1) спілкується з crond через файл cron.update, який повинен знаходитись разом з рештою файлів каталогу crontab, як правило - /var/spool/cron/crontabs.

Останнім важливим дією init є запуск деякої кількості getty. Mingetty - віртуальні термінали, призначенням яких пявляется спостереження за консолями користувачів.

getty запускає програму login - початок сеансу роботи користувача в системі. Завдання login"а - реєстрація користувача в системі. пА вже після успішної реєстрації найчастіше завантажуватись командний інтерпретатор користувача (shell), наприклад, bash, вірніше після реєстрації користувача вантажиться програма, вказана для даного користувача в файлі /etc/passwd (у більшості випадків це bash).

Запуск процесів

Існує два шляхи запуску процесів в залежності від типу процесу.

Для користувача процесів запуск здійснюється в інтерактивному режимі шляхом введення довільній команди або запуску ппроизвольного скрипта. Для системних процесів і демонів використовуються инициализационные скрипти (init-скрипти). Дані скрипти писпользуется процесом init для запусків інших процесів при завантаженні ОС. Инициализационные скрипти зберігаються у каталозі /etc. У даному пкаталоге існують підкаталоги, іменовані rcO.d - rc6.d, кожен з яких асоційований з певним рівнем виконання (runlevel). У кожному з цих каталогів знаходяться символьні посилання на инициализационные скрипти, що знаходяться безпосередньо в каталозі /etc/rc.d/init.d .

Слід зауважити, що в каталозі /etc/init.d присутні жорсткі посилання на скрипти каталогу /etc/rc.d/init.d, тому при зміні пскриптов в цих каталогах змінені дані відображаються однаково незалежно від шляху до файлу скрипта.

 Перегляд init-скриптів

Bee init-скрипти можливо повторно запускати або зупиняти, тим самим керуючи статусом сервісу, до якого вони належать. пЗапуск даних скриптів здійснюється з командного рядка і має наступний синтаксис:

/etc/init.d/script-name start|stoplrestart|condrestart|status|reload

Тут як script-name використовується конкретне ім'я init-скрипта, а в якості аргументів можуть виступати наступні значення:

* start (Запуск сервісу);

* stop (Зупинка сервісу);

* restart (Зупинка і подальший запуск сервісу);

* condrestart (Умовна зупинка і наступний запуск сервісу);

* status (Отримання статусу стану сервісу);

* reload (Повторне зчитування конфігураційного файлу сервісу).

Наприклад, для умовного перезапуску сервісу sshd використовується наступна команда:

[root@rhe!5 ~]# /etc/init.d/sshd condrestart

Stopping sshd: [ ok ]

Starting sshd: [ ok ]

Условний перезапуск сервісу sshd.

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

В ОС Linux для управління сервісами, окрім безпосереднього звернення до файлу init-скрипта, існує спеціальна команда service (другий спосіб), в якості аргументу якій необхідно вказати аргументи аналогічні тим, що використовуються при безпосередньому пзапуске демонів через init-скрипты:

[root@rhel5 ~]# service sshd reload

Reloading sshd: [ ok ]

Використання команди service.

В даному прикладі здійснюється повторне зчитування конфігураційного файлу сервісу sshd.

Однак керувати демонами у більшості випадків може тільки root.

Команди запуску процесів

Команда nice використовується для запуску ще не запущених процесів з заданим пріоритетом.

Команда renice використовується для зміни пріоритету вже запущених процесів.

 Моніторинг процесів

Для перегляду запущених процесів в ОС Linux використовуються утиліти

  • - Вивести список процесів

  • ps - Інтерактивно спостерігати за процесами (в реальному часі)

  • uptime - Подивитися завантаження системи

  • w, Вивести список активних процесів для всіх користувачів

  • free - Вивести обсяг вільної пам'яті

  • pstree - Відображає всі запущені процеси у вигляді ієрархії

При виконанні top у верхній частині вікна відображається астрономічне час, що минув з моменту запуску системи, пчисло користувачів в системі, кількість запущених процесів і кількість процесів, що знаходяться в різних станах, пданные про використання ЦП, пам'яті і свопу. А далі йде таблиця, що характеризує окремі процеси. пЧисло рядків, що відображаються в цій таблиці, визначається розміром вікна: скільки рядків поміщається, стільки і виводиться. пСодержимое вікна оновлюється кожні 5 секунд. Список процесів може бути відсортований за свого часу ЦП (за замовчуванням), ппо використання пам'яті, за PID, за часом виконання. Перемикати режими відображення можна за допомогою команд, які програма top сприймає. Це наступні команди (просто натискайте відповідні клавіші, тільки з урахуванням регістру, тобто разом з клавішею Shift):

* Shift+N - сортування по PID;

* Shift+A - сортувати процеси за віком;

* Shift+P - сортувати процеси щодо використання ЦП;

* Shift+M - сортувати процеси з використання пам'яті;

* Shift+T - сортування по часу виконання.

Кроме команд, що визначають режим сортування, команда top сприймає ще ряд команд, дозволяють управляти процесами в інтерактивному режимі. пС допомогою команди можна завершити деякий процес (його PID буде запитано), а за допомогою команди R> <можна ппереопределить значення nice для деякого процесу. Таким чином, ці дві команди аналогічні командам kill і renice.

Вивід команди ps схожий з висновком команди top, однак він відображає статичний пснимок процесів.

За умовчанням, команда ps виводить тільки інформацію про процеси, запущені в поточній сесії інтерпретатора командного bash. пДля виводу інформації з усіх процесів необхідно ввести команду ps з ключем-е. Для відображення бажаних полів необхідно пввести пкоманду ps з ключем-про поле1, поле2,... , де перераховуються через кому поля, які необхідно відобразити.

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

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

До команд управління процесами відносяться команди nice renice , описані вище, а також:

  • kill Завершити процес (або послати йому сигнал).

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

  • killall Завершити процес по імені.

  • pgrep - Переглядає запущені процеси, і виводить на стандартний вивід. псписок ідентифікатори процесів, які відповідають критеріям відбору. Всі критерії повинні збігатися.

  • sleep - Зупиняє виконання на вказану КІЛЬКІСТЬ секунд.

  • fuser - Определенея того, який процес тримає відкритим якийсь файл або сокет.

команда kill посилає сигнал процесу з вказаним ідентифікатором (pid). Використовується в наступній формі:

kill [номер] pid

Тут pid - це ідентифікатор процесу, яким надсилається сигнал, а номер - номер сигналу, який надсилається процесу. пПослать сигнал (якщо у вас немає повноважень суперкористувача) можна тільки процесу, у якого ефективний ідентифікатор користувача псовпадает з ідентифікатором користувача, посилає сигнал. Якщо параметр-номер відсутній, то надсилається сигнал SIGTERM, пзазвичай має номер 15, і реакція на нього за замовчуванням - завершити роботу процесу, який отримав сигнал. (детальніше див. kill).r

Щоб завершити який небудь процес, потрібно надіслати йому сигнал за допомогою команди kill. Для цього необхідно пузнать Pid процесу за допомогою команди ps (наприклад, Pid процесу дорівнює 11839) та надіслати процесу сигнал на завершення, наприклад сигнал SIGKILL:

kill -9 11839

або kill-SIGKILL 11839

або kill-KILL 11839

Що ж таке сигнали?

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

насамперед, кожен сигнал має власне ім ’ я та номер. Імена всіх сигналів починаються з послідовності SIG. пНапример, SIGALRM - генерується, коли таймер, встановленої функцією alarm(), відміряє вказаний проміжок часу. Linux підтримує 31 сигнал (номери від 1 до 31).

Сигнали можуть породжуватися різними умовами:

1. Генеруватися терміналом, при натисканні певної комбінації клавіш, наприклад, натискання Ctrl+C генерує сигнал SIGINT, таким чином можна перервати виконання програми, яка вийшла з під контролю;

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

3. Іншим процесом (у тому числі і ядром і системним процесом), виконали системний виклик передачі сигналу kill();

4. При виконанні команди kill.

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

В разі отримання сигналу процес може запросити ядро виконати одну з трьох реакції на сигнал:

1. Примусово проігнорувати сигнал (практично будь-який сигнал може бути проігноровано, крім SIGKILL і SIGSTOP).

2. Провести обробку сигналу за замовчуванням: проігнорувати, зупинити процес, перевести в стан очікування до отримання пдругого спеціального сигналу або завершити роботу.

3. Перехопити сигнал (виконати обробку сигналу, специфіковану користувачем).

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

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

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

  • jobs - Перераховує ваші завдання.

  • & - Виконати завдання у фоновому режимі.

  • Ctrl+Z - Призупинити виконання поточної (інтерактивної) завдання.

  • suspend - Призупинити командний процесор.

  • fg Перевести завдання в інтерактивний режим виконання.

  • bg Перевести припинену завдання у фоновий режим виконання.

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

Завдання (job) - це просто робоча одиниця пкомандного процесора.

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

  • інтерактивне завдання (foreground job) - що Виконується в пкомандном процесорі, що займає сеанс командного процесора, ptak що ви не можете виконати іншу команду.

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

  • призупинити (suspend) - Тимчасово призупинити інтерактивний процес.

  • відновити (resume) - Повернутися до вьполнению discontinued завдання.

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

якщо ви запустили з командного процесора команду пинтерактивном режимі і хочете негайно припинити виконання команди, введіть Ctrl-З. Командний процесор сприйме Ctrl-З як "зупинити виконання поточної задачі негайно". Тому, пякщо ви виводите дуже довгий файл (скажімо, командою cat) та пхочете зупинити висновок, натисніть Ctrl-З. Насправді поточної задачі ппо натискання Ctrl-З відправиться сигналу SIGINT.

Щоб припинити виконання програми, що працює в фоновом режимі, ви можете перевести її в інтерактивний режим з допомогою команди fg і потім натиснути Ctrl-З, або використовувати команду kill.

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

Синхронізаційні механізми поділяють на такі основні категорії:

універсальні, низького рівня, які можна використовувати різними способами (семафори);

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

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

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

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

Семафор — це спільно використовуваний невід'ємний цілочисловий лічильник, для якого задано початкове значення і визначено такі атомарні операції.

Зменшення семафора (down): якщо значення семафора більше від нуля, його зменшують на одиницю, якщо ж значення дорівнює нулю, цей потік переходить у стан очікування доти, поки воно не стане більше від нуля (кажуть, що потік «очікує на семафорі» або «заблокований на семафорі»). Цю операцію називають також очікуванням — wait.

Збільшення семафора (up): значення семафора збільшують на одиницю; коли при цьому є потоки, які очікують на семафорі, один із них виходить із очікування і виконує свою операцію down. Якщо на семафорі очікують кілька потоків, то внаслідок виконання операції up його значення залишається нульовим, але один із потоків продовжує виконання (у більшості реалізацій вибір цього потоку буде випадковим). Цю операцію також називають сигналізацією — post.

Фактично значення семафора визначає кількість потоків, що може пройти через цей семафор без блокування. Коли для семафора задане нульове початкове значення, то він блокуватиме всі потоки доти, поки якийсь потік його не «відкриє», виконавши операцію up. Операції up і down можуть бути виконані будь-якими потоками, що мають доступ до семафора.

Окремий випадок: якщо семафор може приймати лише значення 0 і 1 (двійковий семафор), він фактично є змінною блокування.

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

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

М'ютекс реалізує взаємне виключення. Його основне завдання — блокувати всі потоки, які намагаються отримати доступ до коду, коли цей код уже виконує деякий потік.

М'ютекс може перебувати у двох станах: вільному і зайнятому. Початковим станом є «вільний». Над м'ютексом можливі дві атомарні операції.

1. Зайняти м'ютекс (mutex_lоск): якщо м'ютекс був вільний, він стає зайнятим, і потік продовжує своє виконання (входячи у критичну секцію); якщо м'ютекс був зайнятий, потік переходить у стан очікування (кажуть, що потік «очікує на м'ютексі», або «заблокований на м'ютексі»), виконання продовжує інший потік.

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

Поняття умовної змінної

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

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

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

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

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

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

Ідея монітора була вперше запропонована в 1974 році відомим ученим у галузі комп'ютерних наук Ч. А. Хоаром. Монітор часто розуміють як високорівневу конструкцію мови програмування (як приклад такої мови звичайно наводять Java), а саме як набір функцій або методів класу, всередині яких автоматично зберігається неявний загальний м'ютекс разом із операціями очікування і сигналізації. Насправді, як ми бачимо, концепція монітора може ґрунтуватися на базових примітивах — м'ютексах і умовних змінних — і не повинна бути обмежена якоюсь однією мовою.

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

У моніторах Хоара після сигналізації потік Ts негайно припиняють, і керування переходить до потоку Tw, який при цьому захоплює блокування. Коли потік Tw вийде із критичної секції або знову виконає операцію очікування, потік Ts буде поновлено.

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

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

Ці недоліки призводять до того, що на практиці використовують переважно MESA-монітори.

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

Блокування читання-записування — це синхронізаційний примітив, для якого визначені два режими використання: відкриття для читання і відкриття для записування. При цьому повинні виконуватися такі умови:

будь-яка кількість потоків може відкривати таке блокування для читання, коли немає жодного потоку, що відкрив його для записування;

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

Такі блокування корисні для даних, які зчитуються частіше, ніж модифікуються (наприклад, більшість СУБД реалізує блокування такого роду для забезпечення доступу до бази даних).

Типи блокувань

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

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

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

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

Операції блокувань

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

Відкриття для читання (rwl ock_rd1 ock). Якщо є потік, який відкрив блокування для записування, поточний потік припиняють. Якщо такий потік відсутній:

- у разі кращого читання блокування відкривають для читання і потік продовжує своє виконання;

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

Відкриття для записування (rwlock_wrlock). Якшо є потік, який відкрив блокування для читання або записування, поточний потік припиняють; коли жодного такого потоку немає, блокування відкривають для записування і потік продовжує своє виконання.

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

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

Механізми синхронізації ядра Linux

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

Аналогом потоків у ядрі виступають шляхи передачі керування ядра (kernel control paths). Таким шляхом є послідовність інструкцій, виконуваних ядром для реалізації реакції на системний виклик або обробку переривання. Така послідовність звичайно зводиться до виконання кількох функцій ядра. Наприклад, для обробки системного виклику шлях передачі керування починають з виклику функції system_cal 1 () і завершують викликом ret_systefli_call (). Надалі іноді говоритимемо не про шляхи керування, а про процеси в режимі ядра, маючи на увазі шляхи передачі керування, що відповідають системним викликам, виконаним процесами.

Витісняльність ядра

До останнього часу ядро Linux належало до категорії невитісняльних (nonpre-emptive). Це означало, що процес, виконуваний в режимі ядра, не міг бути призупинений (витиснений іншим процесом), поки він сам не вирішить віддати керування. У разі переривання керування після виклику обробника мало бути повернуте в той самий процес.

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

Необхідність синхронізації у ядрі

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

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

- у коді, виконуваному на іншому процесорі;

- у коді, що витиснув розглядуваний (у разі витісняльного ядра).

Для забезпечення коректної роботи потрібно завжди забезпечувати синхронізацію доступу до цих структур. Розглянемо механізми такої синхронізації.

Заборона переривань

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

Атомарні операції

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

Спін-блокування

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

Семафори ядра

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

Блокування читання-записування

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

Синхронізація процесів користувача у Linux. Ф'ютекси

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

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

Використання для операції очікування аналогів системного виклику sleep() і реалізація операції поновлення потоків на основі сигналів. Цей підхід було прийнято у бібліотеці LinuxThreads. Застосування сигналів знову передбачало використання системних викликів, а виконання sleep() призводило до втрат часу на перемикання контексту.

На практиці найкращим є вирішення, яке використовує системні виклики тільки в разі гострої необхідності й обходиться без перемикання контексту. Вирішення, що відповідає цим вимогам, реалізує швидке блокування режиму користувача (fast user-level locking).

Розглянемо реалізацію такого блокування для двох випадків.

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

Є потік, що вже зайняв це блокування. У цьому разі можна виконати системний виклик, щоб призупинити поточний потік. Така ситуація виникає рідше, і втрати продуктивності будуть менші.

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

Реалізація такого блокування була інтегрована у ядро Linux версії 2.6. Вона дістала назву ф'ютекс (futex, від fast user-level mutex — швидкий м'ютекс користувача).

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

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

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

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

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

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

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

Використана література

  1. Шеховцев В.А. Операційні системи. – К.:Видавнича група BHV, 2005.- 576с.: іл.

  2. Таненбаум Э., Вудхалл А.. Операционные системы. Разработка и реализация. Класика CS. 3-е изд – СПб.: Питер 2007. – 704 с.: ил.

  3. Інтернет ресурс - http://wiki.kspu.kr.ua

  4. А.В. Гордеев "Системное програмное обеспечение" Питер 2003.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]