我是靠谱客的博主 阳光哈密瓜,最近开发中收集的这篇文章主要介绍OMNET实践总结——signal机制概述1、引言2、Signal-Based Statistics Recording(基于信号的统计记录)3. 高阶操作,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

1、引言

1.1、信号机制的用途

本节描述模拟信号,简称信号。信号是最早出现在 O M N e T + + 4.1 OMNeT++4.1 OMNeT++4.1中的一个通用概念。

应用场景

  1. 作为public模型的统计属性,而不要指定记录他们的细节以及具体实现;
  2. 运行时响应仿真模型更改的通知;
  3. 实现模块之间的发布-订阅式通信功能
    1. 当信息的生产者和消费者不了解彼此时,这是有利的,并且它们之间可能存在多对一或多对多的关系;;
  4. 用作其他的发射信息:例如作为自定义动画效果的输入
    1. 信号由组件 c o m p o n e n t s components components(模块 m o d u l e s modules modules和通道 c h a n n e l s channels channels)发出——继承自模块或者通道 c l a s s   H o s t : p u b l i c   c S i m p l e M o d u l e class space Host : public space cSimpleModule class Host:public cSimpleModule
    2. 信号在模块层次结构上传播到根。从根往下任何级别上,都可以使用回调方法注册侦听器 r e g i s t e r S i g n a l ( " s t a t e " ) registerSignal("state") registerSignal("state")
    3. 每当发出信号值 e m i t ( s t a t e S i g n a l , s t a t e ) emit(stateSignal, state) emit(stateSignal,state)(注册的都会接收到相关信号),将通知这些侦听器(调用它们相应的方法)。向上传播的结果是,注册在复合模块上的侦听器可以接收来自该子模块树中所有组件的信号。在系统模块注册的监听器可以接收来自整个模拟的信号。
    4. 大致调用例子源代码示例:
class Host : public cSimpleModule
{
simsignal_t stateSignal;//设置一个信号句柄
//实现部分
stateSignal = registerSignal("state");//1.句柄初始化注册为state信号监听器,对应到名为state的信号
emit(stateSignal, state);//2.将state的值发送给句柄
}
//ned文件中
simple Host
{
parameters:
@signal[state](type="long");//信号定义
//统计数据说明,主要其他都是一个信号发送等必要代码,这行是在信号绑定之后
@statistic[radioState](source="state";title="Radio state";enum="IDLE=0,TRANSMIT=1";record=vector);
}

注意

  1. 通道channel的父级是包含连接的(复合)模块,包含的是连接而不是所连接门对应的所有者

  2. 信号这样一种机制,虽然不是太懂????,但是我们需要记住的????就是这样一种机制所拥有的那些特性: a .   a. space a. 主要是为了提高效率所采取的的一种做法,所以效率提高了自然有些地方就应该去规避它,就是尽量避免使用相同的信号名称 b .   b. space b. 具体的做法如下:

    1. 信号由信号名(即字符串)标识;
    2. 运行时我们使用动态分配的数字标识符——句柄(类型是 s i m s i g n a l _ t simsignal_t simsignal_t)获得信号对象;
  3. 监听器可以订阅信号名称或id,而不管它们的来源如何,所以需要命名的时候注意不要重复冲突(可能冲突也没有警告的)。例如,如果两个不同且不相关的模块类型(如队列和缓冲区)都发出一个名为“length”的信号,那么在某个较高的复合模块订阅“length”的侦听器将从队列和缓冲区模块实例中获取通知。如果监听器想要区分这两种情况(它可以作为回调函数的参数使用),那么它仍然可以查看信号的来源,但是信号框架本身没有这样的功能。

  4. 当信号发出时,可以携带一个值。这个值可以是: a .   a. space a. 选定的基本类型; b .   b. space b. 对象指针。不同的数据类型 e m i t ( ) emit() emit()和对方法进行重载以进行相对应的实现,而我们使用者只需要注意当不是基本类型的时候转换为对象指针使用就行

  5. 对象可以发射emit很多信息,但是基本数据类型是不能的,所以在emit重载的函数里面增加了一个参数details,用于传递额外的信息。

1.2 设计考虑和基本原理

信号的实现基于以下假设

  1. e m i t ( ) emit() emit()调用相比, s u b s c r i b e / u n s u b s c r i b e subscribe/unsubscribe subscribe/unsubscribe操作很少,因此 e m i t ( ) emit() emit()需要高效;
  2. 每个模块都可以有信号机制,因此为了使得总体占用低就必须做到每个模块的内存开销尽可能地低;
  3. 预计模块 m o d u l e s modules modules和信道 c h a n n e l s channels channels将做大量的信号检测,所以在内存操作中我们需要对使用的以及未使用的信号做一个区别,尽量让未使用的信号占用很少的内存消耗,

而这些工作在实际编程中不会有明显的体现,主要是一个检测对应了一个信号检测器,也就是我们在使用的时候需要进行先注册的那个。优化的结果就是如果你需要使用某一个信号就进行注册,否则就不注册,不注册的信号在内存中的表现就只是一个指针的大小。

其次,另外,每个组件 c o m p o n e n t component component中都有两个位字段,用于存储前64个信号中的哪一个在祖父模块中具有本地侦听器和侦听器,并将本地侦听器和侦听器记录下来。——这里有一个假设前提就是:在一次模拟中常用信号一般少于64个;这样的做法其实是为了利于我们对于信号的获取,做了两种分类:

  1. 前64个信号有无侦听器;
  2. 已知有侦听器的情况,对侦听器进行分类: a .   a. space a. 常用信号,快速获得一个特定的id,例如: t x B e g i n txBegin txBegin a .   a. space a. 不频繁的信号,系统去进行分配id,例如: r o u t e r D o w n routerDown routerDown;。

之所以将组件 c o m p o n e n t component component设计成这些功能,是因为 c S i m p l e M o d u l e cSimpleModule cSimpleModule c C h a n n e l cChannel cChannel都是 c o m p o n e n t component component的子类继承,关系图如下所示:

csimplemoudle

image.png

1.3 信号机制实现模块

回顾1.2与信号相关的方法在 c C o m p o n e n t cComponent cComponent上声明,因此它们可用于 c M o d u l e cModule cModule c C h a n n e l cChannel cChannel

下面的讲解都是基于1.1节中源代码示例而来的。

1.注册监听器
信号数据的返回获得
2.将变量或者对象数据与信息一起发送

1.3.1 信号数据的返回

