cplus-NS3-Tracing

NS3 Tutorial Tracing

Tracing

背景

在Using the Tracing System中提到,ns3的整个目的在于使用仿真来产生用于研究的数据。目前,ns3提供了两种策略来获取数据:通过通用的预先定义的块输出机制,并将他们的内容转换为可以提取有效信息方式(我觉得应该说的是系统定义的NS_LOG);或者,使用一种输出机制,只传递我们想要的信息。

使用预定义的块输出机制有一些优点,比如不需要对ns3有额外的修改,但是需要我们写额外的脚本来转换和过滤我们感兴趣的消息。通常,PCAP和NS_LOG输出消息会在仿真运行中被收集起来,并通过运行额外的脚本程序(grep,sed,awk)来将输出精简转换成方便管理的形式。我们必须自己写一些转换程序,因此也不能说是完全不费功夫的。NS_LOG的输出不被认为是ns3 API的一部分,并且可能不经通知就在不同版本之间做出修改。此外,NS_LOG输出只在DEBUG模式下有效,因此依赖于LOG必然会导致一些性能的损失。当然,如果我们感兴趣的信息并不以预定义的信息块模式出现,这种方式就没法用了。

如果你需要给预先定义的信息增加一些额外的小片段,这当然也是可以的;如果你使用了一种预先定义的ns3机制,你的工作可以作为对ns3的一些contribution。

ns3提供了另一套机制,叫做Tracing,来避免从块状输出机制固有的一些问题。它有一些重要的优点。首先,你可以通过只Tracing你感兴趣的事件来降低你需要处理的数据量(在一些大型的仿真中,记录所有的事件会造成IO瓶颈)。第二,如果你使用这种方式,你可以直接控制输出的格式,因此可以避免后期处理的时候使用grep,sed,awk,perl或者python脚本。如果你希望的话,可以直接讲格式定义为gnuplot可以识别的格式。你可以在core中添加钩子(hook)来给别人使用,但是这不会给出明确的信息,除非明确被要求这么做。因为这些理由,我们相信ns3的Tracing系统使我们获取仿真信息的最好方式,所以也是在ns3中需要我们理解的重要机制。

Blunt Instrument

有许多从程序获取信息的办法,最直接的方式就是通过标准输入输出来直接打印信息(C语言是的printf,cout等等)。

1#include  <iostream>
2...
3void SomeFunction(void)
4{
5uint32_t x= Some_INTERESTING_VALUE;
6...
7std::cout<<"The Value of x is "<<x<<std::endl;
8...
9}

没人会制止你在ns3的核心代码从加入额外的输出信息。这种做法也不是很难,然而,你需要完全掌控你自己的ns3分支。从长期来看,这并不会得到令人满意的结果

随着在自己代码中添加的输出信息越来越多,管理和处理这些输出信息也变得越来越复杂。最终,你可能觉得需要一些方式来控制哪些信息需要被输出,而哪些不需要;或许通过打开或关闭某些输出类,或者增加或减少所需信息的数量。如果在这条路上越走越远,最终你可能发现自己重新实现了一遍NS_LOG机制。为了避免这一点,我们应该合理的最大化利用NS_LOG本身。

我们在上面提到过,一个获取ns3有效信息的方式是将现存的NS_LOG输出转换成感兴趣的消息。如果你发现你感兴趣的信息片段不在LOG信息中,你可以编辑ns3的core模块将你需要的信息加入输出流中。目前,这显然比添加自己的输出语句要好,因为这遵循了ns3的惯例,并且有可能作为一个补丁加入到ns3内核中,从而帮助其他人。

让我们随机选一个例子。如果你希望增加一些额外的logging给ns3的Tcp Socket(tcp-scoket-base.cc),你可以只是在实现中增加一个新的消息。注意,在 TCPSocketBase::ReceivedAck(),原本没有任何关于no ack 情况下的log信息。你可以简单地增加一句话。

