在程序设计的时候,我们一般都会在程序中输出一些信息。无论是记录程序的运行过程也好,还是输出程序的结果,都需要输出一些信息。这些输出的信息我们称为日志。本章主要介绍NS-3的日志系统:日志的级别,如何记录日志,如何启用和禁用日志等。
1. NS-3日志概述
在程序中,经常都需要输出一些信息。例如为了程序调试的时候方便,我们需要输出程序的运行过程的记录,或者需要输出一些中间结果。或者程序运行出现逻辑错误的时候,我们需要输出一些错误信息等等。有些程序直接将结果输出到标准输出(stdout)或者是标准错误(stderr)。除此之外,一些大型的软件系统提供一种更灵活的方式来记录这些程序输出的信息,这种机制称为日志系统。日志系统可以灵活地控制输出的信息的详细程度、输出方式等。
在NS-3当中,日志系统主要是用来提供程序的调试信息的,例如输出警告、错误、程序逻辑等信息或者输出一些程序的中间结果以提供验证。对于仿真的结果数据,NS-3提供了跟踪机制(tracing)来收集这些结果,而不应该直接使用日志来进行输出。跟踪机制将在后面的章节进行介绍。
2. 日志的级别
在NS-3当中,日志是继续组件(Component)定义的,能够方便地控制开启和关闭一个组件的日志。同时,NS-3提供了七个级别的日志水平,可以在输出日志是选择相应的级别,那么在显示日志是可以控制日志的显示详细程度。这些级别由粗到细依次为:
- LOG_ERROR:记录错误信息,程序中使用NS_LOG_ERROR来输出信息
- LOG_WARN:记录警告信息,程序中使用NS_LOG_WARN来输出信息
- LOG_DEBUG:记录一些不常见出现、奇怪的调试信息,程序中使用NS_LOG_DEBUG来输出信息
- LOG_INFO:记录记录程序运行进度的信息,程序中使用NS_LOG_INFO来输出信息
- LOG_FUNCTION:记录函数调用的信息,程序中使用NS_LOG_FUNCTION来输出信息
- LOG_LOGIC:记录程序中执行流程的信息,程序中使用NS_LOG_LOGIC来输出信息
- LOG_ALL:显示任何级别的日志
除此之外,NS-3还提供了一种特殊的输出方式:
- NS_LOG_UNCOND:这种输出方式不会绑定到上述任何一个级别当中,只要所属组件的日志开启,不论当前显示哪个级别,都将输出日志信息。
3. 定义和使用日志组件
3.1. 定义日志组件
要输出日志,首先需要定义一个日志组件。其语法如下:
1 | NS_LOG_COMPONENT_DEFINE("MyTestComponent"); |
其中NS_LOG_COMPONENT_DEFINE是NS-3定义的一个宏,用于定义一个日志组件,在该组件中输出的日志,可以一次性启用或者禁用。组件的名字使用字符串表示。此外,要注意组件的名字不可重复。因此前面最好加上前缀,以免和NS-3官方的组件或者第三方的组件名称冲突。
3.2. 启用日志组件
在NS-3当中,有多种方法可以启用日志组件。这里介绍一种最简单的方法,即在程序中通过编程方式,直接启用某个组件的日志输出。其语法如下:
1 | LogComponentEnable("MyTestComponent", LOG_INFO); |
这个语句启用了MyTestComponent组件当中的INFO级别的日志。
3.3. 示例程序
下面的程序,定义了一个日志组件,然后输出了一些日志信息:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
输入./waf –run “test-log-component”运行该程序,得到输出结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.24.1/ns-3.24.1/build' |
可见,启用了LOG_INFO级别的日志,就只能输出INFO级别的日志内容。如果注释掉启用的语句:
1 | …… |
则运行程序后,将没有任何输出,因为只有启用日志组件之后,在该组件内输出的日志信息才有效。
随后,我们将程序更改为:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
运行程序后,输出结果为:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.24.1/ns-3.24.1/build' |
由此可见,使用NS_LOG_UNCOND输出的日志,无论日志组件启用与否,或者使用了任何的日志级别,其内容一定会输出。
最后,如果想禁用所有级别的日志,可以使用LOG_NONE。
4. 累计日志级别
在启用日志组件时,除了使用类似LOG_INFO或者LOG_WARN这样的单一级别之外,还可以使用LOG_LEVEL_INFO或者LOG_LEVEL_WARN这样的累计日志级别。其累计原则是,排在,编号越小的级别输出的级别越高。例如,如果启用LOG_LEVEL_WARN级别,那么其上的ERROR级别的日志也会同时输出,但是其下的任何一个级别都将不输出。
其中LOG_LEVEL_ERROR和LOG_ERROR是一样的,因为他们都只显示自己级别的日志,此外LOG_LEVEL_ALL和LOG_ALL是一致的,因为他们都显示所有级别的日志。
来看以下程序:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
运行后将得到以下输出:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.24.1/ns-3.24.1/build' |
由此可见,所有INFO以上的级别的日志都将输出,而INFO以下的级别的日志都将不输出。此外,UNCOND级别不受任何影响。
5. 函数日志
NS-3提供了函数日志来跟踪函数的调用情况。NS-3中一共有两个宏命令来协助跟踪函数日志:
- NS_LOG_FUNCTION:必须带一个参数,适用于类的方法,或带参数的静态方法。
- NS_LOG_FUNCTION_NOARGS:可以不带参数,适用于不带参数的静态方法。
在记录非静态方法调用的时候,一般将this传入作为第一个参数,如果后面还有参数,则继续传入参数列表,例如:
1 | void MyClass::Check (int value, char * item) |
记录静态有参方法的时候,除了不需传入this作为第一个参数以外,其他和非静态方法一致,例如:
1 | //static method |
此外,如果静态方法没有参数,则使用NS_LOG_FUNCTION_NOARGS来记录调用日志,而不需要传入任何参数,例如:
1 | //static method |
来看一个完整的例子:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
其运行结果为:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.24.1/ns-3.24.1/build' |
可以看出,无论是NS_LOG_FUNCTION还是NS_LOG_FUNCTION_NOARGS都可能正常记录函数的调用过程,包括函数的名称和参数。然而,值得注意的是,函数名前面的前缀,并不是类的名字,而是模块的名字,而NS-3使用的前缀分隔符是冒号(:),而不是C++当中的两个冒号(::),也从侧面提示了这并不是类名,而是模块名。
最佳实践:在编写程序时应该始终提供函数日志,这样调试程序会比较方便。
6. 日志前缀
除了可以控制显示什么级别的日志之外,NS-3还提供了日志前缀,来控制显示日志的来源,从而更加方便我们进行调试。NS-3中可用的前缀有如下几种:
- LOG_PREFIX_FUNC:输出日志所在的函数
- LOG_PREFIX_TIME:输出日志产生的时间(注意此时间是仿真时间)
- LOG_PREFIX_NODE:输出日志所在的节点编号
- LOG_PREFIX_LEVEL:输出日志的级别
- LOG_PREFIX_ALL:输出以上所有信息
可以使用LogComponentEnable和级别叠加使用,例如:
1 | LogComponentEnable("MyTestLevel", LOG_LEVEL_INFO); |
需要注意的是,日志产生时间指的是仿真时间,而非机器的实际时间。如果仿真并没有启动,则不会输出任何时间。只有在Simulation::Run()运行之后输出的日志才会具有时间。同样的,只有绑定在节点中的NS-3模块在仿真过程中输出的日志才会具有节点前缀。
下面是一个使用前缀的完整例子:
1 | /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */ |
运行程序后得到如下的结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.24.1/ns-3.24.1/build' |
从程序和输出结果可以看出:
- 所有输出都有级别作为前缀,例如:[INFO]或者[LOGIC]
- 在仿真开始之前的输出是不会有时间前缀的
- 输出带有函数前缀,函数的名字和NS_LOG_FUNCTION的输出结果一致,冒号前面始终是日志组件的名字而不是类名
- 未提供函数日志的函数也可以具有函数前缀
- 在仿真时间开始之前输出的结果,或者不在节点中的函数输出的结果其节点编号为-1
- 在节点中的模块输出的结果,具有节点编号作为前缀
- NS_LOG_UNCOND的输出不受前缀的影响
- NS-3中节点的编号从0开始
由这个例子可以看出,NS-3提供的日志前缀机制可以方便地获取任何日志输出的时间、节点号、从哪个函数中打印的。充分、合理地利用这个机制,我们可以更快地查找问题,并修正问题。
7. 命令行控制日志
除了在程序中使用编程的方式控制日志的开启与否、输出级别、前缀等选项之外,还可以使用命令行进行控制。相对于在程序中控制日志,使用命令行控制最大的好处在于更新配置后不需要重新编译程序。
7.1. 语法
命令行控制的方式主要是通过设置环境变量NS_LOG来实现的。一般是在启动具体的仿真之前设置,具体方法如下:
1 | $ NS_LOG="日志模块1=level|prefix:日志模块2=level|prefix:..." ./waf --run "test" |
7.2. 日志级别
在命令行中,可以使用的级别和程序控制中一致,但是有更简洁的写法:
- error (level_error)
- warn (level_warn)
- debug (level_debug)
- info (level_info)
- function (level_function)
- logic (level_logic)
- all (level_all)
- *
前面的部分是单个的级别,括号中的部分是累计级别。
7.3. 日志前缀
和程序中一样,也可以使用前缀定义日志输出,写法更加简洁:
- func
- time
- node
- level
- all
- *
日志级别可以和日志前缀组合使用,之间使用竖线(|)分隔。
7.4. 使用方法
在上一个程序中,我们使用程序的方式定义了日志模块MyTestPrefix的日志为LOGIC级别以上,并且同时输出所有前缀。这次,我们使用命令行的方法定义输出级别和前缀,首先注释程序中的LogComponentEnable语句,然后在命令行中输入:
1 | $ NS_LOG="MyTestPrefix=level_warn|level" ./waf --run "my-test-prefix" |
需要注意的是,由于竖线(|)在linux命令当中表示管道,因此NS_LOG变量的值一定要用引号包裹起来。运行上面的命令,可以得到输出结果:
1 | Waf: Entering directory `/home/rainsia/Applications/ns-allinone-3.24.1/ns-3.24.1/build' |
由此可见,程序中设置日志级别优先级比命令行中设置要高。要命令行控制起效,必须将程序中的日志启用语句注释。命令行的控制将更加简介,并且更改日志的配置不需要重新编译程序。