Ядрото на Linux предоставя разнообразие от заключващи примитиви. Всеки един от тях се държи по уникален начин, което го прави повече или по-малко подходящ за конкретен случай на употреба. Някои заключващи примитиви са подходящи, когато дадена задача може да заспи, други могат да се използват в случаите, когато не искате да блокирате четците на вашата критична секция с код и т.н.

Изборът кой заключващ механизъм да се използва често се свежда до:

  • Дали конкуренцията се очаква да бъде висока или ниска
  • Дали заключването е във или извън контекста на процеса
  • Дали спането е допустимо

Ние сме склонни да приемаме тези неща за даденост, тъй като можем удобно просто да използваме тези заключващи примитиви и да знаем, че условията на състезанието, за които се тревожим, няма да се случат, но знаете ли как работят под капака? Не? Страхотен! Вие сте на правилното място 😃

В тази публикация в блога ще разгледам как механизмът spinlock е внедрен в ядрото на Linux и ще ви преведа през изходния код.

Какво представляват Spinlocks?

Spinlock е може би „най-простият“ заключващ примитив, който защитава критични секции от кода. Това е концептуално как работи:

Ключовият момент, който трябва да имате предвид, е, че ако Задача B не може да получи заключването, тя ще изчака в цикъл ('spin'), докато заключването бъде освободено от Задача A, което автоматично означава, че задача B не може да заспи. Това поведение диктува къде не трябва да се използва.

Spinlock предоставя набор от удобни функции на разработчиците на ядрото. Някои от най-използваните функции са:

Как да използвам Spinlocks в моя код?

Използването на спинлокове във вашия код е доста лесно.

Ако трябва да деактивирате прекъсванията на текущия процесор по някаква причина:

Отказ от отговорност: Използвана е конфигурация на ядрото

Това, което прави кода на ядрото труден за четене, е фактът, че кодът се добавя/премахва в зависимост от използваните конфигурации. Като казах това, това е конфигурацията, с която създадох ядрото си:

CONFIG_PREEMPT_RT = не е зададено
CONFIG_LOCKDEP = не е зададено
CONFIG_PREEMPT_COUNT=y
CONFIG_DEBUG_SPINLOCK = не е зададено
CONFIG_MMIOWB = не е зададено
CONFIG_PROFILE_ALL_BRANCHES = не е зададено
CONFIG_PARAVIRT_SPINLOCKS = не set
CONFIG_SMP = y

Сега, след като разбирате основите, можем да се потопим във всяка функция, за да разберем как работят. Ще се съсредоточа върху функциите spinlock_tstruct, spin_lock_init() и spin_lock()тъй като те са най-важните.

spinlock_t структура

Дефиницията на spinlock и нейните членове зависят от това кои конфигурации на ядрото са зададени. В нашия случай ето как изглежда структурата spinlock_t:

Когато CONFIG_PREEMPT_RT не е зададено, ядрото картографира spinlock към raw_spinlock_t, което по същество обвива специфичното за архитектурата spinlock, наречено arch_spinlock_t:

Зависещият от архитектурата arch_spinlock_t в x86 се дефинира, както следва:

PS.: Прекарайте известно време, за да се запознаете с членовете на структурата и техните имена, което ще ви бъде полезно по-късно в тази публикация в блога

Може би сте забелязали, че:

  1. Споменах платформата x86
  2. има #ifdef __LITTLE_ENDIAN в структурата

Ако това по някакъв начин е привлякло вниманието ви, не сте сами 😅. От друга страна, всички знаем, че x86 endian е малко endian.

Както се оказва, arch_spinlock_t, използван в x86 архитектурата, не е под linux/arch/x86/, а в общ заглавен файл, наречен include/asm- generic/qspinlock_types.h.

Обосновката е много проста, освен ако нещата не са много различни от една платформа на друга, тогава е по-добре да централизираме кода на едно място за целите на четливостта, което обяснява защо трябва да се справяме с различни endianness там. Това обаче не е така за следните платформи по-долу:

spin_lock_init (заключване)

Това е функцията, която инициализира променлива spinlock_t. Като алтернатива можете да използвате този удобен макрос, който едновременно декларира и инициализира spinlock

Това е, което макросът spin_lock_init() се разширява до:

Обърнете внимание как atomic_t -›val от arch_spinlock_t се инициализира на 0 (помнете това).

Освен това, ако сте опитен C програмист, тогава „do {} while (0)“ е втора природа за вас, но ако все още се опитвате да се справите със C, тези връзки може да ви помогнат:

Накратко, “do {} while(0)” адресира няколко добре познати проблема, като поглъщане на точка и запетая, дублиращи се имена на променливи и т.н., когато работите с макроси, които се състоят от съставни твърдения.