1. Signal IDs

  1. 作用 I D s IDs IDs所获得的就是我们所说的监听器,通过它与变量一起发送就可让组件(也就是这个类本身其他属性或者方法)获得相关的信号数据。
  2. 参数以及返回
    1. 参数:接受一个信号名作为参数;
    2. 返回:相应的$simsignal_t $值,一个信号的句柄——也就是监听器;
  3. 该方法是静态的,说明了信号名称是全局的。
  4. 例如
simsignal_t lengthSignalId = registerSignal("length");
  1. 与之相反的函数:相应的我们知道 g e t S i g n a l N a m e ( ) getSignalName() getSignalName()方法(也是静态全局的),但执行相反的操作——它接受模拟信号的句柄,将信号的名称返回为 c o n s t   c h a r ∗ const space char* const char(对于无效的信号句柄,返回为 n u l l p t r nullptr nullptr):

  2. 注意

    1. 由于OMNeT++4.3,信号id的生命周期是整个程序,因此可以从全局变量的初始值设定项调用registerSignal(),例如静态类成员。在早期版本中,信号ID通常在initialize()中分配,并且仅对模拟运行有效。

2. Emitting Signals

e m i t ( ) emit() emit()函数族从模块或通道发出信号。 e m i t ( ) emit() emit()接受一个信号 I D ( s i m s i g n a l _ t ) ID(simsignal_t) ID(simsignal_t)和一个值作为参数:

emit(lengthSignalId, queue.length());

该值可以是 b o o l 、 l o n g 、 d o u b l e 、 s i m t i m e _ t 、 c o n s t   c h a r bool、long、double、simtime_t、const space char boollongdoublesimtime_tconst char*或 ( c o n s t ) c O b j e c t   ∗ (const)cObjectspace * (const)cObject 类型。其他类型可以强制转换为这些类型中的一种,或者包装为来自 c O b j e c t cObject cObject类对象。

e m i t ( ) emit() emit()还有一个额外的可选对象指针参数details,类型为 c O b j e c t ∗ cObject* cObject。这个论点可以用来向听众传达额外的信息。

注意:

  1. d e t a i l s details details参数是在OMNeT++5.0中添加的。您应该更新模型以使用新的侦听器接口,或者作为临时解决方案,使用 W I T H _ O M N E T P P 4 x _ L I S T E N E R _ S U P P O R T WITH_OMNETPP4x_LISTENER_SUPPORT WITH_OMNETPP4x_LISTENER_SUPPORT宏编译OMNeT++;

  2. 对内存开销的解决方案:当没有侦听器时,emit()的运行时开销通常最小。但是,如果生成值的运行时开销很大,则可以使用 m a y H a v e L i s t e n e r s ( ) mayHaveListeners() mayHaveListeners() h a s L i s t e n e r s ( ) hasListeners() hasListeners()方法预先检查给定信号是否有任何侦听器——如果没有,则可以跳过生成值和发出信号;

    1. 使用例子:

      if (mayHaveListeners(distanceToTargetSignal)) {
      double d = sqrt((x-targetX)*(x-targetX) + (y-targetY)*(y-targetY));
      emit(distanceToTargetSignal, d);
      }
      
    2. 在检测监听器的时候有不同的策略,他们应用于不同的场景,其中:

      1. m a y H a v e L i s t e n e r s ( ) mayHaveListeners() mayHaveListeners()方法非常有效(一个常量时间操作),但可能产生误报。
      2. 如果没有缓存信号数据, h a s L i s t e n e r s ( ) hasListeners() hasListeners()将搜索到模块树的顶部,因此速度通常较慢。
      3. 基本上他们的差异化是表现在有无缓存信号数据,因为 h a s L i s t e n e r s ( ) hasListeners() hasListeners()在对无缓存的时候做无用功,依旧会进行到根的搜索,所以最终我们视哪一种情况最多,然后我们去选择这两种方法。不同的方法对应不同的时间消耗。

1.3.3 Signal信号的描述

N E D NED NED文件中进行声明和描述

以下示例声明了发出名为queueLength的信号的队列模块:

simple Host
{
parameters:// @signal[信号索引=信号名称 ](数据类型type=bool)
@signal[state](type="long");
...
}

声明格式:

  1. @signal[信号的索引](属性)
    1. 信号的索引:就是通过信号的名字去获得信号;
    2. 属性:表示和属性一起发送的数据类型是什么,当存在时,它的值应该是 b o o l 、 l o n g 、 u n s i g n e d l o n g 、 d o u b l e 、 s i m t i m e _ t 、 s t r i n g bool、long、unsigned long、double、simtime_t、string boollongunsignedlongdoublesimtime_tstring一个注册的类名(后面有问号)。
      1. 问号表示允许信号发出 n u l l p t r nullptr nullptr指针,例如,名为PPP的模块可以在每次开始传输时发出帧(包)对象,并在传输完成时发出 n u l l p t r nullptr nullptr
      2. 当属性是注册的类的时候,后面的都会在msg文件中中自动生成,自己是不用再经过编程的,注册类的时候可以使用 R e g i s t e r _ C l a s s ( ) Register_Class() Register_Class() R e g i s t e r _ A b s t r a c t _ C l a s s ( ) Register_Abstract_Class() Register_Abstract_Class()宏注册;后面的就主要用于检测发送的对象与在生成的对象,是不是它的子类或者同一个类,详细过程如下:
    • 用于注册抽象类的这些宏创建一个 c O b j e c t F a c t o r y cObjectFactory cObjectFactory实例,模拟内核将调用 c O b j e c t F a c t o r y cObjectFactory cObjectFactory i s I n s t a n c e ( ) isInstance() isInstance()方法来检查发出的对象是否真的是声明类的子类。 i s I n s t a n c e ( ) isInstance() isInstance()只封装一个C++动态映射。

//在ned文件中进行声明信号以及相关属性
simple PPP
{
parameters:
@signal[txFrame](type=PPPFrame?);
// a PPPFrame or nullptr
...
}

//在PPPFrame_m.cc中进行注册,这个文件是msg编写之后自动生成的
namespace inet{
...
Register_Class(PPPFrame)
...
}

3. **使用通配符**:因为信号有着仅仅在运行时才知道名称的,所以属性索引可以包含通配符,这在编程的时候带来很大的方便。
- 例如:如果模块发出称为session-1-seqno, session-2-seqno, session-3-seqno等的信号,则这些信号可以声明为:
@signal[session-*-seqno]();

