前面有讲到过在函数ip_append_data中实现了对IP数据报的分片,这个讲法是错误的,需要纠正一下,ip_append_data的主要任务只是创建发送网络数据的套接字缓冲区(skb),它根据输出路由查询得到的输出网络设备接口的MTU,把超过MTU长度的应用数据分割开,并创建了多个skb,放入套接字的发送缓冲队列(sk_
write_queue),但它并没有为任何一个skb数据加上网络层首部,并且,随后在ip_push_pending_frames函数中,又把发送缓冲队列中的所有的skb,以一个链表的形式追加到第一个skb的end成员后面的struct skb_shared_info结构体中的frag_list上,并只为第一个skb加上了网络层首部,所以,实际上,整个应用数据还只是在一个skb中,ip_append_data这样做只是为接下来的真正的IP数据的分片作好准备。
ip_push_pending_frames在完成了skb的组装后,把它交给了函数ip_output,ip_output又调用了函数ip_finish_output,该函数对skb的长度再次进行判断,如果长度超过输出设备的mtu的值,并且符合其它分片条件,则调用ip_fragment进行数据报的分片,否则直接调用ip_finish_output2输出到数据链路层。
IP数据的分片涉及到IP首部中的两个字段,即结构体struct iphdr的成员frag_off,其高三位是三个标志位,第二位是不允许分片标志,置该位,表示该IP数据报不允许被分片,如果发送这样的数据报,并且数据报本身长度已经超出MTU的很制,则向发送方发一个icmp出错报文,报文类型为目的不可达(3),代码为需要进行分片但被设置了不允许分片的位(4);第三位如果置1,表示后面还有分片,置0表示本分片是一个完整的IP数据报的最后一个分片。frag_off的低13位表示本分片的第一个字节在整个IP数据报中的偏移量,单位是字节数除以8,所以需要把这13位左移3位,才是真正的偏移字节数。
有了先前ip_append_data的工作,ip_fragment的分片工作相对简单很多。struct sk_buff的成员cb在inet域被存入了结构体struct inet_skb_parm,其定义如下:
struct inet_skb_parm
{
struct ip_options opt;
unsigned char flags;
#define IPSKB_FORWARDED 1
#define IPSKB_XFRM_TUNNEL_SIZE 2
#define IPSKB_XFRM_TRANSFORMED 4
#define IPSKB_FRAG_COMPLETE 8
#define IPSKB_REROUTED 16
};
分片完成的一个IP数据报,它的每一个skb的cb->flags被置上IPSKB_FRAG_COMPLETE标志。ip_fragment首先为frag_list列表中的每个skb的成员sk和destructor赋上跟第一个skb同样的值,使它们成为正常的skb。然后,为每个skb从第一个skb中拷贝元数据和网络层首部,并设置正确的iphdr->frag_off的值,并把它们一一输出到数据链路层。至此,网络层的数据发送工作全部完成。
被分片后的IP数据报,其每一个分片都在网络中独立传输,所以,它们到达目的主机一般是不会同时的,并有可能乱序的。并且,它们在中间的传输路径上有可能被组装,也有可能被再次分片。由于网络层首部中frag_off的存在,使得重新正确组装成为可能。下面看看IP数据分片到达目的主机后,是如何被重新组装起来的。
协议栈收到一个IP数据报,进入网络层的第一个函数是ip_rcv,ip_rcv对数据报进行一些正确性检查后,交给ip_rcv_finish,ip_rcv_finish查询输入路由后,交给dst_input,dst_input调用skb->dst->input,如果是本地接收的IP数据报,该函数即ip_local_deliver,ip_local_deliver一开始就检查该数据报的IP首部的frag_off成员,如果发现其低13位不为0,或者高三位中的第三位被置1,则表示这是一个IP分片(第一个分片的低13位为0,但高三位中的第三位被置1,表示后面还有分片,最后一个分片标志位置0,但低13位不为0,中间的分片,两者都不为0)。对于IP分片,该函数调用ip_defrag把它与已经收到的IP分片重组,并等待后来的IP分片,直至形成一个完整的IP数据报。
一个完整IP数据报的全部IP分片组织存放在一个结构体struct ipq中,该结构体保存有足够的信息,等最后一个分片到达后,把它们还原成一个IP数据报。下面是struct ipq的完整定义:
struct ipq {
struct hlist_node list;
struct list_head lru_list;
u32 user;
u32 saddr;
u32 daddr;
u16 id;
u8 protocol;
u8 last_in;
#define COMPLETE 4
#define FIRST_IN 2
#define LAST_IN 1
struct sk_buff *fragments;
int len;
int meat;
spinlock_t lock;
atomic_t refcnt;
struct timer_list timer;
struct timeval stamp;
int iif;
unsigned int rid;
struct inet_peer *peer;
};
user是一个标志,用于标识该IP分片组的来源,协议栈收到的来自网络其它主机或本地环回接口的IP分片,该标识值是IP_DEFRAG_LOCAL_DELIVER,saddr,daddr,id,protocol的值都来源于IP首部,用于确定这些IP分片确实是来自唯一的一个IP数据报。因为一台主机的协议栈可能同时跟网络中多台主机在进行通讯,所以,某一时刻,协议栈一般总有多组IP分片等待被重组,也就是说会有多个struct ipq结构的实例,多个struct ipq被组织在一个哈希表ipq_hash中,当收到一个IP分片时,首先用IP首部的相应字段计算一个哈项值,找到哈希表ipq_hash中的一项,然后去匹配上述字段完全符合的一个struct ipq,把分片加入到该ipq中即可。如果在哈希表中找不到跟当前的IP分片首部完全符合的项,则需要重新创建一个struct ipq的实例,并加到哈希表中。新创建的ipq都带有一个定时器(timer成员),超时时间缺省为IP_FRAG_TIME(30秒),如果30秒后,某一个IP数据报的分片还没有全部被收到,则这个ipq超时,超时处理函数被执行,超时处理函数会删除这个ipq,并向接收端发送一个icmp出错报文,该报文类型为超时(11),代码为在数据报组装期间生存时间为0(1)。
得到了一个ipq后,开始把收到的分片的skb放到这个ipq中,首先检查ipq的last_in,如果它的值为COMPLETE,则表示这个分片组已经完整了,新收到的IP分片是错误的,直接扔掉。再检查新收到的skb的cb->flags,因为在发送数据报进行分片时,每一个分片的flag会被置上IPSKB_FRAG_COMPLETE标志。
接下来检查收到的IP分片的IP_MF(frag_off的高三位中的第三位),如果为0,表示这已经是最后一个分片了,置ipq的last_in为LAST_IN,len始终被更新为当前收到的IP分片中偏移最大的那个分片的偏移值加上长度,最后如果正确,则为整个IP数据报的长度。然后把这个skb剥离网络层首部后,加入到fragments链表中,该链表以frag_off中的偏移量为顺序组织,也就是真正的IP分片的顺序,如果当前收到的这个分片的偏移量为0,则置last_in的值为FIRST_IN,meat始终被更新为当前收到的IP分片的总长,最后,如果正确,meat应该等于len。
IP分片添加完毕后,如果meat确定等于len了,可以考虑进行重组了,函数ip_frag_reasm完成重组,它取得fragments链表,把第二个skb开始的链表又重新放到第一个skb的end后面的struct skb_shared_info结构体的frag_list链表上,并重设IP首部,一个完整的IP数据报就被重组完成了。返回前,还要释放ipq。
ip_push_pending_frames在完成了skb的组装后,把它交给了函数ip_output,ip_output又调用了函数ip_finish_output,该函数对skb的长度再次进行判断,如果长度超过输出设备的mtu的值,并且符合其它分片条件,则调用ip_fragment进行数据报的分片,否则直接调用ip_finish_output2输出到数据链路层。
IP数据的分片涉及到IP首部中的两个字段,即结构体struct iphdr的成员frag_off,其高三位是三个标志位,第二位是不允许分片标志,置该位,表示该IP数据报不允许被分片,如果发送这样的数据报,并且数据报本身长度已经超出MTU的很制,则向发送方发一个icmp出错报文,报文类型为目的不可达(3),代码为需要进行分片但被设置了不允许分片的位(4);第三位如果置1,表示后面还有分片,置0表示本分片是一个完整的IP数据报的最后一个分片。frag_off的低13位表示本分片的第一个字节在整个IP数据报中的偏移量,单位是字节数除以8,所以需要把这13位左移3位,才是真正的偏移字节数。
有了先前ip_append_data的工作,ip_fragment的分片工作相对简单很多。struct sk_buff的成员cb在inet域被存入了结构体struct inet_skb_parm,其定义如下:
struct inet_skb_parm
{
struct ip_options opt;
unsigned char flags;
#define IPSKB_FORWARDED 1
#define IPSKB_XFRM_TUNNEL_SIZE 2
#define IPSKB_XFRM_TRANSFORMED 4
#define IPSKB_FRAG_COMPLETE 8
#define IPSKB_REROUTED 16
};
分片完成的一个IP数据报,它的每一个skb的cb->flags被置上IPSKB_FRAG_COMPLETE标志。ip_fragment首先为frag_list列表中的每个skb的成员sk和destructor赋上跟第一个skb同样的值,使它们成为正常的skb。然后,为每个skb从第一个skb中拷贝元数据和网络层首部,并设置正确的iphdr->frag_off的值,并把它们一一输出到数据链路层。至此,网络层的数据发送工作全部完成。
被分片后的IP数据报,其每一个分片都在网络中独立传输,所以,它们到达目的主机一般是不会同时的,并有可能乱序的。并且,它们在中间的传输路径上有可能被组装,也有可能被再次分片。由于网络层首部中frag_off的存在,使得重新正确组装成为可能。下面看看IP数据分片到达目的主机后,是如何被重新组装起来的。
协议栈收到一个IP数据报,进入网络层的第一个函数是ip_rcv,ip_rcv对数据报进行一些正确性检查后,交给ip_rcv_finish,ip_rcv_finish查询输入路由后,交给dst_input,dst_input调用skb->dst->input,如果是本地接收的IP数据报,该函数即ip_local_deliver,ip_local_deliver一开始就检查该数据报的IP首部的frag_off成员,如果发现其低13位不为0,或者高三位中的第三位被置1,则表示这是一个IP分片(第一个分片的低13位为0,但高三位中的第三位被置1,表示后面还有分片,最后一个分片标志位置0,但低13位不为0,中间的分片,两者都不为0)。对于IP分片,该函数调用ip_defrag把它与已经收到的IP分片重组,并等待后来的IP分片,直至形成一个完整的IP数据报。
一个完整IP数据报的全部IP分片组织存放在一个结构体struct ipq中,该结构体保存有足够的信息,等最后一个分片到达后,把它们还原成一个IP数据报。下面是struct ipq的完整定义:
struct ipq {
struct hlist_node list;
struct list_head lru_list;
u32 user;
u32 saddr;
u32 daddr;
u16 id;
u8 protocol;
u8 last_in;
#define COMPLETE 4
#define FIRST_IN 2
#define LAST_IN 1
struct sk_buff *fragments;
int len;
int meat;
spinlock_t lock;
atomic_t refcnt;
struct timer_list timer;
struct timeval stamp;
int iif;
unsigned int rid;
struct inet_peer *peer;
};
user是一个标志,用于标识该IP分片组的来源,协议栈收到的来自网络其它主机或本地环回接口的IP分片,该标识值是IP_DEFRAG_LOCAL_DELIVER,saddr,daddr,id,protocol的值都来源于IP首部,用于确定这些IP分片确实是来自唯一的一个IP数据报。因为一台主机的协议栈可能同时跟网络中多台主机在进行通讯,所以,某一时刻,协议栈一般总有多组IP分片等待被重组,也就是说会有多个struct ipq结构的实例,多个struct ipq被组织在一个哈希表ipq_hash中,当收到一个IP分片时,首先用IP首部的相应字段计算一个哈项值,找到哈希表ipq_hash中的一项,然后去匹配上述字段完全符合的一个struct ipq,把分片加入到该ipq中即可。如果在哈希表中找不到跟当前的IP分片首部完全符合的项,则需要重新创建一个struct ipq的实例,并加到哈希表中。新创建的ipq都带有一个定时器(timer成员),超时时间缺省为IP_FRAG_TIME(30秒),如果30秒后,某一个IP数据报的分片还没有全部被收到,则这个ipq超时,超时处理函数被执行,超时处理函数会删除这个ipq,并向接收端发送一个icmp出错报文,该报文类型为超时(11),代码为在数据报组装期间生存时间为0(1)。
得到了一个ipq后,开始把收到的分片的skb放到这个ipq中,首先检查ipq的last_in,如果它的值为COMPLETE,则表示这个分片组已经完整了,新收到的IP分片是错误的,直接扔掉。再检查新收到的skb的cb->flags,因为在发送数据报进行分片时,每一个分片的flag会被置上IPSKB_FRAG_COMPLETE标志。
接下来检查收到的IP分片的IP_MF(frag_off的高三位中的第三位),如果为0,表示这已经是最后一个分片了,置ipq的last_in为LAST_IN,len始终被更新为当前收到的IP分片中偏移最大的那个分片的偏移值加上长度,最后如果正确,则为整个IP数据报的长度。然后把这个skb剥离网络层首部后,加入到fragments链表中,该链表以frag_off中的偏移量为顺序组织,也就是真正的IP分片的顺序,如果当前收到的这个分片的偏移量为0,则置last_in的值为FIRST_IN,meat始终被更新为当前收到的IP分片的总长,最后,如果正确,meat应该等于len。
IP分片添加完毕后,如果meat确定等于len了,可以考虑进行重组了,函数ip_frag_reasm完成重组,它取得fragments链表,把第二个skb开始的链表又重新放到第一个skb的end后面的struct skb_shared_info结构体的frag_list链表上,并重设IP首部,一个完整的IP数据报就被重组完成了。返回前,还要释放ipq。