1、如何收发数据包

流量控制层位于 NetDevices (L2) 和任何网络协议(例如 IP)之间。它负责处理数据包并对其执行操作:调度、丢弃、标记、监管等。

流量控制层拦截从网络层向向网络设备的传出数据包和沿相反方向流动的传入数据包。目前,流量控制层仅处理传出数据包。特别是,传出数据包在排队规则中排队,该规则可以对其执行多项操作。

我们想在以下段落中回答的主要问题是:ns-3 节点如何发送/接收数据包?

如果我们分析任何一个例子,节点接收/传输数据包的能力来自两个 helper 的交互:

  • L2 Helper (派生自 NetDevice)

  • L3 Helper (通常来自 Internet 模块)

1.1、L2 Helper

任何优秀的 L2 Helper 都会执行以下操作:

  • 创建 n 个netdevices网络设备 (n>1)

  • 在这些设备之间附加通道channel

  • 调用Node::AddDevice ()

Node::AddDevice(network/model/node.cc:128)为设备分配接口索引,调用 NetDevice::SetNode,将设备的接收回调设置为 Node::NonPromiscReceiveFromDevice。然后,它将 NetDevice::Initialize() 方法安排在 Seconds(0) 处,然后通知已注册的 DeviceAdditionListener 处理程序。

Node::NonPromiscReceiveFromDevice 调用 Node::ReceiveFromDeviceNode::ReceiveFromDevice 遍历 ProtocolHandlers

ProtocolHandler (Ptr<NetDevice>, Ptr<const Packet>, protocol, from_addr, to_addr, packetType)

如果 device、protocol number 和 promiscuous flag 对应,则执行handler。

谁负责设置 ProtocolHandler ?我们将在下一节中对此进行分析。

1.2、L3 Helper

只有 internet 模块提供网络协议,这个模块有两个Helper:InternetStackHelperIpv{4,6}AddressHelper

InternetStackHelper::Install (internet/helper/internet-stack-helper.cc:423) 创建并聚合协议 {ArpL3,Ipv4L3,Icmpv4}Protocol。它会创建路由协议,如果启用了 Ipv6,则添加 {Ipv6L3,Icmpv6L4}Protocol。在任何情况下,它都会实例化并聚合一个 UdpL4Protocol 对象以及一个 PacketSocketFactory。最终,它会创建所需的对象并将其聚合到节点。

Ipv4AddressHelper::Assign 注册处理程序。这个过程有点长。使用 NetDevice 列表调用该方法。对于每个指针,将检索 node 和 Ipv4L3Protocol 指针;如果已经为设备注册了 Ipv4Interface,则在此设置地址。否则,在添加地址之前调用方法 Ipv4L3Protocol::AddInterface

1.3、IP interfaces

Ipv4L3Protocol::AddInterface中安装了两个协议处理程序:一个对 IPv4 协议号做出反应,另一个对 arp 协议号做出反应(分别为 Ipv4L3Protocol::ReceiveArpL3Protocol::Receive)。然后创建、初始化并返回接口。

Ipv4L3Protocol::Receive遍历该接口。一旦找到与作为参数传递的设备具有相同设备的 Ipv4Interface,就会调用 rxTrace 回调。如果接口关闭,则丢弃数据包。然后,它会删除 Header 并修剪任何残留的帧填充。如果校验和不正常,则丢弃数据包。然后,它会询问路由协议该数据包的命运是什么。选项为:Ipv4L3Protocol::{IpForward, IpMulticastForward,LocalDeliver,RouteInputError}。

2、队列规则 Queue disciplines

Traffic Control layer接收的用于传输到网络设备的数据包可以传递到队列规则Queue Disc以执行调度和管制。

ns-3 术语“queue disc”对应于 Linux 所说的“qdisc”。网络设备上可以安装单个 queue disc。在 netdevice 上安装 queue disc 不是必需的。如果 netdevice 上没有安装queue disc,则流量控制层会将数据包直接发送到 netdevice。

queue discs 可以是简单的队列,也可以是复杂的分层结构。queue discs可以包含不同的元素:

  • queues,它实际上存储了等待传输的数据包

  • classes,允许对不同的流量细分定义不同的处理方式

  • filters,用于确定数据包的目的地 queue 或 class

