Засоби синхронізації в ядрі – ЧАСТИНА 2

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

Атомарні операції, які обговорюються в цьому розділі, гарантують тільки атомарность Порядок виконання гарантується за допомогою операційбарєрів (barrier), які будуть розглянуті далі в поточній чолі

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

Бітові атомарні операції

На додаток до атомарним операціях з цілими числами, ядро ​​також надає сімейство функцій, які дозволяють працювати на рівні окремих бітів Не дивно, що ці операції залежать від апаратної платформи і визначені у файлі

Проте може викликати подив те, що функції, які реалізують бітові операції, працюють із звичайними адресами памяті Аргументами функцій є покажчик і номер біта Біт 0 – це найменш значущий біт числа, яке знаходиться за вказаною адресою На 32-розрядних машинах біт 31 – це найбільш значущий біт, а біт 0 – найменш значущий біт машинного слова Немає обмежень на значення номера біта, яке передається в функцію, хоча більшість користувачів працюють з машинними словами і номерами бітів від 0 до 31 (або до 63 для

64-бітових машин)

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

unsigned long word = 0

set_bit (0, & word) / * Атомарне встановлюється біт 0 * / set_bit (l, & word) / * Атомарне встановлюється біт 1 * / printk (% ul \ n, word) / * Буде надруковано З * / Clear_bit (1, & Word) / * Атомарне очищається біт 1 * /

change_bit (0, & word) / * Атомарне змінюється значення біта 1,

тепер він очищений * /

/ * Атомарне встановлюється біт нуль і повертається попереднє значення цього біта (нуль) * /

if (test_and_set_bit(0, &ampword)) {

/ * Умова ніколи не виконається .. * /

}

Список стандартних атомарних бітових операцій наведено в табл 92

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

Таблиця 92 Список стандартних атомарних бітових операцій

Атомарна битовая операція Опис

void set_bit (int nr, void * addr) атомарний встановити nr-й біт в області памяті, яка починається з адреси add r

void clear_bit (int nr, void * addr) атомарний очистити nr-й біт в області памяті, яка починається з адреси add r

void change_bit (in t nr, void * addr) атомарний змінити значення nr-го біта в області памяті, яка починається з адреси addr, на Інвертований

in t test_and_set_bit (in t nr, voi d * addr) атомарний встановити значення nr-р про біта в області памяті, яка починається з адреси addr,

і повернути попереднє значення цього біта

in t test_and_clear_bit (int nr, void * addr) атомарний очистити значення nr-го біта в області памяті, яка починається з адреси addr, і повернути попереднє значення цього біта

in t test_and_change_bit (in t nr, void * addr) атомарний змінити значення nr-го біта в області памяті, яка починається з адреси addr, на Інвертований і повернути попереднє значення цього біта

int test_bit (int nr, void * addr) атомарний повернути значення nr-го біта в області памяті, яка починається з адреси add r

Звідки беруться неатомарние бітові операції

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

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

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

Іноді може вимагатися саме така поведінка, особливо якщо критичний порядок виконання

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

int find_first_bit(unsigned long *addr, unsigned int size)

int find_first_zero_bit(unsigned  long *addr, unsigned int size)

Обидві функції як перший аргумент приймають покажчик на область памяті і як другий аргумент – кількість бітів, за якими буде проводитись пошук Ці функції повертають номер першого встановленого або не встановленої біти відповідно Якщо код проводить пошук в одному машинному слові, то оптимальним рішенням буде використовувати функції    ffs () і _ffz (), які в якості єдиного параметра приймають машинне слово, де буде проводитись пошук

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

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

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

