Замітка про спосіб налагодження блокувань в ядрі Linux

Всім привіт,
Дана замітка стосується одного практичного прийому, який я використовував при вирішенні задачі визначення місця в ядрі ОС Linux, де утримується певна спін-блокування (спін-лок). Дана проблема виникла досить спонтанно і, враховуючи складність ядра, могла б вимагати велику кількість часу для вирішення. Якщо Вам це цікаво, прошу читати далі...
Отже, ось реальна ситуація, яка виникла у проекті Tempesta FW, де мені довелося брати участь, беручи участь в розробка ядерної WEB-аккселератора/брандмауера (так-так, реалізованого в ядрі Linux).
У ході роботи модуля було помічено, що іноді при навантаженні в балці ядра з'являються налагоджувальні повідомлення, які свідчать про те, що здійснюється спроба виконання блоку коду, який передбачає, що обробляється в ній об'єкт (сокет) не буде утримуватися ніким іншим.
Завдяки високій культурі кодування в проекті, а саме рясному використанню конструкцій типу
BUG_ON/WARN_ON
та інших всіляких assert-ів, ситуації, коли щось пішло в розріз з логікою, що реалізується розробником, отлавливались досить просто. Так і цього разу — володіючи знанням про внутрішній логіці роботи ядра, розробник вважає за необхідне убезпечити себе та інших і на всяк випадок додав перевірку виду:
if (unlikely(spin_is_locked(&sk->sk_lock.slock)))
SS_WARN("Socket is used by two cpus, action=%d"
" state=%d sk_cpu=%d curr_cpu=%d\n",
sw.action, sk->sk_state,
sk->sk_incoming_cpu, smp_processor_id());

І все було добре, поки в балці ядра дійсно не стали з'являтися повідомлення
"Socket is used by two cpus, ..."
. Тобто, в даному місці спрацьовувало умова
if (unlikely(spin_is_locked(&sk->sk_lock.slock))) { ... }
, що було свідченням факту утримання блокування кимось ще. Далі, я розповім яким чином мені вдалося досить легко з'ясувати, хто ж утримує цей спін-лок.
Отже, маємо наступне. Є структура-описувач сокета
struct sock
, в якій є блокування
sk_lock
типу
socket_lock_t
, що включає в себе спін-блокування
slock
типу
spinlock_t
(див. сюди і сюди). Сам же спін-лок заснований на структурі
raw_spinlock_t
, що включає в себе карту
dep_map
типу
struct lockdep_map
, містить (в тому числі) інформацію про холдерах. Остання, стає доступною тільки при складанні ядра з опцією
CONFIG_DEBUG_LOCK_ALLOC
. Але ми ж хакери :)
Таким чином, для налагодження мені знадобилося зібрати ядро з опцією
CONFIG_DEBUG_LOCK_ALLOC
та внести наступну зміну до код:
--- a/tempesta_fw/sock.c
+++ b/tempesta_fw/sock.c
@@ -1101,11 +1101,14 @@ ss_tx_action(void)
while (!tfw_wq_pop(this_cpu_ptr(&si_wq), &sw)) {
struct sock *sk = sw.sk;

- if (unlikely(spin_is_locked(&sk->sk_lock.slock)))
+ if (unlikely(spin_is_locked(&sk->sk_lock.slock))) {
SS_WARN("Socket is used by two cpus, action=%d"
" state=%d sk_cpu=%d curr_cpu=%d\n",
sw.action, sk->sk_state,
sk->sk_incoming_cpu, smp_processor_id());
+ printk("raw_spinlock = %p\n", &sk->sk_lock.slock.rlock);
+ BUG();
+ }

bh_lock_sock(sk);
switch (sw.action) {

Зміст зміни в тому, щоб при виникненні потрібної ситуації сдампить адреса захопленого спинлока в лог та аварійно завершити роботу ядра через
BUG
. Насправді, це дає відмінну можливість сдампить образ ядра станом на момент помилки і, знаючи адресу об'єкта, дослідити його вміст пізніше.
Для захоплення дампа використовувалася зв'язка QEMU + kdump. Останній є механізмом ядра, що дозволяє зупинити поточне ядро і через kexec завантажити резервне (більш просте), яке і сдампит образ попереднього у вигляді файлу.
Для аналізу отриманого дампа використовувалася утиліта crash. Її краще викачувати і збирати вручну, тому що у мене були проблеми зі штатними версіями в складі дистрибутивів. Маємо наступне:
crash> log
[ 56.934038] [sync_sockets] Warning: Socket is used by two cpus, action=0 state=1 sk_cpu=2 curr_cpu=2
[ 56.934066] raw_spinlock = ffff880001f28b70

Відмінно, адреса об'єкта відомий (
ffff880001f28b70
). Подивимося що там:
crash> raw_spinlock_t ffff880001f28b70
struct raw_spinlock_t {
raw_lock = {
{
head_tail = 0x3434, 
tickets = {
head = 0x34, 
tail = 0x34
}
}
}, 
magic = 0xdead4ead, 
owner_cpu = 0xffffffff, 
owner = 0xffffffffffffffff, 
dep_map = {
key = 0xffffffff82cd95f0 <af_family_slock_keys+16>, 
class_cache = {0xffffffff8261be30 <lock_classes+319920>, 0xffffffff826283d0 <lock_classes+370512>}, 
name = 0xffffffff815b719d "slock-AF_INET", 
cpu = 0x1, 
ip = 0xffffffff813c667f
}
}

За адресою
ip = 0xffffffff813c667f
знаходимо функцію, в якій була захоплена блокування:
$ addr2line -f -e vmlinux 0xffffffff813c667f
tcp_tasklet_func
/home/milabs/linux-stable-4.1/net/ipv4/tcp_output.c:773

Ось і відповідь — блокування на сокеті була захоплена в функії
tcp_tasklet_func
.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.