流量控制层以一种简单的方式和队列规则交互:在请求数据包加入队列后,流量控制层请求 qdisc 运行,将一组数据包出队,直到预定义数量的数据包出队或者网络设备停止队列规则。当netdevice的传输队列没有空间容纳另外一个数据包时,网络设备应停止队列规则。此外,当netdevice检测到其传输队列中有空间容纳另一个数据包,但传输队列已经停止时,它应唤醒队列规则。

每个队列规则都收集有关从上层或者父队列规则接收的数据包/字节总数的统计信息,入队、出队、重新入队、丢弃、入队前丢弃、出队后斗气、标记并存储在队列规则里,发送到网络设备活父队列规则。

C++ 抽象基类 QueueDisc 类被子类化以实现特定的队列光盘。需要 subclass 来实现以下方法:

  • bool DoEnqueue (Ptr<QueueDiscItem> item) :将数据包加入队列

  • Ptr<QueueDiscItem> DoDequeue () :将数据包出队

  • bool CheckConfig () const: 检查配置是否正确

  • void InitializeParams ():初始化队列光盘参数

  • Ptr<const QueueDiscItem> DoPeek () const :查看要提取的下一个数据包

2.1、TrafficControlHelper

典型的使用模式是创建流量控制帮助程序,并从帮助程序配置队列规则、队列、类和筛选器的类型和属性,例如,pfifo_fast可以按如下方式配置:

TrafficControlHelper tch;
uint16_t handle = tch.SetRootQueueDisc("ns3::PfifoFastQueueDisc");
tch.AddInternalQueues(handle, 3, "ns3::DropTailQueue", "MaxSize", StringValue("1000p"));
QueueDiscContainer qdiscs = tch.Install(devices);

上面的代码将三个内部队列添加到 PfifoFast 类型的根队列规则中。通过上述配置,安装在第 i 个节点的第 j 个设备上的根队列规则的配置路径(一个设备的索引与 DeviceList 中的相同)为:

/NodeList/[i]/$ns3::TrafficControlLayer/RootQueueDiscList/[j]

第二个内部队列的配置路径为:

/NodeList/[i]/$ns3::TrafficControlLayer/RootQueueDiscList/[j]/InternalQueueList/1

如果希望在设备上不安装队列规则,则必须使用 TrafficControlHelper 的 Uninstall 方法:

TrafficControlHelper tch;
tch.Uninstall(device);

2.2、Fifo queue disc

FifoQueueDisc 实现 FIFO (先进先出) 策略。数据包将排入唯一的内部队列中,该队列作为 DropTail 队列实现。队列容量可以根据数据包或字节指定,具体取决于 Mode 属性的值。

FifoQueueDisc 类具有以下属性:

  • MaxSize:队列磁盘可以容纳的最大数据包数/字节数。默认值为 1000 个数据包。

2.3、pfifo_fast queue disc

PfifoFast,即优先级先进先出队列,是一种基于优先级的排队策略:

基本原理:数据包根据优先级被放入不同的队列中。每个队列都是FIFO(先进先出)方式处理数据包。

优先级处理:路由器首先尝试从最高优先级的队列中发送数据包,只有当高优先级的队列为空时,才会从低优先级的队列中发送数据包。

适用场景:适合于需要区分服务质量或处理紧急数据包的场合。

数据包排入三个优先级带,根据其优先级。优先级的四个最低有效位用于根据下表确定所选Band:

系统的行为类似于三个一起运行的 ns3::DropTail 队列,其中来自较高优先级Band的数据包始终在来自较低优先级Band的数据包出队之前出队。

// 配置了一个具有三个内部队列的 PfifoFastQueueDisc 队列,每个队列都是 DropTailQueue 类型,且每个队列的最大容量是1000个数据包。
TrafficControlHelper tchPfifo;
uint16_t handle = tchPfifo.SetRootQueueDisc ("ns3::PfifoFastQueueDisc");
tchPfifo.AddInternalQueues (handle, 3, "ns3::DropTailQueue", "MaxSize", StringValue ("1000p"));

2.4、Prio queue disc

PrioQueueDisc 实现严格的优先级策略,其中仅当较高优先级的带区全部为空时,数据包才会从带区中出队。

