网络代码分析第二部分
――网络子系统在IP层的收发过程剖析
R.wen ()
一、IP层数据包处理全景
q版恐龙
图1.1
1、接收的全过程
在上一节可以看到,链路层将数据包上传到IP层时,由IP层相关协议的处理例程处理。对于IP协议,这个注册的处理例程是ip_rcv(),它处理完成后交给NETFILTE(PRE-ROUTING)R 过滤,再上递给ip_rcv_finish(), 这个函数根据skb包中的路由信息,决定这个数据包是转发还是上交给本机,由此产生两条路径,一为ip_local_deliver(),它首先检查这个包是否是一个
分片包,如果是,它要调动ip_defrag()将分片重装,然后再次将包将给NETFILTER (LOCAL_IN)过滤后,再由ip_local_deliver_finish()将数据上传到L4层,这样就完成了IP 层的处理;它负责将数据上传,另一路径为ip_forward(),它负责将数据转发,经由NETFILTER (FORWARD)过滤后将给ip_forward_finish(),然后调用dst_output()将数据包发送出去。
2、发送全过程
由上图可以看到,当L4层有数据包待发送时,它将调用
ip_append_data/ip_push_pending_frams(udp,icmp, Raw IP), 或ip_append_page(UDP),
ip_queue_xmit(TCP,SCTP), 或者raw_nd_hdrinc(Raw IP, IGMP),它们将这些包交由NETFILTER(LOLACL_OUT)处理后,然后交给dst_output,这会根据是多播或单播选择合适的发送函数。如果是单播,它会调用ip_output(),然后是ip_finish_output(),这个函数主要是检查待发送的数据包大小是否超过MTU,如果是,则要首先调用ip_fragment()将其分片,然后再传给ip_finish_output2(),由它交给链路层处理了。
二、接收的详细过程
1、我们已经知道,链路层首先将数据包上传给IP层的ip_rcv()函数,这个函数主要做一些检查工作:
首先,这个函数不会接收不是发给这个主机的数据包,如果主机是工作在混杂模式,这个数据包已经由netif_receive_skb()去完成处理了。注意,这里所说的“不属于”这个主机,是指在这个包目标主机的MAC地址不是本机,而不是L3层的ip地址。所以,它不包括路由的包。
if (skb->pkt_type == PACKET_OTHERHOST)
drop;
goto
接下来是一个共享的检查,如果是共享的数据包,因为它可能需要修改skb中的信息,所以要先复制一个副本,再作进一步的处理。
if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL) {
IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
goto
out;
}
再下来就是检查首部的长度是否够长,校检和等等:
(!pskb_may_pull(skb, sizeof(struct iphdr)))
if
inhdr_error;
goto
…
…
去掉padded部分的长度:
len = ntohs(iph->tot_len);
/* Our transport medium may have padded the buffer out. Now we know it
* is IP we can trim to the true length of the frame.
* Note this now means skb->len holds ntohs(iph->tot_len).
*/
if (pskb_trim_rcsum(skb, len)) {
IP_INC_STATS_BH(IPSTATS_MIB_INDISCARDS);
drop;
goto
}
return NF_HOOK(PF_INET, NF_IP_PRE_ROUTING, skb, dev, NULL,
ip_rcv_finish);
2、ip_rcv_finish()
ip_rcv将数据传给ip_rcv_finish()继续处理,这个函数工作也比较简单:
它首先查找路由信息,在这里先忽略这部分:
if (skb->dst == NULL) {
int err = ip_route_input(skb, iph->daddr, iph->saddr, iph->tos,
skb->dev);
…
}
然后就是对IP头选项部分的处理了:
if (iph->ihl > 5 && ip_rcv_options(skb))
drop;
goto
最后就是dst_input了:
return dst_input(skb);
dst_input的工作更为简单,它只是根据skb的路由信息调用相应的input函数了:skb->dst->input(skb);
由全景图可以看到,这个input有可能是ip_local_deliver()或ip_forward()。
3、ip_forward()
检查skb是否共享,或是否头部预留的空间是否足够存放L2的头部,因为在转发这个数据包的时候要将L2的头部拷贝进去。
/* We are about to mangle packet. Copy it! */
if (skb_cow(skb, LL_RESERVED_SPACE(rt->u.dst.dev)+rt->u.dst.header_len))
drop;
goto
接着就是ip_forward_finish()了。
return NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->dev, rt->u.dst.dev,
ip_forward_finish);
ip_forward_finish:
简铂金首先处理未处理的头部
(unlikely(opt->optlen))
if
ip_forward_options(skb);
然后就是dst_output:
dst_output(skb);
return
这个已经是发送部分所做的工作了,我们将在下一部分讨论它。
4、ip_local_deliver()
首先,确定接收到的包是不是分片,如果是,则要将分片重装成一个完整的IP包再上传给L4层。
(skb->nh.iph->frag_off & htons(IP_MF|IP_OFFSET))
if
skb = ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER);
然后就是ip_local_deliver_finish:
return NF_HOOK(PF_INET, NF_IP_LOCAL_IN, skb, skb->dev, NULL,
ip_local_deliver_finish);
5、ip_local_deliver_finish
至此,已经确定将这个数据包传送给L4层,而L3层的头已没有作用,所以,先去掉L3的头部:
int ihl = skb->nh.iph->ihl*4;
ihl);//将skb->data指向L4的头部
__skb_pull(skb,
/* Point into the IP datagram, just past the header. */
skb->h.raw = skb->data;//重新设备skb->h.raw值,
//它在receive_skb()中被置为L3层头部的位置。
接下来就要处理与L4层相关的协议了,首先处理的是Raw IP,它先查看raw_v4_htable有没有注册到这个L4协议的Raw IP:
hash = protocol & (MAX_INET_PROTOS - 1);
raw_sk = sk_head(&raw_v4_htable[hash]);
如果有,则要执行raw_v4_input(),为其提交一份副本:
if (raw_sk && !raw_v4_input(skb, skb->nh.iph, hash))
raw_sk = NULL;
//如何处理接收所有协议的Raw IP?
当socket(AF_INET, SOCK_RAW, IPPROTO_RAW)时,它会接收所有协议的数据包,并且
IP_HDRINCL是默认打开的,即是说应用层要提供L3和L4层的头。再如,如果是IPPROTO_TCP时,它只接收到TCP包。而IP_HDRINCL是默认不打开的,即系统会处理L3的头部。
接着就是对特定L4协议的处理:
if ((ipprot = rcu_dereference(inet_protos[hash])) != NULL) {
ret;
int
诗两首ipprot->handler(skb);
=
ret
}
它首先查找inet_protos数组,看有没有相关的注册的协议,如果有,则执行它的处理例程。6、协议的注册
在inet_init()的时候,系统会注册几个常用的L4层协议:
if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
经典春联大全printk(KERN_CRIT "inet_init: Cannot add ICMP protocol\n");
if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n");
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0);
printk(KERN_CRIT "inet_init: Cannot add TCP protocol\n");
其中,协议的结构如下:
/* This is ud to register protocols. */
struct net_protocol {
业务工作计划int (*handler)(struct sk_buff *skb);
怎样煮面条void (*err_handler)(struct sk_buff *skb, u32 info);
int (*gso_nd_check)(struct sk_buff *skb);
struct sk_buff *(*gso_gment)(struct sk_buff *skb, int features);
int
no_policy;
};
关键之处在于handler域,它用于将数据包上传给L4层处理,如UDP协议初始化如下:
再来看看inet_add_protocol函数:
int inet_add_protocol(struct net_protocol *prot, unsigned char protocol)
{
int hash, ret;
hash = protocol & (MAX_INET_PROTOS - 1);
spin_lock_bh(&inet_proto_lock);
if (inet_protos[hash]) {
ret = -1;
} el {
inet_protos[hash] = prot;
ret = 0;
自告}
spin_unlock_bh(&inet_proto_lock);
return ret;
}
过年禁忌
可以看到,这个函数只是将待注册的协议填入全局数组,要注意的是,每个协议只能注册一个相关的实例。但是在用户态是,一个协议可以注册多个实例。
二、发送详细过程
由全景图可以看到,L4层在发送数据时会根据协议的不同调用上面提到的几个辅助函数之一,它们的主要作用是将数据分成合适大小的块然后再传递给L3层,理论上L4层可以不做这些工作,因为分片对L4层来说是透明的,这样做的目的是为了实现快速分片。
1.1、ip_queue_xmit()
这个函数是由TCP协议使用的,由于L4层处理了部分分片工作,这个函数的工作主要有以下几个:
a.查找路由信息:
rt = (struct rtable *) skb->dst;
if (rt != NULL)
goto packet_routed;
… …
如果还没有,它则要在路由表中查找相关的rt,但我们这里不关心路由部分。
b. 填充IP头信息,这里用skb_push为IP头留出空间
b.创建选项部分和计算校检: