Як реалізувати критичні розділи на ARM Cortex A9


15

Я переношу деякий застарілий код з ядра ARM926 в CortexA9. Цей код є бареметальним і не включає ОС або стандартні бібліотеки, усі користувацькі. У мене виникає збій, який, як видається, пов'язаний з умовою перегонів, яку слід запобігти критичним розділенням коду.

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

Крім того, чи існує бібліотека з відкритими джерелами, яка має такі типи примітивів для ARM (або навіть хороша легка бібліотека спінлок / семефор)?

#define ARM_INT_KEY_TYPE            unsigned int
#define ARM_INT_LOCK(key_)   \
asm volatile(\
    "mrs %[key], cpsr\n\t"\
    "orr r1, %[key], #0xC0\n\t"\
    "msr cpsr_c, r1\n\t" : [key]"=r"(key_) :: "r1", "cc" );

#define ARM_INT_UNLOCK(key_) asm volatile ("MSR cpsr_c,%0" : : "r" (key_))

Код використовується наступним чином:

/* lock interrupts */
ARM_INT_KEY_TYPE key;
ARM_INT_LOCK(key);

<access registers, shared globals, etc...>

ARM_INT_UNLOCK(key);

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

Спасибі!


1
будь ласка, посилайтесь на infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dht0008a/… , не робіть це у вбудованому asm btw. зробіть це функцією, як це робить стаття.
Джейсон Ху

Я нічого не знаю про ARM, але я би сподівався, що для mutex (або будь-якої міжпотокової або перехресної функції синхронізації) вам слід використовувати клоб "пам'яті", щоб переконатися, що: а) усі значення пам'яті, які зараз є в регістрах, змиваються повернутися в пам'ять перед виконанням asm та b) будь-які значення в пам'яті, до яких можна отримати доступ після того, як ASM перезавантажуються. Зауважте, що виконання дзвінка (як рекомендує HuStmpHrrr) повинно неявно виконувати цю клобіру для вас.

Крім того, хоча я досі не розмовляю ARM, ваші обмеження для "key_" не виглядають правильними. Оскільки ви говорите, це призначене для повторного входу, оголошення в блокуванні "= r" видається підозрілим. '=' означає, що ви маєте намір замінити його, а існуюче значення є неважливим. Здається, більш ймовірно, що ви мали намір використовувати "+", щоб вказати намір оновити існуюче значення. І знову для розблокування, описуючи його як вхідний сигнал, говорить gcc, що ви не маєте наміру його змінювати, але якщо я не помиляюся, ви робите (міняєте). Я здогадуюсь, це також повинно бути вказане як вихід "+".

1
+1 для кодування в зборі для такого високого специфічного ядра. У будь-якому випадку, це може бути пов'язано з режимами привілеїв?
Дзарда

Я впевнений, що вам потрібно буде користуватися ldrexта strexробити це належним чином. Ось веб-сторінка, яка показує вам, як використовувати ldrexта strexреалізувати спінлок.

Відповіді:


14

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

