Chinaunix首页 | 论坛 | 博客
  • 博客访问: 523680
  • 博文数量: 118
  • 博客积分: 10028
  • 博客等级: 上将
  • 技术积分: 1820
  • 用 户 组: 普通用户
  • 注册时间: 2007-11-07 18:46
文章分类

全部博文(118)

文章存档

2009年(12)

2008年(106)

我的朋友

分类: LINUX

2008-09-23 17:39:15

艰难的翻译完第二篇,为什么说艰难呢,因为翻译到中段时发现这篇所讲的内容并不是我希望得到的~ 不过还是坚持翻译完了,中间跳过了两段。第三篇就不翻译了,如果时间允许,我将翻译BPF相关资料,前提还是找不到中文资料的情况。最后,依然希望大家多多给于指正,先谢谢了!

深入Linux套接口过滤器


网络发烧友们也许还记得我的上一篇文章吧,“Linux Socket Filter: Sniffing Bytes over the Network”, 是2001年六月写的,发布在LJ上,讲了关于如何使用建立在Linux内核中的包过滤机制。在那篇文章中我只是简单介绍了下包过滤本身的功能等;现在,我将深入研究过滤器是如何在内核中工作的,并且跟大家分享一下我在Linux包处理方面的一些领悟。


上文回顾

在上一篇文章中,一些关于内核包处理的观点是不错的。所以有必要在这里简单回顾一下其中很重要的部分:

● 包的接收最先由网卡驱动层处理,更精确的说是在中断服务程序中。次中断服务程

       序查看接收到的帧头部的协议类型,并将起放入队列为后面处理做准备。

● 在包的接收和处理过程中,数据包可能会因为网络拥挤而被丢弃。而且,当数据包

   被传输到给用户时,数据包已经丢掉了网络层及以下的信息。

● 作为在用户接口之前的套接口层,内核将检测是否有打开的套接口来监听接收到的

   数据包,若没有,数据包将被丢弃。

● Linux内核执行一个叫PF_PACKET的协议,它使得你可以建立一个套接字来实现从

   网卡接收数据包。这样,所有其他的协议处理都被跳过,并且所有的数据包都能接

   收。

● 以太网卡通常只接收发送给自己的数据包,而丢弃所有其他的包。不过,通过设置

   网卡来实现使得所有经过网卡的包都被抓取是可以的,而且不依赖MAC地址(混杂

   模式)。

● 最后,你还可以关联一个过滤器到套接字,这样就只有匹配你的过滤规则的包才能

   被接收并传递给套接字。使其与PF_PACKET结合,这种机制使你可以有选择的且

   有效率的抓取局域网数据包。

尽管我们是用PF_PACKET建立的嗅探器,Linux包过滤器并没有限制。实际上,这个过滤器也可以用在TCP和UDP套接字上来过滤不需要的包---当然,这个用法很少见。

接下来,我会提到socket或sock结构体。就和这篇文章有关的来讲,它们都指的是同一个东西,并且和过去的内核内部表达一致。事实上,内核同时有socket 结构体和sock结构体,而它们之间的差异在这里是不重要的。

另一个数据结构将经常出现,叫sk_buff(套接字缓冲区的简化版),表示内核中的数据包。这个结构体被用来对数据包进行增加或去掉头部和跟踪信息,可以很简单的做到:并不需要拷贝任何数据因为只需要移动指针就可以完成这些操作了。

继续下文之前,有必要弄清楚可能存在的歧义问题。尽管有着类似的名字,但linux包过滤和网络过滤框架有着完全不同的用途,定义在早些的2.3版本内核中。尽管网络过滤器允许传递数据包到用户空间并提供给你的程序,但其主要用于网络地址转换(NAT),数据包变换,连接跟踪,基于安全目的的包过滤等等。如果你只是想嗅探数据包并使用一定的规则来过滤他们,最简单的工具就是LSF。

现在我们继续随着数据包的传送进入计算机并被传送到用户层的套接字。我们首先考虑一个简单的套接字(非PF_PACKET)。我们的链路层分析基于以太网,因为这是最广泛最具代表性的局域网技术。其他的链路层技术并不存在什么特别大的不同。


Ethernet Card and Lower-Kernel Reception

