欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

NS-3学习笔记 5

程序员文章站 2022-07-06 13:20:30
...

说明:今天学习《ns-3 tutorial》第7章 Tracing。

7. Tracing

7.1 Background

运行NS-3模拟的落脚点是得到运行结果的输出,而获得输出有两类基本方法:1.使用通用的预定义的大量输出方法并解析输出内容,以获取感兴趣的信息;2.开发新的输出机制来获取感兴趣的信息。
方法1的好处是不需要改变ns3,但要求编写脚本解析和过滤兴趣数据,通常在脚本运行时,可以PCAP或NS_LOG形式输出的信息,可以利用grep,sed或awk等linux工具解析信息,减少无用数据和转化数据格式。NS_LOG不是NS-3API的一部分,它可能在不同版本中表现不一样,而且NS_LOG仅能在debug builds时获得,所以会影响模拟程序的运行性能。

NS3提供了Tracing机制来避免其他内置输出方式的各种问题。使用Tracing机制来得到输出,有几个优势:

  • 可以通过跟踪较少的事件来减少输出数据
  • 可以直接控制输出格式,不再需要其他脚本工具来整理数据
  • 可以在内核中增加钩子
    NS3官方手册认为ns-3的tracing系统是获得模拟输出的最佳机制,也是在学NS3时需要重点理解的内容。

7.1.1 钝器(Blunt Instruments)

最简单的获得数据输出的方法是使用标准输出cout或print方法:

void
SomeFunction(void)
{
    uinit32_t x = SOME_INTERESTING_VALUE;
    ...
    std::cout << "The value of x is " << x << std::endl;
}

但在大型模拟程序中,类似输出语句的增多会使的程序越来越复杂。最后你可能会发现自己在重新实现NS_LOG模块的种种功能。
在使用NS-LOG时,如果发现一些信息无法得到,你可以改变源代码,追加NS_LOG输出,例如原始的TcpSocketBase中并不记录在ESTABLISHED状态时的SYN+ACK包,源代码如下:

