参看:
http://www.linuxjournal.com/article/6345
http://www.ibm.com/developerworks/cn/java/j-zerocopy/index.html
http://blog.chinaunix.net/u/5251/showart_411109.html
1.传统的报文捕获实现过程
1.1 协议栈运行机制分析
当操作系统协议栈正常工作时,应用程序只能接收到发往本机的数据包,其它地址的数据包将被丢弃。数据包通路分为数据链路层、网络层、传输层、套接字层和应用层几个层次。
数据链路层通过网卡驱动程序判断报文的目的物理地址是否为广播地址或者组播地址,若不是则再判断是否为本机 MAC 地址,若不是将直接丢弃该报文,不向上层提交,不然上交报文;报文被提交到网络 IP 层以后,IP 网络层判断报文(struct sk_buff) IP 头部所含目标 IP 地址是否为本机 IP 地址,如果不是,则不向上层提交,若是则将其提交到 TCP 或者 UDP 层;传输层获取传输层头部目标端口,判断该端口是否已被打开,如果没有打开,则不作处理,不向上层提交,若该端口已经被打开,则将该报文送到该连接(struct sock 和 struct socket 对应一个网络连接)的报文接收队列中,同时唤醒该连接上休眠的等待进程,进程从报文接收队列中取得报文然后返回。下面的例子是 Linux 下:
a. TCP/IP 协议栈框架结构
操作系统的内核协议栈基本可以分为数据链路层、IP 层、TCP/UDP 层、INET Socket 层、BSD Socket 层,以及应用层几个部分。
内核协议栈包括一组发送接收函数和相关的关键数据结构。用户层的网络连接信息通过内核数据结构 struct socket 和 struct sock 来维护。数据缓冲区是由两个数据结构 struct msghdr 和 struct sk_buff 来维护的。其中 struct msghdr 由 BSD socket 层和 INET socket 层来维护,struct msghdr 用于存放应用程序数据缓冲区的地址和大小,而在 TCP、IP 及以下各层使用 struct sk_buff 来管理报文数据缓冲区。内核在 tcp_sendmsg() 和 tcp_recvmsg() 函数中通过内存拷贝操作实现 2 种数据结构(struct msghdr 和 struct sk_buff)之间的信息传递。报文信息在传输层(TCP 层),网络层(IP 层)及以下各层时,不会进行拷贝操作,仅在结构 sk_buff 的不同协议头之间移动数据指针,这样可以避免不必要的拷贝开销。
b. TCP/IP 协议栈接收数据包的过程
本文以 Intel 1000M 网卡驱动 e1000 为例,对照图 2.3 中的内核协议栈框架,详细分析在 Linux 内核协议栈中数据报文通过的路径。
数据报文在 Linux 内核协议中流动分为 2 个过程,即自上而下和自下而上 2 个过程。其中,内核协议栈捕获网络报文是一个自下而上的过程,而上层应用获取网络数据的过程则是一个自上而下的过程。下面简要分析内核协议栈捕获数据报文的过程。
(1)网络报文到达后,网卡通过 DMA 方式将数据报文送到网卡驱动程序缓冲区并在传输结束时产生硬件接收中断。系统根据中断类型调用相应硬中断处理程序,由此便完成数据由物理层到链路层的传递。
(2)硬中断处理程序 do_IRQ() 根据寄存器状态会间接调用 e1000_intr() 处理接收到的报文,最后函数 e1000_clean_rx_irq() 会调用 netif_rx() 将驱动程序接收缓冲环(rx_ring) 报文添加到系统接收队列 backlog 中,操作系统在空闲时调用上层软中断处理函数 net_rx_action()。在这个函数中,根据已注册的数据报文类型(如 ETH_P_IP 或者 ETH_P_ALL)调用相应类型处理函数(通常情况下为 ip_rcv(),后面将以该函数为例对 IP 层的报文处理路径进行说明),这样网络数据由链路层进入到了 IP 层。
(3)在报文处理函数 ip_rcv() 中,当根据路由结果判断数据应发送到上层 TCP 协议处理时,系统协议栈会根据 tcp_v4_rcv(),随后调用 tcp_v4_do_rcv(),通过 tcp_v4_do_rcv() 将 sk->backlog 队列中的报文填充到 sk->receive_queue 队列中,同时唤醒在连接控制结构(struct sock)等待队列(即休眠队列 sk_sleep)上的所有进程,激活由上到下的接收过程。
1.2 传统的报文捕获机制分析
以太网采用了CSMA/CD 技术,通过广播机制实现数据传输。在系统正常工作时,应用程序只能接收到以本机为目标逐句的数据报文,以及广播报文和组播报文。因此要捕获不属于自己的报文,必须直接访问网络底层,首先将网卡置于混杂模式,使之可以接收其它类型的数据,然后系统直接访问数据链路层,捕获数据,最后提交给应用,这样就实现了捕获流经网卡的所有报文。
通过设置网卡工作模式,报文捕获系统便可以在网络上截获位于 OSI 协议模型中数据链路层上的报文。目前大多数操作系统都为应用提供了直接访问数据链路层的手段,它使应用可以监视数据链路层上的报文而不需使用特殊硬件设备。
目前 UNIX 操作系统中有 3 种常用的数据链路层访问机制,它们分别是:BSD 系统中采用的 BPF,SVR4 的数据链路提供者接口(DLPI)和 Linux 的 SOCKET_PACKET 接口,还有其它类似的链路层访问机制。
a. BSD 分组过滤器 BPF
BSD 系统及许多源于 Berkeley 的实现均使用 BPF 机制作为数据链路层访问手段。在支持 BPF 机制的系统上,每个数据链路层核心程序都在收到一个保温后直接调用 BPF 接口,并将该报文的拷贝传递给该接口。
BPF 接口不仅可以对数据链路层进行直接监听,而且还可以实现信息过滤。任何一个打开 BPF 接口的应用程序都可以安装自己的过滤器,然后由 BPF 接口对捕获到的每个报文分别进行处理。BPF 的过滤机制基于寄存器级别,并且可以对每个报文应用进程专有的过滤机制,这样,进程就可以实现只捕获自己感兴趣的报文,甚至还可以选择只捕获报文的特定部分。为了减小系统开销,BPF 接口采用了下面 3 项技术:
(1)BPF 过滤只在内核实现,减少了拷贝数据量。不然的话,由于从内核空间到用户空间的拷贝比较昂贵,而如果对于每个报文都进行拷贝,那么 BPF 接口将不能满足高速数据传输的需求。
(2)每个报文中只有部分敏感数据需要 BPF 接口捕获,该部分的长度和被称为报文捕获长度。大多数应用通常只需要报文头,而不需要报文中的数据内容。该方法同样减少了内核空间到用户空间的数据拷贝量。
(3)BPF 批量地向应用传递数据,而不是对于每个到达的报文都进行一次专门的数据拷贝操作。该缓冲区只有在满或者超时发生后才将数据拷贝到应用空间。缓冲区和超时值可由应用在初始化时灵活指定。设置缓冲区的目的在于减少拷贝以及系统调用次数。BPF 给每个应用进程维护 2 个缓冲区,当一个缓冲区向应用拷贝数据时,启用另一个缓冲区接收新的报文。
为了访问 BPF 接口,应用进程需要打开一个 BPF 设备。当该设备被打开后,便会被锁定,随后应用可以使用一系列操作来设置设备属性,包括:装在过滤规则、设置超时时间和缓冲区大小、设置网卡混杂工作模式等。这一切预备工作完成后,应用就可以开始报文捕获循环了。
b. SVR4 的数据链路提供者接口 DLPI
用得不多,省略。
c. Linux 的 SOCK_PACKET 接口
Linux 中访问数据链路层可以通过创建 SOCK_PACKET 套接字接口来实现。与原始套接字接口的实现相类似,也需要 root 权限,创建该类型的套接字时第 3 个参数指定捕获数据帧的类型。例如,若想获取所有网络数据帧,我们可以按下面的方式创建套接字:
fd = socket(AF_INET, SOCK_PACKET, htons(ETH_P_ALL))
而如果按照下列方式创建套接字后,上层应用程序将只会获得 IP 协议类型的数据帧。
fd = socket(AF_INET, SOCK_PACKET, htons(ETH_P_IP))
与 BPF 相比,Linux 下的 SOCK_PACKET 机制有以下几点不同:
(1)该套接字不提供类似于 BPF 接口中基于内核的缓冲和过滤机制。它只提供套接字接收缓冲区,但由于多个网络帧不能存储在一起,应用便不可能一次性读取多个报文,因此从内核向应用拷贝数据的开销必然会增加。
(2)Linux 不提供针对设备的过滤。应用必须自己丢弃来自不感兴趣设备的数据。存在的问题是这可能会给应用返回过多的数据,而这可能造成数据拥塞。
Libpcap 作为一个公开的报文捕获函数库,在其实现上完全支持上述几种接口,因此使用它的应用程序可以独立于操作系统所提供的实际链路层访问方式。大致流程如下:
(1)应用调用 pcap_lookupdev() 函数,这是一个与平台无关的接口,主要用来查找可以实现数据报文捕获的设备。
(2)根据(1)中所找到的设备,调用 pcap_open_live(),创建 struct pcap_t 类型的捕获句柄,准备开始捕获。
(3)如果用户设置了过滤规则,应用会编译和安装该过滤规则,具体是通过 pcap_compile() 和 pcap_setfilter() 这 2 个接口函数来实现。
(4)应用开始进入报文捕获循环,调用 pcap_loop() 捕获报文。在抓到报文后,应用会将数据流转换成数据帧,随后提起帧中信息,判断报文类型。如果为 IP 报文,则转入 IP 报文处理流程;如果为其它类型的报文,则会转入相应类型的报文处理类型之中。在处理完一个报文之后,执行流会转到报文捕获循环的开始处继续运行。
(5)最后,应用通过调用 pcap_close() 关闭捕获句柄,释放系统初始化时所申请的全部资源。
在自下而上的数据捕获流程中:
(1)网络数据包进入网卡缓存后设置网卡相关状态寄存器并触发网卡接收终端,即网卡向中断控制寄存器发出中断信号。中断控制寄存器接到中断信号后,向 CPU 发出中断处理请求信号。
(2)CPU 在执行完当前正在执行的指令后视状态寄存器的情况而决定是否响应中断。
(3)如允许响应中断,则向中断控制器发出响应电信号。
(4)接到 CPU 的响应信号后,中断控制器准备好当前中断的类型号放入相关数据寄存器。随后,CPU 从该寄存器内读取中断类型号,读取中断向量表,获得中断处理例程的入口地址,然后转入中断处理过程。
(5)中断处理例程根据中断类型号,调用当前发出中断的设备的中断处理函数。
(6)在该中断处理的函数调用中,通过读取网卡相关状态寄存器来判别该中断是属于发送中断还是接收终端,或是其他的由于错误导致的中断,然后根据该状态寄存器调用相应的处理函数。经过一些处理,如校验和验证,接收中断处理例程开始分配套接字缓存器 sk_buff。
(7)套接字缓冲区 sk_buff 分配成功以后,执行流将会拷贝网卡数据到缓冲区内(或者在 DMA 数据传输方式下,数据传输绕开 CPU 处理,此处分配的缓冲区可以直接用于下一次的报文接收工作)。
在网卡驱动的接收中断处理例程完成对报文的处理后,报文便被送入内核协议栈层。
Linux 系统其协议栈对于工作在数据链路层的 SOCK_PACKET 机制提供了很好的支持,同时它也完全支持 BPF机制,下面将基于 Linux 操作系统基础上,分析 libpcap 函数库对于上述两种机制的实现。
1. libpcap 函数库如利用工作在数据链路层的套接字 sock_packet 来完成网络数据报文的读取,具体实现流程大概是:
(1)当网络数据报文到达以后,网卡通过 DMA 方式将数据报文传输到网卡驱动程序接收缓冲环(rx_ring)中接收描述符所指示的位置上,并且在数据传输结束时触发硬件接收中断。系统根据寄存器中的中断类型号调用相应的中断处理程序,最后执行流会间接调用到网卡中断处理程序中的报文接收处理子程序。
(2)在报文接收处理例程中,内核处理数据报文并通过函数 netif_rx() 将驱动程序接收缓冲环(rx_ring)中的数据添加到系统接收队列 backlog 中。操作系统在空闲时调用中断处理程序下半部分即软中断处理函数 net_rx_action()。此函数的主要功能是,检查系统中已经注册的数据包类型并调用相应的处理函数来处理数据报文。libpcap 函数库注册的报文接收类型为 ETH_P_ALL,即接收所有的网络数据帧,其处理函数为 packet_rcv()。该函数工作在数据链路层。
(3)packet_rcv() 函数将直接调用 skb_queue_tail() 将数据报文存放在代表相应网络连接控制结构(struct sock)的接收队列 receive_queue 中。这样数据报文在接收过程中就绕过了 TCP 层和 IP 层繁琐的协议处理过程。
(4)最后,睡眠在 sk 等待队列上的函数 packet_recvmsg() 会接收链路层数据帧并将该数据帧直接拷贝到应用程序缓冲区中。
2. libpcap 如利用工作在数据链路层的 BSD 分组过滤器来完成网络数据报文的读取,大致过程是:
(1)当网络数据报文到达以后,网卡通过 DMA 方式将数据报文传输到网卡驱动程序接收缓冲环(rx_ring)中接收描述符所指示的位置上,并且在数据传输结束时触发硬件接收中断。随后,网卡驱动程序会将报文提交到系统协议栈。
(2)如果 BPF 正在进行侦听,则网卡驱动程序会首先调用 BPF 模块,该模块会将数据报文发送给过滤器模块,过滤器模块随后按照预先配置的规则对数据报文进行处理,随后经过过滤的数据报文被提交给上层应用程序。
(3)网卡驱动重新获取控制权,数据报文被提交给上层的系统协议栈,进行正常的协议处理工作。
BPF 提供了直接对数据链路层进行访问的接口,并且可以在该层实现对于 IP 地址,数据报文类型以及端口的选择性过滤,最终只向上提交用户关心的数据部分,减少了内存操作,大大提高了报文捕获系统的工作效率。
libpcap 函数库绕过 TCP 层(UDP 层)和 IP 层繁杂的协议处理过程,直接将数据帧从数据链路层拷贝到应用程序缓冲区中。虽然这样的实现机制能够大量节省数据报文在传输过程中所消耗的 CPU 时间,但是在 libpcap 的整个报文捕获过程中,系统调用、数据拷贝和网卡接收中断处理仍然是系统主要的性能影响因素。