Інструменти програмування в ядрі частина 73

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

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

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

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

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

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

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

Семафори (м'ютекси)

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

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

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

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

  1. у захопленого мютекса завжди буде єдиний власник, який захопив його;
  2. звільнити блоковані на мютексе потоки (звільнити мютекс) може тільки один володіє мютексом потік.

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

Статична визначення та ініціалізація семафорів виконується макросом:

Для створення взаємовиключної блокування (mutex) є більш короткий синтаксис:

- де в обох випадках name - це ім'я змінної типу семафор.

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

А для ініціалізації бінарних семафорів (мютексов) використовуються макроси:

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

  • void down (struct semaphore * sem) - переводить завдання в блоковане стан очікування з прапором TASK_UNINTERRUPTIBLE. У більшості випадків це небажано, так як процес, який очікує звільнення семафора, не відповідатиме на сигнали.
  • int down_interruptible (struct semaphore * sem) - виконує спробу захопити семафор. Якщо ця спроба невдала, то завдання переводиться в блоковане стан з прапором TASK_INTERRUPTIBLE (в структурі завдання). Такий стан процесу означає, що завдання може бути повернуто до виконання за допомогою сигналу, а така можливість звичайно дуже цінна. Якщо сигнал приходить в той час, коли завдання блокована на семафорі, то завдання повертається до виконання, а функція down_interruptible () повертає значення - EINTR.
  • int down_trylock (struct semaphore * sem) - використовується для неблокуючим захоплення семафора. Якщо семафор уже захоплений, то функція негайно повертає нульове значення. У разі успішного захоплення семафора повертається нульове значення і захоплюється блокування.
  • int down_timeout (struct semaphore * sem, long jiffies) - використовується для спроби захоплення семафора протягом інтервалу часу jiffies системних тиків.

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

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

  • контекст виконання не дозволяє переходити в блоковане стан (контекст переривання);
  • або потрібно короткочасна блокування без перемикання контексту.

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

Для ініціалізації spinlock_t і родинного типу rwlock_t. про яке буде детально розказано нижче, раніше (і в літературі) використовувалися макроси:

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

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

Основний інтерфейс spinlock_t містить пару викликів для захоплення і звільнення блокування:

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

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

Рекурсивний захоплення спін-блокування може неявно статися в обробнику переривань, тому перед захопленням такого блокування потрібно заборонити переривання на локальному процесорі. Це загальний випадок, тому для нього надається спеціальний інтерфейс:

Для спін-блокування визначені ще такі виклики, як:

  • int spin_try_lock (spinlock_t * sl) - спроба захоплення без блокування, якщо блокування вже захоплена, функція поверне нульове значення;
  • int spin_is_locked (spinlock_t * sl) - повертає нульове значення, якщо блокування в даний момент захоплена.

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

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

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

Для семафорів замість структури struct semaphore вводиться структура struct rw_semaphore. а набір інтерфейсних функцій для захоплення / звільнення (прості down () / up ()) розширюється до:

  • down_read ( rwsem) - спроба захопити семафор для читання;
  • up_read ( rwsem - звільнення семафора для читання;
  • down_write ( rwsem) - спроба захопити семафор для запису;
  • up_write ( rwsem) - звільнення семафора для запису;

Семантика цих операцій наступна:

  • якщо семафор ще не захоплений, то будь-який захоплення (down_read () або down_write ()) буде успішним (без блокування);
  • якщо семафор захоплений вже для читання. то наступні спроби захоплення семафора для читання (down_read ()) будуть завершуватися успішно (без блокування), але запит на захоплення такого семафора для запису (down_write ()) закінчиться блокуванням;
  • якщо семафор захоплений вже для запису. то будь-яка подальша спроба захоплення семафора (down_read () або down_write ()) закінчиться блокуванням;

Статично визначений семафор читання-запису створюється макросом:

Семафори читання-запису, які створюються динамічно, повинні бути ініційовані за допомогою функції:

Примітка. З опису ініціалізації видно, що семафори читання-запису є виключно бінарними (НЕ рахунковими), тобто (в термінології Linux) практично не семафора, а мютексамі.

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

Точно так же, як це зроблено для семафорів, вводиться і блокування читання-запису для спін-блокування:

З набором операцій:

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

Також, блокування, захоплену для читання, вже не можна далі підвищити до блокування, захопленої для запису .:

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

Але кілька потоків читання можуть безпечно захоплювати одну і ту ж блокування читання-запису, тому один потік також може безпечно рекурсивно захоплювати одну і ту ж блокування для читання кілька разів, наприклад в обробнику переривань без заборони переривань.

висновок

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

Отримати продукти і технології

  • Випробуйте програмне забезпечення від IBM. Завантажуйте пробні версії, використовуйте online демо-версії, працюйте з продуктами в sandbox або хмарної середовищі. Вам доступні для вивчення понад 100 продуктів від IBM.

Схожі статті