AF_XDP的原理与应用

AF_XDP是一个协议族,能将DMA缓冲区映射到用户空间,实现zero-copy,用于高性能的数据包处理。

XDP

XDP能在数据包进入协议栈之前就进行处理,根据XDP hook的位置不同,分为:

  • Native XDP:在网卡驱动的接收路径中,需要网卡驱动的支持,比如ixgbe驱动的软中断处理中调用。
  • Offloaded XDP:不再通过CPU执行XDP程序,而是在网卡中执行XDP程序,由JIT将XDP程序编译成网卡(一般是智能网卡)可运行的指令码,卸载到网卡上。
  • Generic XDP:针对不支持Native XDP与Offloaded XDP的情况,提供的一种模式。在内核协议栈中运行。

XDP对数据包的处理结果(Action)包括DROP、PASS、TX或REDIRECT。其中TX是将数据包从接收网卡中转发出去,REDIRECT是根据BPF MAP进行重定向,可以将数据包从其他网卡中转发出去,也可以将数据包重定向到AF_XDP socket中。

AF_XDP的实现

AF_XDP的应用分为用户空间与内核空间两部分逻辑。在用户空间使用socket()系统调用创建一个AF_XDP套接字(XSK),用于应用接受、发送数据包;在内核空间利用XDP的bpf_redirect_map(),将数据包重定向到用户态中(可以通过XDP程序,决定哪些数据包走AF_XDP,哪些数据包走内核协议栈),实现数据包的高性能处理(跳过内核协议栈、sk_buff的创建)。


