НОВОСТИ [Перевод] Учебник по симулятору сети ns-3. Глава 7

Alvaros
Онлайн
Регистрация
14.05.16
Сообщения
21.452
Реакции
101
Репутация
204
oqg3-xyphklp7lki3udrmwh5xks.png








Глава 7 Трассировка

7.1 История вопроса

7.1.1 Тупые инструменты

7.2 Обзор

7.2.1 Простой пример

7.2.2 Подключение через Config

7.2.3 Поиск источников

7.2.4 Доступные источники

7.2.5 Config-пути

7.2.6 Сигнатуры обратных вызовов

7.2.7 TracedValue

7.3 Реальный пример

7.3.1 Доступные источники

7.3.2 Поиск примеров

7.3.3 Источники динамической трассировки

7.3.4 Разбор fifth.cc

Запуск/останов приложений

Приложение MyApp

Приемник трассировки

Основная программа

7.3.5 Запуск fith.cc

7.3.6 Использование помощников среднего уровня

7.4 Помощники трассировки

7.4.1 Помощники устройств

PCAP

ASCII

7.4.2 Помощники протоколов

7.5 Резюме


Глава 7 Трассировка


7.1 История вопроса


Как упоминалось в разделе 5.3, весь смысл симуляции ns-3 заключается в генерации выходных данных для изучения. У вас есть две основные стратегии получения выходных данных от ns-3: использование общих предопределенных механизмов и анализ содержимого их массового вывода для извлечения интересующей информации или разработать некий механизм вывода, который передаст только ту информацию, которую вы хотели.

Использование предопределенных механизмов массового вывода имеет то преимущество, что не требует каких-либо изменений в ns-3, но может потребовать написания сценариев для анализа и фильтрации интересующих данных. Часто, выходные сообщения PCAP или NS_LOG, собранные во время симуляции, дополнительно прогоняются через скрипты, которые используют grep, sed или awk для разбора сообщений, уменьшения, преобразования данных в удобную форму. Для выполнения преобразований должны быть написаны программы, так что это не проходит без затрат. Вывод NS_LOG не считается частью API ns-3 и может меняться от релиза к релизу без предупреждения. Кроме того, вывод NS_LOG доступен только в отладочных сборках, поэтому его использование влечет за собой снижение производительности. Более того, этот подход не сработает, если интересующей информации нет ни в одном из предопределенных механизмах вывода.

Если вам нужно добавить немного информации к предопределенным механизмам, это, безусловно, можно сделать и если Вы используете один из механизмов ns-3, то можете добавить свой код как контрибутор.

Ns-3 предоставляет другой механизм, называемый трассировкой, который позволяет избежать некоторых проблем, присущих механизмам массового вывода. У него есть несколько важных преимуществ. Во-первых, вы можете уменьшить объем данных, которыми нужно управлять, отслеживать только интересующие вас события (выгрузка всех данных на диск, для последующей обработки, операции ввода/вывода могут стать узким местом для больших симуляций). Во-вторых, если вы используете этот метод, то можете напрямую управлять форматом вывода, избавляя себя от необходимости постобработки с использованием сценариев sed, awk, perl или python. По вашему желанию вывод может быть отформатирован сразу, например, в форму, приемлемую для gnuplot (см. также ). Вы можете добавить в ядро ns-3 крючки (хуки), которые затем могут быть доступны для других пользователей, но они не будут давать никакой информации, пока их об этом прямо не попросят. По этой причине мы считаем, что система трассировки ns-3 — это лучший способ извлечения информации из моделирования, и поэтому один из самых важных для понимания механизмов в ns-3.

7.1.1 «Тупые» инструменты


Есть много способов получить информацию из программы. Самый простой способ — это распечатать информацию непосредственно в стандартный вывод, как здесь:


