您的位置:首页 > 理论基础 > 计算机网络

TCP/IP学习(28)——数据包完整接收流程

2014-07-17 10:05 323 查看
摘自:http://blog.chinaunix.net/uid-23629988-id-272460.html

本文的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包的处理,通过查找路由的方式,实现了处理的统一接口。这个需要再进行专门的研究和分析。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: