ЕВГЕНИЙ ИЛЬИН
Дао DTrace
Познай себя и познай своего врага.
Тогда в сотне битв ты не будешь знать поражения.
Сан дзу. Искусство войны
Снять гипс с «клиента» без «шума и пыли», можно, как известно, несколькими способами. Если говорить о трассировке, DTrace предлагает свой, элегантный метод добраться до бриллиантов, где бы они ни были спрятаны в системе.
Недавно я делал презентацию об открытых технологиях Sun Microsystems. Технологий было много и получалось, что обзор каждой должен занимать не более пяти минут, в том числе и рассказ о таком мощном средстве наблюдения за системой, каким является DTrace. Чтобы дать аудитории краткое и понятное представление об этом инструменте, я придумал, как мне кажется, удачную аналогию: «DTrace – рентген для операционной системы». Ведь как рентген может «заглянуть» внутрь живого организма, так и DTrace позволяет увидеть процессы, происходящие внутри операционной системы и пользовательских приложений. С помощью этой технологии можно получить исчерпывающую информацию о внутреннем состоянии приложения, причём сам процесс исследования для «пациента» абсолютно безопасен, так как он не оказывает никакого влияния на «жизнедеятельность» объекта исследования.
Правда, это не совсем полная аналогия, поскольку принципы работы DTrace и рентгеновского аппарата отличаются. DTrace получает информацию от многочисленных датчиков, что находятся в ядре, библиотеках и приложениях, которые срабатывают только в том случае, когда кто-то включил их в явном виде. Выключенные датчики не оказывают никакого влияния на производительность, и система в целом работает так, как будто их вообще нет. Весь секрет в том, что DTrace умеет динамически модифицировать объект исследования.
Пожалуй, можно провести еще одну параллель между DTrace и рентгеном. Для успешного рентгеновского исследования необходимо знание физиологии, анатомии и устройства рентгеновского оборудования. Аналогично и для эффективного использования DTrace необходимо понимание принципов работы операционной системы и устройства инструмента, который позволяет заглянуть внутрь работающей системы.
В этой статье пойдёт рассказ об архитектуре DTrace, её составляющих элементах и методологиях модификации системы. Для того чтобы приведённые примеры были понятны, представлено краткое описание языка программирования D, который предоставляет универсальный доступ ко всем возможностям DTrace.
Обзор архитектуры DTrace
Основные архитектурные компоненты DTrace – это потребители, корневой модуль DTrace, датчики и провайдеры. Процессы становятся потребителями (consumers) DTrace, инициируя общение с корневым модулем DTrace. Корневой модуль находится в ядре операционной системы и обеспечивает доступ к средствам модификации кода, буферизацию и обработку событий от датчиков (probes). У потребителя две основные задачи: передать спецификации трассировки модулю DTrace (далее по тексту термины корневой модуль и модуль DTrace суть синонимы) и обрабатывать данные, полученные в процессе трассировки. Потребители обращаются к корневому модулю через интерфейс, предоставляемый библиотекой libdtrace(3LIB). Данные между потребителем и ядром передаются посредством вызовов ioctl(2) для псевдо-устройства dtrace, реализованного драйвером dtrace(7d).
Рисунок 1. Архитектура DTrace
Потребителем DTrace может быть любая программа. В состав Solaris 10 включены утилиты lockstat(1M), plockstat(1M) и intrstat(1M), являющиеся оными (это не полный список утилит Solaris 10, что являются потребителями DTrace). Однако каноническим потребителем является утилита dtrace(1M), которая предоставляет универсальный доступ ко всем средствам DTrace, так как является драйвером для компилятора языка программирования D (сам компилятор находится в библиотеке libdtrace(3LIB)). Также можно использовать dtrace(1M) в качестве самостоятельной утилиты трассировки.
Примечание: в компиляторостроении под драйвером понимается утилита, которая позволяет управлять процессом трансляции и вызывает все остальные компоненты, как то собственно компилятор, оптимизатор, кодогенератор, и т. д. Например команда сс – это драйвер компилятора С.
Провайдеры и датчики
Модуль DTrace не оснащает код инструментальными средствами, делегируя эту задачу провайдерам, которые в свою очередь также являются модулями ядра. Когда модуль DTrace даёт соответствующую команду, провайдеры определяют точки в системе, куда они потенциально могут вставить инструментальные средства. Для каждой найденной точки провайдер делает обратный вызов модуля DTrace, чтобы создать датчик (probe). То есть, по сути, провайдер – это модуль ядра, который создаёт множество датчиков, сгруппированных по тому или иному принципу (например, функции ядра, системные вызовы или функции какого-то пользовательского приложения). При помощи команды modinfo(1M) можно посмотреть на некоторые модули ядра некоторых стандартных провайдеров в Solaris:
jedi# modinfo | grep Tracing
4 f93dc000 16438 155 1 dtrace (Dynamic Tracing)
207 f930631c d98 156 1 profile (Profile Interrupt Tracing)
208 f9200ab8 554 157 1 systrace (System Call Tracing)
209 feb56c04 c5c 158 1 fbt (Function Boundary Tracing)
211 f93894f8 1304 159 1 sdt (Statically Defined Tracing)
212 f927b000 3fcc 167 1 fasttrap (Fasttrap Tracing)
225 f91358bc 68c 241 1 lx_systrace (Linux Brand System Call Tracing)
|
В примере вы видите модули ядра самого DTrace и провайдеры, которые присутствуют при установке Solaris 10 в базовой конфигурации. О некоторых из них я расскажу подробнее, сейчас же хочу обратить ваше внимание на модуль lx_systrace, который реализует провайдер lxsyscall для типизированных зон Linux (BrandZ) и содержит множество датчиков для трассировки системных вызовов Linux. На данный момент, насколько мне известно, это единственно возможный способ воспользоваться DTrace для трассировки Linux-приложений.
Для того чтобы создать датчик, провайдер специфицирует имена модуля и функции для точки инструментальной модификации кода и собственно его семантическое имя. Причем под модулем в данном контексте надо понимать программную принадлежность датчика. Для ядра это будет название модуля ядра (например, zfs), для приложений это будет или библиотека (libjvm.so, libc.so), или даже некоторая пользовательская подсистема (Xorg). Функция – это имя функции, в которой находится датчик, как, например, ufs_read() в ядре или же printf() из libc для пользовательского приложения, а семантическое имя – по сути осмысленное название, как, например, on-cpu и off-cpu для датчиков планировщика или же start для датчиков ввода-вывода.
Таким образом, каждый датчик полностью идентифицируется следующей четвёркой:
<провайдер, модуль, функция, имя>
Создание датчиков ещё не модифицирует систему, это только лишь определение возможных точек модификации системы. Это по сути создание необходимых структур данных, после чего DTrace только возвращает идентификатор датчика провайдеру. В какой же момент происходит модификация? Созданные провайдером датчики становятся видимыми для потребителей, которые могут включить группу датчиков, задав комбинацию элементов из вышеупомянутой четвёрки. После включения DTrace создаёт и привязывает к датчику блок управления (ECB – enabling control block), где определены действия (actions) и предикат (predicate), то есть что и при каком условии будет выполнено в случае срабатывания датчика. Если во время создания данного ECB других связанных с текущим датчиком ECB нет, DTrace обращается к провайдеру с указанием включить данный датчик (кстати, если ECB уже есть, то новый блок становится в хвост цепочки ECB-блоков для этого датчика). И в этот момент провайдер динамически модифицирует систему таким образом, что при срабатывании датчика управление переходит к модулю DTrace, причём первым аргументом в обращении к нему следует идентификатор датчика.
Итак, DTrace получил управление. Как только это произошло, на текущем процессоре запрещаются прерывания, и DTrace отрабатывает действия, определённые в каждом ECB-блоке из цепочки ECB-блоков сработавшего датчика. Затем прерывания разрешаются вновь, и управление возвращается провайдеру.
Пример модификации кода провайдером
Теперь давайте посмотрим, как же реально модифицируется код на платформе x86 в Solaris . Для этого нам понадобится Solaris 10 или Solaris Express Developer Edition, рутовый доступ, штатный отладчик модулей ядра mdb и 2 терминальных окошка. В одном из окошек запускаем mdb и дизассемблируем функцию ufs_lookup(), ограничив вывод тремя первыми командами, чтобы его не загромождать:
jedi# mdb -k
Loading modules: [ unix genunix specfs dtrace uppc pcplusmp scsi_vhci
ufs ip hook neti sctp arp usba uhci fctl nca lofs zfs random audiosup
sppp ptm md cpc crypto fcip fcp logindmux nsctl sdbc sv ii rdc ipc ]
> ufs_lookup::dis -n 3
ufs_lookup: pushl %ebp
ufs_lookup+1: movl %esp,%ebp
ufs_lookup+3: subl $0x10,%esp
|
Выходим из mdb при помощи <Ctrl+D>, а затем включим датчики на модуле ufs, запустив DTrace в другом терминальном окне:
jedi# dtrace -m 'ufs { trace(execname);}'
dtrace: description 'ufs ' matched 836 probes
CPU ID FUNCTION:NAME
6 20874 ufs_getpage:entry dtrace
6 21348 ufs_lockfs_begin_getpage:entry dtrace
6 20930 ufs_lockfs_is_under_rawlockfs:entry dtrace
6 20931 ufs_lockfs_is_under_rawlockfs:return dtrace
6 21349 ufs_lockfs_begin_getpage:return dtrace
6 21512 bmap_has_holes:entry dtrace
...
|
Вывод показывает, какие исполняемые файлы в системе вызывают функции модуля ufs. В нашем случае это команда dtrace, которую мы же сами и запустили. Оставим dtrace работать дальше, а сами снова вернёмся к первому окошку и снова запустим mdb:
jedi# mdb -k
Loading modules: [ unix genunix specfs dtrace uppc pcplusmp scsi_vhci
ufs ip hook neti sctp arp usba uhci fctl nca lofs zfs random audiosup
sppp ptm md cpc crypto fcip fcp logindmux nsctl sdbc sv ii rdc ipc ]
> ufs_lookup::dis -n 3
ufs_lookup: lock movl %esp,%ebp
ufs_lookup+3: subl $0x10,%esp
ufs_lookup+6: andl $0xfffffff0,%esp
|
Как нетрудно увидеть, в начале первой инструкции появился префикс lock. Это не совсем корректный код с точки зрения ассемблера x86, потому что по спецификации #lock нельзя использовать в комбинации с инструкцией movl. Поэтому эта комбинация генерирует программное прерывание (illegal opcode), управление перехватывается ловушкой (trap), где в итоге передаётся DTrace. Если вы попробуете воспроизвести эти действия, то возможно, что вместо #lock увидите нечто другое. Полученный после модификации ассемблерный код для x86, amd64 и, разумеется, для SPARC будет различен.
Подробнее об этом позже, а теперь снова вернёмся к обзору архитектуры.
Предикаты и действия. Буферы и DIF
У каждого ЕCB-блока может быть ассоциированый с ним предикат. Если таковой имеется, но его условие не выполняется, то DTrace по цепочке переходит к следующему ECB-блоку. Помимо предиката у каждого блока ECB есть список действий и, если условие предиката удовлетворено, то обработка ECB-блока продолжится последовательным исполнением всех действий данного блока. Если действие предполагает запись каких-то данных, то они будут сохранены в специальном буфере, который выделяется для каждого CPU и однозначно привязывается к потребителю, создавшему ECB-блок. Это делается потому, что для обеспечения безопасности использования действия не могут содержать явную запись в память ядра, изменение регистров, равно как и выполнение каких-то других явных операций, изменяющих состояние системы. Они могут делать это косвенно, но строго определённым способом, например остановить текущий процесс или сгенерировать точку останова в ядре. Такие действия можно делать только пользователям с определёнными полномочиями и явно разрешив деструктивные действия.
Рисунок 2. ECB, предикаты и действия
Еще одна линия защиты системы от нанесения непреднамеренного вреда – это виртуальная машина DTrace с собственным набором машинно-независимых команд RISC, который называется DIF (D Intermediate Format) и является целевым языком компиляции для libdtrace. D-скрипты транслируются в DIF и эмулируются в ядре при срабатывании датчика, подобно тому, как виртуальная машина Java (JVM) интерпретирует байткод Java. Использование эмуляции в момент исполнения гарантирует, что возможные ошибки, способные дестабилизировать систему, будут выловлены и обработаны безопасным способом. При помощи ключа -S команды dtrace можно посмотреть на генерируемые DIF-объекты (DIFO):
jedi# cat difo.d && dtrace -S -s difo.d
syscall::ioctl:entry
{
self->follow = 1;
}
DIFO 0x6f8cc0 refcnt=1 returns D type (integer) (size 4)
OFF OPCODE INSTRUCTION
00: 25000001 setx DT_INTEGER[0], %r1 ! 0x1
01: 2d050001 stts %r1, DT_VAR(1280) ! DT_VAR(1280) = "follow"
02: 23000001 ret %r1
NAME ID KND SCP FLAG TYPE
follow 500 scl tls w D type (integer) (size 4)
dtrace: script 'difo.d' matched 1 probe
^C
|
На этом я заканчиваю обзор архитектуры и предлагаю перейти к краткому знакомству со скриптовым языком программирования D.
Шпрехен зи 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»(http://docs.sun.com/app/docs/doc/817-6223).
Последнее, что следует сказать про переменные,– это их области видимости. Глобальные переменные декларируются с использованием синтаксиса языка С либо могут быть объявлены неявно при присваивании. В последнем случае такой переменной назначается тип выражения в правой части присваивания. Помимо глобальных переменных, которые видны всем компонентам (clauses) скрипта на D, можно создавать thread-local и clause-local переменные любого типа. Доступ к таким переменным осуществляется при помощи префиксов self-> и this-> соответственно. Префиксы служат как для того, чтобы разделить пространство имён для переменных, так и для того, чтобы без необходимости предварительной декларации использовать их в выражениях присваивания. Clause-local переменные содержатся в области памяти, которая используется повторно при исполнении данной компоненты и сродни автоматическим переменным в языке C. Thread-local переменные привязывают каждый индентификатор переменной к отдельным областям памяти для каждого потока команд в операционной системе.
Провайдеры
Вы уже знаете, что модификацию системы инструментальным кодом осуществляют провайдеры, и поскольку они формально отделены от ядра подсистемы трассировки, то это значит, что в DTrace возможно использовать неоднородные методологии внедрения кода инструментальных средств. Более того, количество методов со временем увеличивается, поскольку новые методы очень легко встроить в существующую реализацию DTrace. Но хотя провайдеры и используют разные методы внедрения инструментального кода, их объединяет тот факт, что все они не влияют на работу системы, если датчики находятся в выключенном состоянии.
Далее мы рассмотрим методологии, которые применяются в некоторых популярных провайдерах. Но сперва упомяну про три датчика, которые предоставляются провайдером DTrace – BEGIN, END и ERROR.
Датчик BEGIN всегда срабатывает только один раз в момент запуска скрипта, прежде чем сработает любой другой датчик. Причём до тех пор, пока не отработают все действия компоненты c идентификатором датчика BEGIN, никакой другой датчик сработать не может. Благодаря такому свойству BEGIN обеспечивает предварительную инициализацию переменных, которые могут понадобиться другим компонентам программы на D.
Датчик END тоже срабатывает только в единственный момент жизненного цикла программы, соответственно в самом конце и только тогда, когда отработали все другие датчики. Его удобно использовать для того чтобы обработать собранные в момент работы программы данные, отформатировать вывод и красиво показать итоговые результаты.
Датчик ERROR срабатывает в случае, когда при исполнении скрипта произошла ошибка времени исполнения, скажем, попытка разыменовывания указателя NULL.
Провайдер profile
Большая часть провайдеров привязывается к определённой точке в коде пользовательского приложения, программы или ядра. На этом фоне провайдер profile стоит особняком, поскольку вместо изменения кода использует источник асинхронных событий. Источником таких событий для profile являются прерывания по времени с заданным интервалом. И его датчики могут использоваться для того, чтобы производить снятие данных, которые отображают определённые аспекты состояния системы, или выполнять действия через строго определённые промежутки времени. В качестве простого, но часто встречающегося шаблона использования этого провайдера модифицируем пример с агрегациями следующим образом:
syscall::write:entry
{
@count[execname]=count();
}
tick-7s
{
В результате, для того чтобы посмотреть итоговые данные, нам больше не надо специально жать <Ctrl+C>, скрипт автоматически закончит работу через 7 секунд, когда сработает датчик tick-7s и сработает действие exit() с целочисленным аргументом, что вызовет срабатывание датчика END и приведёт к нормальному завершению работы.
В качестве более серьёзного примера использования этого провайдера стоит посмотреть на D-Light. Этот инструмент входит в пакет Sun Studio Express (см. меню Tools) – экпресс-релизе интегрированной среды разработки Sun Studio (чтобы посмотреть на него зайдите на страницу http://developers.sun.com/sunstudio/downloads/express/index.jsp). Там этот провайдер используется для сбора различной сэмплинговой информации на всём жизненном цикле приложения.
Провайдер syscall
Этот провайдер создаёт датчики на входе и возврате из каждого системного вызова в Solaris. Поскольку системные вызовы являются основным интерфейсом между пользовательскими приложениями и ядром операционной системы, провайдер syscall позволяет получить массу полезной информации о поведении приложения с точки зрения системы. Метод, по которому работает провайдер syscall – динамическая подмена соответствующей записи в таблице системных вызовов при включении датчика.
Примером для данного провайдера будет почти детективная история, которую можно найти на блогах Sun. Дело было так: в один прекрасный день, на одном сервере, который предоставлял терминальный доступ, пользователи после ввода пары логин-пароль вместо привычного приглашения оболочки к вводу команды имели неудовольствие лицезреть, как по терминальному окну бежала последовательность строк, состоящих из «непечатных» символов. Довольно быстро выяснилось, что это безобразие происходит из-за того, что некий шутник сделал /etc/motd символической ссылкой на один из служебных файлов. После удаления симлинка система некоторое время работала нормально, однако с упорством, достойным лучшего применения, через какое-то время /etc/motd вновь ссылался на тот же самый файл. В системе явно работал некий злоумышленник (пусть это будет daemon), которого нужно было обезвредить.
Вооружившись 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 (это, кстати, примеры деструктивных действий, для того чтобы разрешить их, необходимо было снабдить вызов dtrace(1M) ключиком -w). Ну и, наконец, действием 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 служит более наглядному представлению результата. Некоторые вышеизложенные детали позволяют понять, почему срабатывание датчика не заставит себя долго ждать и на каком исполняемом файле это произойдёт с очень большой вероятностью.
Заключение
Мы рассмотрели базовую архитектуру DTrace и основные принципы её работы. Многое осталось за пределами первой части статьи, как, например, возможности, предоставляемые провайдером sdt (statically-defined tracing) для создания произвольных датчиков при разработке приложений. Используя такие датчики, можно реализовать динамический аналог вызова assert() для получения отладочной информации на работающем приложении или динамически включать дополнительные детальные логи при необычном поведении серверных приложений. Не было упомянуто о том, что провайдер pid умеет создавать динамические датчики для точек входа и выхода любого приложения, не требуя модификации его исходного кода. Более того, с его помощью можно даже трассировать отдельные инструкции любой функции. Рассказ об этих провайдерах и средствах визуализации будет в продолжении. Пока же приведу несколько ссылок, которые помогут вам продолжить изучение самостоятельно.
В руководстве «Dynamic Tracing Guide» (последняя ревизия: http://docs.sun.com/app/docs/doc/819-3620) приводится подробное описание всех базовых провайдеров. Этот документ вкупе с «DTrace User Guide» (описывает основы DTrace: http://docs.sun.com/app/docs/doc/819-5488) содержит полную документацию по Dtrace. К качестве исходной точки для поиска дальнейших материалов в Интернет рекомендую посмотреть страницу сообщества DTrace на портале opensolaris.org (http://www.opensolaris.org/os/community/dtrace). Там вы найдёте ссылки на литературу, статьи и публикации других сайтов.
Из русскоязычных материалов в Интернете можно перечислить недавно запущеный в пилотном режиме – http://developers.sun.ru, жж-сообщество – http://community.livejournal.com/ru_opensolaris и сайт российской группы пользователей OpenSolaris – http://osug.ru.