图来自(https://medium.com/high-performance-network-programming/recapitulating-af-xdp-ef6c1ebead8)

具体的,内核空间与用户空间共享一片内存区域,称为UMEM,UMEM划分为许多个chunk,使用chunk存储数据包。内核与用户空间的应用通过RX RINGTX RINGFILL RINGCOMPLETION RING四个ring来操作UMEM,ring中的元素(称为desc)存储了指向UMEM某个chunk的addr。


图来自(https://rexrock.github.io/post/af_xdp1/)

  • RX RING:存储待XSK处理接收的数据包。内核为生产者,XSK为消费者。内核消费FILL RING,将数据包拷贝到FILL RING的desc指向的UMEM chunk中,然后将desc填充到RX RING
  • TX RING:存储待内核发送的数据包。XSK为生产者,内核为消费者。XSK消费COMPLETION RING,将要发送的数据包拷贝到COMPLETION RING的desc指向的UMEM chunk中,然后将desc填充到TX RING
  • FILL RING:存储可以承载数据包的chunk。XSK为生产者,内核为消费者
  • COMPLETION RING:存储已经发送完成的数据包。内核为生产者,XSK为消费者

以接收数据包的过程为例,用户态应用先填充FILL RING,等待数据包的接收。内核将接收到的数据包,拷贝到FILL RING中desc的UMEM chunk中,然后填充RX RING。用户态应用从RX RING中获取接收到的数据包,然后将desc填充回FILL RING,供下次内核接收数据包使用。发送数据包的过程类似,为COMPLETION RINGTX RING的配合使用。

AF_XDP实例

AF_XDP的实现分用户态程序与XDP程序。用户态程序初始化过程为:

  • 创建AF_XDP socket
  • 申请UMEM
  • 注册UMEM
  • 创建四个RING
  • 绑定到指定设备的某一队列

XDP程序主要是根据BPF_MAP_TYPE_XSKMAP重定向到对应队列的XSK上。

C语言的实例在samples/bpf/xdpsock_user.c,可以参考https://rexrock.github.io/post/af_xdp1/ 的分析。

Go语言的库可以使用asavie/xdp,封装程度更高,以examples/l2fwd为例:

// asavie/xdp/examples/l2fwd/l2fwd.go
// 指定两个网卡,将其中任意一个网卡接收的帧以指定的mac地址,通过另一个网卡转发出去
func main() {  
   ...
   forwardL2(verbose, inLink, inLinkQueueID, inLinkDst, outLink, outLinkQueueID, outLinkDst)  
}

func forwardL2(verbose bool, inLink netlink.Link, inLinkQueueID int, inLinkDst net.HardwareAddr, outLink netlink.Link, outLinkQueueID int, outLinkDst net.HardwareAddr) {  
	...
   // 创建xdp BPF程序,默认的XDP程序为<linux>/tools/lib/bpf/xsk.c下的xsk_load_xdp_prog(),进行简单的转发,也可以自定义xdp程序  
   // This is the C-program:  
// SEC("xdp_sock") int xdp_sock_prog(struct xdp_md *ctx)  
// {  
//     int *qidconf, index = ctx->rx_queue_index;  
//  
//     // A set entry here means that the correspnding queue_id  
//     // has an active AF_XDP socket bound to it.  
//     qidconf = bpf_map_lookup_elem(&qidconf_map, &index);  
//     if (!qidconf)  
//         return XDP_ABORTED;  
//  
//     if (*qidconf)  
//         return bpf_redirect_map(&xsks_map, index, 0);  
//  
//     return XDP_PASS;  
// }
   inProg, err := xdp.NewProgram(inLinkQueueID + 1)  
	...
   // 加载到input的网卡上
   if err := inProg.Attach(inLink.Attrs().Index);
	...
   // 创建input的XSK,需要指定网卡的队列
   inXsk, err := xdp.NewSocket(inLink.Attrs().Index, inLinkQueueID, nil)  
	...  
	// 将网卡队列的ID与XSK fd更新到map中,供xdp BPF程序转发
   if err := inProg.Register(inLinkQueueID, inXsk.FD());
	...
   // 创建output的XSK,这里可能有问题?因为根据vagrant目录下的测试场景,outXsk仍需要转发回包,而不仅仅是作为output,后面的poll逻辑也说明了是个双向的转发,因此还是需要有bpf pro
   // Note: The XDP socket used for transmitting data does not need an EBPF program.
   outXsk, err := xdp.NewSocket(outLink.Attrs().Index, outLinkQueueID, nil)  
	...
	// 使用poll监听发送和接收,也可以用xsk的Poll()方法,逻辑一样
	// func (xsk *Socket) Poll(timeout int) (numReceived int, numCompleted int, err error)
   var fds [2]unix.PollFd  
   fds[0].Fd = int32(inXsk.FD())  
   fds[1].Fd = int32(outXsk.FD())  
   for {  
	   // 先填充两个socket的fill ring。xsk的实现里面,fill ring与rx ring指向的缓存区域在umem的前半部分,complete ring与tx ring指向的缓存区域在umem的后半部分
      inXsk.Fill(inXsk.GetDescs(inXsk.NumFreeFillSlots(), true))  
      outXsk.Fill(outXsk.GetDescs(outXsk.NumFreeFillSlots(), true))  

		// 监听socket的读写。NumTransmitted()返回的是通过tx ring进行发送,但未调用Complete()对complete ring进行消费的
      fds[0].Events = unix.POLLIN  
      if inXsk.NumTransmitted() > 0 {  
         fds[0].Events |= unix.POLLOUT  
      }  
  
      fds[1].Events = unix.POLLIN  
      if outXsk.NumTransmitted() > 0 {  
         fds[1].Events |= unix.POLLOUT  
      }  
  
      fds[0].Revents = 0  
      fds[1].Revents = 0  
      _, err := unix.Poll(fds[:], -1)  
	    ...
		// inXsk收到POLLIN,将inXsk接收的数据发送到outXsk
      if (fds[0].Revents & unix.POLLIN) != 0 {  
         numBytes, numFrames := forwardFrames(inXsk, outXsk, inLinkDst)  
         ...
      }  
      // inXsk收到POLLOUT,消费complete ring,标记对应的descs为freeTXDescs,以供下次Transmit使用
      if (fds[0].Revents & unix.POLLOUT) != 0 {  
         inXsk.Complete(inXsk.NumCompleted())  
      }  
      // 同上,将outXsk接收的数据发送到inXsk
      if (fds[1].Revents & unix.POLLIN) != 0 {  
         numBytes, numFrames := forwardFrames(outXsk, inXsk, outLinkDst)  
         numBytesTotal += numBytes  
         numFramesTotal += numFrames  
      } 
      if (fds[1].Revents & unix.POLLOUT) != 0 {  
         outXsk.Complete(outXsk.NumCompleted())  
      }  
   }  
}

func forwardFrames(input *xdp.Socket, output *xdp.Socket, dstMac net.HardwareAddr) (numBytes uint64, numFrames uint64) {  
	// 接收
   inDescs := input.Receive(input.NumReceived())  
   // 替换数据包的mac地址,直接将dstMac拷贝到对应的umem chuck地址上
   replaceDstMac(input, inDescs, dstMac)  
	// 获取tx ring中空的descs,其实就是在Complete()中标记为freeTXDescs的descs
   outDescs := output.GetDescs(output.NumFreeTxSlots(), false)  
   // tx ring不足,进行rx的截断
   if len(inDescs) > len(outDescs) {  
      inDescs = inDescs[:len(outDescs)]  
   }  
   numFrames = uint64(len(inDescs))  

	// 将inFrame拷贝到outFrame,这里是数据包的拷贝,而非ring中元素的拷贝,因为input、output xsk有各自的umem。如果是一个xsk的接收与发送,则可以直接修改descs即可
   for i := 0; i < len(inDescs); i++ {  
      outFrame := output.GetFrame(outDescs[i])  
      inFrame := input.GetFrame(inDescs[i])  
      numBytes += uint64(len(inFrame))  
      outDescs[i].Len = uint32(copy(outFrame, inFrame))  
   }  
   outDescs = outDescs[:len(inDescs)]  
  
   output.Transmit(outDescs)  
  
   return  
}

AF_XDP的应用

参考

https://rexrock.github.io/post/af_xdp1/
https://www.kernel.org/doc/html/next/networking/af_xdp.html
https://archive.fosdem.org/2018/schedule/event/af_xdp/