Механізми профілювання Linux



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

У ядрі Linux таких механізмів 3:
  1. tracepoints
  2. kprobes
  3. perf events


На основі цих 3 фіч ядра будуються абсолютно всі профилировщики і трасувальники, які доступні для Linux, в тому числі
ftrace
,
perf
,
SystemTap
,
ktap
та інші. У цій статті я розповім, що вони (механізми) дають, як працюють, а в наступні рази ми подивимося вже на конкретні тулзы.

Kernel tracepoints
Kernel tracepoints — це фреймворк для трасування ядра, зроблений через статичну инструментирование коду. Так, ви правильно зрозуміли, більшість важливих функцій ядра (мережа, управління пам'яттю, планувальник) статично инструментировано. На моєму ядрі кількість tracepoint'ів таке:
[etn]$ perf list tracepointt | wc-l
1271
Серед них є kmalloc:
[etn]$ perf list tracepoint | grep "kmalloc "
kmem:kmalloc [Tracepoint event]
А ось так воно виглядає на насправді в ядерної функції
__do_kmalloc
:
/**
* __do_kmalloc - allocate memory
* @size: how many bytes of memory are required.
* @flags: the type of memory to allocate (see kmalloc).
* @caller: function caller debug for tracking of the caller
*/
static __always_inline void *__do_kmalloc(size_t size, gfp_t flags,
unsigned long caller)
{
struct kmem_cache *cachep;
void *ret;

cachep = kmalloc_slab(size, flags);
if (unlikely(ZERO_OR_NULL_PTR(cachep)))
return cachep;
ret = slab_alloc(cachep, flags, caller);

trace_kmalloc(caller, ret,
size, cachep->size, flags);

return ret;
}


trace_kmalloc
   є tracepoint.

Такі tracepoint'и пишуть висновок налагоджувальний кільцевої буфер, який можна подивитися в /sys/kernel/debug/tracing/trace:
[~]# mount-t debugfs none /sys/kernel/debug
[~]# cd /sys/kernel/debug/tracing/
[tracing]# echo 1 > events/kmem/kmalloc/enable
[tracing]# tail trace
bash-10921 [000]… 1127940.937139: kmalloc: call_site=ffffffff8122f0f5 ptr=ffff8800aaecb900 bytes_req=48 bytes_alloc=64 gfp_flags=GFP_KERNEL
bash-10921 [000]… 1127940.937139: kmalloc: call_site=ffffffff8122f084 ptr=ffff8800ca008800 bytes_req=2048 bytes_alloc=2048 gfp_flags=GFP_KERNEL|GFP_NOWARN|GFP_NORETRY
bash-10921 [000]… 1127940.937139: kmalloc: call_site=ffffffff8122f084 ptr=ffff8800aaecbd80 bytes_req=64 bytes_alloc=64 gfp_flags=GFP_KERNEL|GFP_NOWARN|GFP_NORETRY
tail-11005 [001]… 1127940.937451: kmalloc: call_site=ffffffff81219297 ptr=ffff8800aecf5f00 bytes_req=240 bytes_alloc=256 gfp_flags=GFP_KERNEL|GFP_ZERO
tail-11005 [000]… 1127940.937518: kmalloc: call_site=ffffffff81267801 ptr=ffff880123e8bd80 bytes_req=128 bytes_alloc=128 gfp_flags=GFP_KERNEL
tail-11005 [000]… 1127940.937519: kmalloc: call_site=ffffffff81267786 ptr=ffff880077faca00 bytes_req=504 bytes_alloc=512 gfp_flags=GFP_KERNEL
Зауважте, що traсepoint «kmem:kmalloc», як і всі інші, за замовчуванням вимкнено, так що його треба включити, сказавши 1 
enable
файл.

Ви можете самі написати tracepoint для свого модуля ядра. Але, по-перше, це не так вже і просто, тому що tracepoint's   не найзручніший і зрозумілий API: дивись приклади в samples/trace_events/ (взагалі всі ці tracepoint's — це чорна магія C-макросів, зрозуміти які якщо і сильно захочеться, то не відразу вийде). А по-друге, швидше за все, вони не запрацюють у вашому модулі, поки у вас включений
CONFIG_MODULE_SIG
(майже завжди так) і немає закритого ключа для підписування (він у вендора ядра вашого дистрибутива). Дивись несамовиті подробиці в lkml [1], [2].