1.3.4 Enabling/Disabling Signal Checking

从OMNeT++5.0开始,默认对信号进行检查这只是处于安全的考虑,对每一个注册的信号在ned文件那里必须进行声明(由于性能原因,它在发布模式模拟内核中关闭。)

所以这是非必须的,你也可以使用检查信号配置选项禁用信号检查:

check-signals = false

1.3.5 Signal Data Objects

1. 通知类的继承关系

当使用 c O b j e c t ∗ cObject* cObject指针发出信号时,只要手头有合适的对象,就可以将模型中已有的对象作为数据传递。但是,通常需要声明一个自定义类来保存所有细节,并填充一个实例,以便发出信号。

自定义通知类必须从 c O b j e c t cObject cObject派生。我们建议您也将 n o n c o p y a b l e noncopyable noncopyable添加为基类。如图所示它具有以下好处:

  1. 完成了必须派生 c O b j e c t cObject cObject这一要求
  2. 省略了大部分情况中要求实现的函数:构造函数、赋值运算符、 d u p dup dup函数
通知类
cObject
自定义通知类要求从它派生
noncopyable
省略了很多函数的实现,因为继承了这个类的原因
1.复制构造函数
2.赋值运算符
3.dup函数

2. 通知类的源码实现

如下源码所示, a .   a. space a. 完成对两个类的继承, b .   b. space b. 然后发送它的对象的指针就可以。主要的区别还是在第一步。

class cPreGateAddNotification : public cObject, noncopyable
{
public:
cModule *module;
const char *gateName;
cGate::Type gateType;
bool isVector;
};

以及发出信号的代码:

if (hasListeners(PRE_MODEL_CHANGE))
{
cPreGateAddNotification tmp;
tmp.module = this;
tmp.gateName = gatename;
tmp.gateType = type;
tmp.isVector = isVector;
emit(PRE_MODEL_CHANGE, &tmp);
}

1.3.6 Subscribing to Signals订阅信号

订阅信号的意义就是模块 M o d u l e Module Module通过订阅某一信号实时地去获得一个数据的变化。就像qt中的发布、订阅。我们可以类比为向报社订阅报纸,只要有新的报纸印刷,那么你就会收到一份报纸。

  1. 发布

    void ChildDialog::on_pushButton_clicked()
    {
    auto time = QDateTime::currentDateTime();
    QuickApplication::publishEvent("show_time", Qt::AutoConnection, time);
    }
    
  2. 订阅

    MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
    {
    ui->setupUi(this);
    childDialog = new ChildDialog(this);
    childDialog->show();
    QuickApplication::subscibeEvent(this, "show_time");
    }
    void MainWindow::event_show_time(const QDateTime &time)
    {
    qDebug() << "recv time:" << time.toString();
    }
    
  3. 结果示意图:

    订阅与发布事件.gif

1. 订阅( s u b s c r i b e ( ) subscribe() subscribe()方法)

  1. 作用

    1. 将侦听器与信号做绑定,侦听器随时侦听以及获取信息的数据变化;
    2. 同一侦听器可以通过多次订阅可以订阅多个对象;
  2. 类继承关系:继承自 c I L i s t e n e r cIListener cIListener类;

  3. 参数:信号和指向侦听器对象的指针

  4. 重载函数

    1. s u b s c r i b e ( 信 号 句 柄 , l i s t e n e r ) subscribe(信号句柄, listener) subscribe(,listener):需要注册信号去获得操作的句柄;

      cIListener *listener = ...;
      simsignal_t lengthSignalId = registerSignal("length");
      subscribe(lengthSignalId, listener);
      
    2. s u b s c r i b e ( 信 号 名 称 字 符 串 , l i s t e n e r ) subscribe(信号名称字符串, listener) subscribe(,listener):重载的函数具体包括了注册信号,所以不需要进行注册的操作;

      cIListener *listener = ...;
      subscribe("length", listener);
      
  5. 可以在其他模块上订阅(暂时还没找到示例,并且自己没怎么看懂太菜????,看懂之后更新),而不仅仅是本地模块。例如,为了从模型的所有部分获取信号,可以在系统模块级别订阅:

    cIListener *listener = ...;
    getSimulation()->getSystemModule()->subscribe("length", listener);
    

2. 取消订阅

  1. 函数重载 u n s u b s c r i b e ( ) unsubscribe() unsubscribe()方法与 s u b s c r i b e ( ) subscribe() subscribe()一样重载了同样的参数列表,功能都是根据给定的信号取消订阅侦听器:

    1. 通过信号句柄

      unsubscribe(lengthSignalId, listener);
      
    2. 通过信号名称字符串

      unsubscribe("length", listener);
      
  2. 注意:

    1. 将同一侦听器订阅同一信号两次是错误的;

    2. 删除侦听器时,必须已从其订阅的所有组件中取消订阅该侦听器。

      1. 如果您的模块已将侦听器添加到其他模块(例如顶层模块),则这些侦听器必须最迟在该模块的析构函数中取消订阅;

      2. 记住,在对模块调用 u n s u b s c r i b e ( ) unsubscribe() unsubscribe()之前,请确保这些模块仍然存在,除非它们是模块树中模块的祖先,可以使用 i s S u b s c r i b e d isSubscribed isSubscribed测试侦听器是否订阅了信号,调用如下:

        if (isSubscribed(lengthSignalId, listener))
        {
        ...
        }
        
      3. 有时候需要进行查看相应组件订阅的信号列表或者给定信号的侦听器列表,方法分别如下:

        1. g e t L o c a l L i s t e n e d S i g n a l s ( ) getLocalListenedSignals() getLocalListenedSignals():获取组件订阅的信号列表,返回 s t d : : v e c t o r < s i m s i g n a l _ t > std::vector<simsignal_t> std::vector<simsignal_t>

        2. g e t L o c a l S i g n a l L i s t e n e r s ( ) getLocalSignalListeners() getLocalSignalListeners():给定信号的侦听器列表,返回 s t d : : v e c t o r < [ c I L i s t e n e r ] std::vector<[cIListener] std::vector<[cIListener]

        3. 示例:

          EV << "Signal listeners:n";
          std::vector<simsignal_t> signals = getLocalListenedSignals();
          for (unsigned int i = 0; i < signals.size(); i++) {
          simsignal_t signalID = signals[i];
          std::vector<cIListener*> listeners = getLocalSignalListeners(signalID);
          EV << getSignalName(signalID) << ": " << listeners.size() << " signalsn";
          }
          