spin_lock(заключване)

Това е функцията, която се опитва да получи ключалката. Ако успее при първия опит, той просто се връща, в противен случай поема по „бавния път“ и зацикля („върти“), докато се опитва да получи заключването, докато успее.

Отказ от отговорност: Тук има много за разопаковане и честно казано, всеки ред в предишния кодов фрагмент заслужава специална публикация в блога. Като се има предвид, че това е за spinlocks, ще обясня основната цел на всеки ред и ще оставя връзки, които да използвате, за да научите повече по темата.

preempt_disable

Изпреварването на ядрото е свойство, притежавано от ядрото на Linux, при което процесорът може да бъде прекъснат по време на изпълнение на кода на ядрото и да му бъдат възложени други задачи (от които той по-късно се връща, за да завърши своите задачи на ядрото).

Има 3 модела за изпреварване, от които потребителите могат да избират, когато компилират ядрото, както е показано по-долу.

Макросът preempt_disable() се разширява до следното:

Изводът на макроса preempt_disable() е, че той увеличава вътрешна структура, която се чете от Linux Scheduler, така че Scheduler знае, че текущата задача не може да бъде изпреварена, докато preempt_enable().

Ако искате да научите повече за изпреварването на ядрото, LWN има няколко стари, но наистина добри статии по темата:

spin_acquire(…);

Тази функция е част от системата lockdep на ядрото, което е очарователна идея. Системата Lockdep следи заключванията, докато се придобиват, и отпечатва предупреждаващи съобщения, когато може да възникне потенциално блокиране по време на изпълнение.

Не мога да преувелича колко полезна може да бъде тази система по време на фазата на разработка на всеки код на ядрото, тъй като отстраняването на грешки в условията на състезание понякога може да бъде абсурдно трудно.

В нашия случай, като се има предвид, че CONFIG_LOCKDEP не е зададен, този ред се превежда като no-op, така че няма да се гмуркам повече по този въпрос. Ако искате да научите повече по темата, тук трябва да намерите добра информация:

LOCK_CONTENDED(…);

Както споменахме по-рано, поради използваните конфигурации на ядрото, този макрос се превежда в do_raw_spin_lock(lock);, което по същество е мястото, където се случва магията.

queued_spin_lock(…);

Именно в тази функция можем да видим откъде spinlock получава името си. Той се опитва да промени стойността на spinlock_t -› raw_spinlock_t -› arch_spinlock_t -› atomic_t (val)и ако това успее, заключването се придобива ефективно. Ако не, тогава ще бъде поет бавният път, така че да може да се върти безкрайно, докато не получи шанса да придобие ключалката.

вероятно (x)

Това е макрос, който се разширява до __builtin_expect(!!(x), 1). По същество това дава индикации за оптимизации на прогнозиране на разклонения на компилатора на GCC, така че да можем да се възползваме от някои от функциите за кеш на процесора/проактивно извличане, които някои архитектури имат (разбира се, свръхопростяване).

Ако искате да научите повече за това, може да искате да погледнете документите на GCC:

atomic_try_cmpxchg_acquire(…);

От само себе си се разбира, че за да се справят с множество едновременни задачи, които се опитват да се конкурират за желан ресурс (заключване), трябва да се гарантира, че действието на заключване се извършва в една инструкция.

atomic_try_cmpxchg_acquire прави точно това и, както може да се очаква, това зависи от архитектурата. В случай на x86–64 arch, ако следвате кодовия лабиринт на макроса, в крайна сметка ще намерите тази част от кода:

Инструкцията за асемблиране x86 CMPXCHG ще сравни в една инструкция atomic_t-›val с 0, ако това е вярно, тогава _Q_LOCKED_VAL (което е 1) е зададено на atomic_t- ›вал.

Друго нещо, което е важно да се спомене над кода по-горе, е променливата lockв ред 12. Константата LOCK_PREFIX_HERE се предава надолу по веригата от извиквания на функции, докато не се окаже споменат като аргумент за заключване на ред 12.

За еднопроцесорни системи (UP) това ще бъде празен низ, но за симетрични многопроцесорни системи (SMP) ще добави префикса на инструкцията x86 LOCK, за да гарантира операцията се изпълнява атомно между всички процесори, както е показано по-долу:

Заключение

уф! Това беше доста за една публикация в блог, нали? 😅

За да гарантирам, че тази публикация в блога няма да стане ненужно дълга, ще завърша част 1 тук и ще създам част 2 за бавния път, тъй като това е доста малко код (около 250–300 реда).

Надявам се, че прочетеното ви е харесало и ако съм пропуснал нещо, просто ме уведомете и ще го поправя 😉

Приятно кодиране на всички! 😃

Пауло Алмейда