19.06.16
Linux Kernel tracepoints 19.06.16

Доброго времени суток, $USER. Прошло уже много времени с момента последней тематической публикации в этом блоге. Разумеется, я узнал кое- что новое и хочу поделиться этим с тобой. Сегодня речь пойдет про такой функционал ядра Linux, как tracepoints.

File:TuxFlat.svg

Tracepoints

Что же это, собственно, такое?

Tracepoints — элемент статической инструментации кода. Если по простому, то это способ мониторинга работы программы в ходе ее выполнения через добавление особых комманд в код программы. Например, все знакомы с отладкой print’ами. Здесь принцип отличается не сильно- в ключевых точках кода Linux расставлены различные точки трассировки, к которым можно подключаться во время работы ядра.

Для того, чтобы проверить, включена ли система tracepoints, нужно распарсить конфиг ядра:

Иначе необходимо пересобрать ядро с соответствующими опциями.

 

Всего есть 2 способа работы с этой подсистемой:

1) С помощью debugfs (в общем- то тут все описано просто замечательно)

2) С помощью загрузки в пространство ядра своего собственного модуля

User space

Если не примонтирована debugfs, то сделать это можно так:

А далее все просто. В каталоге /sys/kernel/debug можно увидеть кучу файлов, которые описаны тут.

В подкаталоге events по подсистемам с очевидными названиями поделены те или иные точки трассировки. Рассмотрим для примера одну из точек планировщика процессов:

В коде ядра эту метку можно найти в файле kernel/signal.c.

Файлы внутри:

enable — по- умолчанию точка трассировки выключена. Для включения:

 

filter — файл для отбора только тех tracepoint events, которые удовлетворяют условиям. Например, можно попросить ядро отдавать в буфер трассировки только сигналы для интересных вам pid`ов. Хорошо описано здесь.

id — идентификатор точки трассировки в ядре

trigger — позволяет описать зависимость одного tracepoint event от другого (6.2 Supported trigger commands)

format —

 

Нетрудно заметить, что этот псевдофайл дает пользователю полное описание метки включая ее сигнатуру.

 

 

Как видно из сигнатуры метки в kernel/signal.c и include/trace/events/signal.h, в нее действительно передаются в том или ином виде те параметры, что мы видели в /sys/kernel/debug/tracing/events/signal/signal_generate/format

Именно в таком виде информация о tracepoint будет выводится в tracepoint buffer.

 

Kernel

В качестве примера я возьму свою учебную практику со второго курса университета. Мне было необходимо реализовать перехват и обработку заданных сигналов.

Под «обработкой» подразумевается одна из следующих реакций на сигнал:

  • Игнорирование
  • Принудительное выполнение действия «по- умолчанию»
  • Выполнение shell- кода (Треш полный, но для логгирования сойдет)

Я согласен, пример не самый жизнеспособный. Тут важно понять, что вообще сложно придумать что-то для tracepoints, что не является исключительно мониторингом работы системы. Да и вообще целью практики было скорее повысить скилл максимально быстрого раздупления в огромные исходники.

Итак, необходимо подготовиться к выполнению работы.

*Для простоты не будем рассматривать сигналы реального времени*

Во- первых, посмотрим, какие вообще tracepoints существуют для подсистемы сигналов.

*Вся работа теперь будет производиться для ядра v4.5*

signal_generate:

Этот tracepoint вызывается процессом- отправителем сигнала (или ядром, если речь про какой- нибудь SIGSEGV) в самом конце стека вызовов.

В заголовочном файле (include/trace/events/signal.h) видим следующую сигнатуру:

int sig — номер сгенерированного сигнала

struct siginfo *info — ты ведь знаешь про siginfo, да?

struct task_struct *task — процесс, которому отправляется сигнал

int group — сигнал предназначен группе потоков или же одному определенному потоку (ЧСХ, в линуксах group то int, то bool. Консистентность? Не, не слышал)

int result — что в итоге произошло с сигналом

 

signal_deliver:

Этот tracepoint вызывается процессом- получаетелем перед тем, как выполнить действие, полагающееся для этого сигнала (выполнить пользовательский обработчик, IGN, DFL, KILL, STOP).

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.

Пишем код

Для перехвата сигнала необходимо:

  1. Реализовать обработчик сигнала, который будет вызываться процессом- получателем сигнала в режиме ядра
  2. Подключить обработчик сигнала к списку процедур, вызываемых нужным tracepoint

После этого, при выгрузке модуля, обязательно нужно убрать обработчик и дождаться, пока все процессы закончат выполнять код модуля. Иначе мы получим забавную ситуацию, схожую с той, когда процесс стучится в запрещенную ему память и получает SIGSEGV. Только в ядре. И фатальнее.

 

Итак, обработчик:

Очевидно, сигнатура обработчика должна совпадать с сигнатурой соответствующего tracepoint.

Когда процесс генерирует сигнал, ядро сразу проверяет, какую реакцию он вызовет у процесса- получателя. Если он должен убить получателя, то в функции complete_signal в маску сигналов процесса добавится SIGKILL, который будет обработан получателем в первую очередь. Поэтому есть кейс, что не всегда, когда мы получаем SIGKILL, был сгенерирован SIGKILL.

В таком случае необходимо извлечь из pending маски настоящий сигнал и далее работать с ним.

Смысл этой конструкции максимально очевиден. В зависимости от того, что было наконфигурировано пользователем при загрузке модуля, будет принято то или иное решение по обработке сигнала. Т.е. оставить как есть (т.е. не вмешиваться), выполнять shell- команду, заставить выполнить действие, предусмотренное системой для этого сигнала, или же проигнорировать его.

 

Что касается прикрепления и открепления обработчика к tracepoint:

Процедура ядра for_each_kernel_tracepoint- аналог функции map, выполняющей функцию для каждого элемента массива/списке/etc. Нам нужно пройтись по списку точек трассировки и сохранить указатель на ту, имя которой signal_deliver (собственно, callback для этого передается в for_each_kernel_tracepoint). Дальше вызывается процедура для прикрепления обработчика к точке трассировки. Хотя вообще- то это очевидно.

 

Как я уже писал выше, при выгрузке модуля необходимо убрать все, что было установлено.

Тут все очевидно. И опять же, как я писал выше, вызвать процедуру tracepoint_synchronize_unregister так же необходимо, потому что из нее поток ядра вернется только тогда, когда последний сигнал, вошедший в обработчик до tracepoint_probe_unregister, завершит его выполнение.

Весь остальной код можно найти в моем репозитории github. Он достаточно очевиден для человека, знакомого с программированием настолько, что он читает статью про kernel space O_o.

Теперь, пожалуй, я рассказал все, что подчерпнул из учебной практики на кафедре о подсистеме tracepoints в Linux

 

Вместо заключения

Ты, читатель этой статьи, один из первых, кто вообще видит код моей учебной практики. Он полон огрехов как со стороны общего подхода, стилистики программирования, так и с вероятностью 99% со стороны фактической. Где- то я мог приврать, где- то не понять, возможно, мой подход неверен. В таком случае я буду только благодарен комментарию от человека, который знает сабж лучше меня.

Удачи в освоении программирования под ядро, $USER. Пусть в современном мире Java и всяких модных web- приблуд такие навыки не очень востребованы, но я рад, что кому- то интересны те же темы, что и мне. Потому что я считаю, что настоящий программист должен хотя бы в общих чертах быть знакомым с тем стеком технологий, которыми он пользуется.

Удачи в освоении программирования под ядро, $USER!