1.3.7 Listeners

侦听器是 c I L i s t e n e r cIListener cIListener类的子类对象,它声明并重载了以下函数:

class cIListener
{
public:
virtual ~cIListener() {}
virtual void receiveSignal(cComponent *src, simsignal_t id,
bool value, cObject *details) = 0;
virtual void receiveSignal(cComponent *src, simsignal_t id,
long value, cObject *details) = 0;
virtual void receiveSignal(cComponent *src, simsignal_t id,
double value, cObject *details) = 0;
virtual void receiveSignal(cComponent *src, simsignal_t id,
simtime_t value, cObject *details) = 0;
virtual void receiveSignal(cComponent *src, simsignal_t id,
const char *value, cObject *details) = 0;
virtual void receiveSignal(cComponent *src, simsignal_t id,
cObject *value, cObject *details) = 0;
virtual void finish(cComponent *component, simsignal_t id) {}
virtual void subscribedTo(cComponent *component, simsignal_t id) {}
virtual void unsubscribedFrom(cComponent *component, simsignal_t id) {}
};

此类有许多虚函数:

  1. 几个重载的 r e c e i v e S i g n a l ( 源 组 件 , 信 号 句 柄 , 数 据 类 型 ( 包 括 基 本 数 据 类 型 以 及 c O b j e c t 对 象 指 针 ) , 附 加 的 细 节 d e t a i l s 对 象 方 法 ) receiveSignal(源组件,信号句柄,数据类型(包括基本数据类型以及cObject对象指针),附加的细节details对象方法) receiveSignal(,,(cObject),details),其中第三个参数是区分重载函数的关键,每种数据类型对应一个重载函数;

  2. 系统已经实现,不用自己再去编写:

    1. 对于一个组件无论何时发出信号(通过 e m i t ( ) emit() emit()),都会在其订阅的侦听器上调用匹配的 r e c e i v e S i g n a l ( ) receiveSignal() receiveSignal()函数,并且这个函数在 c L i s t e n e r cListener cListener中是实现了的,不用自己再去实现;
    2. f i n i s h ( ) finish() finish()由其本地侦听器上的组件调用,在调用之后如果有多个信号或多个组件则会进行多次调用以完成组件或者信号的完成清理。请注意:如果模拟因错误而终止,则通常不会调用 f i n i s h ( ) finish() finish()方法。
  3. 需要用的时候进行调用:

    1. s u b s c r i b e d T o ( ) , u n s u b s c r i b e d F r o m ( ) subscribedTo(),unsubscribedFrom() subscribedTo(),unsubscribedFrom()在侦听器对象订阅/取消订阅信号时调用。完成两个功能的实现: a .   a. space a. 是否订阅; b .   b. space b. 订阅位置(来自或者去往哪?)。
      • 我们可以利用第二个功能使得监听器也能在 u n s u b s c r i b e d F r o m ( ) unsubscribedFrom() unsubscribedFrom()方法的最后一条语句中删除自己,但必须确保没有其他地方仍然订阅了同一监听器,因为删除组件的时候必须保证将其所有相关订阅全部取消,这样才能够删除侦听器
  4. 由于 c I L i s t e n e r cIListener cIListener有大量纯虚方法,因此从 c L i s t e n e r cListener cListener(一个不做任何事情的实现)派生子类更方便。

    1. 它定义了相关方法,如: f i n i s h ( ) finish() finish() s u b s c r i b e d T o ( ) subscribedTo() subscribedTo() u n s u b s c r i b e d F r o m ( ) unsubscribedFrom() unsubscribedFrom()以及 r e c e i v e S i g n a l ( ) receiveSignal() receiveSignal()
    2. 通知侦听器的顺序未定义(它不一定与订阅侦听器的顺序相同)——这个貌似对运行什么的并没有并没有影响;
    3. 对于一些不支持这些数据类型的时候:
      1. 默认函数会抛出“不支持数据类型”错误。这对于出错的时候是比较友好的;
      2. 有时候为了使错误更加清晰调试更加方便,您可以重新定义要支持其数据类型的 r e c e i v e S i g n a l ( ) receiveSignal() receiveSignal()方法,使用其他(意外的)数据类型发出的信号将抛出错误而不是不被注意。

1.3.8 Listener Life Cycle

  1. 当组件(模块或通道)被删除时,它会自动取消订阅(但不会删除)它拥有的侦听器。

    1. 删除步骤:首先从其子模块树中的所有模块和通道中取消订阅所有侦听器,然后开始递归删除模块和通道本身;
    2. 删除侦听器时,此时必须已从所有组件中取消订阅该侦听器。如果没有取消但是这个侦听器指针又存在而且本身又没有什么值,当组件运行 e m i t ( ) emit() emit()进行调用的时候就会奔溃,并且此时在 c I L i s t e n e r cIListener cIListener类中的订阅技术就不是 0 0 0这时候就会发出警告,我们看到警告的时候需要想到是这种情况
      1. 为什么我们平时不用写?以及什么时候需要overwrite这个函数呢?
        • 因为继承组件的话就自动继承了它的函数——即 u n s u b s c r i b e d F r o m ( ) unsubscribedFrom() unsubscribedFrom()函数,所以关键的问题就是如果你没有继承的这个类的话,一定记住
  2. 注意

    • 如果您的模块已将侦听器添加到其他模块(例如顶级模块),则必须在模块析构函数中最迟取消订阅这些侦听器。请记住,在调用模块之前,要确保模块仍然存在,除非它们是模块树中模块的祖先。——一般都是自己是侦听器侦听自己的某一些信号,所以需要遇到这种情况在另外去总结。下面是一般情况的代码,这里从this就可以看出自身是侦听器,也就是说侦听器在本地的情况系统已经实现了取消订阅的实现,不用我们自己去管:

      void PPP::initialize(int stage)
      {
      subscribe(POST_MODEL_CHANGE, this);
      }
      

1.3.9 侦听模型的相关变动Listening to Model Changes

1. 背景

在仿真模型中,保存对其他模块、连接通道或其他对象的引用,或缓存从模型拓扑派生的信息通常很有用。由于连接的是引用那么他就有可能本身数值发生变动,那么就需要有一个跟踪模块本身的这样一个功能,始终伴随着变动一起更新

