Доброго времени суток, $USER. Прошло уже много времени с момента последней тематической публикации в этом блоге. Разумеется, я узнал кое- что новое и хочу поделиться этим с тобой. Сегодня речь пойдет про такой функционал ядра Linux, как tracepoints.
Tracepoints
Что же это, собственно, такое?
Tracepoints — элемент статической инструментации кода. Если по простому, то это способ мониторинга работы программы в ходе ее выполнения через добавление особых комманд в код программы. Например, все знакомы с отладкой print’ами. Здесь принцип отличается не сильно- в ключевых точках кода Linux расставлены различные точки трассировки, к которым можно подключаться во время работы ядра.
Для того, чтобы проверить, включена ли система tracepoints, нужно распарсить конфиг ядра:
1 2 3 |
$ cat /boot/config-4.5.0-1-amd64 | grep TRACEPOINT CONFIG_TRACEPOINTS=y CONFIG_HAVE_SYSCALL_TRACEPOINTS=y |
Иначе необходимо пересобрать ядро с соответствующими опциями.
Всего есть 2 способа работы с этой подсистемой:
1) С помощью debugfs (в общем- то тут все описано просто замечательно)
2) С помощью загрузки в пространство ядра своего собственного модуля
User space
Если не примонтирована debugfs, то сделать это можно так:
1 |
$ mount -t debugfs nodev /sys/kernel/debug |
А далее все просто. В каталоге /sys/kernel/debug можно увидеть кучу файлов, которые описаны тут.
В подкаталоге events по подсистемам с очевидными названиями поделены те или иные точки трассировки. Рассмотрим для примера одну из точек планировщика процессов:
1 2 3 |
$ cd /sys/kernel/debug/tracing/events/signal/signal_generate/ $ ls enable filter format id trigger |
В коде ядра эту метку можно найти в файле kernel/signal.c.
Файлы внутри:
enable — по- умолчанию точка трассировки выключена. Для включения:
1 |
$ echo 1 > enable |
filter — файл для отбора только тех tracepoint events, которые удовлетворяют условиям. Например, можно попросить ядро отдавать в буфер трассировки только сигналы для интересных вам pid`ов. Хорошо описано здесь.
id — идентификатор точки трассировки в ядре
trigger — позволяет описать зависимость одного tracepoint event от другого (6.2 Supported trigger commands)
format —
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
$ cat format name: signal_generate ID: 146 format: field:unsigned short common_type; offset:0; size:2; signed:0; field:unsigned char common_flags; offset:2; size:1; signed:0; field:unsigned char common_preempt_count; offset:3; size:1; signed:0; field:int common_pid; offset:4; size:4; signed:1; field:int sig; offset:8; size:4; signed:1; field:int errno; offset:12; size:4; signed:1; field:int code; offset:16; size:4; signed:1; field:char comm[16]; offset:20; size:16; signed:1; field:pid_t pid; offset:36; size:4; signed:1; field:int group; offset:40; size:4; signed:1; field:int result; offset:44; size:4; signed:1; print fmt: "sig=%d errno=%d code=%d comm=%s pid=%d grp=%d res=%d", REC->sig, REC->errno, REC->code, REC->comm, REC->pid, REC->group, REC->result |
Нетрудно заметить, что этот псевдофайл дает пользователю полное описание метки включая ее сигнатуру.
1 2 |
1135 /* kernel/signal.c */ 1136 trace_signal_generate(sig, info, t, group, result); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
50 TRACE_EVENT(signal_generate, 51 52 TP_PROTO(int sig, struct siginfo *info, struct task_struct *task, 53 int group, int result), 54 55 TP_ARGS(sig, info, task, group, result), 56 57 TP_STRUCT__entry( 58 __field( int, sig ) 59 __field( int, errno ) 60 __field( int, code ) 61 __array( char, comm, TASK_COMM_LEN ) 62 __field( pid_t, pid ) 63 __field( int, group ) 64 __field( int, result ) 65 ), 66 67 TP_fast_assign( 68 __entry->sig = sig; 69 TP_STORE_SIGINFO(__entry, info); 70 memcpy(__entry->comm, task->comm, TASK_COMM_LEN); 71 __entry->pid = task->pid; 72 __entry->group = group; 73 __entry->result = result; 74 ), 75 76 TP_printk("sig=%d errno=%d code=%d comm=%s pid=%d grp=%d re s=%d", 77 __entry->sig, __entry->errno, __entry->code, 78 __entry->comm, __entry->pid, __entry->group, 79 __entry->result) 80 ); |
Как видно из сигнатуры метки в kernel/signal.c и include/trace/events/signal.h, в нее действительно передаются в том или ином виде те параметры, что мы видели в /sys/kernel/debug/tracing/events/signal/signal_generate/format
Именно в таком виде информация о tracepoint будет выводится в tracepoint buffer.
Kernel
В качестве примера я возьму свою учебную практику со второго курса университета. Мне было необходимо реализовать перехват и обработку заданных сигналов.
Под «обработкой» подразумевается одна из следующих реакций на сигнал:
- Игнорирование
- Принудительное выполнение действия «по- умолчанию»
- Выполнение shell- кода (Треш полный, но для логгирования сойдет)
Я согласен, пример не самый жизнеспособный. Тут важно понять, что вообще сложно придумать что-то для tracepoints, что не является исключительно мониторингом работы системы. Да и вообще целью практики было скорее повысить скилл максимально быстрого раздупления в огромные исходники.
Итак, необходимо подготовиться к выполнению работы.
*Для простоты не будем рассматривать сигналы реального времени*
Во- первых, посмотрим, какие вообще tracepoints существуют для подсистемы сигналов.
1 2 |
$ ls /sys/kernel/debug/tracing/events/signal/ enable filter signal_deliver signal_generate |
*Вся работа теперь будет производиться для ядра v4.5*
signal_generate:
Этот tracepoint вызывается процессом- отправителем сигнала (или ядром, если речь про какой- нибудь SIGSEGV) в самом конце стека вызовов.
В заголовочном файле (include/trace/events/signal.h) видим следующую сигнатуру:
1 2 3 |
TRACE_EVENT(signal_generate, TP_PROTO(int sig, struct siginfo *info, struct task_struct *task, int group, int result), |
int sig — номер сгенерированного сигнала
struct siginfo *info — ты ведь знаешь про siginfo, да?
struct task_struct *task — процесс, которому отправляется сигнал
int group — сигнал предназначен группе потоков или же одному определенному потоку (ЧСХ, в линуксах group то int, то bool. Консистентность? Не, не слышал)
int result — что в итоге произошло с сигналом
signal_deliver:
Этот tracepoint вызывается процессом- получаетелем перед тем, как выполнить действие, полагающееся для этого сигнала (выполнить пользовательский обработчик, IGN, DFL, KILL, STOP).
1 2 |
TRACE_EVENT(signal_deliver, TP_PROTO(int sig, struct siginfo *info, struct k_sigaction *ka), |
int sig — номер сигнала
struct siginfo *info — все тот же siginfo
struct k_sigaction *ka — то же самое, что struct sigaction
Таким образом, обладая указателем на k_sigaction, мы можем изменить реакцию ядра на сигнал (что и нужно). В то же время, ядро само завершит доставку сигнала, подчистив соответствующие структуры (pending, queue etc.)
Эту точку трассировки можно использовать для модификации поведения подсистемы сигналов Linux.
Я не буду приводить how-to о создании, сборке, конфигурировании и загрузке модулей Linux. Ограничусь только информацией по теме статьи. Хотя бы потому что это все и так весьма хорошо описано в сети.
Так же за кадром останется то, как я разбираю конфиг внутри модуля. Возможно, это стоит описать хотя бы из- за парочки лулзов, связанных с ТЗ на учебную практику. Согласно ему, мне надо было сделать возможность выполнения shell- скрипта при наступлении сигнала. Все прекрасно знают, что в shell используются пробелы. Пробелы. Те самые пробелы, которые нельзя передать ядру в качестве значения параметра при его загрузке (ядро просто отрезает кусок строки, стоящей после пробела). В связи с этим родилась больная идея использовать сочетание <spc> для обозначения пробелов, «;» для разделения пар SIGNAL:ACTION и <sc> для обозначения «;» в shell- скрипте. Я уже не помню, зачем я сделал это, но так даже веселее :)
В итоге получится то, что лежит в моем репозитории на github.
Пишем код
Для перехвата сигнала необходимо:
- Реализовать обработчик сигнала, который будет вызываться процессом- получателем сигнала в режиме ядра
- Подключить обработчик сигнала к списку процедур, вызываемых нужным tracepoint
После этого, при выгрузке модуля, обязательно нужно убрать обработчик и дождаться, пока все процессы закончат выполнять код модуля. Иначе мы получим забавную ситуацию, схожую с той, когда процесс стучится в запрещенную ему память и получает SIGSEGV. Только в ядре. И фатальнее.
Итак, обработчик:
1 2 3 |
static void signal_deliver_probe( void *data, int sig, struct siginfo *info, struct k_sigaction *ka) { |
Очевидно, сигнатура обработчика должна совпадать с сигнатурой соответствующего tracepoint.
1 2 3 4 5 6 7 8 9 10 11 12 |
int signr = sig; struct signal_struct *signal = current->signal; if(sig == SIGKILL) { signr = select_next_signal(&signal->shared_pending, ¤t->blocked); if(sig_fatal(current,signr) && interceptors[signr].sig_handler != SIG_PROC){ signr = dequeue_signal(current, ¤t->blocked, info); sig = signr; signal->flags &= ~SIGNAL_GROUP_EXIT; } } |
Когда процесс генерирует сигнал, ядро сразу проверяет, какую реакцию он вызовет у процесса- получателя. Если он должен убить получателя, то в функции complete_signal в маску сигналов процесса добавится SIGKILL, который будет обработан получателем в первую очередь. Поэтому есть кейс, что не всегда, когда мы получаем SIGKILL, был сгенерирован SIGKILL.
В таком случае необходимо извлечь из pending маски настоящий сигнал и далее работать с ним.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
switch((unsigned long)interceptors[sig].sig_handler) { case (unsigned long)SIG_PROC: break; case (unsigned long)SIG_DFL: ka->sa.sa_handler = SIG_DFL; break; case (unsigned long)SIG_USR: printk(KERN_INFO "+User defined command %s", interceptors[sig].cmd); int status = shell_command_exec(interceptors[sig].cmd); if(!status){ printk(KERN_INFO "+Success on executing shell command"); } else { printk(KERN_ALERT "+Error on executing shell command"); } case (unsigned long)SIG_IGN ka->sa.sa_handler = SIG_IGN; break; } return; |
Смысл этой конструкции максимально очевиден. В зависимости от того, что было наконфигурировано пользователем при загрузке модуля, будет принято то или иное решение по обработке сигнала. Т.е. оставить как есть (т.е. не вмешиваться), выполнять shell- команду, заставить выполнить действие, предусмотренное системой для этого сигнала, или же проигнорировать его.
Что касается прикрепления и открепления обработчика к tracepoint:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
static void find_signal_deliver(struct tracepoint *tp, void *priv) { if (!strcmp(tp->name, "signal_deliver")){ signal_deliver_tp = tp; } } static int connect_probes(void) { int ret; for_each_kernel_tracepoint(find_signal_deliver, NULL); if (!signal_deliver_tp) return -ENODEV; ret = tracepoint_probe_register(signal_deliver_tp, signal_deliver_probe, NULL); if (ret) return ret; return 0; } |
Процедура ядра for_each_kernel_tracepoint- аналог функции map, выполняющей функцию для каждого элемента массива/списке/etc. Нам нужно пройтись по списку точек трассировки и сохранить указатель на ту, имя которой signal_deliver (собственно, callback для этого передается в for_each_kernel_tracepoint). Дальше вызывается процедура для прикрепления обработчика к точке трассировки. Хотя вообще- то это очевидно.
Как я уже писал выше, при выгрузке модуля необходимо убрать все, что было установлено.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
static void __exit unload_module(void) { if (signal_deliver_tp) tracepoint_probe_unregister(signal_deliver_tp, signal_deliver_probe, NULL); tracepoint_synchronize_unregister(); int i; for(i = 0; i < _NSIG; i++){ if(interceptors[i].cmd != NULL){ kfree(interceptors[i].cmd); } } } |
Тут все очевидно. И опять же, как я писал выше, вызвать процедуру tracepoint_synchronize_unregister так же необходимо, потому что из нее поток ядра вернется только тогда, когда последний сигнал, вошедший в обработчик до tracepoint_probe_unregister, завершит его выполнение.
Весь остальной код можно найти в моем репозитории github. Он достаточно очевиден для человека, знакомого с программированием настолько, что он читает статью про kernel space O_o.
Теперь, пожалуй, я рассказал все, что подчерпнул из учебной практики на кафедре о подсистеме tracepoints в Linux
Вместо заключения
Ты, читатель этой статьи, один из первых, кто вообще видит код моей учебной практики. Он полон огрехов как со стороны общего подхода, стилистики программирования, так и с вероятностью 99% со стороны фактической. Где- то я мог приврать, где- то не понять, возможно, мой подход неверен. В таком случае я буду только благодарен комментарию от человека, который знает сабж лучше меня.
Удачи в освоении программирования под ядро, $USER. Пусть в современном мире Java и всяких модных web- приблуд такие навыки не очень востребованы, но я рад, что кому- то интересны те же темы, что и мне. Потому что я считаю, что настоящий программист должен хотя бы в общих чертах быть знакомым с тем стеком технологий, которыми он пользуется.
Удачи в освоении программирования под ядро, $USER!