Практично будь-яке вбудовується додаток використовує переривання, а багато - багатозадачність і багатопоточність. Такі додатки повинні бути реалізовані з урахуванням того факту, що контекст виконання може змінитися в будь-який час. Наприклад, виникло переривання викликає планувальник, який перемикає контекст на більш пріоритетне завдання. Що трапиться, якщо завдання і функції використовують загальні змінні? З великою часткою ймовірності можна припустити, що станеться катастрофа - одна з задач зіпсує дані іншого завдання. Прим. перекладача: звичайно, те ж саме можна сказати про загальні змінних переривань і основного циклу в звичайних, однозадачних програмах.
У світі вбудованих систем реєнтерабельним функція повинна відповідати таким правилам:
Правило 1. Функція здійснює атомарний доступ до глобальних змінних, або змінна створюється для кожного виклику функції
Правило 2. Функція не викликає нереентерабельние функції
Правило 3. Функція використовує атомарний доступ до периферійних модулів
У Правилах 1 і 3 зустрічається слово "атомарний", в основі якого лежить грецьке слово, що означає "неподільний". У програмуванні термін "атомарний" використовується для операцій, які не можуть бути перервані. Розглянемо інструкцію:
Вона є атомарної, так як нічого крім скидання не може перервати виконання це інструкції - Іструкції виконується незалежно від інших завдань або переривань.
Перша частина Правила 1 вимагає атомарного використання глобальних змінних. Нехай дві функції використовують загальну глобальну змінну foobar. Якщо функція A містить код
то вона не є реєнтерабельним, так як доступ до змінної foobar не є атомарним, тому що для зміни foobar використовується три дії а не одне. Цей код може перервати переривання, в якому викликається функція B, яка теж виконує якісь дії з foobar. Після повернення з переривання значення temp може не відповідати актуальному значенням foobar.
У наявності конфлікт, літак падає і сотні людей кричать: "Чому цього блядское програміста ніхто не навчив використовувати реєнтерабельним функції.". Однак, уявімо, що функція А виглядає по іншому:
Тепер операція атомарна, переривання не зупинить операцію над foobar посередині, конфлікт не виникне, значить ця операція реєнтерабельним.
Стійте ... А ви дійсно знаєте, що ваш Сі компілятор згенерує для цієї операції? На x86 процесорі цей код може виглядати наступні чином:
що звичайно ж не є атомарної опраціям, а значить вираз foobar + = 1; не є реєнтерабельним. Атомарна версія буде виглядати так:
Мораль в наступному: будьте дуже обережні в припущеннях про генерацію компілятором атомарного коду. В іншому випадку ви можете виявити репортерів програми "Максимум" під своїми дверима - "Хто винен у відмові гальм у мопеда Філліпа Кіркорова?". А взагалі, завжди потрібно виходити з того, що для подібних виразів компілятор завжди генерує неатомарний код
Друга частина Правила 1 говорить про те, що якщо функція використовує неаторманий доступ, то дані повинні створюватися для кожного виклику функції. Під "викликом" будемо розуміти безпосереднє виконання функції. У багатозадачних додатках функція може викликатися одночасно декількома завданнями.
Розглянемо наступний код:
foo - глобальна змінна, область видимості якої виходить за межі функції some_function (). Навіть якщо більше жодна функція не використовує foo. дані можуть бути пошкоджені, якщо some_function () викликається з різних завдань (ну і, природно, може бути викликана одночасно).
Сі і Сі ++ можуть захистити нас від цієї небезпеки. Використовуємо локальну змінну. Тобто визначимо foo всередині функції. У цьому випадку кожен виклик some_function () буде використовувати змінну, виділену в стеці (що, звичайно, не відноситься до PIC16 і PIC18. З останнім правда можна використовувати компілятор Microchip C18, який може реалізувати програмний стек і, відповідно, реєнтерабельним. Але чи потрібна вона там?).
Іншим варіантом може стати використання динамічного виділення пам'яті. Правда, при цьому потрібно переконатися, що самі бібліотечні функції динамічного виділення реєнтерабельним. Зазвичай немає. Як і у випадку з локальними змінними для кожного виклику буде генеруватися свій шматок пам'яті - таким чином вирішується фундаментальна проблема реєнтерабельним.
Правило 2 говорить про те, що викликає функція успадковує відсутність реєнтерабельним в спричиненої. Тобто якщо дані псуються в функції B яка була викликана з функції A, говорити про реєнтерабельним А безглуздо.
Використовуючи комілятори з мов високого рівня ми натрапляємо на підступну проблему. Ви впевнені - дійсно впевнені - що бібліотеки компілятора реєнтерабельним? Очевидно, що строкові (до Сі це не відноситься) та інші складні операції викликають функції з бібліотеки компілятора. Більшість компіляторів генерують виклики бібліотечних функцій при математичних операціях, навіть для тривіального множення і ділення цілих чисел.
Якщо ваш код, який використовує бібліотечні функції, повинен бути реєнтерабельним - проконсультуйтеся з тих. підтримкою виробника компілятора і почитайте мануал - там зазвичай про це пишуть. Обережність необхідна і при використанні сторонніх бібліотек - стеків протоколів, файлових систем та ін.
Правило 3 відноситься тільки до вбудовуваним системам. Периферію можна розглядати як глобальну змінну і якщо для обслуговування периферійного модуля потрібно кілька дій, можна отримати проблему спільного доступу.
Який найкращий спосіб зробити функцію реєнтерабельним? Звичайно ж уникнути використання глобальних змінних.
У загальному випадку глобальні змінні сильно ускладнюють налагодження коду і викликають важковловимий баги. Намагайтеся використовувати локальні змінні або динамічне виділення пам'яті.
Проте, глобальні змінні це найбільш швидкий спосіб передачі даних. Як правило, повністю позбутися від глобальних змінних у вбудованих системах реального часу неможливо. Тому при використанні поділюваних ресурсів (глобальних змінних або периферії) ми повинні використовувати різні методи забезпечення атомарности.
Найпоширеніший спосіб - забороняти переривання на час виконання нереентерабельного коду. Якщо переривання заборонені, система з багатозадачного перетворюється в однозадачних. Насправді це не зовсім так - хто заважає після заборони переривання викликати сервіс RTOS переключающий контекст? Забороніть переривання, виконайте нереентерабельную частина коду, дозвольте переривання. Досить часто це виглядає наступним чином:
Однак цей код небезпечний! Якщо do_something () - глобальна фукция, яка викликається з різних місць, то в якийсь момент вона може бути викликана при заборонених перериваннях. А перед поверненням переривання будуть дозволені, контекст буде змінений. Дуже небезпечний спосіб, який може привести до серйозних проблем.
І не потрібно використовувати стару відмовку: "Так, але коли я пишу код, я дуже акуратний. Я викликаю цю функцію тільки коли впевнений, що переривання дозволені". Програміст, який вас замінить (а з такими відмовками це може дуже скоро статися) може не знати про такий серйозний обмеження (тим більше, що навряд чи ви внесли його в документацію).
Набагато краще виглядає наступна функція:
Заборона переривань збільшують час реакції системи на зовнішні події (що часто просто неприпустимо). Більш м'яким способом забезпечення реєнтерабельним є використання семафорів. индицируют зайнятість ресурсу. Семафор - це найпростіший двійковий індикатор типу "включено-виключено", доступ до якого здійснюється атомарному. Семафор використовується як прапор дозволу, що утримує завдання в стані простою, поки необхідний ресурс зайнятий.
Майже всі комерційні RTOS мають об'єкт синхронізації типу "семафор" (часто семафор, який реалізує захист від одночасного доступу називають мютексом; мютекс має додаткові властивості). Якщо ви використовуєте RTOS то семафори - це ваш шлях забезпечити реєнтерабельним. Чи не використовуєте RTOS? Часто я бачу програми, що використовують змінну-прапор для захисту ресурсу:
Виглядай просто і елегантно, але цей код небезпечний!