2. 所以需要注意一下几点:

  1. 每当您在一个简单模块中看到 c M o d u l e ∗ 、 c C h a n n e l ∗ 、 c G a t e ∗ cModule*、cChannel*、cGate* cModulecChannelcGate或类似的指针保持为状态时,您应该考虑如果模型在运行时发生更改,它将如何保持最新。

    1. 解决方案:

      1. 思路:发出信号,模型更改前/后的通知是由带有更改详细信息的数据对象发出的,其中数据类包括:

        • 都是 c M o d e l C h a n g e N o t i f i c a t i o n cModelChangeNotification cModelChangeNotification的子类,当然这是一个 c O b j e c t cObject cObject。在监听器中,可以使用dynamic_cast<>将基类指针转换为继承类的指针,这时候我们就能够知道通知大类型了;
        • 需要在 A P I API API文档中查找这些类,以查看它们的数据字段、何时被触发,以及在使用它们时需要注意的事项,当然我们一般是在实际例子看到这些然后对应进行分析
        cPreModuleAddNotification / cPostModuleAddNotification
        cPreModuleDeleteNotification / cPostModuleDeleteNotification
        cPreModuleReparentNotification / cPostModuleReparentNotification
        cPreGateAddNotification / cPostGateAddNotification
        cPreGateDeleteNotification / cPostGateDeleteNotification
        cPreGateVectorResizeNotification / cPostGateVectorResizeNotification
        cPreGateConnectNotification / cPostGateConnectNotification
        cPreGateDisconnectNotification / cPostGateDisconnectNotification
        cPrePathCreateNotification / cPostPathCreateNotification
        cPrePathCutNotification / cPostPathCutNotification
        cPreParameterChangeNotification / cPostParameterChangeNotification
        cPreDisplayStringChangeNotification / cPostDisplayStringChangeNotification
        
        • 示例,删除模块时打印消息的示例侦听器:

          class MyListener : public cListener
          {
          ...
          };
          void MyListener::receiveSignal(cComponent *src, simsignal_t id, cObject *value,
          cObject *details)
          {
          if (dynamic_cast<cPreModuleDeleteNotification *>(value)) {
          cPreModuleDeleteNotification *data = (cPreModuleDeleteNotification *)value;
          EV << "Module " << data->module->getFullPath() << " is about to be deletedn";
          }
          }
          
      2. O M N e T + + OMNeT++ OMNeT++有两个内置信号, P R E _ M O D E L _ C H A N G E 和 P O S T _ M O D E L _ C H A N G E PRE_MODEL_CHANGE 和POST_MODEL_CHANGE PRE_MODEL_CHANGEPOST_MODEL_CHANGE (这些宏是模拟信号值,而不是名称),在每次模型更改之前和之后发出,这是一个系统级的通知,能够通知到每一个应该被通知到的组件,然后触发相应事件(如果有相关实现的话)

        getSimulation()->getSystemModule()->subscribe(PRE_MODEL_CHANGE, listener);
        
  2. 如果包含订阅点的整个子模块树被删除,则侦听器将不会收到pre/post-module-deleted的通知。这是因为复合模块析构函数首先取消订阅子树中的所有模块/通道,然后再开始递归删除。

2、Signal-Based Statistics Recording(基于信号的统计记录)

2.1 Motivation(用途,动机)

信号的一个用途是公开用于结果收集的变量,信号本身是不需要知道变量在哪里、以及如何是否要记录它们。

1. 使用信号的方法

  • 模块随信号一起发送变量,相应的结果记录在侦听器中;
    • 侦听器可以由仿真框架(基于配置)或其他模块(例如由专用结果收集模块)添加。

2. 使用信号允许几种可能性操作

  1. 提供一个可控的细节级别,能够在模拟运行中实现不同的输出,其中包括:时间序列、平均值/最大值/最小值/标准偏差等或者直方图;
  2. 根据模拟实验的目的,对数据进行预处理,比如平滑或者过滤某一些值;
  3. 需要汇总统计,例如记录整个网络的丢包总数或平均端到端延迟;
  4. 需要记录组合的统计信息,例如丢弃百分比(丢弃计数/数据包总数);
  5. 需要忽略在预热期间或其他瞬态期间生成的结果。

随着信号机制一步步完善,上述目标可以意一一实现。

2.2 Declaring Statistics(描述统计)

2.2.1 引言

1. 基本参数

为了记录基于信号的仿真结果,必须将@statistic属性添加到简单模块(或通道)的NED定义中。其中它所具有的参数如下:

  1. 名称:定义这个统计量的名称(是统计量的名称而不是什么表格的名称);
  2. input:使用哪个信号作为输入;
  3. 处理:需要对这些数据做哪些处理,比如:平滑、滤波、求和、微分商;
  4. 记录(是可选的,也就是可以明确写出也可以不写,不写的话就是选择默认记录的方式)
    1. 记录内容:应该记录哪一些属性;
    2. 记录的形式:以何种形式进行记录,其中记录形式包括:向量、标量、直方图
    3. 相关调整:标题、度量单位等等。

以下示例声明了具有队列长度统计信息的队列模块:

//统计数据的标准形式:@statistic[索引:一般是信号名](相关属性值)
simple Queue
{
parameters:
@statistic[queueLength](record=max,timeavg,vector?);
gates:
input in;
output out;
}