我们在前一篇文章中提到过,以太网卡有这特有的链路层地址(或MAC地址),并一直在其接口上监听数据包的到来。当它接收到一个MAC地址和自己的匹配或者是链路层广播地址(FF:FF:FF:FF:FF)时就读取数据包到存储区中。

当完成上述接收后,网卡产生一个中断请求。中断服务程序用网卡驱动来处理此中断请求,这个操作是不可中断的并且典型的操作如下:

● 分配一个sk_buff结构体,定义在/include/linux/skbuff.h中,用来表示内核级数据包。

● 从网卡缓冲区获取数据包到刚分配的sk_buff结构体中,可能会用到DMA。

● 调用netif_rx()函数,这是一般网络接收调用

● 当netif_rx()返回,重新开启中断并结束服务子程序。

然后Netif_rx()函数准备下一次接收;此函数将sk_buff放到输入数据包队列中给CPU处理并标记NET_RX softirq(softirq在后面解释)来执行_cpu_raise_softirq()调用。在这里有两点需要注意。第一,如果队列已经满了那么数据包讲被永远的丢弃。第二,我们为每个CPU都准备了一个队列;连同新的内核延迟处理模块(用softirqs替换bottom halves),允许在SMP机器同时进行数据包接收。

如果你想知道网卡驱动是如何工作的,你可以参考NE2000的PCI驱动,可以在/drivers/net.8390.c中看到;终端服务子程序依次轮流调用ei_interrupt()和ei_receive(),执行操作如下:

● 通过dev_alloc_skb()分配一个新的sk_buff结构体

● 从网卡的缓冲区读取数据包(通过ei_block_input()调用),并设置相应的skb->protocol

● 调用netif_rx()

● 重复执行以上步骤来连续处理最多10个包

稍微复杂一些的是3COM的驱动,在3c59x.c中,它使用DMA来从网卡转换数据包到sk_buff。


网络核心处理

下面让我们深入了解一下netif_rx()函数。前面已经提到,这个函数的任务是从网卡接收数据包并将其排列起来以被上层处理。它从不同的网卡驱动器接收数据包,并提供给上层协议处理。

因为这个功能运行在中断环境下(也就是说,执行流程是按终端服务流程来进行的),其他中断都被屏蔽,所以这个操作必须很快而且很简短。它不能执行较长的检测或者复杂的任务因为这可能在它运行时造成系统开始丢包。所以,它所要做的就是从softnet_date数组选出数据包队列,这个数组的索引基于最近的CPU执行。选出后检查队列的状态,从五个可能的拥塞等级中指定一个,分别为:NET_RX_SUCCESS(不拥挤)、NET_RX_CN_LOW、NET_RX_CN_MOD、NET_RX_CN_HIGH(分别是低、中等、严重阻塞)或者NET_RX_DROP(因为阻塞到大临界值而丢包)。

当严重阻塞达到临界值时,netif_rx()会采取一种策略允许数据包队列返回到无阻塞状态,以避免由于内核过载造成服务中断。这样做还有其他的好处,它可以防止可能的DoS攻击。

一般情况下,数据包最终被排进队列(_skb_queue_tail()),并调用_cpu_raise_softirq(cpuid,NET_IF_SOFTIRQ)。后者能使softirq执行。

Netif_rx()函数结束并返回给调用者拥塞级别。到这里,中断处理结束,数据包也已被准备好被上层协议处理。这个操作会延迟一点时间,在确保中断程序重新开启且执行时间没有越界。这种延迟执行机制已经从内核2.2(基于bottom havles)到2.4(基于softirqs)被彻底改变了。


软中断(softirqs)和下半部分(bottom havles)

解释bottom halves的细节已经超出了这篇文章的范围。但有些观点还是有必要简短的回顾一下。

首先,这样设计它是遵循一个原则,那就是内核应该在中断时应该进行尽量少的计算。这样,当一条长的操作请求中断时,相应的驱动会标志使用BH来执行,实际上并没做然和复杂操作。一段时间过后,内核会检查BH标志来决定是否让一些已标记为的BH在应用级任务之前执行。