Коротше кажучи, tracepoint'и проста і легка річ, але користуватися їй руками незручно і не рекомендується   використовувати
ftrace
або
perf
.

kprobes
Якщо tracepoint's — це мітки статичного инструментирования, то 
kprobes
 — це механізм динамічного инструментирования коду. З допомогою kprobes ви можете перервати виконання ядерного коду будь-якому місці, викликати свій обробник, зробити в нім що хочете і повернутися назад як ні в не бувало.

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

Працює це все таким чином:
  • Ми робимо свій модуль ядра, в якому пишемо наш обробник.
  • Ми реєструємо наш обробник на якийсь адреса A, будь то просто адресу або якась функція.
  • Підсистема kprobe копіює інструкції з адресою, А і замінює їх на CPU trap (
    int 3
    для x86).
  • Тепер, коли виконання коду доходить до адреси A, генерується виняток, за якому зберігаються регістри, а управління передається обробникові виняткової ситуації, яким в врешті-решт стає kprobes.
  • Підсистема kprobes дивиться на адреса винятку, знаходить, хто був зареєстрований за адресою, А і викликає наш обробник.
  • Коли наш обробник закінчується, регістри відновлюються, виконуються збережені інструкції, і виконання продовжується далі.
У ядрі є 3 види kprobes:
  • kprobes — «базова» проба, яка дозволяє перервати будь-яке місце ядра.
  • jprobes   jump probe, вставляється тільки в початок функції, але зате дає зручний механізм доступу до аргументам переривається функції для нашого обробника. Також працює не за рахунок trap'ів, а через
    setjmp/longjmp
    (звідси і назва), то є більш легковесна.
  • kretprobes   return probe, вставляється перед виходом з функції і дає зручний доступ до результату функції.
З допомогою kprobes ми можемо трасувати все що завгодно, включаючи код сторонніх модулів. Давайте зробимо це для нашого miscdevice драйвера. Я хочу знати, що хтось намагається писати в моє пристрій, знати якого відступу і скільки байт.
У моєму miscdevice драйвері функція виглядає так:
ssize_t etn_write(struct file *filp, const char __user *buf,
size_t count, loff_t *f_pos)
Я написав простий jprobe модуль, який пише ядерний лог кількість байт і зміщення.
root@etn:~# tail-F /var/log/kern.log
Jan 1 00:00:42 etn kernel: [ 42.923717] ETN JPROBE: jprobe_init:46: Planted jprobe at bf00f7a8, handler addr bf071000
Jan 1 00:00:43 etn kernel: [ 43.194840] ETN JPROBE: trace_etn_write:23: Writing 2 bytes at offset 4
Jan 1 00:00:43 etn kernel: [ 43.201827] ETN JPROBE: trace_etn_write:23: Writing 2 bytes at offset 4
Коротше, річ потужна, проте користуватися не дуже зручно: доступу до локальним змінним немає (тільки через відступ від 
ebp
), потрібно писати модуль ядра, налагоджувати, завантажувати і т. п. Є доступні приклади в samples/kprobes. Але навіщо все це, якщо є SystemTap?

Perf events
Відразу скажу, що не треба плутати «perf events» та програму
perf
   про програму буде сказано окремо.

«Perf events» — це інтерфейс доступу до лічильниками в PMU (Performance Monitoring Unit), який є частиною CPU. Завдяки цим метрик, ви можете з легкістю попросити ядро показати вам скільки було промахів у L1 кешу, незалежно від того, яка в вас архітектура, будь то ARM або amd64. Правда, для вашого процесора повинна бути підтримка в ядрі:-) Щодо актуальну інформацію по цього приводу можна знайти на тут.

Для доступу до цим лічильниками, а також величезній купі іншого добра, була написана програма
perf
. З її допомогою можна подивитися, які залізні події нам доступні.

Приклад для x86$ perf list pmu hw sw cache
branch-instructions OR cpu/branch-instructions/ [Kernel PMU event]
branch-misses OR cpu/branch-misses/ [Kernel PMU event]
bus-cycles OR cpu/bus-cycles/ [Kernel PMU event]
cache-misses OR cpu/cache-misses/ [Kernel PMU event]
cache-references OR cpu/cache-references/ [Kernel PMU event]
cpu-cycles OR cpu/cpu-cycles/ [Kernel PMU event]
instructions OR cpu/instructions/ [Kernel PMU event]