2. 统计过程以及模式配置

  1. 统计过程:上面的@statistic声明假定,通过注册信号→发送信号数据,那么模块的 C + + C++ C++代码都会以信号 q u e u e L e n g t h queueLength queueLength的形式发出队列的更新长度。

  2. 相关模式

    1. 默认情况:队列长度的最大和平均时间将记录为标量。 还可以指示仿真(或仿真的一部分)记录“所有”结果。 这将打开可选记录项,这些记录项带有问号,然后队列长度也将记录到输出向量中。

    2. 配置结果列表:

      1. 配置允许您微调结果项列表,甚至超出默认设置和所有设置;

        1. 更方便地进行记录,比如在记录信号之前对其进行处理:

          //记录了作为标量的分组丢弃的总数,并且可选地将时间函数中丢弃的数据包的数量作为向量,只要在每次丢弃分组时C++代码发出一个丢弃信号。下降信号的值甚至数据类型都是无关紧要的,因为只计算发射次数。这里,count()是一个结果过滤器。
          @statistic[dropCount](source=count(drop); record=last,vector?);
          
      2. 所以目前这是两种方式,一种是声明加统计属性(索引就是信号名),另外一种属性通过resource设定

        1. 属性通过resource设定:要记录的信号取自统计名称。当这不合适时,源属性键允许您指定不同的信号作为统计信息的输入。

          //为qlen信号添加了一个信号声明(@signal property)。声明信号目前是可选的,实际上@signal属性目前被系统忽略,但是我们推荐这么做,甚至之后可能会强制使用
          //1.没有经过预处理
          simple Queue
          {
          parameters:
          @signal[qlen](type=int); // optional
          @statistic[queueLength](source=qlen; record=max,timeavg,vector?);
          ...
          }
          //2.经过预处理(可以对统计的数据进行总和或者计数之类的计算)
          //2.1 使用算数表达式
          //C++代码以包(cPacket*指针)作为值发出pkdrop 信号。基于这个信号,它记录掉的字节总数(作为标量,也可以选择作为向量)。packetBytes()过滤器使用cPacket的getByteLength()方法从每个包中提取字节数,sum()过滤器对它们进行汇总。
          @statistic[droppedBytes](source=sum(packetBytes(pkdrop)); record=last,
          vector?);
          //2.2 组合多个不同的信号
          //当使用多个信号时,到达任一信号的值将产生一个输出值。计算将使用其他信号的最后值(采样保持插值)。关于多个信号的一个限制是,同一个信号不能出现两次,因为它会导致输出故障。
          @statistic[dropRate](source=count(drop)/count(pk); record=last,vector?);
          //2.3 加上记录器进行协同计算
          //记录项目也可以是表达式,并包含过滤器。 例如,下面的统计信息在功能上等同于上面的示例之一:使用cPacket *值信号作为输入,它还将标出的总字节数计算为标量并将其记录为矢量; 但是,某些计算已移至记录器部分。
          @statistic[droppedBytes](source=packetBits(pkdrop); record=last(8*sum),
          vector(8*sum)?);
          
        2. 声明加统计属性(索引就是信号名)

          simple Queue
          {
          parameters:
          @group(Queueing);
          @display("i=block/queue;q=queue");
          @signal[dropped](type="long");
          @signal[queueLength](type="long");
          @signal[queueingTime](type="simtime_t");
          @signal[busy](type="bool");
          @statistic[dropped](title="drop event";record=vector?,count;interpolationmode=none);
          @statistic[queueLength](title="queue length";record=vector,timeavg,max;interpolationmode=sample-hold);
          @statistic[queueingTime](title="queueing time at dequeue";record=vector?,mean,max;unit=s;interpolationmode=none);
          @statistic[busy](title="server busy state";record=vector?,timeavg;interpolationmode=sample-hold);
          int capacity = default(-1);
          // negative capacity means unlimited queue
          bool fifo = default(true);
          // whether the module works as a queue (fifo=true) or a stack (fifo=false)
          volatile double serviceTime @unit(s);
          gates:
          input in[];
          output out;
          }
          
      3. 示例:假定C++代码发出 q l e n qlen qlen 信号,并基于该声明做出$queueLength $统计

        //为qlen信号添加了一个信号声明(@signal property)。声明信号目前是可选的,实际上@signal属性目前被系统忽略,但是我们推荐这么做,甚至之后可能会强制使用
        所以目前这是两种方式,一种是声明加统计属性,另外一种就是在统计属性中设置好
        simple Queue
        {
        parameters:
        @signal[qlen](type=int); // optional
        @statistic[queueLength](source=qlen; record=max,timeavg,vector?);
        ...
        }
        

2.2.2 Property Keys(属性键)

  1. 一般的统计格式→@statistic[索引:一般是信号名](属性1;属性2;…)

  2. 属性值分别包含以下几种:

    1. source a .   a. space a. 定义记录器的输入(参见record=key)。 b .   b. space b. 缺省时,统计名称作为信号名称;
    2. record 记录 a .   a. space a. 包含记录模式列表,用逗号分隔。 b .   b. space b. 录制模式定义如何录制源(请参见source=key);
    3. title 标题 a .   a. space a. 统计信号的较长描述性名称; b .   b. space b. 结果可视化工具可以将其用作图表标签;
    4. unit单位 a .   a. space a. 数值的测量单位。这也可能出现在图表中。
    5. i n t e r p o l a t i o n m o d e interpolationmode interpolationmode插值模式 a .   a. space a. 定义如何在需要时插值信号值(例如用于绘图); b .   b. space b. 可能的值为none, sample-hold, backward-sample-hold, linear;
    6. 枚举 a .   a. space a. 定义各种整数信号值的符号名; b .   b. space b. 属性值必须是字符串,并且格式是用逗号分隔的名称=值对(name=value); c .   c. space c. 示例:“IDLE=1,BUSY=2,DOWN=3”。

2.2.3 可用的筛选器和记录器(Filters and Recorders)

在统计过程以及模式配置中我们说了可以加上记录器进行协同计算统计,而相关的结果记录器如下:

1.筛选器列表

下表包含预定义结果函数(筛选器)的列表。

注意:表中的所有筛选器都为每个输入值输出一个值。

FilterDescription
countComputes and outputs the count of values received so far.
sumComputes and outputs the sum of values received so far.
minComputes and outputs the minimum of values received so far.
maxComputes and outputs the maximum of values received so far.
meanComputes and outputs the average (sum / count) of values received so far.
timeavgRegards the input values and their timestamps as a step function (sample-hold style), and computes and outputs its time average (integral divided by duration).
constant0Outputs a constant 0 for each received value (independent of the value).
constant1Outputs a constant 1 for each received value (independent of the value).
packetBitsExpects cPacket pointers as value, and outputs the bit length for each received one. Non-cPacket values are ignored.
packetBytesExpects cPacket pointers as value, and outputs the byte length for each received one. Non-cPacket values are ignored.
sumPerDurationFor each value, computes the sum of values received so far, divides it by the duration, and outputs the result.
removeRepeatsRemoves repeated values, i.e. discards values that are the same as the previous value.

2. 结果记录器列表

RecorderDescription
lastRecords the last value into an output scalar.
countRecords the count of the input values into an output scalar; functionally equivalent to last(count)
sumRecords the sum of the input values into an output scalar (or zero if there was none); functionally equivalent to last(sum)
minRecords the minimum of the input values into an output scalar (or positive infinity if there was none); functionally equivalent to last(min)
maxRecords the maximum of the input values into an output scalar (or negative infinity if there was none); functionally equivalent to last(max)
meanRecords the mean of the input values into an output scalar (or NaN if there was none); functionally equivalent to last(mean)
timeavgRegards the input values with their timestamps as a step function (sample-hold style), and records the time average of the input values into an output scalar; functionally equivalent to last(timeavg)
statsComputes basic statistics (count, mean, std.dev, min, max) from the input values, and records them into the output scalar file as a statistic object.
histogramComputes a histogram and basic statistics (count, mean, std.dev, min, max) from the input values, and records the reslut into the output scalar file as a histogram object.
vectorRecords the input values with their timestamps into an output vector.