BH工作的很好,但有一个严重的缺点:由于它们的结构,它们的执行是完全串行的。也就是说,同一个BH在同一时刻只能为一个CPU执行。这样明显的影响了内核在多处理器平台上的性能。Softirqs作为BH的2.4版的演进版,和tasklets一样,属于软中断,代码在被请求是由内核执行,不需要精确的响应时间保证。

与BH主要的不同在于,一个softirq可以同时运行在多个CPU上。。。。。


(下面两段省略。。。。。。。。。。)


IP包处理器

IP数据包接收函数叫ip_rcv()(定义在net/ipv4/ip_input.c中),它指向一个内核启动(ip_init(),在net/ipv4/ip_output.c 中)时创建的一个数据包类型结构体。当然,创建IP协议类型是ETH_P_IP。

于是,ip_rcv()在软中断操作中由net_rx_action()调用,任何时候协议类型为ETH_P_IP的包总是被抛出队列的。这个函数执行所有的IP包初始检测,主要确保它的完整性(IP校验和,头部域和包长度)。如果检测出来包是正确的那么调用ip_rcv_finish()。需要注意,调用此函数是经过网络过滤器预选路控制点,是被NF_HOOK宏执行的。

Ip_rcv_finish(),也在ip_input.c里,主要处理IP的路由功能。它决定IP数据包应该是应该被传递给另一台机器还是指向本机。前一种情况,路由被执行,数据包从相应的接口发送出去;否则,本地传输被执行。所有这些都由ip_route_input()函数来实现,在ip_rcv_finish()的最开始调用它,以此通过指定skb->dst->input中的函数指针来决定下一步操作。本地数据包的情况,这个指针是指向ip_local_deliver()函数的指针。Ip_rcv_finish()函数被skb->dst->input()调用终止。

此时,数据包肯定会被传递到更上层协议。由ip_local_deliver()来控制;这个函数只处理IP的分片重组(IP数据包有分片的情况下)然后执行ip_local_deliver_finish()函数。在调用它之前,另一个网络驱动器将被执行。

接下来就是IP层的最后一个操作;ip_local_deliver_finish()一直执行知道第三层的上面部分。这时IP头部已被裁剪掉所以数据包就可以被传递到第四层了。这里将检测这个包是否是原始IP包,如果是这种情况那么相应的句柄(raw_v4_input())。

原始IP允许应用程序伪造或直接接收IP数据包,不会有第四层的处理过程。主要用于一些需要发送特定数据包来完成任务的网络工具。总所周知的例子有ping和traceroute,他们都使用原始IP来建立自己的IP头部。另一种可能的应用是,在用户层实现custom协议(如RSVP,资源预留协议)。原始IP被认为是PF_PACKET协议族的标准使用,几乎是一个OSI标准级别的。

一般情况下,数据包会被继续传递到更进一步的内核协议处理。为了决定传递给谁,需要检查IP头部中的协议字段。在这里内核使用的方法和net_rx_action()函数采用的方法非常相似;定义一个名为inet_protos的散列表,用来存放所有已注册到内核的传输IP协议句柄。这个表的关键字当然从IP头部的协议字段获得。Inet_protos表在内核启动是使用inet_init()(定义在/net/ipv4/af_init.c中)初始化,它循环调用inet_add_protocol()函数来注册TCP、UDP、ICMP和IGMP句柄(后者仅在多播被启用时有效)。完整的协议表定义在/net/ipv4/protocol.c中。

对各个协议,句柄定义为:tcp_v4_rcv(),udp_rcv(),icmp()和igmp_rcv()依次对应上面提到的协议。调用其中一个函数来继续进行数据包的处理。这些函数的返回值被用来决定是否返回一个ICMP目标不可达消息给发送者。这种情况发生在上层协议不能识别此包时。如果你回顾上一篇文章,嗅探网络数据包的其中一个问题就是指定IP和端口对应来建立一个套接口来接收数据包。这(刚提到的那些*_rcv()函数)就是局限所在。


总结

到这里,数据包的旅程也已经过半了。因为我们深爱的杂志的篇幅限制,我们将让它呆在第三层里面知道下个月。我们将继续研究第四层协议(TCP和UDP)、PF_PACKET处理,当然还有套接口过滤器挂钩及其执行方式。敬请期待!


译)zuii  转载注明


英文原文:http://blog.chinaunix.net/u1/53217/showart_1211037.html



阅读(1046) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~