PrioQueueDisc 是一个有类队列规则,可以有任意数量的条带,每个条带都由任何类型的队列规则处理。PrioQueueDisc 的容量没有限制;数据包只能由子队列规则(可能具有有限的容量)丢弃。如果未安装数据包过滤器或无法对数据包进行分类,则数据包将根据其优先级(模 16)排队到优先级带中,该优先级带用作名为 priomap 的数组的索引。如果数据包被分类通过已安装的数据包过滤器,并且返回值 i 为非负值且小于优先级带数,则数据包将排入第 i 个优先级带。否则,数据包将排队到 priomap 数组的第一个元素指定的优先级带中。

TrafficControlHelper tch;
uint16_t handle = tch.SetRootQueueDisc("ns3::PrioQueueDisc", "Priomap", StringValue("0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1"));
TrafficControlHelper::ClassIdList cid = tch.AddQueueDiscClasses(handle, 2, "ns3::QueueDiscClass");
tch.AddChildQueueDisc(handle, cid[0], "ns3::FifoQueueDisc");
tch.AddChildQueueDisc(handle, cid[1], "ns3::RedQueueDisc");

上面的代码向 PrioQueueDisc 添加了两个类(带)。优先级最高的一个是 FifoQueueDisc,另一个是 RedQueueDisc。属性 Priomap 被设置为仅包含 0 和 1 的数组(因为 PrioQueueDisc 只有两个频段)。

2.5、TBF queue disc

TBF 是一个 qdisc,它允许根据设定的速率控制输出的带宽,也可以管理突发条件。

class TbfQueueDisc 这个类实现了主要的TBF算法:

  • TbfQueueDisc::DoEnqueue():如果队列未满,则此例程将传入的数据包排入队列,否则丢弃数据包。

  • TbfQueueDisc::DoPeek():此例程查看队列中的顶部项目,如果队列不为空,则返回最顶层的项目。

  • TbfQueueDisc::DoDequeue()

2.6、RED queue disc

随机早期检测 (RED) 是一种队列规则,旨在向传输协议拥塞控制(例如 TCP)提供拥塞迫在眉睫的早期信号,以便它们优雅地降低其速率,而不是出现一堆尾部丢弃丢失(可能导致 TCP 超时)。

要开启 ARED 算法,必须将属性 ARED 设置为 true

Config::SetDefault("ns3::RedQueueDisc::ARED", BooleanValue(true));

2.7、CoDel queue disc

CoDel(Controlled Delay)是为了解决网络中的缓冲区膨胀问题而设计的队列管理算法:

目标:主要目标是减少网络延迟,通过动态调整队列中的数据包停留时间来减少队列的长度。

操作方式:CoDel会检查队列中数据包的停留时间,如果数据包的停留时间超过了设定的阈值,CoDel就会开始丢弃(或标记)队列前端的数据包,以减少队列长度和延迟。

自适应性:CoDel不依赖于数据流的特定行为,能够自适应网络条件变化,对各种网络环境都有较好的表现。

CoDel 模型的源代码位于 src/traffic-control/model 目录中 由 2 个文件 codel-queue-disc.hcodel-queue-disc.cc组成,用于定义 CoDelQueueDisc 类和帮助程序 CoDelTimestampTag 类。

class CoDelQueueDisc:此类实现主要的 CoDel 算法:

  • CoDelQueueDisc::DoEnqueue():此例程在将数据包推送到队列之前使用当前时间标记数据包。CoDelQueue::DoDequeue() 使用 timestamp 标签来计算数据包的逗留时间。如果数据包到达时队列已满,则此例程将丢弃数据包并记录由于队列溢出而导致的丢弃次数,该次数存储在 m_dropOverLimit 中。

  • CoDelQueueDisc::ShouldDrop():此例程是 CoDelQueueDisc::DoDequeue() 的帮助程序例程,用于根据数据包的逗留时间确定是否应丢弃数据包。如果逗留时间超过 m_target 并且持续保持至少 m_interval,则例程将返回 true,表示可以丢弃数据包。否则,它将返回 false。

class CoDelTimestampTag:此类实现数据包的时间戳标记。此标签用于计算数据包的逗留时间(数据包出列的时间与数据包被推入队列的时间之间的差值)。

属性:

  • MaxSize:队列可以容纳的最大数据包数/字节

  • MinBytes:CoDel算法minbytes参数

  • Interval:最小滑动窗口

  • Target:CoDel算法以队列时延为目标

  • UseEcn:如果为True,则使用ECn(标记数据包而不是丢弃数据包)

  • CeThreshold:用于标记数据包的CoDel CE阈值