3. 打印相关列表

通过执行opp_run -h resultfilters 和opp_run -h resultrecorders命令,可以打印可用结果筛选器和结果记录器的列表。

2.2.4 记录结果的命名和属性

1. 命名

记录的结果项名称(最后我们能在输出项里面看到的结果)将由统计名称和记录模式之间的冒号连接而成: " < s t a t i s t i c N a m e > : < r e c o r d i n g M o d e > " "<statisticName>:<recordingMode>" "<statisticName>:<recordingMode>"

因此,以下统计数据

@statistic[dropRate](source=count(drop)/count(pk); record=last,vector?);
@statistic[droppedBytes](source=packetBytes(pkdrop); record=sum,vector(sum)?);

将生成以下标量: d r o p R a t e : l a s t , d r o p p e d B y t e s : s u m dropRate:last, droppedBytes:sum dropRate:last,droppedBytes:sum, 和这些矢量: d r o p R a t e : v e c t o r , d r o p p e d B y t e s : v e c t o r ( s u m ) dropRate:vector, droppedBytes:vector(sum) dropRate:vector,droppedBytes:vector(sum).

2. 属性

  1. 所有属性键(记录除外)都作为结果属性记录到矢量文件或标量文件中。

  2. title名称的设置将在录制之前稍作调整:录制模式将添加在逗号之后,否则从同一统计中保存的所有结果项将具有完全相同的名称。

    1. 示例:“Dropped Bytes, sum”, “Dropped Bytes, vector(sum)”
  3. 也允许使用其他属性键。

2.2.5 详细的源和记录表达式

更好地理解源和记录对如何设置结果记录将非常有用。

1. 信号、结果记录器以及过滤器(也叫做筛选器)

当在模拟中创建模块或通道时,OMNeT++运行时检查其NED声明中的@statistic属性,并在它们作为输入提到的信号上添加侦听器。 a .   a. space a. 与结果记录关联的侦听器有两种:结果筛选器和结果记录器。结果过滤器可以进行多个链接,并且在链的末端总是有一个记录器; b .   b. space b. 因此,可以有直接订阅信号的记录器,或者可以有一个或多个滤波器链加上记录器。

形成链接
结果过滤器1
...
结果过滤器n
记录器
  1. 结果过滤器通常对它们在输入端接收到的值(链中的前一个过滤器或直接是一个信号)执行一些处理,并将它们传播到输出端(链过滤器和记录器)。
  2. 过滤器也可以接受(即不传播)值。记录器可以将接收到的值写入输出向量,或者在模拟结束时记录输出标量。

过滤器和记录器形式都存在许多操作。 例如:

  • 求和(sum)过滤器会将在其输入上接收到的值做一个和然后传播到其输出——主要体现的是一个过滤功能;
  • 但求和记录器仅计算接收值的总和,以便在模拟完成时将其记录为输出标量;

2. 图示连接方式

下一个图说明了为以下统计信息创建哪些筛选器和记录器以及如何连接它们:

@statistic[droppedBits](source=8*packetBytes(pkdrop); record=sum,vector(sum));

结果图

3. 查看如何为设置筛选器和记录器的方式

使用debug-statistics-recording配置选项运行模拟,例如:

  • 在命令行上指定–debug-statistics-recording=true。

3. 高阶操作

3.1 Statistics Recording for Dynamically Registered Signals(动态注册信号的统计记录)

最终的目标:每个会话、每个连接、每个客户端等都有一个模块记录统计信息通常很方便。——也就是以会话为单位,而之前都是整个类的运行周期

解决方案

  1. 动态注册信号(e.g. session1-jitter, session2-jitter, …);
  2. 并在每个信号上设置 @statistic-style的结果记录。

NED文件如下所示:

@signal[session*-jitter](type=simtime_t); // note the wildcard
@statisticTemplate[sessionJitter](record=mean,vector?);

在模块的C++代码中,您需要用registerSignal()注册每个新的信号,此外,请告诉OMNET++来为它建立统计记录,如@统计模板属性所描述的。后者可以通过调用getEnvir()->addResultRecorders().来实现。

  1. 在@statisticTemplate属性中,源键将被忽略(因为作为参数给定的信号将用作源)。属性的实际名称和索引也将被忽略。(对于@statistic,索引保存结果名,但此处的名称在statisticName参数中显式指定。)
  2. 当使用公共的@statisticTemplate属性记录多个信号时,您将希望每个信号的记录统计信息的标题有所不同。这可以通过使用@statisticTemplate标题键中的美元变量来实现。以下变量可用:
    1. $name:统计信息的名称
    2. $component:组件完整路径
    3. $mode模式:录制模式
    4. $namePart[0-9]+:当沿着冒号(:)拆分时,统计名称的给定部分;编号以1开头
    5. 示例:如果统计名称为"conn:host1-to-host4(3):bytesSent",标题为"bytes sent in connection $namePart2",则它将变为"bytes sent in connection host1-to-host4(3)"。
char signalName[32];
sprintf(signalName, "session%d-jitter", sessionNum);
simsignal_t signal = registerSignal(signalName);
char statisticName[32];
sprintf(statisticName, "session%d-jitter", sessionNum);
cProperty *statisticTemplate =
getProperties()->get("statisticTemplate", "sessionJitter");
getEnvir()->addResultRecorders(this, signal, statisticName, statisticTemplate);

3.2 以编程方式添加结果筛选器和记录器(Filters and Recorders)

作为@statisticTemplate和addResultRecorders()的替代方法,还可以通过创建结果过滤器和记录器并将其附加到所需信号,以编程方式设置结果记录。

注意

重要的是要知道@statistic通过在过滤器/记录器链的前面包含一个特殊的预热期过滤器来实现预热期支持。手动添加结果筛选器和记录器时,还需要手动添加此筛选器。

下面的代码示例在删除重复值后设置到输出向量的录制,并且基本上等同于以下@statistic行:

@statistic[queueLength](source=qlen; record=vector(removeRepeats);
title="Queue Length"; unit=packets);

c++代码:

