NS-3源码学习(二)Channel和NetDevice

发布时间 2023-11-15 21:41:17作者: PolarisZg

NS-3源码学习(二)Channel和NetDevice

对于一个新的802.11协议的实现,仅需要完成对两个抽象类的实现即可,一个是Channel抽象类,一个是NetDevice接口,这两个类对上层来说是透明的,而且据我查阅代码了解,这两个类仅需知道上一层(网络层)是IPv4协议还是IPv6协议即可,并没有对上层有更高的要求。

MAC和PHY归NetDevice管,以上归其他模块。

而NetDevice和Channel的耦合度就很高了,基本是拆不开。

至于Helper类,并不一定需要,只是一层外壳而已。

Channel

NS-3中关于Channel类的描述:

Abstract Channel Base Class.

A channel is a logical path over which information flows. The path can be as simple as a short piece of wire, or as complicated as space-time.

Subclasses must use Simulator::ScheduleWithContext to correctly update event contexts when scheduling an event from one node to another one.

翻译一遍的话:

Channel,即通道,是一个抽象类;

通道是一条数据在两个结点间流动的抽象“路径”。这个“路径”既可以指一条导线,比如说子类PointToPointChannel代表了一个点对点通信的导线,仅支持两个node接入;也可以是一个时空,比如子类CsmaChannel,其允许多个node接入;

当一个结点调度另一个结点的事件时,比如说发送数据时,必须使用Simulator::ScheduleWithContext来更新事件和设置事件的延迟;

对于NS-3来说,a结点向b结点发送信息,就是a结点调用b结点对应device的receive()方法,这种调用的格式很固定:

Simulator::ScheduleWithContext(it->devicePtr->GetNode()->GetId(),        // 获取目的结点的id
                               m_delay,                                  // 设置延迟
                               &CsmaNetDevice::Receive,                  // 设置调用的方法
                               it->devicePtr,                            // 参数0 一般是目的结点
                               m_currentPkt->Copy(),                     // 参数1 被上面调用方法使用的参数
                               m_deviceList[m_currentSrc].devicePtr);    // 参数2 被上面调用方法使用的参数

被上述方法调用的函数为:

void
CsmaNetDevice::Receive(Ptr<Packet> packet, Ptr<CsmaNetDevice> senderDevice)

NetDevice

NS-3中关于nNetDevice类的描述:

Network layer to device interface.

This interface defines the API which the IP and ARP layers need to access to manage an instance of a network device layer. It currently does not support MAC-level multicast but this should not be too hard to add by adding extra methods to register MAC multicast addresses to filter out unwanted packets before handing them to the higher layers.

In Linux, this interface is analogous to the interface just above dev_queue_xmit() (i.e., IP packet is fully constructed with destination MAC address already selected).

If you want to write a new MAC layer, you need to subclass this base class and implement your own version of the pure virtual methods in this class.

This class was designed to hide as many MAC-level details as possible from the perspective of layer 3 to allow a single layer 3 to work with any kind of MAC layer. Specifically, this class encapsulates the specific format of MAC addresses used by a device such that the layer 3 does not need any modification to handle new address formats. This means obviously that the NetDevice class must know about the address format of all potential layer 3 protocols through its GetMulticast methods: the current API has been optimized to make it easy to add new MAC protocols, not to add new layer 3 protocols.

Devices aiming to support flow control and dynamic queue limits must perform the following operations:

  • in the NotifyNewAggregate method

    • cache the pointer to the netdevice queue interface aggregated to the device

    • set the select queue callback through the netdevice queue interface, if the device is multi-queue

  • anytime before initialization

    • set the number of device transmission queues (and optionally create them) through the netdevice queue interface, if the device is multi-queue
  • when the device queues have been created, invoke NetDeviceQueueInterface::ConnectQueueTraces, which

    • connects the Enqueue traced callback of the device queues to the PacketEnqueued static method of the NetDeviceQueue class

    • connects the Dequeue and DropAfterDequeue traced callback of the device queues to the PacketDequeued static method of the NetDeviceQueue class

    • connects the DropBeforeEnqueue traced callback of the device queues to the PacketDiscarded static method of the NetDeviceQueue class

翻译一遍的话:

NetDevice是网络层(IP,IPv4,IPv6等协议工作的层次)和设备(MAC层和PHY层)的接口;

该接口定义了管理MAC层的API,如果想要创建一个新的MAC层的话,需要实现这些接口,以便于管理设备层的实例;

目前,NetDevice并不支持多播,即一对多的数据传送方式。但是可以通过添加额外的方法来注册MAC多播地址,在交给更高层之前先过滤掉不需要的数据包,这些不难实现;