#include
...
void
SomeFunction (void)
{
uint32_t x = SOME_INTERESTING_VALUE;
...
std::cout pre>



Никто не помешает вам углубиться в ядро ns-3 и добавить операторы печати. Это безумно легко сделать, и, в конце концов, у вас есть полный контроль над собственной веткой ns-3. Хотя это, вероятно, не окупится в долгосрочной перспективе.



По мере увеличения числа операторов печати в ваших программах, задача обработки большого количества выходных данных будет становиться все более и более сложной. В конце концов, вы можете почувствовать необходимость управлять тем, какую информацию и как печатать, возможно, путем включения и выключения определенных категорий трасс, или увеличения⁄уменьшения объема нужной информации. Если вы продолжите идти по этому пути, то обнаружите, что повторно реализовали механизм NS_LOG (см. 5.1). Чтобы избежать этого, одной из первых вещей, которые вы могли бы сделать, это пользоваться NS_LOG.



Мы упоминали выше, что один из способов получить информацию из ns-3 — это проанализировать существующий вывод NS_LOG на предмет интересующей информации. Если вы обнаружите, что некоторая нужная вам информация отсутствует в существующем выводе журнала, вы можете отредактировать ядро ns-3 и просто направить интересующую информацию в поток вывода. Это, конечно, лучше чем добавление ваших собственных операторов печати, поскольку оно следует соглашениям о кодировании ns-3 и потенциально может быть полезна для других людей как патч к существующему ядру.



Давайте возьмем случайный пример. Если вы хотите добавить больше журналирования в TCP-сокет ns-3 (tcp-socket-base.cc) вы можете просто добавить в реализацию новое сообщение. Обратите внимание, что в TcpSocketBase::processEstablished() нет сообщения журнала на получение SYN + ACK в состоянии ESTABLISHED. Вы можете его легко добавить, изменив код. Вот оригинал:




/* Received a packet upon ESTABLISHED state. This function is mimicking the
role of tcp_rcv_established() in tcp_input.c in Linux kernel. */
void
TcpSocketBase::processEstablished (Ptr
packet,
const TcpHeader& tcpHeader)
{
NS_LOG_FUNCTION (this / No action for received SYN+ACK, it is probably a duplicated packet
}
...




Чтобы добавить возможность журналирования SYN + ACK, вы можете добавить новый NS_LOG_LOGIC в тело оператора if:




/* Received a packet upon ESTABLISHED state. This function is mimicking the
role of tcp_rcv_established() in tcp_input.c in Linux kernel. */
void
TcpSocketBase::processEstablished (Ptr
packet, const TcpHeader& tcpHeader)
{
NS_LOG_FUNCTION (this / No action for received SYN+ACK, it is probably a duplicated packet
NS_LOG_LOGIC ("TcpSocketBase " pre>



На первый взгляд это может показаться довольно просто и достаточно, но нужно учитывать, что вы будете писать код для добавления операторов NS_LOG, а для разбора журнала также придется написать код (как в сценариях grep, sed или awk), чтобы извлечь вашу информацию. Это потому, что хоть у вас и есть некоторый контроль над тем, что выводится в журнал, но этот контроль не ниже уровня компонента журнала, который обычно представляет собой целый файл исходного кода. Если вы добавляете код в существующий модуль, ваш вывод в журнал будет сосуществовать с тем выводом, который разработчик счел интересным добавить. Вы можете обнаружить, что для получения небольшого количества необходимой информации вам, возможно, придется пробираться через огромное количество посторонних неинтересующих сообщений. Всякий раз когда вам понадобится что-то сделать, вы будете вынуждены сохранять на диск огромные файлы журнала и обрабатывать их ради нескольких строк.



Поскольку в ns-3 нет никаких гарантий относительно стабильности вывода NS_LOG, вы также можете обнаружить, что важные для вас фрагменты вывода журнала, исчезают или меняются при смене релиза. Если вы зависите от структуры вывода, вы может найти другие сообщения, добавляемые или удаляемые, которые могут повлиять на код вашего синтаксического анализа.



Наконец, вывод NS_LOG доступен только в отладочных сборках, вы не можете получить вывод журнала из оптимизированных сборок, которые запускаются примерно в два раза быстрее. Использование NS_LOG влечет за собой снижение производительности.



По этим причинам мы считаем, что печать в сообщения std::cout и NS_LOG — быстрый и грязный способ получить больше информация из ns-3, но он не подходит для серьезной работы.



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



Система трассировки ns-3 разработана для работы в этом направлении и хорошо интегрирована подсистемами атрибутов (Attribute) и настроек (Config), позволяющими сравнительно легко использовать сценарии.




7.2 Обзор



Система трассировки ns-3 построена на принципах независимых источников и приемников трассировки, наряду с унифицированным механизмом подключения источников к приемникам. Источники трассировки — это объекты, которые могут сигнализировать о событиях, происходящих в симуляции, и предоставлять доступ к интересующим базовым данным. Например, источник трассировки может сигнализировать, о приеме сетевым устройством пакета и предоставлять доступ к содержимому этого пакета для заинтересованных приемников трассировки. Источник трассировки также может указывать, когда происходит интересующее изменение состояния модели. Например, окно заторов в модели TCP является основным кандидатом в источники трассировки. Каждый раз, когда окно заторов изменяется, подключенные приемники трассировки информируются об изменении с помощью отправки им старого и нового значения.



Источники трассировки сами по себе не имеют ценности, пока они не связаны с другими частями кода, которые реально делают с информацией, предоставленной источником, что-то полезное. Объекты, использующие информацию трассировки, называются приемниками трассировки. Источники трассировки являются генераторами данных, а приемники трассировки — потребителями. Такое явное разделение позволяет размещать большое количество источников по всей системе, там где авторы моделей считают их полезными. Вставка источников трассировки лишь немного увеличивает накладные расходы при выполнении.



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



Если пользователь не подключит приемник трассировки к одному из этих источников, то выводить приемнику будет нечего. Используя систему трассировки, вы и другие пользователи, подключенные к тому же источнику трассировки, получите из системы только то, что вам нужно. Никто из вас, изменяя информацию, выводимую системой не повлияет на других пользователей. Если вы, как добропорядочный гражданин open source, добавите источник трассировки, то ваша работа, может открыть возможность другим пользователям без внесения каких-либо изменений в ядро ns-3, создать новые инструменты, которые возможно, окажутся очень полезными для всех в целом.




7.2.1 Простой пример



Давайте уделим несколько минут и пройдемся по простому примеру трассировки. Чтобы понять, что происходит в примере, нам понадобится некоторое представление об обратных вызовах (обратный вызов — callback), поэтому мы должны сделать небольшой обзор прямо сейчас.




Обратные вызовы (Callbacks)



Задача системы обратных вызовов в ns-3 состоит в том, чтобы позволить одному коду вызывать функцию (или метод в C++) другого кода без какой-либо специфической межмодульной зависимости. В конечном итоге это означает, что вам необходим какой-то из вариантов косвенности — чтобы рассматривать адрес вызываемой функции как переменную. Эта переменная называется переменной-указателем на функцию. Отношение между функцией и указателем на функцию на самом деле ничем не отличаются от отношения объекта и указателя на объект.



В Си каноническим примером указателя на функцию является указатель на функцию возвращающую целое число (pointer-to-function-returning-integer или />

PFI). PFI, принимающая один параметр int, может быть объявлена как,




int (*pfi)(int arg) = 0;




(Но прежде чем писать такой код, прочитайте :



( ))



После этого объявления вы просто получаете переменную с именем pfi, которая инициализирована значением 0. Если вы хотите инициализировать этот указатель чем-то значимым, то вам нужно иметь функцию с совпадающей сигнатурой. Для этого вы могли бы использовать функцию, которая выглядит следующим образом:




int MyFunction (int arg) {}




Если у вас есть такая функция, вы можете инициализировать переменную так, чтобы она указала на вашу функцию:




pfi = MyFunction;




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




int result = (*pfi) (1234);




Это наглядно потому, что наводит на мысль, что это похоже на разыменование указателя функции, подобно тому, как вы разыменовываете любой другой указатель. Однако, как правило, люди используют тот факт, что компилятор понимает, что происходит, и просто используют более краткую форму:


int result = pfi (1234);




Выглядит так, как будто, вы вызываете функцию с именем pfi, но компилятор достаточно умен, чтобы догадаться, что через переменную pfi нужно косвенно вызвать функцию MyFunction.



Концептуально, система трассировки работает точно также. По сути, приемник трассировки является функцией обратного вызова. Когда приемник выражает интерес к получению событий трассировки, он добавляет себя в качестве обратного вызова в список обратных вызовов, хранящийся в источнике трассировки. Когда происходит интересующее событие, источник трассировки вызывает свой operator(...), подставляя ноль или больше аргументов. В конце концов operator(...) заходит в систему и делает нечто похожее на косвенный вызов, который вы только что видели, предоставляя ноль или более параметров, так же как выше вызов pfi передал бы целевой функции MyFunction один параметр.



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



Если вам нужна более подробная информации о том, как это на самом деле устроено в ns-3, не стесняйтесь просматривать раздел «Обратный вызов» руководства ns-3.




Прогулка по fourth.cc



Мы предоставили некоторый код для реализации того, что действительно является самым простым примером трассировки, который можно собрать. Вы можете найти этот код в директории examples/tutorial как fourth.cc. Давайте пройдемся по нему:




/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
/*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation;
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "ns3/object.h"
#include "ns3/uinteger.h"
#include "ns3/traced-value.h"
#include "ns3/trace-source-accessor.h"
#include
using namespace ns3;




По большей части, этот код должен быть вам знаком. Как уже упоминалось выше, система трассировки интенсивно использует системы Object и Attribute, поэтому вам нужно будет их подключить. Первые два include явно включают в себя декларации для этих систем. Вы можете использовать заголовок основного модуля, чтобы подключить все одним файлом, но мы делаем здесь так, чтобы проиллюстрировать, насколько все просто на самом деле.



Файл traced-value.h содержит требуемые объявления для трассировки данных, которые подчиняются семантике значений(Если объект можно копировать и присваивать, то о нем говорят, что он обладает семантикой значений.).

Вообще, семантика значения просто означает, что вы можете передавать сам объект, а не передавать адрес объекта. На самом деле все это означает, что вы сможете простым путём отслеживать все изменения, внесенные в TracedValue.



Поскольку система трассировки интегрирована с атрибутами (Atrrribute), а атрибуты работают с объектами Object, то должен существовать объект ns-3 со встроенным источником трассировки. Следующий фрагмент кода объявляет и определяет простой объект, с которым мы сможем работать.


class MyObject : public Object
{
public:
static TypeId GetTypeId (void)
{
static TypeId tid = TypeId ("MyObject")
.SetParent (Object::GetTypeId ())
.SetGroupName ("MyGroup")
.AddConstructor ()
.AddTraceSource ("MyInteger",
"An integer value to trace.",
MakeTraceSourceAccessor (&MyObject::m_myInt),
"ns3::TracedValueCallback::Int32");
return tid;
}

MyObject () {}
TracedValue m_myInt;
};




Выше приведены две важные в отношении трассировки строки кода .AddTraceSource и объявление TracedValue m_myInt.



AddTraceSource предоставляет «крючки» (хуки), используемые для подключения источника трассировки к внешнему миру через систему Config. Первый аргумент — это имя данного источника трассировки, которое делает его видимым в системе Config. Второй аргумент — это строка справки. Теперь посмотрим на третий аргумент, а точнее на аргумент третьего аргумента: &MyObject::m_myInt. Это TracedValue, который будучи добавленным в класс, всегда становится членом данных класса. Последний аргумент — это имя typedef для типа TracedValue в текстовом виде. Это используется для корректной генерации в документации сигнатуры функции обратного вызова, что особенно полезно для более общих типов обратных вызовов.



Объявление TracedValue<> предоставляет инфраструктуру, которая управляет обработкой обратного вызова. Каждый раз, когда «подопечная» переменная изменяется, механизм TracedValue будет предоставлять как старое, так и новое значение этой переменной, в данном случае значение int32_t. Функция приемника трассировки traceSink для такого TracedValue должна иметь сигнатуру




void (* traceSink)(int32_t oldValue, int32_t newValue);




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



Продолжая просматривать fourth.cc, мы видим:




void
IntTrace (int32_t oldValue, int32_t newValue)
{
std::cout pre>



Это подходящая декларация приемника трассировки. Она напрямую соответствует сигнатуре функции обратного вызова. Как только он будет подключен, эта функция начнет вызываться при каждом изменении TracedValue.



Мы посмотрели на источник и приемник трасс. Остается код для подключения источника к приемнику, который расположен в main():




int
main (int argc, char *argv[])
{
Ptr myObject = CreateObject ();
myObject->TraceConnectWithoutContext ("MyInteger", MakeCallback(&IntTrace));
myObject->m_myInt = 1234;
}




Здесь мы сначала создаем экземпляр MyObject, в котором имеется источник трассировки.



Следующий шаг, TraceConnectWithoutContext, формирует соединение между источником трассировки и и приемником. Первый аргумент — это просто имя источника трассировки «MyInteger», которое мы видели выше. Обратите внимание на шаблонную функцию MakeCallback. Эта функция реализует магию, необходимую для создания базового объекта обратного вызова ns-3 и связывания его с функцией IntTrace. TraceConnect устанавливает связь между предоставленной функцией и перегруженным operator() в трассируемой переменной, на которую ссылается атрибут «MyInteger». После того, как эта связь установлена, источник трассировки будет запускать предоставленную вами функцию обратного вызова.



Код для выполнения всего этого, конечно, нетривиален, но суть в том, что вы делаете что-то похожее на приведенный выше пример pfi(), который вызывается источником трассировки. Объявление TracedValue m_myInt; в самом объекте создает магию, необходимую для обеспечения перегрузки оператора присваивания, которые будут использовать operator() для фактического обратного вызова с нужными параметрами. Строка .AddTraceSource ворожит соединение обратного вызова с системой Config, а TraceConnectWithoutContext колдует над подключением вашей функции к источнику трассировки, который определен именем атрибута.



Давайте пока не будем говорить о контексте.



Наконец, строку, присваивающую значение m_myInt:


myObject->m_myInt = 1234;




следует понимать как вызов operator= для переменной-члена m_myInt с целым числом 1234 передаваемым в качестве параметра.



Поскольку m_myInt является TracedValue, этот оператор определен для выполнения обратного вызова, который возвращает void и принимает два целочисленные значения в качестве параметров — старое и новое значения для рассматриваемого целого числа. Это функция с той же сигнатурой, что и у определенной нами функции обратного вызова — IntTrace.



Подводя итог, можно сказать, что источник трассировки — это, по сути, переменная, которая хранит список обратных вызовов. Функция трассировки используется как цель обратного вызова. Информационные системы типов Attribute и Object используются для обеспечения возможности подключения источников трассировки к приемникам трассировки. Акт «воздействия» на источник трассировки — это выполнение оператора источника трассировки, который запускает обратные вызовы. Они используют данные переданные источником как параметры обратного вызова.



Если вы сейчас скомпилируете и запустите этот пример,


$ ./waf --run fourth




вы увидите, что выходные данные из функции IntTrace будут выведены сразу после доступа к источнику трассировки:




Traced 0 to 1234




Когда мы выполнили код, источник трассировки сработал и автоматически передал для трассировки значения переменной до и после. Функция IntTrace затем распечатывает это в стандартный вывод.


myObject-> m_myInt = 1234;





7.2.2 Подключение через Config



Вызов TraceConnectWithoutContext, показанный выше в простом примере, на самом деле очень редко используется в системе. В более общем случае для выбора источника трассировки используется подсистема Config с применением так называемого Config‑пути. Мы видели пример этого в предыдущем разделе, где при эксперименте с third.cc мы подключили событие «CourseChange».



Напомним, что при моделировании мы определили приемник трассировки для печати информации об изменении курса в модели мобильности. Теперь должно быть намного понятнее, что делает эта функция:


void
CourseChange (std::string context, Ptr model)
{
Vector position = model->GetPosition ();
NS_LOG_UNCOND (context NodeList/"
GetId ()
$ns3::MobilityModel/CourseChange";

Config::Connect (oss.str (), MakeCallback (&CourseChange));




Давайте попробуем разобраться в том, что может вызвать затруднение в понимании этого кода. Предположим, что номер узла, возвращаемый функцией GetId (), равен «7». В этом случае показанный выше путь будет содержать




"/NodeList/7/$ns3::MobilityModel/CourseChange"




Последний сегмент Config-пути должен быть атрибутом объекта. На самом деле, если у вас был указатель на Object который имеет атрибут «CourseChange», вы можете написать так же, как мы это делали в предыдущем примере. Вам к настоящему времени известно, что мы обычно храним указатели на наши узлы в NodeContainer. В примере third.cc интересующие узлы хранятся в wifiStaNodes NodeContainer. На самом деле, когда мы выстраивали путь, то использовали этот контейнер, чтобы получить Ptr , который мы использовали для вызова GetId (). Мы могли бы использовать этот Ptr для вызова. Подключите метод напрямую:




Ptr
 
Сверху Снизу