这是原来的代码:

 1/** Process the newly received ACK */
 2void
 3TcpSocketBase::ReceivedAck (Ptr<Packet> packet, const TcpHeader& tcpHeader)
 4{
 5NS_LOG_FUNCTION (this << tcpHeader);
 6
 7// Received ACK. Compare the ACK number against highest unacked seqno
 8if (0 == (tcpHeader.GetFlags () & TcpHeader::ACK))
 9{ // Ignore if no ACK flag
10}
11...

为了记录no ack的情况,你可以增加一个NS_LOG_LOGIC在if语句中:

 1/** Process the newly received ACK */
 2void
 3TcpSocketBase::ReceivedAck (Ptr<Packet> packet, const TcpHeader& tcpHeader)
 4{
 5NS_LOG_FUNCTION (this << tcpHeader);
 6
 7// Received ACK. Compare the ACK number against highest unacked seqno
 8if (0 == (tcpHeader.GetFlags () & TcpHeader::ACK))
 9{ // Ignore if no ACK flag
10  NS_LOG_LOGIC ("TcpSocketBase " << this << " no ACK flag");
11}
12...

这样乍看上去看起来似乎相当简单和令人满意。但是必须考虑到,如果你在NS_LOG中添加额外的代码,你同时也必须使用文本处理脚本(grep、sed、awk)来转换输出的信息以分离出你需要的内容。这是因为即使你通某种方式控制了logging系统,也只是控制了输出的级别,是一种非常粗粒度的管理。

如果你正在向ns3模块中添加新的输出信息,也需要考虑到其他开发者也正在添加他们自己所感兴趣的信息。你最终可能发现,为了得到自己所需要的一小片信息,你必须处理一大片自己完全没有兴趣的大段输出内容。最终这些大量的log文件会占用大量的磁盘,无论你需要什么信息都必须进行了大量额外处理。

既然,对ns3的NS_LOG输出的稳定我们并没有严格的稳定性保证,你可能发现你需要或依赖的log信息可能在不同的版本是不一样的,甚至可能在下一个版本完全删除了。如果你依赖于这种类型的输出,你会发现不同版本的ns3会需要不同的文本处理脚本!

最后,NS_LOG只在DEBUG模式下有效,在优化模式下是没有log输出的(优化模式运行速度快了两倍)。依赖NS_LOG会严重影响仿真的性能。

由于这些原因,我们认为使用std::coutNS_LOG 是一种快速不标准的获取ns3更多信息的方式,但是并不适合严格的工作。

我们实际上十分需要一种稳定的API同时能够只获得我们所需要的信息的机制。同时不需要改变额重新编译仿真平台的核心系统。更好的是,如果能通知用户代码他所感兴趣的事件改变或者发生了,这样用户就不用在系统的大段输出中到处找东西了。

ns3的Tracing系统被设计和代码一起工作,并且和AttributeConfig子系统融合在一起,来提供相对简单的使用环境。

概要

ns3的Tracing系统是由独立的跟踪源(Tracing Source)和跟踪槽(Tracing Sink)以及一种统一的机制连接跟踪源和跟踪槽组成的。

跟踪源是仿真过程中信号事件产生的实体,并提供底层数据的获取。例如,一个跟踪源可能意味着当一个数据包被一个网络设备收到,并能够为跟踪槽提供数据包内容。一个跟踪源也可能是系统模型的可改变的状态,比如,一个TCP模型的拥塞窗口是一个初始状态代表着一个跟踪源。每当拥塞窗口发生变化时,跟踪槽就会收到原先的值和现在的值。

跟踪源本身是没有什么用的,它必须和代码中其他部分联系起来,这些代码会使用源的这些有用信息做一些有用的事情。这些使用跟踪源信息的实体叫做跟踪槽。跟踪源是数据的产生者,而跟踪槽是数据的消费者。这种显式的分离模式可以使大量的跟踪源分布在系统不同的地方,这些地方往往是建模者认为有用的地方。插入跟踪源只需要增加很小的负载。

一个跟踪源产生的信息可能被一个或多个使用者使用。我们可以想象一个跟踪源是一个点到多点的链路。你的代码和可以和其他使用这个被追踪事件源的代码愉快地共存。

除非一个用户将一个跟踪槽和其中一个跟踪源联系起来,否则不会有任何信息输出。通过使用Tracing系统,你和其他关注同一个跟踪源上的人都可以获取,并只获取你们所关心的内容。你们之间不会因为改变输出的信息二队他人造成影响。如果你增加了一个跟踪源,你在为良好开源工作者的工作将会允许其他人来提供新的对大家都有益的工作,而不需要对ns3的内核做出任何改进。

简单的例子

让我们花几分钟来过一个简单的例子。我们需要一些有关回调的背景知识来理解这个例子,所以我们先来绕一点路。

回调

ns3中的回调系统目的在于让一段代码调用一个函数而不需要模块间依赖。这意味这你需要一些间接的方式,你需要将被调用函数的地址作为一个变量。这种变量叫做函数指针变量,函数和函数指针的关系和类与类指针的关系没有什么区别。

在C语言中,函数指针的典型例子是返回整形的函数指针(pointer to function returning integer, PFI)。PFI使用一个整形参数,它可能像如下这样被声明:

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

从这个声明中,你可以获得一个名叫pfi的变量,被初始化为0。如果你想将这个指针初始化为更加有意义的值,你首先需要一个参数和返回值类型都匹配的函数,比如:

1int MyFunction(int arg){}

如果你有了这个目标函数,你可以通过如下方式初始化你的函数指针:

1pfi = MyFunction

然后你可以使用更具有提示性的方式来间接地调用MyFunction:

1int result = (*pfi)(1234);

这个可以清楚的看出,你解引用了一个函数指针,但是通常编译器知道函数指针和函数的队友了关系,因此写成这样也是没有问题的:

1int result = pfi(1234);

看上去就像你调用了一个叫做pfi 的函数一样。但是编译器足够智能,知道我们使用的是变量 pfi ,它间接的调用了函数 MyFunction

概念上来说,这就是Tracing系统的工作方式。基本上,跟踪槽就是一个回调。当一个跟踪槽表示出对某个跟踪事件的兴趣时,它将自己加入到一个回调列表中,这个回调列表由跟踪源内部维护。当一个感兴趣的事件发生后,跟踪源唤醒它的 operator(...) 提供0个或者多个参数。 operator(...) 最终深入到系统中,做一些和刚刚的间接调用非常相似的工作,提供0个或多个参数,就像上面调用 pfi 传递一个参数到目标函数MyFunction 一样。

Tracing系统和回调的重要不同是对于每一个跟踪源,内部有一系列的回调。不仅仅是触发一个回调,一个跟踪源可能出发多个回调。当一个跟踪槽对一个跟踪源表示出兴趣,只是意味着讲自己的回调加入到回调列表中。

如果你对这个部分的细节希望有更多的了解,可以去查看ns3 manual中关于回调的部分。

WalkThrough:fourth.cc

我们提供了最简单例子来解释Tracing系统的使用方式。你可以在fourth.cc 中发现这段代码,让我们一起看一下:

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

大部分代码看上去都是十分熟悉的。就像上面提到那样,Tracing系统非常依赖于Object和Attribute系统,所以我们需要先include它们。前面两个include表示将它们的声明引入系统。我们可以使用 #include "ns3/core-module.h" 来将它们一次性引入,我们这样分别引入只是表明这种功能食很简单的。

文件 traced-value.h 将引入跟踪数据所需要遵循的语义。由于Tracing 系统和attribute系统融合在一起,而Attribute系统和Object 一起工作。因此跟踪源必须依附于一个类,接下来的代码段定义并声明了我们需要的一个类。

 1class MyObject : public Object
 2{
 3public:
 4static TypeId GetTypeId (void)
 5{
 6static TypeId tid = TypeId ("MyObject")
 7  .SetParent (Object::GetTypeId ())
 8  .SetGroupName ("MyGroup")
 9  .AddConstructor<MyObject> ()
10  .AddTraceSource ("MyInteger",
11                   "An integer value to trace.",
12                   MakeTraceSourceAccessor (&MyObject::m_myInt),
13                   "ns3::TracedValueCallback::Int32")
14  ;
15return tid;
16}
17
18MyObject () {}
19TracedValue<int32_t> m_myInt;
20};

在以上的两行代码中,和Tracing密切相关的就是.AddTraceSource 和 TracedValue声明的m_myInt。

.AddTraceSource提供了通过Config系统连接跟踪源和外界的钩子。第一个参数是给跟踪源的命名,名字可以自己取;第二个参数是一个提示字符串;我们需要关注的是第三个参数“&MyObject::m_myInt”。这表示着需要被加到跟踪类中的需要跟踪的值,第三个参数总是类的成员变量。 第四个参数是TracedValue的一个typedef,是一个字符串。这个正确的回调函数签名产生文档,在一些更为通用的回调函数中十分有用。

TracedValue<>声明提供了驱动回调进程的基础。任何时候,只要底层的值发生变化,TracedValue机制就会提供新的值和旧的值,在本例中是一个int32_t类型的值。跟踪槽函数traceSink跟踪这个TracedValue,并需要签名:

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

所有和这个跟踪源关联的跟踪槽都需要这个签名。我们接下来就讨论在其他场景下,如何决定所需回调函数的签名。

我们继续过fourth.cc我们看到:

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

这里定义了一个匹配的跟踪槽函数。他直接对应与回调函数的签名。一旦被连接,这个函数就会在TracedValue 改变时被调用。

我们已经看过了跟踪源和跟踪槽,代码接下来要做的就是连接跟踪源和跟踪槽,这个在main函数中完成:

1 void main(int argc, char* argv[])
2 {
3     Ptr<MyObject> myObject = CreateObject<MyObject>();
4     myObject->TraceConnectWithoutContext("MyInteger",MakeCallback(&IntTrace));
5
6     myObject->m_myInt = 1234;
7 }

我们首先创造一个MyObject实例,这样我们的跟踪源可以存在。

下一步,TraceConnectWithoutContext 形成了跟踪源和跟踪槽之间的连接。第一个参数就是跟踪源的名称,我们在上面已经定义过了。需要注意的是,MakeCallback 是一个函数模板,这个函数模板做一些ns3底层的回调类生成,关联函数(和IntTrace函数)等等工作。TraceConnect将会关联你提供的回调函数,同时重载()操作符。关联之后,跟踪源的被跟踪的变量将会对准你提供的回调函数。第二个参数就是MakeCallback(你的函数地址)。

实现这些功能的代码并不平凡,但是本质上,就像一开始我们提到的使用pfi 回调一样。在类中TracedValue<int32_t> m_myInt; 的声明做一些重载分配操作符的工作,将会调用operator()来实际唤醒Callback并提供所需的参数。.AddTraceSource将会连接回调函数和Config系统,而TraceConnectWithoutContext将会通过Attribute系统的Attribute Name连接你的回调函数和跟踪源的名字。

我们先不管其他的一些内容。最后一行是给m_myInt重新分配值。

1     myObject->m_myInt = 1234;

需要解释的是通过重载operator=,不仅完成了赋值的工作,还将1234作为参数传递了。

由于 m_myInt是一个被跟踪的值,因此它的回调函数是将两个整数值作为参数(一个旧值,一个新值),且返回空值。这也正如我们的例子所表示的那样,IntTrace。

总结一下,一个跟踪源本质上是一个有一系列函调函数的变量。一个跟踪槽是一个函数被用为回调的目标。Attribute和Object类信息系统,被用来连接跟踪源和跟踪槽。当一个跟踪的数据有变动时,就是重载一个操作符触发回调。这样,就使得在跟踪源注册的回调被源所提供的参数调用。 如果你编译运行这个例子:

1 me~$ ./waf --run fourth

你将会看到IntTrace函数被执行,一旦跟踪源有变动:

1 Traced 0 to 1234

当我们执行到代码,myObject->m_myInt = 1234;,跟踪源被击中,并提供原先和现在的值给跟踪槽。函数IntTrace使用便准输入输出打印。