Stanford CS144 Lab 5

Lab Checkpoint 5: down the stack (the network interface)

我们前面算是已经完成了 TCP 的实现,可以与任意系统交换 TCP 报文。从这个 checkpoint 起,我们将实现 network interface,其位于 TCP/IP 协议栈的相对底部,负责处理上层的 IP 包 (Internet datagram) 和下层的以太网帧 (Ethernet frames)。也就是说,我们最终的目的是实现 TCP-in-IP-in-Ethernet。

Checkpoint 4 是一些测量真实世界互联网的实验,无需在实验框架中填充代码,就不在此系列文章中赘述了。

2 Checkpoint 5: The Address Resolution Protocol #

这个 checkpoint 的主要任务是处理 ARP 协议。具体地,我们需要修改给出的 NetworkInterface 类,完成其中的 send_datagram recv_frame tick 三个方法。

send_datagram 是用来将 IP 包包装为以太网帧发送的,同时也要处理 ARP 报文广播的发送。该方法接受 IP 包和下一跳作为参数。一旦下一跳的 MAC 地址 (Ethernet address) 已知,则包装并发送该包;否则,广播一个 ARP request。为了避免造成 flood,发送 ARP request 的频率还受到一些限制。

recv_frame 接受以太网帧,并维护相应状态。传入的以太网帧可能有两种类型:IP 包和 ARP 报文。如果是 IP 包,解析其 payload 并压入 NetworkInterface 的内部队列;如果是 ARP 报文,更新 ARP 表,并查看有没有因为找不到 MAC 地址而没发出去的 IP 包,有则发之。

tick 是测试的时候用来计时的,主要是 ARP 广播超时需要利用其维护状态。

我个人感觉 send_datagramrecv_frame 两个方法在功能上会有些耦合,并且后者需要直接调用前者,不是很优雅,各种副作用比较影响思考。假如是一个真实的操作系统内核(我没写过或者读过,基于个人猜测),或许可以采用更加“事件驱动”的方式,把 ARP 表管理的部分拆出来,在每一个 tick 进行处理过期的 ARP entry,遍历 pending 状态的 IP 包等操作,缺点是根据 tick 粒度的不同,会有一定延迟。或许等我学习了 OS,会有更正确的理解吧。


实现上,状态管理还是全靠 std::map。给 NetworkInterface 添加了如下 private 成员:

// A map of IP address -> {Ethernet address, timestamp} for ARP lookups
std::map<uint32_t, std::pair<EthernetAddress, size_t>> arp_table_ {};

// A map of IP address in ARP requests -> timestamp for ARP requests that already have been sent
std::map<uint32_t, size_t> arp_requests_sent_ {};

// A map of IP address -> queue of datagrams that are waiting for an ARP reply
std::map<uint32_t, std::queue<InternetDatagram>> datagrams_pending_ {};

size_t last_tick_ms_ = {};

send_datagram 的实现:

void NetworkInterface::send_datagram( const InternetDatagram& dgram, const Address& next_hop )
{
  uint32_t next_hop_ip = next_hop.ipv4_numeric();

  auto arp_entry = arp_table_.find( next_hop_ip );
  if ( arp_entry != arp_table_.end() ) {
    EthernetFrame frame = {
      .header = {
        .dst = arp_entry->second.first,
        .src = ethernet_address_,
        .type = EthernetHeader::TYPE_IPv4,
      },
      .payload = serialize(dgram)
    };
    transmit( frame );
    return;
  }

  datagrams_pending_[next_hop_ip].push( dgram );

  // Ethernet address unknown, send ARP request
  // Check timestamp first to avoid flooding
  auto arp_request_it = arp_requests_sent_.find( next_hop_ip );
  if ( arp_request_it != arp_requests_sent_.end() && last_tick_ms_ - arp_request_it->second < 5000 ) {
    return;
  }

  ARPMessage arp_request = {
    .opcode = ARPMessage::OPCODE_REQUEST,
    .sender_ethernet_address = ethernet_address_,
    .sender_ip_address = ip_address_.ipv4_numeric(),
    .target_ip_address = next_hop_ip,
  };
  EthernetFrame arp_frame = {
    .header = {
      .dst = ETHERNET_BROADCAST,
      .src = ethernet_address_,
      .type = EthernetHeader::TYPE_ARP,
    },
    .payload = serialize(arp_request)
  };
  transmit( arp_frame );
  arp_requests_sent_[next_hop_ip] = last_tick_ms_;
}

recv_frame 的实现。调用了 send_frame 实现 IP 包的重传:

void NetworkInterface::recv_frame( EthernetFrame frame )
{
  if ( frame.header.dst != ethernet_address_ && frame.header.dst != ETHERNET_BROADCAST ) {
    return;
  }

  if ( frame.header.type == EthernetHeader::TYPE_IPv4 ) {
    IPv4Datagram dgram;
    if ( parse( dgram, move( frame.payload ) ) ) {
      datagrams_received_.push( move( dgram ) );
    }
  }

  if ( frame.header.type == EthernetHeader::TYPE_ARP ) {
    ARPMessage msg;
    if ( parse( msg, move( frame.payload ) ) ) {
      arp_table_[msg.sender_ip_address] = { msg.sender_ethernet_address, last_tick_ms_ };
      if ( msg.opcode == ARPMessage::OPCODE_REQUEST && msg.target_ip_address == ip_address_.ipv4_numeric() ) {
        ARPMessage arp_reply = {
          .opcode = ARPMessage::OPCODE_REPLY,
          .sender_ethernet_address = ethernet_address_,
          .sender_ip_address = ip_address_.ipv4_numeric(),
          .target_ethernet_address = msg.sender_ethernet_address,
          .target_ip_address = msg.sender_ip_address,
        };
        EthernetFrame arp_frame = {
          .header = {
            .dst = msg.sender_ethernet_address,
            .src = ethernet_address_,
            .type = EthernetHeader::TYPE_ARP,
          },
          .payload = serialize(arp_reply)
        };
        transmit( arp_frame );
      }

      auto it = datagrams_pending_.find( msg.sender_ip_address );
      if ( it != datagrams_pending_.end() ) {
        while ( !it->second.empty() ) {
          InternetDatagram dgram = move( it->second.front() );
          it->second.pop();

          send_datagram( dgram, Address::from_ipv4_numeric( msg.sender_ip_address ) );
        }
        datagrams_pending_.erase( it );
      }
    }
  }
}

tick 主要处理 ARP 表和未获响应的 ARP requests 的过期:

void NetworkInterface::tick( const size_t ms_since_last_tick )
{
  last_tick_ms_ += ms_since_last_tick;

  for ( auto it = arp_table_.begin(); it != arp_table_.end(); ) {
    if ( last_tick_ms_ - it->second.second > 30000 ) { // 30 seconds
      it = arp_table_.erase( it );
    } else {
      ++it;
    }
  }

  for ( auto it = arp_requests_sent_.begin(); it != arp_requests_sent_.end(); ) {
    if ( last_tick_ms_ - it->second > 5000 ) { // 5 seconds
      datagrams_pending_.erase( it->first );
      it = arp_requests_sent_.erase( it );
    } else {
      ++it;
    }
  }
}

这里还有一个小插曲,测试的第一阶段 compile with bug-checkers 在我的机器上需要 18 秒才能跑完,但限时了 15 秒。可以通过修改 etc/tests.cmake 调大时限。