本文的copyleft归gfree.wind@gmail.com所有,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接,严禁用于任何商业用途。
作者:gfree.wind@gmail.com
博客:linuxfocus.blog.chinaunix.net
今天的学习目标是TCP/IP数据包的完整接收流程!
入口自然是从driver开始,以Intel(R) PRO/1000 Network Driver对应的e1000_main.c为例。
事先声明,因为本人不是内核工程师,所以难免在kernel的代码理解上献丑,希望大家指正。
这里,我们并不需要关心如何去写driver,如何处理中断,只需要关注网卡如何接收的数据包,并传给上层协议。
函数e1000_clean_rx_irq为网卡接收数据包的处理函数。
- static bool e1000_clean_rx_irq(struct e1000_adapter *adapter,
- struct e1000_rx_ring *rx_ring,
- int *work_done, int work_to_do)
- {
- ...... ......
-
- /* 得到了接收缓存buffer */
- i = rx_ring->next_to_clean;
- rx_desc = E1000_RX_DESC(*rx_ring, i);
- buffer_info = &rx_ring->buffer_info[i];
-
- while (rx_desc->status & E1000_RXD_STAT_DD) {
- ...... ......
-
- /* 取得接收缓存buffer的数据包buffer,即skb */
- skb = buffer_info->skb;
- buffer_info->skb = NULL;
...... ......
/* 确定skb的L2数据链路层协议,以及包的类型:broadcast, multicast, otherhost or host */
skb->protocol = eth_type_trans(skb, netdev);
/* 将数据包传递给L3 IP层*/
e1000_receive_skb(adapter, status, rx_desc->special, skb);
- ...... ......
- }
-
- ...... ......
- }
从驱动中的这个函数,可以看出,驱动层即为L2 数据链路层,所以只负责对应L2层的工作,如设置L2层协议,设置包的类型等。当将L2层的工作处理完毕后,即将数据包传递给上层协议。这个动作由e1000_receive_skb->netif_receive_skb->__netif_receive_skb。其中netif_receive_skb和__netif_receive_skb的主要用途主要是执行一些与硬件本身非紧密相关的一些L2层的操作,如设置skb中的skb_iif(接收包的网卡索引),vlan的处理等等。
L2数据链路层如何将数据传递给L3 IP层的呢?
请看我前面的文章《TCP/IP学习(27)——协议初始化与简要的发送/接收流程》,在inet_init函数中,这一语句dev_add_pack(&ip_packet_type);将IP协议添加到了L2层的协议类型中。
- void dev_add_pack(struct packet_type *pt)
- {
- int hash;
-
- spin_lock_bh(&ptype_lock);
- if (pt->type == htons(ETH_P_ALL))
- list_add_rcu(&pt->list, &ptype_all);
- else {
- hash = ntohs(pt->type) & PTYPE_HASH_MASK;
- list_add_rcu(&pt->list, &ptype_base[hash]);
- }
- spin_unlock_bh(&ptype_lock);
- }
inet_init将ip_packet_type挂载了&ptype_base[hash]链表上。
在__netif_receive_skb中,下面的这一段代码遍历了ptype_all链表,找到相符的type,调用其func回调函数。
-
- type = skb->protocol;
- list_for_each_entry_rcu(ptype,
- &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
- if (ptype->type == type && (ptype->dev == null_or_orig ||
- ptype->dev == skb->dev || ptype->dev == orig_dev ||
- ptype->dev == orig_or_bond)) {
- if (pt_prev)
- ret = deliver_skb(skb, pt_prev, orig_dev);
- pt_prev = ptype;
- }
- }
static inline int deliver_skb(struct sk_buff *skb,
struct packet_type *pt_prev,
struct net_device *orig_dev)
{
atomic_inc(&skb->users);
return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
}
而IP对应的数据结构为
- static struct packet_type ip_packet_type __read_mostly = {
- .type = cpu_to_be16(ETH_P_IP),
- .func = ip_rcv,
- .gso_send_check = inet_gso_send_check,
- .gso_segment = inet_gso_segment,
- .gro_receive = inet_gro_receive,
- .gro_complete = inet_gro_complete,
- };
现在,答案已经很明显了。L2层通过注册的数据包的类型,找到了ip_packet_type。然后调用ip_rcv。在TCP/IP的源码中,大多数的上层协议都是通过这种,向底层协议注册相应的数据包类型,使底层协议可以根据包的类型,调用相应的上层协议的处理函数。这基本是一种通用的方式。
好了,现在我们进入了L3 IP层。
首先依然是IP层的通用处理,sanity check等。最后ip_rcv调用netfilt,并把ip_rcv_finish作为回调函数传递给netfilter。
- return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
- ip_rcv_finish);
关于netfilter的处理和为什么要用netfilter,不是本文的重点——其实熟悉iptable的朋友,基本上就明白原因了,请回忆iptable的处理链或者说检测点。在netfilter的PREROUTING链对数据包处理完毕后,则调用ip_rcv_finish。这才是IP层真正的处理函数。(在TCP/IP源码中,有很多类似的函数,一个名字叫做xxx,另外一个叫做xxx_finish。其中xxx主要做sanity check,而xxx_finish才是真正的工作函数)。
到了L3层,这里就有一个问题了。在L2层,正常的情况下,接收到的L2层的数据包一定是发给本机的,不然我们是收不到这个L2数据包的——这里不考虑网卡的混杂模式或者作为bridge等情况。但是到L3层,也就是IP层,如果linux是作为一个switch,那么这个IP包的目的地址就很可能不是发给linux本机的。那么linux是如何处理这个的呢?
Linux在内核维护了两个路由表,一个是本机地址的路由表,另外一个是转发地址的路由表。在L3层,会查询这两个路由表。首先是查本机地址的路由表,如果成功,表明这个IP报文是发给本机的。如果是失败,则继续搜索转发地址的路由表。如果依然失败,那么就表示该包无法转发,直接drop掉。至于是否回复icmp,则根据配置而定。对于接受数据包来说,这两个路由表的查询动作,是封装在一个函数ip_route_input_slow里的(不考虑route cache的情况)。当查找成功时,返回的是一个与协议无关的struct dst_entry结构。对于接收到的IP数据包,则调用dst->input,如是发送的IP数据包,则调用dst->output。这基本上是Linux对于IP数据处理的机制。这里只是简要的描述了一下。以后我会针对这个问题,专门讲解一下的。
在此,我们假设该数据包是发送给linux本机的。ip_rcv_finish在做了IP层的处理,如解析IPoption等,
肯定可以在本机地址的路由表查找成功。然后调用dst->input,也就是ip_local_deliver。
- int ip_local_deliver(struct sk_buff *skb)
- {
- /*
- * Reassemble IP fragments.
- */
-
- if (ip_hdr(skb)->frag_off & htons(IP_MF | IP_OFFSET)) {
- if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
- return 0;
- }
-
- return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
- ip_local_deliver_finish);
- }
与ip_rcv类似,ip_local_deliver同样是做些预处理的操作,如处理IP分片。然后将数据包交给netfilter。最后数据包将传至ip_local_deliver_finish。
- static int ip_local_deliver_finish(struct sk_buff *skb)
- {
- ...... ......
- /* 将数据包传递给匹配的raw socket */
- raw = raw_local_deliver(skb, protocol);
- hash = protocol & (MAX_INET_PROTOS - 1);
- /* 查找对应的proto */
- ipprot = rcu_dereference(inet_protos[hash]);
- if (ipprot) {
- ...... ......
- /* 调用proto对应的处理函数 */
- ret = ipprot->handler(skb);
- ...... ......
- }
- }
在ip_local_deliver_finish中,首先将数据包clone传递给匹配的raw socket。注意,这里是将skb clone了一份,然后将clone传给raw socket。这里说明raw socket在不影响其它socket的情况下,可以收到所有匹配其条件的数据包的clone——具体说明,参加我前面的博文《大师的错误?》。在传递给raw socket后,根据IP头部的protocol字段,从全局的数组inet_protos中选择出正确的L4层协议。这里,最好可以参照前文《协议的初始化》,在inet_init中,使用inet_add_protocol将L4层协议添加到inet_protos中的,如inet_add_protocol(&udp_protocol, IPPROTO_UDP) 。
假设数据包为UDP数据包,udp_protocol的声明如下:
- static const struct net_protocol udp_protocol = {
- .handler = udp_rcv,
- .err_handler = udp_err,
- .gso_send_check = udp4_ufo_send_check,
- .gso_segment = udp4_ufo_fragment,
- .no_policy = 1,
- .netns_ok = 1,
- };
那么现在数据包就传递到了L4层,udp_rcv中。udp_rcv直接调用__udp4_lib_rcv。大家都知道Linux的TCP/IP与OSI的7层协议模型不同,只有5层。到了L4 transport层后,在往上层传递就到了应用层,也就是kernel需要把数据包传递给正确的socket。就完成了整个儿的数据包接收过程。对于UDP数据包,__udp4_lib_rcv调用__udp4_lib_lookup_skb找到最佳匹配的socket,然后调用udp_queue_rcv_skb将这个数据包添加到该socket的接收队列中。
好了,这就是一个发往linux本地的数据包包的完整接收流程。在这个过程中,还是省略了不少细节。但是大体的处理流程基本上就是这样。在处理过程中,虽然是C语言,但是仍然体现了面向对象的思想。每种协议为一个structure,也就是一个object。通过上层协议向下层协议注册,然后调用其回调函数的方式。实现了上层对下层的透明,下层协议无须关系上层的处理。另外,linux对于IP包的处理,通过查找路由的方式,实现了处理的统一接口。这个需要再进行专门的研究和分析。