對網絡比較熟悉的童鞋都知道,當發送的ip報文長度超出了最大的傳輸單位MTU,且允許分片的情況下,就會對ip報文進行分片。在上層要發送數據時就會調用dst_output,dst_output就會調用ip_output,而ip_output就會調用ip_finish_output,在ip_finish_output把數據發送出去之前就會判斷該報文是否進行分片。
static int ip_finish_output(struct sk_buff *skb)
{
if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb))
return ip_fragment(skb, ip_finish_output2);
else
return ip_finish_output2(skb);
}從源碼中可以看出,當報文的長度大於mtu,gso的長度不為0就會調用ip_fragment進行分片。否則就會調用ip_finish_output2把數據發送出去。
ip分片目前有兩種分片方式:1、快速分片;2、慢速分片。在快速分片中,將數據分割成片段已經由傳輸層完成,三層只需將這寫片段組成ip分片;而慢速分片則需要完成全部的工作,即對一個完整的ip數據報根據mtu值循環進行分片,直至完成。整個分片工作都在ip_fragment中完成。
int ip_fragment(struct sk_buff *skb, int (*output)(struct sk_buff *))
{
.......
struct rtable *rt = skb_rtable(skb);
int err = 0;
dev = rt->u.dst.dev;
......
/*
* 如果待分片IP數據包禁止分片,則調用
* icmp_send()向發送方發送一個原因為需要
* 分片而設置了不分片標志的目的不可達
* ICMP報文,並丟棄報文,即設置IP狀態
* 為分片失敗,釋放skb,返回消息過長
* 錯誤碼。
*/
if (unlikely((iph->frag_off & htons(IP_DF)) && !skb->local_df)) {
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
htonl(ip_skb_dst_mtu(skb)));
kfree_skb(skb);
return -EMSGSIZE;
}
hlen = iph->ihl * 4;
mtu = dst_mtu(&rt->u.dst) - hlen;/* Size of data space */
/*
* 在分片之前先給IP數據包的控制塊設置
* IPSKB_FRAG_COMPLETE標志,標識完成分片。
*/
IPCB(skb)->flags |= IPSKB_FRAG_COMPLETE;
if (skb_has_frags(skb)) {
/*
* 獲得此IP數據包第一個分片長度,包括SG類型
* 聚合分散I/O數據區中的數據。
*/
int first_len = skb_pagelen(skb);
if (first_len - hlen > mtu ||
((first_len - hlen) & 7) ||
(iph->frag_off & htons(IP_MF|IP_OFFSET)) ||
skb_cloned(skb))
goto slow_path;
skb_walk_frags(skb, frag) {
if (frag->len > mtu ||
((frag->len & 7) && frag->next) ||
skb_headroom(frag) < hlen)
goto slow_path;
if (skb_shared(frag))
goto slow_path;
if (skb->sk) {
frag->sk = skb->sk;
frag->destructor = sock_wfree;
truesizes += frag->truesize;
}
......
frag = skb_shinfo(skb)->frag_list;
skb_frag_list_init(skb);
......
for (;;) {
if (frag) {
/*
* 設置后一個分片skb中指向三層和四層首部
* 的指針。
*/
skb_reset_transport_header(frag);
__skb_push(frag, hlen);
skb_reset_network_header(frag);
/*
* 將當前分片的IP首部復制給后一個分片,
* 並修改后一個分片IP首部的總長度字段。
*/
memcpy(skb_network_header(frag), iph, hlen);
iph = ip_hdr(frag);
iph->tot_len = htons(frag->len);
/*
* 根據當前分片的skb填充后一個分片
* skb中的參數。
*/
ip_copy_metadata(frag, skb);
/*
* 如果是在處理第一個分片,則調用ip_options_fragment()
* 將第二個分片skb中無需復制到每個分片的IP選項都
* 填充為IPOPT_NOOP,此后所有的分片選項部分都簡單
* 地復制上一個的即可。
*/
if (offset == 0)
ip_options_fragment(frag);
......
offset += skb->len - hlen;
iph->frag_off = htons(offset>>3);
if (frag->next != NULL)
iph->frag_off |= htons(IP_MF);
/* Ready, complete checksum */
ip_send_check(iph);
err = output(skb);
skb = frag;
frag = skb->next;
skb->next = NULL;快速分片和慢速分片主要通過skb_has_frags這個來判斷,也就是判斷該數據的第一個skb中的frag_list是否為空,如果為空就是需要進行慢速分片,否則傳輸層已經為快速分片做好了准備。上面的代碼大部分都有注釋,需要注意一種情況
1、要進行快速分片還需要對傳輸層傳遞的所有的skb進行判斷:
有分片長度大於mtu
除最后一個分片外還有分片長度未與8字節對其
ip首部中的MF或片偏移不為0,說明不是一個完整的ip報文
此skb被克隆 上述四種情況是不能進行ip分片的。上面是快速分片;
當不能進行快速分片時就會轉到慢速分片,慢速分片其實就需要對skb數據進行復制,而快速分片就不需要此操作。
slow_path:
/*
* 獲取待分片的IP數據包的數據長度,此處減去hlen是
* 為二層首部留出空間。
*/
left = skb->len - hlen;/* Space per frame */
/*
* 獲取IP數據包中數據區指針
*/
ptr = raw + hlen;/* Where to start from */
/* for bridged IP traffic encapsulated inside f.e. a vlan header,
* we need to make room for the encapsulating header
*/
/*
* 如果是橋轉發基於VLAN的IP數據包,則需
* 獲得VLAN首部長度,在后面分配skb
* 緩沖區時留下相應的空間,同時還需
* 修改MTU值。
*/
pad = nf_bridge_pad(skb);
/*
* 獲得IP首部中的片偏移值,即每個分片
* 起始處在原始數據包中位置,該值是
* 13位的,因此要乘8.
*/
offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;
/*
* 取MF位值,MF值除最后一個分片外
* 都應該置為1,表示該分片之后還
* 有分片。
*/
not_last_frag = iph->frag_off & htons(IP_MF);
/*
* 循環對left長度的數據進行分片,為
* 每一個分片創建一個新的SKB。
*/
while (left > 0) {
len = left;
/* IF: it doesn't fit, use 'mtu' - the data space left */
/*
* 如果剩余數據的長度大於MTU,則以MTU為
* 分片長度進行分片;否則就以剩余數據
* 的長度作為分片長度,顯然后一種情況
* 只會出現在最后一個分片。
*/
if (len > mtu)
len = mtu;
/* IF: we are not sending upto and including the packet end
then align the next start on an eight byte boundary */
/*
* 除非是最后一個分節,否則分片不包括IP
* 首部的數據部分,需8字節對齊。
*/
if (len < left){
len &= ~7;
}
/*
*Allocate buffer.
*/
/*
* 為分片分配一個SKB,其長度為分片長、
* IP首部長,以及二層首部長之和。
*/
if ((skb2 = alloc_skb(len+hlen+ll_rs, GFP_ATOMIC)) == NULL) {
NETDEBUG(KERN_INFO "IP: frag: no memory for new fragment!\n");
err = -ENOMEM;
goto fail;
}
......
/*
* 復制分片數據,並更新原始數據包剩余未分片數據量。
* 此處調用了skb_copy_bits(),是因為skb中的數據存儲有多種
* 可能性,而skb_copy_bits可以處理這些細節。
*/
if (skb_copy_bits(skb, ptr, skb_transport_header(skb2), len))
/*
* 設置分片的片偏移字段,對於第一個分片,
* 該值即原始IP數據包的片偏移字段值。
*/
iph = ip_hdr(skb2);
iph->frag_off = htons((offset >> 3));
if (offset == 0)
ip_options_fragment(skb);
* 如果不是最后一個分節,則設置IP首部中
* 標識字段的MF位。
*/
if (left > 0 || not_last_frag)
iph->frag_off |= htons(IP_MF);
/*
* 更新后一個分節在整個原始數據包中的偏移量,
* 以及后一個分片在當前被分片數據包中的偏移量。
* 這兩個偏移量是有區別的,因為一個數據包在
* 傳輸過程中可能被多次分片,因此當前被分片
* 數據包也由可能是另外一個數據包的分片。
*/
ptr += len;
offset += len;
/*
*Put this fragment into the sending queue.
*/
/*
* 設置分片IP首部中總長度字段。
*/
iph->tot_len = htons(len + hlen);
......
上述代碼也是有注釋的,只提示兩點:
1、分片的片偏移
分段偏移用於指明分段起始點相對報文起始點的偏移,長度為13位,以8個位組為單位。若MTU=1500時,一個大小為3000字節的數據經過該接口,會被分為端傳輸:
第一段長度為1480+20,第二段為1480,第三段為40,那么第一段分段的偏移為0,第二段為1480/8,第三段為185+185,所以在源碼中需要乘以8,而在設置ip首部片偏移時又除以8的原因
2、ip選項的處理要注意,有的ip選項需要體現在所有的分片中,而有的不需要。