Опубликован: 04.07.2008 | Уровень: специалист | Доступ: платный | ВУЗ: Европейский Университет в Санкт-Петербурге
Лекция 15:

Наблюдение, профилирование и трассировка работы приложений и системы. Концепция DTrace

< Лекция 14 || Лекция 15: 12345 || Лекция 16 >

Краткое введение в язык D

Как уже говорилось ранее, каноническим потребителем, предоставляющим универсальный доступ ко всем средствам, является dtrace(1M). Универсальность достигается благодаря языку D, на котором программируются предикаты и действия. Программа на языке D выглядит как последовательность компонент ( clauses ) вида

дескриптор-датчика, [дескриптор-датчика...]
[ /предикат/ ]
{
действие; [действие; ...]
}

Как можно догадаться, дескриптор-датчика, он же индентификатор датчика, – это та самая четвёрка <провайдер, модуль, функция, имя>. В синтаксисе языка D она в общем виде выглядит следующим образом:

probeprov:probemod:probefunc:probename

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

tick-1s
syscall::read:entry
::exec*:entry

Предикат – это условное выражение. Если он отсутствует в компоненте, то в этом случае считается, что предикат есть и его условие всегда удовлетворено. Действия – это, собственно, то, что будет выполнятся, когда датчик сработал и удовлетворено условие предиката. Кстати, понимая, как порождаются ЕCB блоки, и учитывая, что каждый новый ECB блок становится в хвост списка ECB блоков датчика, мы видим, что последовательность выполняемых компонентами действий определяется порядком их появления в D скрипте и временем, когда происходит модификация кода инструментальными средствами.

Поскольку язык D создавался с оглядкой на С, в нем поддерживаются все встроенные типы языка С, typedef, а также возможность определять типы struct, union и enum. Имеются также собственные встроенные скалярные типы ( string ), ассоциативные массивы и агрегации. Последний тип представляет из собой именованную структуру, хранящую результат некоторой агрегирующей функции, которая индексируется кортежами (n-ками). К примеру, такой скрипт

syscall::write:entry
{
@count[execname]=count();
}

покажет, какое количество системных вызовов произвел каждый исполняемый файл, выполнявшийся за время работы скрипта. А если изменить агрегацию так, чтобы индексировать данные по двум параметрам, скажем, execname и uid, то получим таблицу, где увидим, что общее количество будет еще и разбито по конкретным идентификаторам пользователей:

bash-3.00# cat aggr.d && dtrace -qs aggr.d
syscall::write:entry
{
@count[execname,uid]=count(); printf( "." );
}
...............................^C
dtrace 1234 5
dtrace 0 7
init 0 8
dtgreet 0 13

Запустив команду, нужно подождать некоторое время, после чего нажать Ctrl-C. Приведенный выше пример также демонстрирует использование действия printf() (оно, кстати, полностью повторяет реализацию функции printf() в языке C) и встроенных переменных execname и uid, определённых в языке D, которые часто встречаются в предикатах и действиях скриптов на D. Также довольно часто применяются и следующие встроенные переменные:

  • probeprov, probemod, probefunc, probename: имена провайдера, модуля, функции и датчика для текущего датчика;
  • execname: имя текущего исполняемого модуля;
  • pid, ppid: идентификаторы текущего процесса и родителя текущего процесса;
  • curpsinfo: структура psinfo для текущего процесса;
  • timestamp: время с момента загрузки в наносекундах;
  • args[]: массив аргументов, нумерующийся от 0 до <количество_аргументов – 1>.

Про последний массив надо сказать, что его элементы определяются провайдером по своему усмотрению. Так, для провайдера syscall на датчике entry в массиве arg[0..n] будут представлены аргументы системного вызова, а на датчике return в массиве arg[0..1] – коды возврата. А вот для провайдера io в arg[0] будет указатель на структуру bufinfo. Значения аргументов для всех провайдеров приведены в спецификации "Dynamic Tracing Guide".

Последнее, что следует сказать про переменные,– это рассказать об их областях видимости. Глобальные переменные декларируются с использованием синтаксиса языка С либо могут быть объявлены неявно при присваивании. В последнем случае такой переменной назначается тип выражения в правой части присваивания. Помимо глобальных переменных, которые видны всем компонентам (clauses) скрипта на D, можно создавать thread-local и clause-local переменные любого типа. Доступ к таким переменным осуществляется при помощи префиксов self-> и this-> соответственно. Префиксы служат как для того, чтобы разделить пространство имён для переменных, так и для того, чтобы без предварительной декларации использовать их в выражениях присваивания. Clause-local переменные содержатся в области памяти, которая используется повторно при исполнении данной компоненты и сродни автоматическим переменным в языке C. Thread-local переменные привязывают каждый индентификатор переменной к отдельным областям памяти для каждого потока команд в операционной системе.