Найбільш часто використовуваний тип блокування в ядрі Linux це спін-блокування (spin lock) Спін-блокування – це блокування, яку може утримувати не більш ніж один потік виконання Якщо потік виконання намагається захопити блокування, яка знаходиться в стані конфлікту (contended),тобто вже захоплена, потік починає виконувати постійну циклічну перевірку (busy loop) – Обертатися (Spin),очікуючи на звільнення блокування Якщо блокування не знаходиться в стані конфлікту при захопленні, то потік може відразу ж захопити блокування і продовжити виконання Циклічна перевірка запобігає ситуацію, в якій більше одного потоку одночасно може перебувати в критичному ділянці Слід зауважити, що одна і та ж блокування може використовуватися в декількох різних місцях коду, і при цьому завжди буде гарантований захист і синхронізація при доступі, наприклад, до якої-небудь структурі даних

Той факт, що спін-блокування, яка знаходиться в стані конфлікту, змушує потоки, які очікують на звільнення цього блокування, виконувати замкнутий цикл (і, відповідно, витрачати процесорний час), є важливим Нерозумно утримувати спін-блокування протягом тривалого часу За своєю суттю спін-блокування – це швидка блокування, яка повинна захоплюватися на короткий час одним потоком Альтернативним є поведінка, коли при спробі захопити блокування, яка знаходиться в стані конфлікту, потік переводиться в стан очікування і повертається до виконання, коли блокування звільняється У цьому випадку процесор може почати виконання іншого коду Така поведінка вносить деякі накладні витрати, основні з яких – це два перемикання контексту Спочатку перемикання на новий потік, а потім зворотне перемикання на заблокований потік Тому розумним буде використовувати спін-блокування, коли час утримання цього блокування менше тривалості двох перемикань контексту Так як у більшості людей є цікавіші заняття, ніж вимір часу перемикання контексту, то необхідно намагатися утримувати блокування по можливості протягом максимально короткого періоду часу1 У наступному розділі будуть описані семафори (semaphore) – механізм блокувань, який дозволяє переводити потоки, які очікують на звільнення блокування, в стан очікування, замість того щоб періодично перевіряти, не звільнилася чи є блокування, що знаходиться в стані конфлікту

Спін-блокування япляется залежними від апаратної платформи і реалізовані на мові асемблера Залежний від апаратної платформи код визначений у заголовному файлі Інтерфейс користувача визначений у файлі

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

spinlock_t mr_lock = SPIN_LOCK_UNLOCKED

spin_lock (&ampmr_lock)

/* критичний ділянку .. */

spin_unlock(&ampmr_lock)

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

1 Сейча з пов про требовани е становитс я ще більш е важливим, так як ядр про являетс я преемптівним Час, протягом е которог про утримуються блокування, еквівалентну про часів і затримки (Латентпості) системного планувальника

Увага: спін-блокування не рекурсивного

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

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

Ядро надає інтерфейс, який зручним способом дозволяє заборонити переривання і захопити блокування Використовувати його можна таким чином

spinlock_t  mr_lock = SPIN_LOCK_UNLOCKED unsigned long flags spin_lock_irqsave(&ampmr_lock, flags)

/ * Критичний ділянку .. * /

spin_unlock_irqre_store(&amprnr_lock,  flags)

Підпрограма spin_lock_irqsav e () зберігає поточний стан системи переривань, забороняє переривання і захоплює зазначену блокування Функція spin_unlock_irqrestor e (), навпаки, звільняє зазначену блокування і відновлює попередній стан системи переривань Таким чином, якщо переривання були заборонені, показаний код не дозволить їх помилково Зауважимо, що змінна flag s передається за значенням Це тому, що зазначені функції частково виконані у вигляді макросів

На однопроцессорной машині показаний приклад тільки лише заборонить переривання, щоб запобігти доступ обробника переривання до спільно використовуваних даних, а механізм блокувань скомпільовано НЕ буде Функції захоплення і звільнення блокування також відповідно забороняють і дозволяють преемптівность ядра

Що необхідно блокувати

