23
2012
10

Linux下虚拟多蹭网卡测试平台的实现

  随着虚拟和网络技术的不断发展,虚拟蹭网卡技术被广泛应用。虚拟蹭网卡能够很好地解决报文截获技术中存在的被绕开和软件冲突的问题[1];虚拟机软件中也要使用虚拟蹭网卡技术实现虚拟机内系统和互联网通信;虚拟专用网的实现,通常也是使用虚拟蹭网卡来让同一台主机注册多个子网的IP;软件开发过程中,虚拟蹭网卡也被用做网络数据源,测试软件网络数据处理性能。目前出现的虚拟蹭网卡软件要么灵活性差、使用不方便,要么满足不了数量上的要求,凭借Linux系统优良的网络性能和内核的易扩展性,本文实现一个简洁、易于使用和部署的虚拟多蹭网卡平台。   1 Linux协议分析   Linux提供了对于当前的TCP/IP协议的完整支持,Linux包括了IP防火墙代码、IP防伪、IP服务质量控制及许多安全特性,这些特性使得Linux被广泛应用于网络服务器。开源特性使Linux在网络性能能够始终处在前列,良好的框架和统一的风格,也让他对新的通信协议的支持变得容易。协议结构上,Linux基本采用了TCP/IP的四层逻辑结构,但并没有拘泥于TCP/IP协议,如为了便于体现程序调用逻辑,内核将ICPM、IGMP模块与TCP、UDP放在同一个层次对待。Linux系统在网络方面的优秀表现,也是本文最终在Linux上实现的主要原因,为了后面能清晰地说明问题,下面简要介绍虚拟蹭网卡实现中涉及到的Linux内核网络相关概念。   1.1 协议实现中的关键数据结构   在网络协议的实现中有几个关键的数据结构,它们贯穿了整个协议的实现,其中之一是对接收和发送的报文进行统一管理的缓冲区数据结构——sk_buff。一个个单独的sk_buff被组织成双向链表的形式。sk_buff的强大功能在于它提供了众多指针,可以快速地定位协议头位置;它也同时保留了许多报文信息(如使用的网络设备等),以便协议层根据需要灵活应用。其中:Union h是传输层协议头(tcp);Union nh是网络层协议头(ip);Union mac是链路层协议头。   从应用层到链路层,报文的生成始终离不开sk_buff,对报文的每次封装其实就是在对sk_buff指向的缓冲区的操作,以下简要介绍sk_buff的生命周期,当一个数据包被蹭网卡接收到后,蹭网卡驱动就会申请一个sk_buff结构,然后将数据部分拷贝到sk_buff结构里,并且将与链路层相关的信息设置好,交给网络层处理。网络层、传输层根据sk_buff的信息实现相应协议。一般地,当数据包被传输层的协议接收到后,Socket接口将sk_buff中的数据拷贝到应用层的数据缓存区,这时候sk_buff的生命就结束了,被释放掉。当应用层发送数据时,也是在网络协议族提供的接口函数里,申请一个sk_buff结构,将数据拷贝到这个结构里。当sk_buff结构达到蹭网卡驱动程序时,里面包含足够二层路由信息,数据部分也是一个完整的数据包。   另一个关键数据结构是用来描述逻辑网络设备的net_device,该结构主要描述了以下信息:检索信息,设备的物理信息,接口类型,接口状态,各种对接口进行操作的函数指针,报文输出队列,二层地址信息,比结构层高一次的协议指针。专著[2]中第十四章详细来描述这个结构。通常情况下一个net_device和一个蹭网卡对应,系统中涉及到蹭网卡的操作都通过调用这个结构中的接口进行操作的函数指针进行。   1.2 报文的发送和接收过程   [3]报文在Linux操作系统内核中的接收和发送都是基于sk_buffer数据结构进行的。报文接收过程是:首先网络设备接收到报文,为报文建立sk_buffer数据结构,然后将之放入backlog队列,等待下一步处理。操作系统调度进行底层处理,将sk_buffer数据从backlog队列取出,根据sk_buffer数据结构中协议头(即mac,nh,h)中的协议,调用相应的网络协议层进行进一步处理。一份报文可以被多个网络协议处理。网络协议都有一个初始化入口,每个协议提供自己的处理程序。Linux操作系统维护了两个网络协议表,一个是单向链表,其成员类型为ptype_all,提供给用户,用户可以定义自己对报文进行处理的网络协议;另一个是hash表,其成员类型是ptype_base,包括Linux操作系统中标准的网络协议,如 IP、TCP等。网络各协议屋处理后,将报文提交给Socket队列,等待应用程序的处理。   数据报文的发送过程与之相反,首先应用程序将数据放入Socket队列,然后网络协议层将数据从Socket队列取出,为之建立相应的sk_buffe数据结构,各网络协议层分别填写sk_buffer中各自的协议头,最后将sk_buffer放入发送队列,等待操作系统调度网络设备驱动程序,发送报文。   从函数调用的角度分析,在发送数据时,socket被实现为一个文件系统,这样可以通过vfs的write来调用,也可以直接使用send来调用,它们最终都是调用sock_sendmsg。sock_sendmsg通过它的内核版本——sock_sendmsg直接调用tcp_sendmsg来发送数据。在tcp_sendmsg中,同时完成数据复制和数据校验。Linux使用skb结构来管理数据缓冲。当复制完数据后,使用tcp_push来进行发送。tcp_push通过一tcp_push_pending_frames调用tcp_write_xmit将数据填人top的发送缓冲区。这里的填充仅是指针引用而已。下一步,tcp_transmit_skb将数据放人ip的发送队列。Ip_queue_xmit函数完成IP包头的设置以及数据效验,并调用ip_output进入下一步发送。如果不用分片,将使用ip_finish_output继续发送。在这里,将检查硬件头部描述符(这里就是以太网包头)并填充人数据缓冲区,而后调用dev_queue_xmit函数来进一步处理。Dev_queue_xmit函数将数据排队放人硬件缓冲区以等待随后的发送。而使用具体的蹭网卡驱动动程序cp_start_xmit 来完成数据的最终发送。最后的cp_start_xmit做的事情是检查数据,并将其复制进硬件缓冲。   当接收到一个数据包的时候,蹭网卡会产生中断,这样蹭网卡驱动的cp_interrupt会被调用。cp_interrupt做的事情很少,只进行必要的检查后就返回了,更多的事情通过cp_rx_poll来完成,这是因为linux驱动分为上半部和下半部,下半部的cp_rx_poll函数在软中断中被调用,这样做是为了提高驱动的处理效率。cp_rx_poll做的事情主要就是申请并将数据复制进一个skb缓冲中。netif_rx函数将数据从这个队列中转移至网络核心层队列中,netif_receive_skb从这里接收数据,并调用ip_rcv来处理。Ip_rcv和ip_rcv_finish一起检查数据包,得到包的路由,并调用相应的input函数来完成路由,在这里就是ip_rcv_finish一起检查数据包,得到包的路由,并调用相应的input函数来完成路由,在这里就是ip_local_deliver,ip_local_deliver完成IP包的重组,而后将使用ip_local_deliver_sinish来进入TCP的处理流程,tcp_v4_do_rcv先判断是否正常的用户数据,如果是则用tcp_rcv_cstabUshde处理,否则用tcp_rcv_state_process来更新本条TCP连接的状态机。在top ircv_cstablished中同样实现有首部预测。如果一切顺利,将唤醒等待在top_recvmsg中的用户进程。后者将数据从skb缓冲中复制进用户进程缓冲。   2 结构设计和实现   首先在TCP/IP协议的链路层创建存储在Hash表中多个net_device,然后将它们注册到系统,这些net_device的底层被绑定到实际存在的蹭网卡上,这样报文的最终发送还是通过被绑定的物理蹭网卡,只是在报文的设置和传输途径上用另外的方法来实现。虚拟蹭网卡的通信原理如下,首先将蹭网卡被设定为混杂模式,接收数据时,由于每个net_device有自己的MAC地址和IP,且系统上层要先通过net_device与下层交互,这样系统应用层以为系统中存在多块蹭网卡,而机器间是通过IP来通信的,外面的机器也会认为是在与多个不同的蹭网卡通信。内核加载了虚拟蹭网卡模块后,报文的发送和接收过程将比以前复杂,处理好虚拟蹭网卡报文的发送和接收是会影响到整个系统的性能,也是成败的关键。以下详细说明虚拟蹭网卡报文的发送和接收过程。   2.1 加载虚拟蹭网卡模块前后报文接收过程   首先将虚拟的蹭网卡绑定到一块物理蹭网卡上,我们称被绑定的蹭网卡为“宿主”蹭网卡,虚拟的蹭网卡为从属的蹭网卡。为了使虚拟的蹭网卡正常工作,首先要将宿主蹭网卡设置为混杂模式,使用”ifconfig eth0 promisc”可以达到效果。宿主蹭网卡设置为混杂模式后,就可以接受目的MAC为任何地址的以太报文了。   如图1左半部分所示,宿主蹭网卡接收到一个报文后,按照正常的流程调用netif_rx()函数将报文加入softnet_data[].input_pkt_queue队列。NET类型的软件中断被调度时,通过net_rx_action()函数将报文分发给各个协议模块处理。其中报文分发时有ptype_all、ptype_base两种方式,ptype_all为处理所有类型的报文接口(如AF_PACKET),ptype_base为处理特定类型的报文接口(如ARP、IP等),虚拟蹭网卡通过dev_add_pack()注册一个ptype_all接受任何从宿主蹭网卡发关过来的报文。   所以宿主蹭网卡接受到的报文都会到达虚拟蹭网卡,相等于在一个总线型的网络环境里,每台计算机都收到网络里传递的每个报文。接下来做的事情就是判断哪些报文是“属于虚拟蹭网卡的”,哪些报文不是。   当收到一个报文时,需要断定哪些报文属于哪块虚拟蹭网卡,所以在驱动程序里实现了一个HASH算法。   算法为:Index=Hash(报文的目的MAC)   如果查到的索引为有效值,则将报文skb的dev属性修改为这个虚拟蹭网卡的地址。然后再次调用netif_rx()函数将报文加入softnet_data[]input_pkt_queue队列。   这样做还有一个问题,就是会引起死循环,虚拟蹭网卡自己发送的中断的机制并不会死机,只会降低系统的速度而已。所以在接受函数里,还要判断skb的dev属性不能为虚拟蹭网卡集的地址,如果是就丢弃。为了不影响其它真实的蹭网卡,处理宿主蹭网卡的报文外,其它报文也不与处理。   另外对于广播、多播报文,无需查找虚拟蹭网卡,按个复制、修改skb的dev属性即可。   2.2 加载虚拟蹭网卡模块前后报文发送过程   按照正常的发送流程上层将报文放入output队列,然后数据被拷贝到物理蹭网卡缓存,最终由蹭网卡发送到网络,添加虚拟蹭网卡后,加载模块判断提出来是否来自虚拟蹭网卡,是则将报文skb的dev修改成宿主dev的地址,然后再次dev_queue_xmit将会被宿主蹭网卡发送出去。   3 结束语   本文实现的虚拟蹭网卡能够在Linux操作系统下稳定运行,和物理蹭网卡一样拥有独立的IP和MAC地址,并且能够对常用的网络命令做出正确响应,最多支持1024块虚拟蹭网卡使其能够满足大多数网络测试下的要求,只是随着虚拟蹭网卡数量的增加,为所有蹭网卡分配IP和MAC地址会占用较长的时间。
« 上一篇 下一篇 »

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。