以下是ns3手册内容翻译
在 ns-3 的模拟中,主要有两个方面的设置:
- 模拟拓扑和对象是如何连接的。
- 在拓扑中实例化的模型所使用的值。
本章的重点在于上述的第二项:ns-3 中使用的大量的值是如何组织的、记录的
以及 ns-3 的用户如何来修改这些值。对于跟踪和统计信息是如何在模拟器中收
集的,ns-3 的属性系统也起到了支柱作用。
在深入研究属性值系统之前,回顾 class ns3::Object 的一些基本属性会很有帮
助。
3.1 对象概述
ns-3 本质上是一个基于 C++对象的系统。这意味着新的 C++类(类型)可以像
往常一样被声明、定义以及子类化。
许多 ns-3 的对象继承基类 ns3::Object。这些对象有很多附加的属性,这些属
性是我们为了对系统进行组织和以及改进对象的内存管理而开发的:
“metadata” 系统将类名与大量的 meta-information 进行链接,这些
meta-informatino 是与对象有关的,包括子类的基类、子类中可访问的构造函数集以及子类的 “attributes” 集。
a reference counting smart pointer implementation, for memory management.
使用属性系统的 ns-3 对象是从 ns3::Object 或 ns3::ObjectBase 派生的。我们
将要讨论的大多数 ns-3 对象派生自 ns3::Object,但 一些在 smart pointer 内
存管理框架之外的对象派生自 ns3::ObjectBase。
让我们回顾一下这些对象的一些属性。
3.1.1 Smart pointers
正如 ns-3 tutorial 中介绍的,
ns-3 的对象的内存管理师通过 reference counting
smart pointer implementation,class ns3::Ptr 实现的。
Smart pointers 在 ns-3 的 API 被广泛的使用,来避免 将引用传递给堆分配的
对象而可能引起内存泄漏。对于大多数基本的用法(语法),将 smart pointer
看待为常规指针:
Ptr<WifiNetDevice> nd = ...;
nd->CallSomeFunction ();
// etc.
3.1.2 创建对象
正如我们在@ref{Object Creation}中讨论过的,在最底层的 API 中,具有
ns3::Object 类型的对象不是像通常一样用 operator new 实例化的,而是通过
一个叫做 CreateObject() 的模板函数实例化的。
创建这样的对象的典型方法如下:
Ptr<WifiNetDevice> nd = CreateObject<WifiNetDevice> ();
你可以认为这在功能上等价于:
WifiNetDevice* nd = new WifiNetDevice ();
由 ns3::Object 派生的对象一定是通过 CreateObject()被分配在堆上。而由
ns3::ObjectBase 派生的对象,
比如 ns-3 的 helper functions and packet headers
and trailers, 可以被分配在栈上。在一些脚本中,你可能看不到大量的 CreateObject()调用,那时因为存在很多
helper objects,他们为你执行了 CreateObject()s。
3.1.3 TypeId
ns-3 中由类 ns3::Object 派生出的类可以包含一个叫做 TypeId 的元数据类,
该类记录关于类的元信息,以便在对象聚合以及构件管理系统中使用:
识别该类的一个独一无二的字符串。a unique string identifying the class
在元数据系统中,子类的基类。
子类中 可访问的构造函数的集合。
3.1.4 对象小结
将所有这些概念综合在一起,让我们研究一个特定的例子:class ns3::Node。
公共头文件 node.h 中的一个声明包含了一个静态的 GetTypeId 函数调用:
class Node : public Object
{
public:
static TypeId GetTypeId (void);
...
该函数在文件 node.cc 中定义如下:
TypeId
Node::GetTypeId (void)
{
static TypeId tid = TypeId (“ns3::Node”)
.SetParent<Object> ()
;
return tid;
}
最终,当用户要创建节点时,可以这样调用:
Ptr<Node> n = CreateObject<Node> ();
下边我们会讨论属性(与类的成员变量以及成员函数相关联的值)是如何被加入上述 TypeId 的。
3.2 属性概述
属性系统的目标是组织对模拟的内部成员对象的访问。这个目标的产生式因为:
通常在模拟中,用户将剪切、粘贴或修改现存的模拟脚本,或者将使用更高层的
模拟构造,但通常对研究或跟踪某些特别的内部变量很有兴趣。例如:
“我只想跟踪第一个接入点的无线接口上的包。”
“我想跟踪某个特定 TCP 套接字上 TCP 拥塞窗口的值(每次当它发生变化时)”
“我想获取并记录模拟中所有被使用到的值。”
类似地,用户可能想对模拟中的内部变量进行细致的访问,或者可能想广泛地改
变某个特定参数的初始值,以便涉及到所有随后创建的对象。用户还可能希望知
道在模拟配置中哪些变量是可以设置的和可以获得的。这不仅仅是为了命令行下
直接交互,还考虑到(将来的)图形用户界面,该界面可能提供让用户在节点上
右击鼠标就能获得信息的功能,这些信息可能是一个层次性组织的参数列表,显
示该节点上可以设置的参数以及构成节点的成员对象,还有帮助信息和每个参数
的默认值。
3.2.1 功能概述 Functional overview
我们给用户提供可以访问系统深处的值的方法,而不用在系统中加入存取器(指
针)并通过指针链来获取想要的值。考虑类 DropTailQueue,该类有一个叫做
m_maxPackets 的无符号整型成员变量,该成员变量控制队列的大小。
查看 DropTailQueue 的声明,我们看到:
class DropTailQueue : public Queue {
public:
static TypeId GetTypeId (void);
...
private:
std::queue<Ptr<Packet> > m_packets;
uint32_t m_maxPackets;
};
考虑用户可能对 m_maxPackets 的值想要做的事情:为系统设置一个默认值,以便无论何时一个新的 DropTailQueue 被创建时,这
个成员变量都被初始化成该默认值。
对一个已经被实例化过的队列,设置或获取队列的该值。
上述情况通常需要提供 Set()和 Get()函数,以及某些类型的全局默认值。
在 ns-3 的属性系统中,这些值定义和存取器函数被移入类 TypeId,例如:
TypeId DropTailQueue::GetTypeId (void)
{
static TypeId tid = TypeId (“ns3::DropTailQueue”)
.SetParent<Queue> ()
.AddConstructor<DropTailQueue> ()
.AddAttribute (“MaxPackets”,
“The maximum number of packets accepted by this DropTailQueue.”,
UintegerValue (100),
MakeUintegerAccessor (&DropTailQueue::m_maxPackets),
MakeUintegerChecker<uint32_t> ())
;
return tid;
}
方法 AddAttribute()对该值进行一系列处理:
将变量 m_maxPackets 绑定到一个字符串”MaxPackets”。
提供默认值(100 packets)
提供为该值下定义的帮助信息。
提供”checker”(本例中未使用),可以用来设置所允许的上下界。
关键的一点是,现在该变量的值以及它的默认值在属性名字空间中是可访问的,
基于字符串”MaxPackets”和 TypeId 字符串。在下一节,我们将提供一个例子
来说明用户如何来操纵这些值。
3.2.2 基本用法
我们研究一下用户脚本如何访问这些值,基于文件
samples/main-attribute-value.cc,略去了一些细节。
//
// This is a basic example of how to use the attribute system to// set and get a value in the underlying system; namely, an unsigned
// integer of the maximum number of packets in a queue
//
int
main (int argc, char *argv[])
{
// By default, the MaxPackets attribute has a value of 100 packets
// (this default can be observed in the function DropTailQueue::GetTypeId)
//
// Here, we set it to 80 packets. We could use one of two value types:
// a string-based value or a Uinteger value
Config::SetDefault (“ns3::DropTailQueue::MaxPackets”, StringValue (“80′′));
// The below function call is redundant
Config::SetDefault (“ns3::DropTailQueue::MaxPackets”, UintegerValue (80));
// Allow the user to override any of the defaults and the above
// SetDefaults() at run-time, via command-line arguments
CommandLine cmd;
cmd.Parse (argc, argv);
需要注意的是对 Config::SetDefault 的两次调用。这表明了我们如何设置随后
被实例化的 DropTailQueues 的默认值。我们举例说明了两种类型的 Value 类:
类 StringValue 和类 UintegerValue,这两个类可以被用来将值赋给叫做
“ns3::DropTailQueue::MaxPackets” 的属性。
现在我们使用底层 API 来创建一些对象。由于上述对默认值的操作,新建的队
列的 m_maxPackets 值将被初始化为 80 而不是 100。
Ptr<Node> n0 = CreateObject<Node> ();
Ptr<PointToPointNetDevice> net0 = CreateObject<PointToPointNetDevice>
();
n0->AddDevice (net0);
Ptr<Queue> q = CreateObject<DropTailQueue> ();
net0->AddQueue(q);
这里我们创建了一个单独的节点(节点 0)和一个单独的PointToPointNetDevice(NetDevice 0),并将一个 DropTailQueue 加入到了
该 PointToPointNetDevice 上。
现在,我们可以操纵已经实例化过的 DropTailQueue 的 MaxPackets 值了,有
多种不同的方法来达到这个目的。
3.2.2.1 基于指针的存取 Pointer-based access
假定存在一个指向相关网络设备的智能指针(Ptr),例如这里 net0 的指针。
改变该值的一种方法是 通过存取指向底层的队列的指针,并修改该队列的属性。
首先,我们能够通过 PointToPointNetDevice 的属性获得指向队列(基类)的指
针,该属性叫做 TxQueue。
PointerValue tmp;
net0->GetAttribute (“TxQueue”, tmp);
Ptr<Object> txQueue = tmp.GetObject ();
使用函数 GetObject ,我们能够执行到 DropTailQueue 的安全的 downcast,
MaxPackets 是 DropTailQueue 的成员。
Ptr<DropTailQueue> dtq = txQueue->GetObject <DropTailQueue> ();
NS_ASSERT (dtq != 0);
现在我们能够获取该队列上属性的值。由于属性系统存储的是值,而不是类型,
所以与 Java 类似地,我们为底层数据类型引入了封装的 “Value” 类。这里,属
性值被赋值给了一个 UintegerValue ,对这个值应用 Get()方法会得到
(unwrapped)uint32_t。
UintegerValue limit;
dtq->GetAttribute (“MaxPackets”, limit);
NS_LOG_INFO (“1. dtq limit: ” << limit.Get () << ” packets”);
注意上述的 downcast 不是必须的。尽管该属性是该子类的成员,我们依然能够
使用 Ptr<Queue> 来做同样的事情。
txQueue->GetAttribute (“MaxPackets”, limit);
NS_LOG_INFO (“2. txQueue limit: ” << limit.Get () << ” packets”);
现在,让我们将他设置为另一个值(60)。txQueue->SetAttribute(“MaxPackets”, UintegerValue (60));
txQueue->GetAttribute (“MaxPackets”, limit);
NS_LOG_INFO (“3. txQueue limit changed: ” << limit.Get () << ” packets”);
3.2.2.2 基于名字空间的存取 Namespace-based access
另一种获取属性的方法是使用 configuration namespace。属性位于这个名字空
间的已经路径上。如果用户无法访问底层指针但又想要使用一条语句来配置某个
特定的属性时,这个方法很有用。
Config::Set (“/NodeList/0/DeviceList/0/TxQueue/MaxPackets”,
UintegerValue (25));
txQueue->GetAttribute (“MaxPackets”, limit);
NS_LOG_INFO (“4. txQueue limit changed through namespace: ” <<
limit.Get () << ” packets”);
我们还可以使用通配符来设置所有节点和所有网络设备的该值(例子如下)。
Config::Set (“/NodeList/*/DeviceList/*/TxQueue/MaxPackets”,
UintegerValue (15));
txQueue->GetAttribute (“MaxPackets”, limit);
NS_LOG_INFO (“5. txQueue limit changed through wildcarded namespace: ”
<<
limit.Get () << ” packets”);
3.2.3 通过构造函数和 helper classes 来设置 Setting through constructors
helper classes
任意的属性组合都可以由 helper 和底层 APIs 来设置和获得。通过构造函数本身:
Ptr<Object> p = CreateObject<MyNewObject> (“n1′′, v1, “n2′′, v2, ...);
通过高层 helper APIs,比如:
mobility.SetPositionAllocator (“GridPositionAllocator”,
“MinX”, DoubleValue (-100.0),
“MinY”, DoubleValue (-100.0),
“DeltaX”, DoubleValue (5.0),“DeltaY”, DoubleValue (20.0),
“GridWidth”, UintegerValue (20),
“LayoutType”, StringValue (“RowFirst”));
3.2.4 值类 Value classes
读者将注意到新的某 Value 类是 AttributeValue 基类的子类。这些类可以被看做
中间类,这些中间类可以被用来将 raw types 转换为可以被属性系统使用的值。
属性系统的数据库用一种一般类型来存储许多类型的对象,到该一般类型的转换
可以使用中间类(IntegerValue, DoubleValue for “floating point”)来完成,也
可以通过字符串来完成。从类型到值的直接隐式转换不是很可行,所以用户可以
选择使用字符串还是值:
p->Set (“cwnd”, StringValue (“100′′)); // string-based setter
p->Set (“cwnd”, IntegerValue (100)); // integer-based setter
对于用户想引入属性系统的新的类型,系统提供一些宏来帮助用户为新的类型声
明和定义新的 AttributeValue 子类。
ATTRIBUTE_HELPER_HEADER
ATTRIBUTE_HELPER_CPP
3.3 对属性进行扩展 Extending attributes
ns-3 系统在属性系统下边放置了许多内部值,但毫无疑问,用户将对系统不完
善的地方进行扩展以及加入用户自己的类。
3.3.1 将现存的内部变量加入元数据系统 Adding an existing internal variable to the metadata system
考虑类 TcpSocket 中的这个变量:
uint32_t m_cWnd; // Congestion window
假设 使用 Tcp 的某个人想要使用元数据系统获得或设置该变量的值。如果 ns-3
还没有提供这个,用户可以再元数据系统中添加如下声明
(在 TcpSocket 的
TypeId 声明中):.AddParameter (“Congestion window”,
“Tcp congestion window (bytes)”,
Uinteger (1),
MakeUintegerAccessor (&TcpSocket::m_cWnd),
MakeUintegerChecker<uint16_t> ());
现在,用户可以使用指向该 TcpSocket 的指针来执行设置和获取操作,而不用显
式添加这些函数。此外,访问控制可以被应用,比如使得该参数只读不可写和对
参数进行上下界检查。
3.3.2 添加新的 TypeId Adding a new TypeId
现在我们讨论用户如何往 ns-3 系统中添加新的类。
我们已经介绍过类似如下的 TypeId 定义:
TypeId
RandomWalk2dMobilityModel::GetTypeId (void)
{
static TypeId tid = TypeId (“ns3::RandomWalk2dMobilityModel”)
.SetParent<MobilityModel> ()
.SetGroupName (“Mobility”)
.AddConstructor<RandomWalk2dMobilityModel> ()
.AddAttribute (“Bounds”,
“Bounds of the area to cruise.”,
RectangleValue (Rectangle (0.0, 0.0, 100.0, 100.0)),
MakeRectangleAccessor (&RandomWalk2dMobilityModel::m_bounds),
MakeRectangleChecker ())
.AddAttribute (“Time”,
“Change current direction and speed after moving for this delay.”,
TimeValue (Seconds (1.0)),
MakeTimeAccessor (&RandomWalk2dMobilityModel::m_modeTime),
MakeTimeChecker ())
// etc (more parameters).
;
return tid;
}
类声明中与此相关的声明是一行公共成员方法:
public:
static TypeId GetTypeId (void);
典型的错误包括:
没有调用 SetParent 方法,或者使用了错误的类型来调用他。
没有调用 AddConstructor 方法,或者使用了错误的类型来调用他。
在 TypeId 的构造函数中对于 TypeId 的名字引入了印刷错误。
没有使用封装类的全限定 C++类型名作为 TypeId 的名字。
以上错误都无法被 ns-3 探测到,所以用户应当多次检查以确保正确性。
3.4 给属性系统中添加新的类
从用户的角度来看,编写新的类并将其加入属性系统主要是编写字符串与属性值
之间的转换。多数可以通过“宏化的“(macro-ized)代码来复制/粘贴。例如目
录 src/mobility/ 下的类 Rectangle:
类声明中加入一行:
/**
* brief a 2d rectangle
*/
class Rectangle
{
...
};
在类声明的下边加入一个宏调用和两个操作符:
std::ostream &operator << (std::ostream &os, const Rectangle &rectangle);
std::istream &operator >> (std::istream &is, Rectangle &rectangle);
ATTRIBUTE_HELPER_HEADER (Rectangle);
类定义的代码类似于:
ATTRIBUTE_HELPER_CPP (Rectangle);
std::ostream &
operator << (std::ostream &os, const Rectangle &rectangle){
os << rectangle.xMin << “|” << rectangle.xMax << “|” << rectangle.yMin <<
“|” << rectangle.yMax;
return os;
}
std::istream &
operator >> (std::istream &is, Rectangle &rectangle)
{
char c1, c2, c3;
is >> rectangle.xMin >> c1 >> rectangle.xMax >> c2 >> rectangle.yMin >>
c3 >> rectangle.yMax;
if (c1 != ‘|’ ||c2 != ‘|’ ||c3 != ‘|’)
{
is.setstate (std::ios_base::failbit);
}
return is;
}
这些流操作符将字符串表示形式的 Rectangle (“xMin|xMax|yMin|yMax”)转
化为底层的 Rectangle,模块的编写者必须指定新类的这些操作符以及该类的实
例的字符串句法表示形式。
3.5 ConfigStore
请求反馈: 这是 ns-3 的一个试验性的特色。他不在主要的代码树中。如果您
喜欢该特色并愿意提供关于他的反馈,请给我们写电子邮件。
ns-3 的属性的值可以被存储在 ascii 文本文件中,并在将来的模拟中加载。这个
特色被认为是 ns-3 的 ConfigStore。 ConfigStore 的代码在 src/contrib/ 下。因
为我们还在寻求用户的反馈,所以目前还不在主要的代码树中。
我们用一个例子来探索这个系统。将文件 csma-bridge.cc 复制到 scratch 目录:
cp examples/csma-bridge.cc scratch/
./waf我们编辑该文件以加入 ConfigStore 特色。首先,添加一个 include 语句,然后
加入以下行:
#include “contrib-module.h”
...
int main (...)
{
// setup topology
// Invoke just before entering Simulator::Run ()
ConfigStore config;
config.Configure ();
Simulator::Run ();
}
存在一个控制 Configure()的属性,他决定 Configure()是将模拟的配置存储在文
件中并退出,还是加载模拟的配置文件并继续执行。首先,属性 LoadFilename
被检查,如果不为空,则程序从所提供的文件名来加载配置;如果为空,且属性
StoreFilename 被提供,则配置将被写入指定的输出文件。
虽然生成一个配置文件的样本并修改一些值是可能的,但有些情况这种方法是行
不通的,因为对于同一个自动生成的配置文件,同一个对象上的同一个值可能在
不同的配置路径上出现多次。
同样地,使用这个类的最好方法是用他生成一个初始的配置文件,仅从该文件中
提取严格必须得元素,并将这些元素移动一个新的配置文件。这个新的配置文件
在随后的模拟中可以被安全地编辑和加载。
以此为例运行一次程序来创建一个配置文件。如果你使用的是 bash shell,那么
下边的命令应该能够工作(阐明了如何从命令行设置属性):
./build/debug/scratch/csma-bridge –
ns3::ConfigStore::StoreFilename=test.config
如果上述命令不起作用(上述命令需要 rpath 的支持),试试如下:
./waf –command-template=”%s –
ns3::ConfigStore::StoreFilename=test.config” –run scratch/csma-bridge
运行该程序将产生一个叫做”test.config”的输出配置文件,类似于如下:
/$ns3::NodeListPriv/NodeList/0/$ns3::Node/DeviceList/0/$ns3::CsmaNet
Device/Addre
ss 00:00:00:00:00:01
/$ns3::NodeListPriv/NodeList/0/$ns3::Node/DeviceList/0/$ns3::CsmaNet
Device/Frame
Size 1518
/$ns3::NodeListPriv/NodeList/0/$ns3::Node/DeviceList/0/$ns3::CsmaNet
Device/SendE
nable true
/$ns3::NodeListPriv/NodeList/0/$ns3::Node/DeviceList/0/$ns3::CsmaNet
Device/Recei
veEnable true
/$ns3::NodeListPriv/NodeList/0/$ns3::Node/DeviceList/0/$ns3::CsmaNet
Device/TxQue
ue/$ns3::DropTailQueue/MaxPackets 100
/$ns3::NodeListPriv/NodeList/0/$ns3::Node/DeviceList/0/$ns3::CsmaNet
Device/Mtu 1
500
...
上边列出了拓扑脚本中的每一个对象以及每一个注册过的属性的值。此文件的语
法是每行都标明了属性独一无二的名字,名字后边是值。
该文件是某个给定模拟中的参数的一个方便的记录,可以使用模拟输出文件来存
储。此外,该文件还可以被用来将模拟参数化,而不是编辑脚本或传递命令行参
数。比如:检查并调整一个已经存在的配置文件中的值,然后将该文件传递给模
拟程序。相关的命令:
./build/debug/scratch/csma-bridge –
ns3::ConfigStore::LoadFilename=test.config
如果上述命令不起作用(上述命令需要 rpath 的支持),试试如下:
./waf –command-template=”%s –
ns3::ConfigStore::LoadFilename=test.config” –run scratch/csma-bridge
3.5.1 基于 GTK 的 ConfigStore GTK-based ConfigStore
对于 ConfigStore,存在一个基于 GTK 的前端。这使得用户可以使用 GUI 来存
取和修改变量。该特色的屏幕截图可以在 ns-3 Overview 找到。
要使用这个特色,必须安装 libgtk 和 libgtk-dev。Ubuntu 下安装命令的示例:
sudo apt-get install libgtk2.0-0 libgtk2.0-dev
通过 ./waf configure 阶段的输出来检验是否已经配置好:
—- Summary of optional NS-3 features:
Threading Primitives : enabled
Real Time Simulator : enabled
GtkConfigStore : not enabled (library ‘gtk+-2.0 >= 2.12′ not found)
在上述例子中,GtkConfigStore 没有开启,要想使用他,必须安装合适的版本,
并且再次执行 ./waf configure; ./waf。
用法与 non-GTK-based 版本几乎一样:
// Invoke just before entering Simulator::Run ()
GtkConfigStore config;
config.Configure ();
现在,当你运行脚本时将弹出一个 GUI,使得你可以打开不同节点/对象上属性
的菜单,配置好之后启动模拟。
3.5.2 将来的工作 Future work
可能存在的改进:
- 在文件起始处保存一个包含日期与时间的独一无二的版本号。
- 在某处保存 rng 的初始种子。
- 使每个 RandomVariable 都连续化(serialize)自己的初始种子并在后期重新读取。
- 加入默认值。
网友评论