Важливо, щоб кожна блокування було чітко повязана з тим, що вона блокує Ще більш важливо – це захищати дані, а не кодНезважаючи на те що у всіх прикладах цієї глави розглядаються критичні ділянки, в основі цих критичних ділянок лежать дані, які вимагають захисту, а ніяк не код Якщо блокування просто блокують ділянки коду, то такий код важкозрозумілі і схильний станам гонок Необхідно асоціювати дані з відповідними блокуваннями Наприклад, структура struc t fo o блокується за допомогою блокування foo_lock З даної блокуванням також необхідно асоціювати деякі дані Якщо до деяких даних здійснюється доступ, то необхідно гарантувати, що цей доступ буде безпечним Найбільш часто це означає, що перед тим, як здійснити маніпуляції з даними, необхідно захопити відповідну блокування і звільнити цю блокування потрібно після завершення маніпуляцій

Якщо точно відомо, що переривання дозволені, то немає необхідності відновлювати попередній стан системи переривань Можна просто дозволити переривання при звільненні блокування У цьому випадку оптимальним буде використання функцій spin_lock_irq () і spin_unlock_irq ()

spinlock_t  mr_lock = SPIN_LOCK_UNLOCKED

spin_lock_irq(&ampmr_lock)

/ * Критичний ділянку .. * /

spin_unlock_irq(&ampmr_lock)

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

Налагодження спін-блокувань

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

Інші засоби роботи зі спін-блокуваннями

Функція spin_lock_ini t () використовується для ініціалізації спін-блокувань, які були створені динамічно (змінна типу spinlock_t, до якої немає прямого доступу, а є тільки покажчик на неї)

Функція spin_try_loc k () виробляє спробу захопити зазначену спін-блокування Якщо блокування знаходиться в стані конфлікту, то, замість циклічної перевірки та очікування на звільнення блокування, ця функція повертає нульове значення Якщо блокування була захоплена успішно, то функція повертає нуль Аналогічно функція spin_is_locke d () повертає ненульове значення, якщо

блокування в даний момент захоплена В іншому випадку повертається нуль Ця функція ніколи не захоплює блокіровку2

У табл 93 наведено повний список функцій роботи зі спін-блокуваннями

Таблиця 93 Список функцій роботи зі спін-блокуваннями

Функція Опис

spin_iock () Захопити зазначену блокування

spin_lock_irq () Заборонити переривання на локальному процесорі і захопити зазначену блокування

spin_lock_irqsave () Зберегти поточний стан системи переривань, заборонити переривання на локальному процесорі і захопити зазначену блокування

spin_unlock () Звільнити зазначену блокування

spin_unlock_irq () Звільнити зазначену блокування та дозволити переривання на локальному процесорі

spin_unlock_irqrestore () Звільнити зазначену блокування і відновити стан системи переривань на локальному процесорі в вказане первісне значення

spin_lock_init () Ініціалізувати обєкт типу spinlock_ t в заданій області памяті

spin_trylock () Виконати спробу захоплення зазначеної блокування і в разі невдачі повернути нульове значення

spin_is_locked () Повернути ненульове значення, якщо зазначена блокування в даний момент захоплена, і нульове значення в іншому випадку

Спін-блокування і обробники нижніх половин

Як було зазначено в розділі 7, Обробка нижніх половин і відкладені дії, при використанні блокувань в роботі з обработчиками нижніх половин необхідно приймати деякі запобіжні заходи Функція spin_lock_bh () дозволяє захопити зазначену блокування і заборонити всі обробники нижніх половин Функція spin_unlock_bh () виконує зворотні дії

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

Згадаймо, що два тасклета (tasklet) одного типу не можуть виконуватися паралельно Тому немає необхідності захищати дані, які використовуються тільки тасклетамі одного типу

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

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

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

Джерело: Лав, Роберт Розробка ядра Linux, 2-е видання : Пер з англ – М: ТОВ «ІД Вільямс »2006 – 448 с : Ил – Парал тит англ

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*