Провайдеры

Mы уже знаем, что модификацию системы инструментальным кодом осуществляют провайдеры, и поскольку они формально отделены от ядра подсистемы трассировки, это значит, что в DTrace возможно использовать неоднородные методологии внедрения кода инструментальных средств. Более того, количество методов со временем увеличивается, поскольку новые методы очень легко встроить в существующую реализацию DTrace. Но хотя провайдеры применяют разные методы внедрения инструментального кода, их объединяет тот факт, что все они не влияют на работу системы, если датчики находятся в выключенном состоянии.

Далее мы рассмотрим методологии, которые используются в некоторых популярных провайдерах. Но сперва упомяну про три датчика, которые предоставляются провайдером dtraceBEGIN, END и ERROR. Датчик BEGIN всегда срабатывает только один раз в момент запуска скрипта прежде, чем сработает любой другой датчик. Причём до тех пор, пока не отработают все действия компоненты c идентификатором датчика BEGIN, никакой другой датчик сработать не может. Благодаря такому свойству BEGIN обеспечивает предварительную инициализацию переменных, которые могут понадобится другим компонентам программы на D. Датчик END тоже срабатывает только в единственный момент жизненного цикла программы, соответственно, в самом конце и только после того, как отработали все другие датчики. Его удобно использовать для того, чтобы обработать собранные в момент работы программы данные, отформатировать вывод и красиво показать итоговые результаты. Датчик ERROR срабатывает в случае, когда при исполнении скрипта произошла ошибка времени исполнения, скажем, попытка разыменовывания указателя NULL.

Провайдер profile

Большая часть провайдеров привязывается к определённой точке в коде пользовательского приложения, программы или ядра. На этом фоне провайдер profile стоит особняком, поскольку вместо изменения кода применяет источник асинхронных событий. Источником таких событий для profile являются прерывания по времени с заданным интервалом. И его датчики могут использоваться для того, чтобы производить снятие данных, которые отображают определённые аспекты состояния системы, или выполнять действия через строго определённые промежутки времени. В качестве простого, но довольно часто встречающегося шаблона использования этого провайдера модифицируем пример с агрегациями следующим образом:

syscall::write:entry
{
@count[execname]=count();
}
tick-7s
{
exit(0);
}

