SystemTap был разработан прежде всего для упрощения отладки и трассировки ядерных функций Linux (kernel space). Однако, с появлением в ядре механизма uprobes (в мэйнстриме ядра он присутствует начиная с версии 3.5, а в системах, связанных с Red Hat, таких как Fedora, CentOS и RHEL, он уже существует на протяжении нескольких лет), SystemTap можно использовать и для трассировки в пространстве пользователя (user space). На этой странице можно найти огромное количество примеров скриптов для SystemTap, которые дают представление о мощи этого инструмента. Я взял за основу один из них (уже не помню какой) и значительно переделал, чтобы показать, как работает SystemTap в пространстве пользователя.
Мы не станем запускать SystemTap от имени суперпользователя, а для этого нам придется кое-что подготовить. Во-первых, необходимо установить пакет с серверной частью SystemTap. В моей системе Fedora 20 это делается так:
yum install systemtap-server
Затем нужно добавить вашу учетную запись в группу stapusr и запустить сервер.
usermod -a -G stapusr your-username
service stap-server start
Зачем такие сложности. Дело в том, что SystemTap при запуске нового скрипта строит и устанавливает в ядро Linux новый временный модуль, который задействует механизм uprobes для установки в интересующие нас точки программы интересующих нас обработчиков событий. По завершении работы SystemTap удаляет временный модуль из ядра. Очевидно, что для этого нужны привилегии суперпользователя, которыми обладает stap-server.
Итак, перейдем к примеру. Возьмем простое приложение, написанное на C++ (файл test.cc).
#include <iostream>
#include <unistd.h>
namespace
{
void print( const char * lbl )
{
std::cout << lbl << std::endl;
}
}
class A
{
public:
virtual void print( void ) const
{
::print( "A" );
}
};
class B : public A
{
public:
void print( void ) const
{
::print( "B" );
}
};
namespace
{
void cycle( const A * a, const A * b, int i )
{
if ( i % 3 )
a->print();
else
b->print();
sleep( i / 2 );
}
}
int main( void )
{
A * a( new A );
A * b( new B );
for ( int i( 0 ); i < 10; ++i )
cycle( a, b, i );
delete a;
delete b;
return 0;
}
Имеются два класса: A и его наследник B. В классе A определена виртуальная фукция print(), которая с помощью свободной функции print(), объявленной в анонимном пространстве имен, выводит на экран строку A. В классе B виртуальная функция print() переопределена: она выводит на экран строку B с помощью той же свободной функции print(). В функции main() создаются два полиморфных объекта a и b типов A и B соответственно. Затем в цикле от 1 до 10 эти объекты передаются в свободную функцию cycle(), которая вызывает виртуальный метод print() попеременно для a и b в зависимости от номера цикла, причем общее число вызовов a->print() будет превышать число вызовов b->print() в два раза. Кроме этого, функция cycle() умеет зависать, причем чем больше номер цикла, тем дольше длительность зависания.
Все эти сложности я ввел с той целью, чтобы показать, как будет работать типичный сценарий SystemTap. Итак, давайте посмотрим, сколько раз будет вызвана виртуальная функция A::print(), сколько времени займут эти вызовы (мы соберем статистику и даже выведем гистограмму!), а также будем фиксировать зависания функции cycle() выше заданного порога.
Для написания скрипта нам понадобятся имена функций, которые в C++ обычно не соответствуют именам, объявленным в исходном коде. Чтобы выяснить их значения, можно просто скомпилировать программу test.
g++ -o test test.cc
Заметьте, что мы скомпилировали программу без отладочной информации — SystemTap она не нужна!
Теперь, с помощью SystemTap, можно вывести значения имен функций.
stap -l 'process("./test").function("*")' -c ./test
process("/home/lyokha/tmp/stap-example/test").function("_GLOBAL__sub_I_main")
process("/home/lyokha/tmp/stap-example/test").function("_Z41__static_initialization_and_destruction_0ii")
process("/home/lyokha/tmp/stap-example/test").function("_ZN12_GLOBAL__N_15cycleEPK1AS2_i")
process("/home/lyokha/tmp/stap-example/test").function("_ZN12_GLOBAL__N_15printEPKc")
process("/home/lyokha/tmp/stap-example/test").function("_ZN1AC2Ev")
process("/home/lyokha/tmp/stap-example/test").function("_ZN1BC1Ev")
process("/home/lyokha/tmp/stap-example/test").function("_ZNK1A5printEv")
process("/home/lyokha/tmp/stap-example/test").function("_ZNK1B5printEv")
process("/home/lyokha/tmp/stap-example/test").function("_ZNSt8ios_base4InitD1Ev@@GLIBCXX_3.4")
process("/home/lyokha/tmp/stap-example/test").function("_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@@GLIBCXX_3.4")
process("/home/lyokha/tmp/stap-example/test").function("_ZdlPv@@GLIBCXX_3.4")
process("/home/lyokha/tmp/stap-example/test").function("__do_global_dtors_aux")
process("/home/lyokha/tmp/stap-example/test").function("__libc_csu_fini")
process("/home/lyokha/tmp/stap-example/test").function("__libc_csu_init")
process("/home/lyokha/tmp/stap-example/test").function("_fini")
process("/home/lyokha/tmp/stap-example/test").function("_init")
process("/home/lyokha/tmp/stap-example/test").function("_start")
process("/home/lyokha/tmp/stap-example/test").function("deregister_tm_clones")
process("/home/lyokha/tmp/stap-example/test").function("frame_dummy")
process("/home/lyokha/tmp/stap-example/test").function("main")
process("/home/lyokha/tmp/stap-example/test").function("register_tm_clones")
Отлично, функции _ZNK1A5printEv и _ZNK1B5printEv — это и есть наши A::print() и B::print(), а функция _ZN12_GLOBAL__N_15cycleEPK1AS2_i — это свободная функция cycle() из анонимного пространства имен.
Вот исходный код скрипта (файл test.stp, объяснения ниже).
#!/usr/bin/env stap
global startA, startCycle, intervals, hdr = "<---> "
probe process(@1).function("_ZNK1A5printEv")
{
startA[tid()] = gettimeofday_us()
}
probe process(@1).function("_ZNK1A5printEv").return
{
t = gettimeofday_us()
old_t = startA[tid()]
if (old_t) intervals <<< t - old_t
delete startA[tid()]
}
probe process(@1).function("_ZN12_GLOBAL__N_15cycleEPK1AS2_i")
{
startCycle[tid()] = gettimeofday_us()
}
probe process(@1).function("_ZN12_GLOBAL__N_15cycleEPK1AS2_i").return
{
delete startCycle[tid()]
}
function dump_intervals(print_hist)
{
if (@count(intervals))
{
printf("%s Intervals min:%dus avg:%dus max:%dus count:%d\n", hdr,
@min(intervals), @avg(intervals), @max(intervals),
@count(intervals))
if (print_hist)
{
print(@hist_log(intervals))
}
}
}
probe timer.s(2)
{
foreach (tid in startCycle)
{
old_t = startCycle[tid]
if (old_t > 0)
{
t = gettimeofday_us()
if (t - old_t > $2 * 1000000)
{
printf("%s Pending call detected: %dus (tid %d)\n", hdr, t - old_t, tid)
}
}
}
dump_intervals(0)
}
probe end
{
println()
dump_intervals(1)
}
Первая строка — shebang — понятно. Далее идет объявление четырех глобальных переменных: startA, startCycle, intervals и hdr, которые будут доступны из всех объявлений probe и function в скрипте. Переменная startA — это массив, параметризованный значениями идентификаторов программного потока, возвращаемыми встроенной функцией tid(). Мы будем использовать startA как временну́ю метку входа в функцию A::print() в соответствующем программном потоке. На самом деле, в программе всего лишь один поток, а параметризация переменных-массивов startA и startCycle по идентификаторам потоков сделана просто для обобщения подхода. Переменная startCycle — тоже временна́я метка входа в функцию cycle(). Кстати, как SystemTap определяет, что переменные startA и startCycle являются массивами? Ведь в их объявлении это никак не указано. Дело в том, что парсер SystemTap способен выводить (infer) тип переменной при ее первом использовании.
В переменной intervals мы будем собирать временну́ю статистику пребывания внутри функции A::print(). Переменная hdr — простой префикс для строк, выводимых SystemTap, она инициализируется в месте объявления.
Итак, первое объявление probe process(@1).function("_ZNK1A5printEv")
читается так: для имени процесса, переданного SystemTap в первом аргументе командной строки, при входе в функцию _ZNK1A5printEv, выполнить тело данного probe, которое находится ниже, внутри фигурных скобок. Мы уже применяли подобные объявления, когда выводили список функций в программе test. Тогда это было process("./test").function("*")
, то есть мы использовали имя процесса ./test. Поскольку опция -l не требовала использования сервера SystemTap, мы просто указали в имени процесса текущую директорию. Однако, когда мы запустим наш скрипт, сервер должен будет знать полный путь к программе test, поэтому мы передадим его через аргумент командной строки @1 (в SystemTap есть переменные вида $N, где N — некоторое число, которое, в отличие от bash, может превышать 9 — они подставляют N-ый аргумент командной строки как есть, и @1 — они оборачивают N-ый аргумент командной строки в двойные кавычки).
Внутри probe process(@1).function("_ZNK1A5printEv")
мы присваиваем элементу массива startA[tid()] значение gettimeofday_us(), которое соответствует числу микросекунд, прошедших с начала Эпохи UNIX.
В свою очередь, probe process(@1).function("_ZNK1A5printEv").return
устанавливает обработчик на выходе из A::print(). В нем, при помощи оператора <<<, в массив intervals добавляется новый элемент, равный разности между текущим значением gettimeofday_us() и значением startA[tid()]. Оператор <<< можно рассматривать как аналог оператора вывода в поток << из C++. На выходе из обработчика элемент startA[tid()] удаляется.
Следующие два probe соответствуют входу и выходу в/из cycle(): они, соответственно, устанавливают и удаляют элемент массива startCycle[tid()]. В probe timer.s(2)
каждые 2 секунды осуществляется проход по элементам startCycle и в случае, если разность между текущим значением gettimeofday_us() и значением данного элемента превышает значение в секундах, заданное во втором аргументе командной строки ($2), на экран выводится предупреждающее сообщение, в котором указано время пребывания внутри функции cycle() (собственно, эта разность) и идентификатор потока. В конце данного обработчика вызывается function dump_intervals(print_hist)
, которая выводит статистику, накопленную в intervals, на экран. В случае, когда аргумент print_hist не нулевой, на экран выводится гистограмма распределения событий из intervals. Мы не собираемся выводить гистограмму каждые две секунды, мы ее выведем в завершающем обработчике probe end
.
Итак, запускаем программу test под SystemTap.
stap test.stp -c ./test `pwd`/test 3
B
A
A
B
<---> Intervals min:6us avg:7us max:8us count:2
A
<---> Intervals min:6us avg:14us max:28us count:3
A
<---> Intervals min:6us avg:17us max:28us count:4
B
<---> Intervals min:6us avg:17us max:28us count:4
A
<---> Intervals min:6us avg:21us max:37us count:5
<---> Intervals min:6us avg:21us max:37us count:5
A
<---> Intervals min:6us avg:21us max:37us count:6
<---> Pending call detected: 3977735us (tid 4949)
<---> Intervals min:6us avg:21us max:37us count:6
B
<---> Intervals min:6us avg:21us max:37us count:6
<---> Pending call detected: 3977592us (tid 4949)
<---> Intervals min:6us avg:21us max:37us count:6
<---> Intervals min:6us avg:21us max:37us count:6
value |-------------------------------------------------- count
1 | 0
2 | 0
4 |@ 1
8 |@ 1
16 |@@@ 3
32 |@ 1
64 | 0
128 | 0
Опция -c ./test говорит о том, что SystemTap должен запустить отдельный процесс и выполнить в нем программу test. (SystemTap также поддерживает опцию -x PID, которая позволяет прикрепиться к уже существующему процессу: такой механизм лучше подходит для трассировки в продакшн системе, но я не буду на этом останавливаться.) Первый свободный аргумент командной строки — `pwd`/test — это полный путь к исполняемому файлу test, второй свободный аргумент — таймаут пребывания внутри тела функции cycle(). Вывод программы не принес неожиданностей: функция A::print() вызывалась 6 раз, B::print() — 4 раза, это видно из вывода собственно программы test. SystemTap выводил статистику по intervals каждые 2 секунды, в завершении была выведена соответствующая гистограмма распределения времен пребывания внутри функции A::print() в микросекундах. Кроме того, обработка функции cycle() два раза превышала установленный предел в 3 секунды и SystemTap вывел соответствующие сообщения.
Давайте усложним задачу. Теперь нас дополнительно будет интересовать количество вызовов свободной функции print() из A::print() (их должно оказаться 6), а также значение переменной i на входе в функцию cycle(). Первая задача подразумевает использование механизма process().function().callee() (эта запись условна, так как формально во всех круглых скобках нужно указывать какие-либо строковые значения). Механизм callee требует, чтобы программа test содержала символы DWARF, то есть была скомпилирована с опцией -g. Да и вторая задача тоже требует DWARF.
Вот какие изменения нужно внести в test.stp.
...
global startA, startCycle, intervals, printcalls = 0, hdr = "<---> "
...
probe process(@1).function("_ZN12_GLOBAL__N_15cycleEPK1AS2_i")
{
startCycle[tid()] = gettimeofday_us()
if (@defined($i))
{
printf("%s Stack variable i = %d\n", hdr, $i)
}
}
...
probe process(@1).function("_ZNK1A5printEv").callee("*print*") ?
{
printcalls += 1
}
...
probe end
{
println()
dump_intervals(1)
printf("%s Function print() was called from A %d times\n", hdr, printcalls)
}
Добавлена новая глобальная переменная printcalls, которая будет увеличиваться на единицу при вызове функции print() из A::print(): за это будет отвечать новый обработчик process(@1).function("_ZNK1A5printEv").callee("*print*") ?
. Вопросительный знак в конце имени обработчика говорит о том, что он опционален: мы хотим использовать новый скрипт в том числе и когда программа test не будет содержать отладочной информации. Я не стал записывать полное имя функции в значение аргумента callee, а ограничился глоб-паттерном: они прекрасно работают в скриптах SystemTap. В обработчик входа в функцию cycle() добавлен вывод значения стековой переменной i, а в probe end
— вывод значения printcalls.
Компилируем с опцией -g и запускаем SystemTap.
g++ -g -o test test.cc
stap test.stp -c ./test `pwd`/test 3
B
A
A
B
<---> Intervals min:8us avg:9us max:10us count:2
A
<---> Intervals min:8us avg:14us max:25us count:3
A
<---> Intervals min:8us avg:17us max:26us count:4
B
<---> Intervals min:8us avg:17us max:26us count:4
A
<---> Intervals min:8us avg:20us max:32us count:5
<---> Intervals min:8us avg:20us max:32us count:5
A
<---> Intervals min:8us avg:21us max:32us count:6
<---> Pending call detected: 3977681us (tid 1682)
<---> Intervals min:8us avg:21us max:32us count:6
B
<---> Intervals min:8us avg:21us max:32us count:6
<---> Pending call detected: 3977528us (tid 1682)
<---> Intervals min:8us avg:21us max:32us count:6
<---> Intervals min:8us avg:21us max:32us count:6
value |-------------------------------------------------- count
2 | 0
4 | 0
8 |@@ 2
16 |@@@ 3
32 |@ 1
64 | 0
128 | 0
<---> Function print() was called from A 0 times
Сообщения о переменной i не выводятся, printcalls не увеличивается. Что-то не так. Посмотрим как теперь называются наши функции.
stap -l 'process("./test").function("*")' -c ./test
process("/home/lyokha/tmp/stap-example/test").function("A@/home/lyokha/tmp/stap-example/test.cc:30")
process("/home/lyokha/tmp/stap-example/test").function("B@/home/lyokha/tmp/stap-example/test.cc:39")
process("/home/lyokha/tmp/stap-example/test").function("_GLOBAL__sub_I_main@<unknown source>")
process("/home/lyokha/tmp/stap-example/test").function("_ZNSt8ios_base4InitD1Ev@@GLIBCXX_3.4")
process("/home/lyokha/tmp/stap-example/test").function("_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@@GLIBCXX_3.4")
process("/home/lyokha/tmp/stap-example/test").function("_ZdlPv@@GLIBCXX_3.4")
process("/home/lyokha/tmp/stap-example/test").function("__do_global_dtors_aux")
process("/home/lyokha/tmp/stap-example/test").function("__libc_csu_fini")
process("/home/lyokha/tmp/stap-example/test").function("__libc_csu_init")
process("/home/lyokha/tmp/stap-example/test").function("__static_initialization_and_destruction_0@<unknown source>")
process("/home/lyokha/tmp/stap-example/test").function("_fini")
process("/home/lyokha/tmp/stap-example/test").function("_init")
process("/home/lyokha/tmp/stap-example/test").function("_start")
process("/home/lyokha/tmp/stap-example/test").function("cycle@/home/lyokha/tmp/stap-example/test.cc:50")
process("/home/lyokha/tmp/stap-example/test").function("deregister_tm_clones")
process("/home/lyokha/tmp/stap-example/test").function("frame_dummy")
process("/home/lyokha/tmp/stap-example/test").function("main@/home/lyokha/tmp/stap-example/test.cc:61")
process("/home/lyokha/tmp/stap-example/test").function("print@/home/lyokha/tmp/stap-example/test.cc:24")
process("/home/lyokha/tmp/stap-example/test").function("print@/home/lyokha/tmp/stap-example/test.cc:33")
process("/home/lyokha/tmp/stap-example/test").function("print@/home/lyokha/tmp/stap-example/test.cc:42")
process("/home/lyokha/tmp/stap-example/test").function("register_tm_clones")
Ага, функции стали называться иначе. Давайте я сразу приведу готовый скрипт с учетом всех переименований и возможностью запуска с отладочными символами в программе test и без них.
#!/usr/bin/env stap
global startA, startCycle, intervals, printcalls = 0, hdr = "<---> "
probe process(@1).function("_ZNK1A5printEv")
{
startA[tid()] = gettimeofday_us()
}
probe process(@1).function("_ZNK1A5printEv").return
{
t = gettimeofday_us()
old_t = startA[tid()]
if (old_t) intervals <<< t - old_t
delete startA[tid()]
}
probe process(@1).function("_ZN12_GLOBAL__N_15cycleEPK1AS2_i") ?
{
startCycle[tid()] = gettimeofday_us()
}
probe process(@1).function("_ZN12_GLOBAL__N_15cycleEPK1AS2_i").return ?
{
delete startCycle[tid()]
}
probe process(@1).function("cycle") ?
{
startCycle[tid()] = gettimeofday_us()
if (@defined($i))
{
printf("%s Stack variable i = %d\n", hdr, $i)
}
}
probe process(@1).function("cycle").return ?
{
delete startCycle[tid()]
}
probe process(@1).function("print@*test.cc:33").callee("*print*") ?
{
printcalls += 1
}
function dump_intervals(print_hist)
{
if (@count(intervals))
{
printf("%s Intervals min:%dus avg:%dus max:%dus count:%d\n", hdr,
@min(intervals), @avg(intervals), @max(intervals),
@count(intervals))
if (print_hist)
{
print(@hist_log(intervals))
}
}
}
probe timer.s(2)
{
foreach (tid in startCycle)
{
old_t = startCycle[tid]
if (old_t > 0)
{
t = gettimeofday_us()
if (t - old_t > $2 * 1000000)
{
printf("%s Pending call detected: %dus (tid %d)\n", hdr, t - old_t, tid)
}
}
}
dump_intervals(0)
}
probe end
{
println()
dump_intervals(1)
printf("%s Function print() was called from A %d times\n", hdr, printcalls)
}
Я добавил опциональные probe с function(“cycle”) и function(“print@*test.cc:33”). Поскольку существует единственная функция cycle(), уточнение адреса не требуется. Другое дело — имя print, которое соответствует сразу трем функциям. Для разрешения адреса функции A::print() я воспользовался глоб-паттерном print@*test.cc:33. Важно отметить, что имена функций в случае введения отладочной информации могут сильно зависеть как от опций оптимизации gcc, так и от его версии. Ситуация осложняется наличием багов (например, вот этот), возможностью встраивания функций при оптимизации (inlining) и другими подобными факторами. В общем, правильно определить имя функции для использования в скрипте SystemTap и в итоге получить нужную трассировку может оказаться непростой задачей.
Запустим скрипт test.stp снова.
stap test.stp -c ./test `pwd`/test 3
WARNING: For probing a particular line, use a .statement() probe, not .function(): keyword at test.stp:36:1
source: probe process(@1).function("print@*test.cc:33").callee("*print*") ?
^
B
A
A
<---> Stack variable i = 0
<---> Stack variable i = 1
<---> Stack variable i = 2
B
<---> Stack variable i = 3
<---> Intervals min:12us avg:13us max:14us count:2
A
<---> Stack variable i = 4
<---> Intervals min:12us avg:17us max:27us count:3
A
<---> Stack variable i = 5
<---> Intervals min:12us avg:19us max:27us count:4
B
<---> Stack variable i = 6
<---> Intervals min:12us avg:19us max:27us count:4
A
<---> Stack variable i = 7
<---> Intervals min:12us avg:21us max:28us count:5
<---> Intervals min:12us avg:21us max:28us count:5
A
<---> Stack variable i = 8
<---> Intervals min:12us avg:21us max:28us count:6
<---> Pending call detected: 3977342us (tid 32523)
<---> Intervals min:12us avg:21us max:28us count:6
B
<---> Stack variable i = 9
<---> Intervals min:12us avg:21us max:28us count:6
<---> Pending call detected: 3977157us (tid 32523)
<---> Intervals min:12us avg:21us max:28us count:6
<---> Intervals min:12us avg:21us max:28us count:6
value |-------------------------------------------------- count
2 | 0
4 | 0
8 |@@ 2
16 |@@@@ 4
32 | 0
64 | 0
<---> Function print() was called from A 6 times
Прикольно. Сам выводил print@…test.cc:33 как имя функции, а теперь говорит используйте .statement(). В данном случае мы не можем использовать statement, поскольку callee можно присоединить только к function. Ну да это просто предупреждение. Главное, все заработало! Мы стали получать значение переменной i и printcalls оказалась равной 6, как мы и ожидали.
Хм. Весьма и весьма на dtrace похожа, но с dtrac`ом в линухе - религия, видимо, не позволяет. А как оно при расставленных датчиках производительность просаживает?
ОтветитьУдалитьС dtrace незнаком, но судя по всему, по возможностям они действительно похожи. Насчет производительности, судя по этому документу: http://www.linuxsymposium.org/archives/OLS/Reprints-2007/keniston-Reprint.pdf в 2007 году для Pentium 1475 МГц оверхед uprobes составлял 3.4 микросекунд на один probe. Кстати, весьма неплохой документ по матчасти.
ОтветитьУдалить