simsignal_t signal = registerSignal("qlen");
cResultFilter *warmupFilter =
cResultFilterType::get("warmup")->create();
cResultFilter *removeRepeatsFilter =
cResultFilterType::get("removeRepeats")->create();
cResultRecorder *vectorRecorder =
cResultRecorderType::get("vector")->create();
opp_string_map *attrs = new opp_string_map;
(*attrs)["title"] = "Queue Length";
(*attrs)["unit"] = "packets";
vectorRecorder->init(this, "queueLength", "vector", nullptr, attrs);
subscribe(signal, warmupFilter);
warmupFilter->addDelegate(removeRepeatsFilter);
removeRepeatsFilter->addDelegate(vectorRecorder);

3.3 发送信号

用于统计目的的发射信号与用于任何其他目的的发射信号差别不大。统计信号主要期望包含数值,因此需要long、double和simtime_t 的重载emit()函数将是最有用的函数——主要通过时间戳用以说明

3.3.1 时间戳

时间戳对应的值的发送过程:

  • 用时间戳发出。发射值与当前模拟时间关联。有时可能需要将它们与不同的时间戳相关联,这与 cOutVector的recordWithTimestamp()方法(参见[7.9.1])的方式非常相似。例如,假设要在每次成功的无线帧接收开始时发射信号。然而,只有在接收完成之后,才能知道任何给定的帧接收是否将成功。因此,值只能在接收完成时发出,并且需要与过去的时间戳相关联。

特殊的类cTimestampedValue实现时间戳的发送:

  1. 作用要发出具有不同时间戳的值,需要填充包含(时间戳,值)对的对象,并使用emit(simsignal_t, cObject *)方法发出。

  2. 数据成员:time和value,类型为simtime_t和double。它还有一个方便的构造函数来获取这两个值。

  3. 能够实现的原因:

    • cTimestampedValue不是信号机制的一部分。但是OMNeT++提供的结果记录监听器的编写方式使它们能够理解 cTimestampedValue,并知道如何处理它。
  4. 示例用法:

    simtime_t frameReceptionStartTime = ...;
    double receivePower = ...;
    cTimestampedValue tmp(frameReceptionStartTime, receivePower);
    emit(recvPowerSignal, &tmp);
    
  5. 优化性能:

    1. 如果性能是关键的,则cTimestampedValue对象可以成为类成员或静态变量,以消除对象的构造/销毁时间;
    2. 注意:在这里使用静态变量是安全的,因为模拟程序是单线程的,但是要确保在某个地方没有监听器可以在触发期间修改相同的静态变量。
  6. 对信号的数值的处理:

    1. 是数字值的时候:时间戳必须单调递增;

    2. 发出非数字值时 a .   a.space a. 有时,多用途信号是可行的; b .   b.space b. 或者改造现有的非统计信号,以便可以记录结果。因此,具有非数字类型(即const char和cObject)的信号也可以记录为结果。 c .   c. space c. 当需要将这些值解释为数字时,内置的结果记录侦听器将使用以下规则:

      1. 字符串记录为1.0,但nullptr记录为0.0;

      2. 可以转换为cITimestampedValue的对象使用类的getSignalTime()和getSignalValue()方法进行记录;

        1. cITimestampedValue是一个C++接口,可以用作任何类的附加基类。声明如下:

          class cITimestampedValue {
          public:
          virtual ~cITimestampedValue() {}
          virtual double getSignalValue(simsignal_t signalID) = 0;
          virtual simtime_t getSignalTime(simsignal_t signalID);
          };
          
        2. getSignalValue()是纯虚拟的(它必须返回一些值),但是getSignalTime()有一个默认的实现,它返回当前的模拟时间。注意signalID参数,它允许同一个类为多个信号服务(即,为每个信号返回不同的值)。

      3. 其他对象记录为1.0,但记录为0.0的nullptr除外。

3.4 写入结果筛选器和记录器

1. 自定义过滤器以及记录器

除了内置的结果过滤器和记录器外,还可以定义自己的结果过滤器和记录器。类似于定义模块和新的ND函数,您必须在C++中实现该实现,然后用注册宏注册它,以让OMNET++知道它。之后新的结果过滤器或记录器可以像内置的一样,用于@statistic属性的source=和record=属性中。

1.其中自定义结果过滤器的要求如下

  1. 结果过滤器必须是 c R e s u l t F i l t e r cResultFilter cResultFilter的子类,或者是 c R e s u l t F i l t e r cResultFilter cResultFilter的一个子类 c N u m e r i c R e s u l t F i l t e r cNumericResultFilter cNumericResultFilter c O b j e c t R e s u l t F i l t e r cObjectResultFilter cObjectResultFilter
  2. 需要使用 R e g i s t e r _ R e s u l t F i l t e r ( N A M E , C L A S S N A M E ) Register_ResultFilter(NAME, CLASSNAME) Register_ResultFilter(NAME,CLASSNAME)宏注册新的结果筛选器类。

2.结果记录器

  1. 必须从 c R e s u l t R e c o r d e r cResultRecorder cResultRecorder或更具体的 c N u m e r i c R e s u l t R e c o r d e r cNumericResultRecorder cNumericResultRecorder类中创建子类;
  2. 使用 R e g i s t e r R e s u l t R e c o r d e r ( N A M E , C L A S S N A M E ) Register_ResultRecorder(NAME, CLASSNAME) RegisterResultRecorder(NAME,CLASSNAME)宏进行注册。

结果筛选器和记录器

3.结果筛选器实现示例

/**
* Filter that outputs the sum of signal values divided by the measurement
* interval (simtime minus warmup period).
*/
class SumPerDurationFilter : public cNumericResultFilter
{
protected:
double sum;
protected:
virtual bool process(simtime_t& t, double& value, cObject *details);
public:
SumPerDurationFilter() {sum = 0;}
};
Register_ResultFilter("sumPerDuration", SumPerDurationFilter);
bool SumPerDurationFilter::process(simtime_t& t, double& value, cObject *)
{
sum += value;
value = sum / (simTime() - getSimulation()->getWarmupPeriod());
return true;
}

最后

以上就是阳光哈密瓜为你收集整理的OMNET实践总结——signal机制概述1、引言2、Signal-Based Statistics Recording(基于信号的统计记录)3. 高阶操作的全部内容,希望文章能够帮你解决OMNET实践总结——signal机制概述1、引言2、Signal-Based Statistics Recording(基于信号的统计记录)3. 高阶操作所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(52)

评论列表共有 0 条评论

立即
投稿
返回
顶部