В результате, чтобы посмотреть итоговые данные, нам больше не надо специально жать Ctrl-C, скрипт автоматически закончит работу через 7 секунд, когда сработает датчик tick-7s и сработает действие exit() с целочисленным аргументом, что вызовет срабатывание датчика END и приведёт к нормальному завершению работы. В качестве более серьёзного примера использования этого провайдера, стоит посмотреть на D-Light. Этот инструмент входит в пакет Sun Studio Express (см. меню Tools) – экпресс-релизе интегрированной среды разработки Sun Studio (google вам в помощь, чтобы скачать и посмотреть5Если вы предпочитаете yandex.ru, который не индексирует англоязычные тексты на sun.com, просто зайдите на страницу http://developers.sun.com/sunstudio/downloads/express/index.jsp ). Там этот провайдер используется для сбора различной сэмплинговой информации на всём жизненном цикле приложения.

Провайдер syscall

Этот провайдер создаёт датчики на входе и возврате из каждого системного вызова в Solaris. Поскольку системные вызовы являются основным интерфейсом между пользовательскими приложениями и ядром операционной системы, провайдер syscall позволяет получить массу полезной информации о поведении приложения с точки зрения системы. Метод, по которому работает провайдер syscall – динамическая подмена соответствующей записи в таблице системных вызовов при включении датчика.

Примером для данного провайдера будет почти детективная история, которую можно найти на блогах Sun. Дело было так: в один прекрасный день, на одном сервере, который предоставлял терминальный доступ, пользователи после ввода пары логин-пароль вместо привычного приглашения оболочки к вводу команды имели неудовольствие лицезреть, как по терминальному окну бежала последовательность строк, состоящих из "непечатных" символов. Довольно быстро выяснилось, что это безобразие происходит из-за того, что некий шутник сделал /etc/motd символической ссылкой на один из служебных файлов. После удаления этой ссылки система некоторое время работала нормально, однако с упорством, достойным лучшего применения, через какое-то время /etc/motd вновь ссылался на тот же самый файл. В системе явно работал некий злоумышленник, которого нужно было обезвредить.

Вооружившись DTrace, детектив принялся за работу. Во-первых, было очевидно, что ключ к разгадке даст трассировка системного вызова symlink(), который создаёт ссылки. Следовательно, при помощи провайдера syscall можно отловить момент вызова, а предикатом ограничить область срабатывания датчика таким образом, чтобы запуск действий происходил только при манипуляции с файлом /etc/motd. Далее остаётся вывести pid – и дело сделано. Сие было реализовано таким образом:

#!/usr/sbin/dtrace -qs
syscall::symlink:entry
/basename(copyinstr(arg1))=="motd" /
{
printf("Execname: %s, pid=%d\n", execname, pid);
}

Детектив оставил скрипт сидеть в засаде и стал ждать результатов наблюдений. Через некоторое время датчик сработал, но запуск из консоли команды ptree с выловленным pid'ом в качестве аргумента ничего не дал – такого идентификатора процесса в системе уже не было. Злодей успел сделать своё грязное дело и смылся. Поэтому следующая версия ловушки выглядела более хитроумно:

!#/usr/sbin/dtrace -wqs
syscall::symlink:entry
/basename(copyinstr(arg1))=="motd" /
{
printf("Execname: %s, pid=%d\n", execname, pid);
copyoutstr("/tmp/motd",arg1,9);
stop();
system("ptree %d",pid);
system("prun %d",pid);
system("rm /tmp/motd");
}

Что поменялось? Во-первых, исполнение процесса, на котором сработал датчик, приостанавливалось действием stop(). Во-вторых, аргумент вызова symlink подменялся на /tmp/motd вместо /etc/motd. Ну и, наконец, действием system() запускалась команда ptree на полученом pid, затем prun возобновлял прежде приостановленное выполнение процесса, и напоследок удалялась ссылка /tmp/motd. Можно было бы обойтись чуть меньшим количеством действий, но не лишенный чувства юмора детектив решил не уничтожать процесс, а позволил ему выполняться дальше, не причиняя вреда системе.

Провайдер fbt (function boundary tracing)

Провайдер трассировки границ функции ( fbt ) создаёт датчики для момента входа-в и выхода-из всех функций ядра Solaris. Хотя механизм реализации fbt сильно привязан к конкретной архитектуре набора команд, fbt присутствует и на SPARC, и на x86, и на amd64. Фактически, мы уже успели посмотреть и понять, каким образом fbt модифицирует код на архитектуре x86 в примере, где применялся mdb для дизассемблирования функции ufs_lookup(). Приведённый в том примере трюк с #lock используется по историческим соображениям, на платформе amd64 реализован более элегантый способ – при помощи вызова программного прерывания. На SPARC это реализовано ещё элегантней – при помощи команды перехода ba,a +offset, и если вы обладаете знанием ассемблера SPARC, то можете проделать абсолютно аналогичное упражнение при помощи mdb и даже продвинуться немного дальше. Если мы дизассемблируем в mdb код по адресу ufs_lookup+offset, где offset – операнд команды перехода, то увидим, как именно происходит передача управления к DTrace.

Провайдер fbt позволяет заглянуть глубже и наблюдать за тем, что происходит непосредственно в ядре. Скрипт, приведённый в качестве примера использования провайдера, показывает, какую последовательность вызовов функций ядра генерирует системный вызов ioctl:

#!/usr/bin/dtrace -s
#pragma D option flowindent
syscall::ioctl:entry
{
self->follow = 1;
}
fbt:::
/self->follow/
{ }
syscall::ioctl:return
/self->follow/
{
self->follow = 0;
exit(0);
}

Этот пример также иллюстрирует применение thread-local переменной. Здесь она используется для того, чтобы ограничить вывод только теми вызовами, которые происходят в том же потоке команд, что и сам системный вызов. Прагма flowindent служит более наглядному представлению результата. Некоторые вышеизложенные детали позволяют понять, почему срабатывание датчика не заставит себя долго ждать и на каком исполняемом файле это произойдёт с очень большой вероятностью.

< Лекция 14 || Лекция 15: 12345 || Лекция 16 >
Александр Тагильцев
Александр Тагильцев

Где проводится профессиональная переподготовка "Системное администрирование Windows"? Что-то я не совсем понял как проводится обучение.

Анатолий Натрусов
Анатолий Натрусов
Россия
Виктор Саркисов
Виктор Саркисов
Россия, Нижний Новгород, НГТУ, 2001