void
TcpSocketBase::ProcessEstablished(Ptr<Packet> packet, const TcpHeader& tcpHeader)
{
    NS_LOG_FUNCTION(this << tcpHeader);
    ...
    else if(tcpflags == (TcpHeader::SYN || TcpHeader::ACK)
    {
        //不记录
    }
    ...
}

为了使用NS_LOG输出这类数据包,你可以改变源代码的相应部分:

void
TcpSocketBase::ProcessEstablished(Ptr<Packet> packet, const TcpHeader& tcpHeader)
{
    NS_LOG_FUNCTION(this << tcpHeader);
    ...
    else if(tcpflags == (TcpHeader::SYN || TcpHeader::ACK)
    {
        //记录
        NS_LOG_LOGIC("TcpSocketBase" << this << " ignoring SYN+ACK");
    }
    ...
}

上面的方法虽然可以解决一些问题,但不是最好的方法。理想的方法是使用API访问核心系统,仅获取所需信息,且不改变原始的NS3源代码。

7.2 Overview

ns-3的tracing系统建立在跟踪source与跟踪sink相互独立的概念上,并且采用了统一的机制来连接source与sink。source是那些在模拟过程中能够向事件发出信号的并且能够访问相关兴趣数据的实体。例如,一个trace source能够指出何时某个网络设备接收到了一个包,并且向trace sink提供对这个包内容的访问接口。一个trace source能够指出何时模型的状态发生变化。例如tcp的协调窗口就可以作为一个trace source,当窗口大小发生变化时,可以将变化前、后的值都传给trace sink。只有trace source自己产生信息是没有意义的,必须有消费者。
trace sink就是trace source信息的消费者。
ns3中Trace source与Trace sink间这种清晰的划分允许大量的source散落在系统或模型中,凡是模型的作者认为有需要的地方都可以设置source,而插入trace source带来的执行代价是很小的。
每个trace source产生的跟踪事件可以有0个或多个消费者,可以将它们看成是1对多的关系,消费同一trace source消息的不同的trace sink之间不会有任何相互影响。这保证了脚本编写时的独立性和简易性。
Trace source与sink间必须通过一种规范的机制连接起来,否则不会有任何输出。

7.2.1 简单例子

下面看一个简单的tracing例子,这可能需要一些callback知识才能理解,所以先说一下callback。
1.callbacks
在NS-3中,回调系统的目的是使得一段代码可以在没有任何特别的模块间依赖的情况下调用一个函数。这意味着你需要某种间接的方式调用函数——使用函数的地址调用。事实上trace sink就是某个callback。
一个trace sink想消费某个源事件产生的消息时,它可以把自己作为一个callback添加到由trace source控制的callbacks列表中。当相关兴趣事件发生时,source就调用operator()方法,并传给该方法以0个~多个参数,这些参数就是sink函数的指针,通过指针间接调用sink函数。对于这个问题,更多的细节可以参考《ns-3 Manual》。
2. 过一遍fourth.cc
ns3目录下的examples/tutorial/fourth.cc中是一个简单的tracing例子:

#include "ns3/object.h"
#include "ns3/uinteger.h"
#include "ns3/traced-value.h"
#include "ns3/trace-source-accessor.h"

#include <iostream>

using namespace ns3;

class MyObject : public Object
{
public:
  /**
   * Register this type.
   * \return The TypeId.
   */
  static TypeId GetTypeId (void)
  {
    static TypeId tid = TypeId ("MyObject")
      .SetParent<Object> ()
      .SetGroupName ("Tutorial")
      .AddConstructor<MyObject> ()
      .AddTraceSource ("MyInteger",
                       "An integer value to trace.",
                       MakeTraceSourceAccessor (&MyObject::m_myInt),
                       "ns3::TracedValueCallback::Int32")
    ;
    return tid;
  }

  MyObject () {}
  TracedValue<int32_t> m_myInt;
};

void
IntTrace (int32_t oldValue, int32_t newValue)
{
  std::cout << "Traced " << oldValue << " to " << newValue << std::endl;
}

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

  myObject->m_myInt = 1234;
}

由于NS3的trace系统与对象和属性关系密切,所以上面的include语句中首先包含了相关内容。更为常见的方法是使用下列语句包含所有核心库:

#include "ns3/core-module.h"

文件traced-value.h不被包含于core-module中,它引入了一些必要的、遵循值语义的tracing数据声明。通常值语义就是指你可以使用对象本身,而不是使用对象的地址。(这里不太明白
既然tracing系统与属性类们整合在一起,而属性类与对象类们一起工作,那么一定有一个ns-3对象放着trace source。下面有一个简单的例子来解释这一点:

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

上面例子中有关trace的两行特别重要:一是.AddTraceSource(…);二是TraceValue….。
AddTraceSource..提供了用于连接到trace source 的钩子,可以使用Config系统连接到“外部世界”,其中第一个参数是trace source 的名字,使其在Config系统中可见;第二个参数是用于解释的帮助字符串;第三个参数比较重要,其中的&MyObject::m_myInt指明了一个被增加到该类中变量m_myInt,它成为这个类的一个数据成员;第四个参数表示TraceValue类型的typedef名称,它为正确的回调函数签名生成文档。
TracedValue m_myInt 提供了一个用于驱动callback过程的框架,每当相关数据改变时,TracedValue机制将提供该变量的老值和新值,本例中是一个int32_t值。这个值对应的trace sink函数traceSink需要如下形式的签名(注册):

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

所有trace sink挂钩到trace source上时,都必须有这样的签名。从fourth.cc例子中可以看到语句:

void
IntTrace (int32_t oldValue, int32_t newValue)
{
  std::cout << "Traced " << oldValue << " to " << newValue << std::endl;
}

上面代码定义了trace sink函数IntTrace,它直接对应于回调函数签名。一旦它与source 连接起来,函数IntTrace就会在TracedValue改变时被调用。而连接是在第三方函数中完成的(例如fourth.cc中的main)。
ps:这与callback机制相吻合,回调机制中有三方面:回调函数、调用函数、第三方函数;第三方函数连接调用函数与回调函数。
在fourth.cc的main函数中,首先创建了trace source所在类的对象myObject,然后使用TraceConnectWithoutContext 函数建立了与trace sink函数的连接。TraceConnectWithoutContext 中的第一个参数是trace source名,第二个参数使用MakeCallBack方法调用之前定义的sink函数IntTrace。
再补充说明一下:.AddTraceSource将Callback连接到Config系统;TraceConnectWithoutContext将自己定义的trace sink函数连接到由属性名指定的trace source。
总的来看,trace source是一个变量,这个变量管着一个callbacks列表。trace sink是一个函数,这个函数是回调执行的目标。属性和对象类型信息系统用于建立source 与sink间的连接。“hitting”source这个动作就是对trace source执行一次调用callbacks的运算。
运行./waf –run scratch/fourth后会得到一条输出

Traced 0 to 1234

这表示了m_myInt原来旧值是0,后来新值为1234

7.2.2 连接到Config
上面提到的TraceConnectWithoutContext调用事实上在应用中较为少见,更典型的是使用Config系统通过config path来选择一个trace source。
回忆一下在第6章结束时我们定义了一个trace sink来输出移动节点的位置变化信息的,这个sink函数就是CourseChage:

void
CourseChage(std::string context, Ptr<const MobilityModel> model)
{
    Vector position = model->GetPosition();
    NS_LOG_UNCOND(context << "x = " << position.x << ", y = " << position.y);
}

当我们把CourseChange连接到trace source上时,我们使用了config path来指定源:

std::ostringstream oss;
oss << "/NodeList/"
    <<wifiStaNodes.Get(nWifi-1)->GetId()
    << "/$ns3::MobilityModel/CourseChange";
Config::Connect(oss.str(),MakeCallback(&CourseChange));

假设wifiStaNodes.Get(nWifi-1)->GetId()得到的值为7,那么上面的配置路径config path即为:/NodeList/7/$ns3::MobilityModel/CourseChange。config path的最后一部分一定是某个对象的某个属性。如果你有这个“CourseChange”属性所在对象的指针,你可以将上面代码换成下列形式:

#wifiStaNodes这个container中存放的就是各节点的指针,按序号索引可以得到特定节点的指针。
Ptr<Object> theObject = wifiStaNodes.Get(nWifi -1);
theObject->TraceConnectWithoutContext("CourseChange",MakeCallback(&CourseChange));

或者:

Ptr<Object> theObject = wifiStaNodes.Get(nWifi -1);
theObject->TraceConnect("CourseChange",MakeCallback(&CourseChange));

Config函数采用的path表示了一个对象指针链,path的每一部分对应了某个对象属性。最后一段是我们关注的属性,前面的部分是包含或寻找对象用的。Config代码解析且走查这个path直到它得到最后的一段,然后将其解释为路径最后一个对象的一个属性。Config函数之后会对调用近似的TraceConnect方法或TraceConnectWithoutContext方法。下面来分析一下path的细节。
路径中的第一个”/“字符表示所谓的命名空间,在config系统的预定义的命名空间中有个叫”NodeList“,它是在模拟程序中的所有节点的列表,列表中的元素可以通过下标来获取,所以”/NodeList/7“指的就是第8个节点。
阅读《ns3 manual》的”Object Model“章节,我们广泛使用了对象聚合,它允许我们建立不同对象间的关系,而不用建立一个复杂的继承树或预先决定哪些对象是Node的一部分。在聚合中的每个对象都可以由别的对象访问。
路径中的”$“字符会告诉config系统,这一段路径是一个对象类型的名字,所以GetObject方法将被用于寻找这个类型,在上面的例子中,MobilityHelper将被安排聚合或关联一个移动模型ns3::MobilityModel类型的类到每个无线节点。
路径的最后一部分是ns3::MobilityModel的对象的属性CourseChange,你可以在源代码src/mobility/model/mobility-model.cc找到CourseChange:

TypeId 
MobilityModel::GetTypeId (void)
{
  static TypeId tid = TypeId ("ns3::MobilityModel")
    ....
    .AddTraceSource ("CourseChange", 
                     "The value of the position and/or velocity vector changed",
                     MakeTraceSourceAccessor (&MobilityModel::m_courseChangeTrace), 
                     "ns3::MobilityModel::TracedCallback")
  ;
  return tid;
}

在这个源文件对应的头文件mobility-model.h中,还可以找到:

ns3::TracedCallback<Ptr<const MobilityModel> > m_courseChangeTrace;

这个变量的类型ns3::TracedCallback

/NodeList/7/$ns3::MobilityModel/CourseChangex = 11.3273, y = 4.04175

输出的第一个部分就是所谓的Context,它是一段config代码定位source的路径。他指明了由哪个部分**了Callback。可以使用Config::Connect来代替Config::ConnectWithoutContest。

7.2.3 发现跟踪源

对于tracing系统的初学者来说,会常提出:
1. 我知道在模拟核心代码中肯定有某个trace source,但是我怎么找到那些能用的?
2. 我找到了跟踪源,怎么制定config path?
3. 我找到了跟踪源和路径,怎么指定返回类型和回调函数中规范的参数?
4. 我做了上述所有工作,但有错误信息
下面我们依次解决。

7.2.4 可用的trace source

如何找到可用的跟踪源呢?如果你访问ns3项目网站,你会发现有一个名为Documentation的导航栏,访问这个链接进入文档页。其中有一个名为”Latest Release”的连接,点击它可以找到最新版ns3的文档。然后,选择“API Documentation”,进入ns3的API文档页,在侧边栏里你会看到有个层次目录:
- ns3
- ns3-Documentation
- All TraceSources
- All Attributes
- All GlobalValues

点击这个列表中的All TraceSources连接会打开所有当前ns3中可用的跟踪源。

7.2.5 配置路径

如何确定跟踪源的配置路径呢?
如果你知道你关注的对象是哪个,那么你可以在上节中提到的All TraceSources中找到对象所属类的“Detailed Description”。例如点击“API Documentation”——“All TraceSources”——“ns3:::MobilityModel”连接,可以打开MobilityModel的相关信息页。然后在“更多信息”中找到Detailed Description,在这个详细描述中会有三个列表:

  • Config Paths:该类的一个典型Config paths列表
  • Attributes: 该类提供的属性列表
  • TraceSources:该类能够提供的所有TraceSources
    首先讨论Config Paths。假设在所有跟踪源中我们找到了CourseChange,现在考虑如何连接它?而当前使用的移动模型时ns3::RandomWalk2dMobilityModel,那么我们在Detailed Description中找到Config Paths列表:
    NS-3学习笔记 5
ns3::RandomWalk2dMobilityModel is accessible through the following paths with Config::Set and Config::Connect:

"/NodeList/[i]/$ns3::MobilityModel/$ns3::RandomWalk2dMobilityModel"

这段文档告诉你如何获取RandomWalk2dMobilityModel (路径),实际例子里我们使用的是:“/NodeList/7/$ns3::MobilityModel”, 因为CourseChange在基类MobilityModel中也被定义为config paths,所以既可以用基类的路径,也可以用上面给出的详细路径。如果用到某些RandomWalk2dMobilityModel中才有的跟踪源,那么就要使用上面的实现类路径了。

除了看API文档,还有一个从源码找config paths的方法,那就是使用linux中的find命令和grep工具,查看源代码:

$find . -name '*.cc' |xargs grep CourseChange |grep Connect

例如:

aaa@qq.com:~/ns-allinone-3.26/ns-3.26/src$ find . -name '*.cc' |xargs grep CourseChange |grep Connect
./wave/examples/vanet-routing-compare.cc:  Config::Connect ("/NodeList/*/$ns3::MobilityModel/CourseChange",
./mobility/examples/main-random-topology.cc:  Config::Connect ("/NodeList/*/$ns3::MobilityModel/CourseChange",
./mobility/examples/ns2-mobility-trace.cc:  Config::Connect ("/NodeList/*/$ns3::MobilityModel/CourseChange",
./mobility/examples/main-random-walk.cc:  Config::Connect ("/NodeList/*/$ns3::MobilityModel/CourseChange",
./mobility/model/hierarchical-mobility-model.cc:  m_child->TraceConnectWithoutContext ("CourseChange", MakeCallback (&HierarchicalMobilityModel::ChildChanged, this));
./mobility/model/hierarchical-mobility-model.cc:      m_parent->TraceConnectWithoutContext ("CourseChange", MakeCallback (&HierarchicalMobilityModel::ParentChanged, this));
./mobility/test/waypoint-mobility-model-test.cc:      mob->TraceConnectWithoutContext ("CourseChange", MakeCallback (&WaypointMobilityModelNotifyTest::CourseChangeCallback, this));
./mobility/test/waypoint-mobility-model-test.cc:  m_mobilityModel->TraceConnectWithoutContext ("CourseChange", MakeCallback (&WaypointMobilityModelAddWaypointTest::CourseChangeCallback, this));
./mobility/test/ns2-mobility-helper-test-suite.cc:    Config::Connect ("/NodeList/*/$ns3::MobilityModel/CourseChange",
./netanim/model/animation-interface.cc:  Config::ConnectWithoutContext ("/NodeList/*/$ns3::MobilityModel/CourseChange",

7.2.6 Callback Signatures

当我们找到了跟踪源和配置路径,如何知道回调函数的返回类型和形参等相关要求?
最简单的办法是查看回调签名(Callback Signature)的typedef,它在所用类的API文档的“Detailed Description”中被记录。还是以CourseChange为例,在ns3::RandomWalk2MobilityModel中的Detailed Description有如下内容:

引用块内容

(未完)

相关标签: ns3