cpu-cycles OR cycles [Hardware event]
instructions [Hardware event]
cache-references [Hardware event]
cache-misses [Hardware event]
branch-instructions OR branches [Hardware event]
branch-misses [Hardware event]
bus-cycles [Hardware event]
ref-cycles [Hardware event]

cpu-clock [Software event]
task-clock [Software event]
page-faults OR faults [Software event]
context-switches OR cs [Software event]
cpu-migrations OR migrations [Software event]
minor-faults [Software event]
major-faults [Software event]
alignment-faults [Software event]
emulation-faults [Software event]
dummy [Software event]

L1-dcache-loads [Hardware event cache]
L1-dcache-load-misses [Hardware event cache]
L1-dcache-stores [Hardware event cache]
L1-dcache-store-misses [Hardware event cache]
L1-dcache-prefetches [Hardware event cache]
L1-icache-loads [Hardware event cache]
L1-icache-load-misses [Hardware event cache]
LLC-loads [Hardware event cache]
LLC-load-misses [Hardware event cache]
LLC-stores [Hardware event cache]
LLC-store-misses [Hardware event cache]
dTLB-loads [Hardware event cache]
dTLB-load-misses [Hardware event cache]
dTLB-stores [Hardware event cache]
dTLB-store-misses [Hardware event cache]
iTLB-loads [Hardware event cache]
iTLB-load-misses [Hardware event cache]
branch-loads [Hardware event cache]
branch-load-misses [Hardware event cache]


А ось що на ARMroot@etn:~# perf list pmu hw sw cache

cpu-cycles OR cycles [Hardware event]
instructions [Hardware event]
cache-references [Hardware event]
cache-misses [Hardware event]
branch-instructions OR branches [Hardware event]
branch-misses [Hardware event]
stalled-cycles-frontend OR idle-cycles-frontend [Hardware event]
stalled-cycles-backend OR idle-cycles-backend [Hardware event]
ref-cycles [Hardware event]

cpu-clock [Software event]
task-clock [Software event]
page-faults OR faults [Software event]
context-switches OR cs [Software event]
cpu-migrations OR migrations [Software event]
minor-faults [Software event]
major-faults [Software event]
alignment-faults [Software event]
emulation-faults [Software event]
dummy [Software event]

L1-dcache-loads [Hardware event cache]
L1-dcache-load-misses [Hardware event cache]
L1-dcache-stores [Hardware event cache]
L1-dcache-store-misses [Hardware event cache]
L1-icache-load-misses [Hardware event cache]
dTLB-load-misses [Hardware event cache]
dTLB-store-misses [Hardware event cache]
iTLB-load-misses [Hardware event cache]
branch-loads [Hardware event cache]
branch-load-misses [Hardware event cache]


Видно, що x86 багатший на такі речі.

Для доступу до «perf events» був зроблений спеціальний системний виклик
perf_event_open
, якому ви передаєте сам event і конфіг, в якому описуєте, що ви хочете з цією подією робити. У відповідь ви отримуєте файловий дескриптор, з якого можна читати дані, зібрані
perf
'ом  події.

Поверх цього,
perf
містить безліч різних фіч, начебто угруповання подій, фільтрації, виведення різні формати, аналізу зібраних профілів і пр. Тому в 
perf
зараз пхають все, що можна: від tracepoint'ів до eBPF аж до того, що весь
ftrace
хочуть зробити частиною
perf
[3] [4].

Коротше кажучи, «perf_events» самі по собі мало цікаві, а сам
perf
заслуговує окремої статті, тому для початку покажу простий приклад.

Говоримо:
root@etn:~# perf timechart record apt-get update

root@etn:~# perf timechart-i perf.data-o timechart.svg
І отримуємо ось таке чудо:
Perf timechartPerf timechart

Висновок
Таким чином, знаючи трохи більше про трасування і профілювання в ядрі, ви можете сильно полегшити життя собі і товаришам, особливо якщо навчитися користуватися цими інструментами
ftrace
,
perf
та 
SystemTap
, але про в інший раз.

Почитати

Джерело: Хабрахабр

0 коментарів

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