int32_t atomic_swap(int32_t *dest, int32_t new_value)
{
  int32_t old_value;
  do
  {
    old_value = __LDREXW(&dest);
  } while(__STREXW(new_value,&dest);
  return old_value;
}

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

if (atomic_swap(&mutex, 1)==0)
{
   ... do stuff in mutex ... ;
   mutex = 0; // Leave mutex
}
else
{ 
  ... couldn't get mutex...
}

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

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

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

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


7

Це важкий спосіб робити критичні розділи; відключити переривання. Він може не працювати, якщо у вашій системі є / обробляються помилки даних. Це також збільшить затримку переривання. У Linux irqflags.h є деякі макроси, які справляються з цим. cpsieІ cpsidінструкції , може бути корисним; Однак вони не врятують стан і не дозволять гніздування. cpsне використовує реєстр.

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

У певному сенсі вони ldrex/strexздаються ARMv5 swp. Однак їх набагато складніше реалізувати на практиці. Вам потрібен робочий кеш і цільова пам’ять про ldrex/strexпотреби, які повинні бути в кеші. Документація щодо ARM ldrex/strexє досить туманною, оскільки вони хочуть, щоб механізми працювали на процесорах, що не належать Cortex-A. Однак для Cortex-A механізм підтримувати кеш локального процесора в синхронізації з іншими процесорами є тим самим, який використовується для реалізації ldrex/strexінструкцій. Для серії Cortex-A запасний грануляр (розмір ldrex/strexзарезервованої пам'яті) такий же, як і лінія кешу; вам також потрібно вирівняти пам'ять до рядка кешу, якщо ви збираєтесь змінити декілька значень, наприклад, із подвійно пов'язаним списком.

Я підозрюю, що є якась тонка помилка.

mrs %[key], cpsr
orr r1, %[key], #0xC0  ; context switch here?
msr cpsr_c, r1

Вам потрібно переконатися, що послідовність ніколи не може бути попередньо знищена . В іншому випадку ви можете отримати дві ключові змінні з увімкненими перериваннями, і вимкнення блокування буде неправильним. Ви можете використовувати swpінструкцію з ключовою пам'яттю, щоб забезпечити послідовність ARMv5, але ця інструкція застаріла на Cortex-A на користь, ldrex/strexоскільки вона працює краще для систем з декількома процесорами.

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

Крім того, чи існує бібліотека з відкритими джерелами, яка має такі типи примітивів для ARM (або навіть хороша легка бібліотека спінлок / семефор)?

Це важко написати портативним способом. Тобто такі бібліотеки можуть існувати для певних версій процесорів ARM та для конкретних ОС.


2

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

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

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

По-друге, ці критичні розділи економлять і відновлюють набагато більше, ніж лише те, чи включені переривання. Зокрема, вони зберігають та відновлюють більшу частину CPSR (Поточний реєстр статусу програми) (посилання призначено для Cortex-R4, оскільки я не зміг знайти хорошу діаграму для A9, але він повинен бути ідентичним). Є тонкі обмеження , навколо яких на самому ділі можуть бути змінені частини стану, але це більше , ніж потрібно тут.

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

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

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

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

Ось як я реалізував би це таким чином, щоб вирішити всі можливі проблеми, на які я вказав:

#define ARM_INT_KEY_TYPE            unsigned int
#define ARM_INT_LOCK(key_)   \
asm volatile(\
    "mrs %[key], cpsr\n\t"\
    "ands %[key], %[key], #0xC0\n\t"\
    "cpsid if\n\t" : [key]"=r"(key_) :: "memory", "cc" );
#define ARM_INT_UNLOCK(key_) asm volatile (\
    "tst %[key], #0x40\n\t"\
    "beq 0f\n\t"\
    "cpsie f\n\t"\
    "0: tst %[key], #0x80\n\t"\
    "beq 1f\n\t"\
    "cpsie i\n\t"
    "1:\n\t" :: [key]"r" (key_) : "memory", "cc")

Переконайтеся, що компілюйте, -mcpu=cortex-a9оскільки принаймні деякі версії GCC (як-от моя) за замовчуванням старіші процесори ARM, які не підтримують cpsieі cpsid.

Я використовував andsзамість того , щоб тільки andв ARM_INT_LOCKтак що інструкція 16-біт , якщо це використовується в Thumb коду. "cc"Колошматити необхідно в будь-якому випадку, так що строго вигода розмір / продуктивність коду.

0і 1є місцевими етикетками , для довідки.

Вони повинні бути використані всіма тими ж способами, що і ваші версії. Це ARM_INT_LOCKтак само швидко / мало, як ваш оригінальний. На жаль, я не міг придумати спосіб ARM_INT_UNLOCKбезпечно в будь-якому місці поблизу, як мало інструкцій.

Якщо у вашій системі є обмеження щодо відключення IRQ та FIQ, це може бути спрощено. Наприклад, якщо вони завжди відключені разом, ви можете об'єднати в один cbz+ cpsie ifподібний:

#define ARM_INT_UNLOCK(key_) asm volatile (\
    "cbz %[key], 0f\n\t"\
    "cpsie if\n\t"\
    "0:\n\t" :: [key]"r" (key_) : "memory", "cc")

Крім того, якщо ви зовсім не переймаєтесь FIQ, тоді це схоже на те, щоб просто увімкнути / вимкнути їх повністю.

Якщо ви знаєте, що ніщо інше ніколи не змінює будь-який інший біт стану в CPSR між блокуванням і розблокуванням, то ви також можете використовувати продовжити щось дуже схоже на ваш вихідний код, за винятком обох "memory"і "cc"clobber в обох ARM_INT_LOCKіARM_INT_UNLOCK


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.