举例来说,NS-3中CsmaDevice发送信息是以广播的形式发送,也就是CsmaChannel中通过循环调用每一个结点的Receive方法,这样每一个结点都能收到信息,经过过滤后再传递给上层,这就表明我们可以给需要多播的结点再定义一个多播MAC地址的属性,过滤的时候添加新规则就好;

在Linux中,该接口类似于位于dev_queue_xmit()正上方的接口(即,IP数据包已完全构造,目标MAC地址已选择)。

首先要明白dev_queue_xmit()这个函数,这是一个Linux内核中的函数,用于将数据包推送至发送队列。

这样就好理解这个NetDevice的工作:

  1. 将IP地址转换为MAC地址

  2. 构造MAC数据帧

  3. 调用Channel发送数据帧

  4. 接收数据帧

  5. 过滤数据帧

  6. 向上层汇报数据

通过源码src/network/model/net-device.h可以看到我们需要实现的NetDevice的接口,下述方法都需要实现。

  • 分类依据:

    • 重要:编写极其困难,需要根据情况修改大量的代码

    • 容易:实现的代码固定,照抄即可

    • 一般:实现方式较为固定,没有多大的变动,但调用该函数有难度

  • static TypeId GetTypeId();重要

    • 一般来说,每一个需要加载进NS-3的组件都需要实现这个接口

    • 在这个方法返回的TypeId中,最核心的是AddAttribute和AddTraceSource,

    • AddAttribute可以被工厂使用来产生具有特定初始化值的实例,

    • AddTraceSource被用于添加追踪器,用于在某些事件发生时触发回调函数,这样的事件一般是类型为TracedCallback的变量被修改,而这些变量被修改的原因,可能时因为接收到了数据,也有可能是手动进行修改以触发日志记录等等。

  • virtual void SetIfIndex(const uint32_t index) = 0virtual uint32_t GetIfIndex() const = 0;不重要

    • 没看出来这两个函数有什么作用,应该是是属于NS-3仿真器要求的,用于辨识组件的函数。一般来说写法是固定的

      void
      CsmaNetDevice::SetIfIndex(const uint32_t index)
      {
          NS_LOG_FUNCTION(index);
          m_ifIndex = index;
      }
      
      uint32_t
      CsmaNetDevice::GetIfIndex() const
      {
          NS_LOG_FUNCTION_NOARGS();
          return m_ifIndex;
      }
      
  • virtual Ptr<Channel> GetChannel() const = 0;容易

    • 这个函数用于得到该device对应的Channel

    • 可以看到,基类中仅定义了Channel的Get方法,并没有相应的Set方法,也就是说,NS-3并不会自动的为Device分配Channel,而是需要用户根据放着需求自动实现

    • 一般来说,会在自定义的Device类中使用bool Attach(Ptr ch);方法来Set该Device的Channel

  • virtual void SetAddress(Address address) = 0;virtual Address GetAddress() const = 0;一般

    • 这两个函数用于设定和得到该Device的地址

    • 一般是48bit的MAC地址,通过用户的仿真代码进行下述调用进行设置

      device1->SetAddress(Mac48Address::Allocate());
      
    • 也是一个具有固定内容的方法,就是将设定的Mac地址使用一个属性保存下来

      void
      CsmaNetDevice::SetAddress(Address address)
      {
          NS_LOG_FUNCTION_NOARGS();
          m_address = Mac48Address::ConvertFrom(address);
      }
      
  • virtual bool SetMtu(const uint16_t mtu) = 0;virtual uint16_t GetMtu() const = 0;一般

    • 设定该设备的MTU和得到该设备的MTU

    • 设置设备的最大传输单元(MTU)

    • 也没有什么复杂的逻辑,设定好MTU之后需要在传输数据时检查长度。

      • 注意:这里我没找到长度如果大于MTU的解决方案

      • 这个实现既困难又简单,简单的点在于发送时是抽取队列中封装好的数据包进行发送,因此仅需要将一个数据包切开多次入队即可。复杂的点在于帧的分割涉及到对于帧头信息的识别,而且需要竞争txop等行为,这一方面需要去学习协议详细的实现,当然,也有可能是我考虑麻烦了,因为如果入队出队逻辑比较完善的话,长度大于MTU是不成问题的

      bool
      PointToPointNetDevice::SetMtu(uint16_t mtu)
      {
          NS_LOG_FUNCTION(this << mtu);
          m_mtu = mtu;
          return true;
      }
      
  • virtual bool IsLinkUp() const = 0;不重要

    • 获取Device当前的状态,是否已经和某个Channel建立了链接
  • typedef void (*LinkChangeTracedCallback)();不重要

    • 甚至我没有在子类中找到相关的实现,哪怕是给这个函数指针一个确切的函数实体都没有

      img

  • virtual void AddLinkChangeCallback(Callback<void> callback) = 0;一般

    • 这个函数允许用户添加链路状态变化时的回调函数。当链路状态发生变化时,连接的回调函数将被调用,允许用户在特定事件发生时执行自定义的操作。

    • 函数实现的代码也是固定的,调用该函数时,只需要为这个函数传入一个类型为void,参数数量为0的函数名即可,他就会在m_linkChangeCallbacks被调用的时候调用所有被传入的函数。

      void
      CsmaNetDevice::AddLinkChangeCallback(Callback<void> callback)
      {
          NS_LOG_FUNCTION(&callback);
          m_linkChangeCallbacks.ConnectWithoutContext(callback);
      }
      
      void
      CsmaNetDevice::NotifyLinkUp()
      {
          NS_LOG_FUNCTION_NOARGS();
          m_linkUp = true;
          m_linkChangeCallbacks();
      }
      
  • virtual bool IsBroadcast() const = 0;virtual Address GetBroadcast() const = 0;一般

    • 这两个函数实现起来比较固定,比如说PointToPoint就是返回False,csma就是返回trueff:ff:ff:ff:ff:ff

    • 但是,对于返回true的多播devide,对于接收到的广播数据帧处理起来时有一定的难度的。尤其是使用Promiscuous 模式的 csma,在接到数据包之后需要进行过滤才能向上层传输数据

    • Promiscuous 模式是一种网络设备工作模式,它允许网络接口卡在接收网络上所有数据包而不仅仅是目标地址是设备自身的数据包

  • virtual bool IsMulticast() const = 0;virtual Address GetMulticast(Ipv4Address multicastGroup) const = 0;virtual Address GetMulticast(Ipv6Address addr) const = 0;似乎重要

    • 这三个函数实现起来也是比较固定的,对于支持组播的设备返回true

      Address
      CsmaNetDevice::GetMulticast(Ipv6Address addr) const
      {
          Mac48Address ad = Mac48Address::GetMulticast(addr);
      
          NS_LOG_LOGIC("MAC IPv6 multicast address is " << ad);
          return ad;
      }
      
      Address
      CsmaNetDevice::GetMulticast(Ipv4Address multicastGroup) const
      {
          NS_LOG_FUNCTION(multicastGroup);
      
          Mac48Address ad = Mac48Address::GetMulticast(multicastGroup);
      
          NS_LOG_LOGIC("multicast address is " << ad);
      
          return ad;
      }
      
    • 但是对于这个函数的调用有些疑惑,似乎是仅会被上层的网络层调用

  • virtual bool IsBridge() const = 0;似乎容易

    • 查询网络设备是否充当桥接器

    • 一般返回false,除了bridge-net-device

    • 这里添加"似乎"的意思是,对于是否是桥接器应该不需要单独创建一类device,不是很理解这样的作法,还是说对于一个结点,安装多个设备

  • virtual bool IsPointToPoint() const = 0;容易

    • 看名字就能了解到该函数用于判断PointToPoint通信

    • 按照要求返回乡音的bool值即可

  • virtual bool Send(Ptr<Packet> packet, const Address& dest, uint16_t protocolNumber) = 0; virtual bool SendFrom(Ptr<Packet> packet,const Address& source, const Address& dest,uint16_t protocolNumber) = 0;极其重要

    • 这个方法非常重要,该方法可能会被上层,或本层的管理面调用,在完成一系列对帧的封装后发送数据

      img

    • 该方法的难点有很多,以下是我能想到的:

      1. 判断当前设备是否准备发送,这一步可能会因为上次的数据帧还未发送完被卡住,而卡住该代码执行的相关逻辑还不清楚应该怎么写,我看到CsmaNetDevice的实现中并没有进程间通信/线程间通信的相关判断

      2. 发送事件的写法,发送事件TransmitStart();不仅仅是将数据推送至相应的Channel,在WiFi相关的CSMA/CA中还应该有确认ACK的事件,因此就需要要么有一个已发送队列,要么在发送过后卡死,直到收到ACK包或者超时后重发。考虑到WIFI 7中添加了HARQ混合自动重传机制,这就会使发送事件更加的繁琐;即难点在于如何判断发送不成功,以及如何在发送不成功的基础上再次发送数据

      3. 另外还有回退窗口backoff,txop,NAV计时器等其他影响发送的机制

  • 未完