繼續上篇,上次講到了分片隊列的查找操作,剩下的就是分片隊列插入和重組兩個部分了,這個也是分片重組的關鍵部分。
將收到的分片插入到分片隊列是由函數inet_frag_queue()函數完成,這個函數比較長,多看幾遍就好了 :-)
/* Add new segment to existing queue. */
/* 添加一個新的片段到分片隊列裏面 */
static int ip_frag_queue(struct ipq *qp, struct sk_buff *skb)
{
struct sk_buff *prev, *next;
struct net_device *dev;
int flags, offset;
int ihl, end;
int err = -ENOENT;
u8 ecn;
/* last_in標誌位已經置位,這時候再收到報文就不用處理了,
* 一種情況是重組已經完成,這時候又收到了報文,可能是重傳
* 當然,分片隊列被垃圾回收定時器回收的時候也會設置這個標誌位,
* 表示已廢棄。
*/
if (qp->q.last_in & INET_FRAG_COMPLETE)
goto err;
/* 下面這段描述摘自 http://blog.chinaunix.net/uid-23629988-id-3047513.html
* 關於ip_frag_too_far這個函數我還沒有分析清楚,日後搞明白了補上,:-)
* 歡迎懂得大神講一下
* 1. IPCB(skb)->flags只有在本機發送IPv4分片時被置位,那麼這裏的檢查應該是
* 預防收到本機自己發出的IP分片。
* 2. 關於ip_frag_too_far:該函數主要保證了來自同一個peer(相同的源地址)不
* 會佔用過多的IP分片隊列。
* 3. 前面兩個條件爲真時,調用ip_frag_reinit,重新初始化該隊列。出錯,那麼只
* 好kill掉這個隊列了。
*
*/
if (!(IPCB(skb)->flags & IPSKB_FRAG_COMPLETE) &&
unlikely(ip_frag_too_far(qp)) &&
unlikely(err = ip_frag_reinit(qp))) {
ipq_kill(qp);
goto err;
}
/* 獲取ip頭裏面的ecn標誌位 */
ecn = ip4_frag_ecn(ip_hdr(skb)->tos);
offset = ntohs(ip_hdr(skb)->frag_off);
/* 分片標誌位 */
flags = offset & ~IP_OFFSET;
offset &= IP_OFFSET;
/* 得到片偏移位置,相對於原始未分片報文,單位爲8字節 */
offset <<= 3; /* offset is in 8-byte chunks */
ihl = ip_hdrlen(skb);
/* Determine the position of this fragment. */
/* skb的長度減去IP頭就剩下數據部分長度,這個長度加上片偏移的長度
* 就得到了這段報文相對於原始報文的尾偏移
*/
end = offset + skb->len - ihl;
err = -EINVAL;
/* Is this the final fragment? */
/* 如果是最後的一片 */
if ((flags & IP_MF) == 0) {
/* If we already have some bits beyond end
* or have different end, the segment is corrupted.
*/
/*
* 既然是最後一片,尾偏移肯定要大於或者等於當前分片隊列的長度,不是的話就錯了
*
* 如果已經收到過最後分片(分片重傳)並且長度和當前skb所指向的尾偏移不一致,
* 出錯了
*/
if (end < qp->q.len ||
((qp->q.last_in & INET_FRAG_LAST_IN) && end != qp->q.len))
goto err;
/* 一切正常,設置last_in 標誌位,同時將分片隊列長度設置上
* 只有收到了最後一個分片報文才能夠得知完整的報文長度
*/
qp->q.last_in |= INET_FRAG_LAST_IN;
qp->q.len = end;
} else {
/* 如果不是最後一片,並且長度不是的倍數,就截取數據到的8倍數,
* 因爲數據被截取了,校驗和也失效了,這裏重置校驗和
*/
if (end&7) {
end &= ~7;
if (skb->ip_summed != CHECKSUM_UNNECESSARY)
skb->ip_summed = CHECKSUM_NONE;
}
if (end > qp->q.len) {
/* 數據的尾部超出分片隊列總長,如果已經收到了最後的分片,
說明出錯了,直接報錯。不是的話更新隊列長度就好。*/
/* Some bits beyond end -> corruption. */
if (qp->q.last_in & INET_FRAG_LAST_IN)
goto err;
qp->q.len = end;
}
}
/* 說明長度爲空,丟棄 */
if (end == offset)
goto err;
err = -ENOMEM;
/* 去掉IP頭部 */
if (pskb_pull(skb, ihl) == NULL)
goto err;
/* 只保留數據部分 */
err = pskb_trim_rcsum(skb, end - offset);
if (err)
goto err;
/* Find out which fragments are in front and at the back of us
* in the chain of fragments so far. We must know where to put
* this fragment, right?
*/
/* 如果是第一個分片報文則直接插入
* 如果上一個報文的偏移值小於當前偏移值則放在該報文後面即可
*/
prev = qp->q.fragments_tail;
if (!prev || FRAG_CB(prev)->offset < offset) {
next = NULL;
goto found;
}
/* 亂序到達的話找到它下面一個報文即可
* 這裏是遍歷分片列表,找到當前報文的後一個
*/
prev = NULL;
for (next = qp->q.fragments; next != NULL; next = next->next) {
if (FRAG_CB(next)->offset >= offset)
break; /* bingo! */
prev = next;
}
found:
/* We found where to put this one. Check for overlap with
* preceding fragment, and, if needed, align things so that
* any overlaps are eliminated.
* 這時候已經找到在分片隊列中的位置,需要和前後報文檢查看看是否有
* 數據重疊。
*/
if (prev) {
/* i等於與上個報文重疊部分數據長度,如果完全落在上個報文內部則報錯 */
int i = (FRAG_CB(prev)->offset + prev->len) - offset;
if (i > 0) {
/* 重疊的部分直接丟棄,end <= offset說明完全重疊 */
offset += i;
err = -EINVAL;
if (end <= offset)
goto err;
err = -ENOMEM;
/* 去掉重疊部分 */
if (!pskb_pull(skb, i))
goto err;
/* 數據有變更,重置校驗和 */
if (skb->ip_summed != CHECKSUM_UNNECESSARY)
skb->ip_summed = CHECKSUM_NONE;
}
}
err = -ENOMEM;
while (next && FRAG_CB(next)->offset < end) {
/* 與後面緊鄰的報文重疊部分數據長度 */
int i = end - FRAG_CB(next)->offset; /* overlap is 'i' bytes */
/* 如果重疊長度小於後面skb的長度,那麼只需要將next skb
* 的長度減去重疊部分即可,同時更新偏移值和校驗和
*/
if (i < next->len) {
/* Eat head of the next overlapped fragment
* and leave the loop. The next ones cannot overlap.
*/
if (!pskb_pull(next, i))
goto err;
FRAG_CB(next)->offset += i;
qp->q.meat -= i;
if (next->ip_summed != CHECKSUM_UNNECESSARY)
next->ip_summed = CHECKSUM_NONE;
break;
} else {
/* 走到這說明重疊長度大於next的長度,這時候next可以直接從隊列中
* 摘掉了。
*/
struct sk_buff *free_it = next;
/* Old fragment is completely overridden with
* new one drop it.
*/
next = next->next;
if (prev)
prev->next = next;
else
qp->q.fragments = next;
qp->q.meat -= free_it->len;
/* 從分片隊列釋放該skb */
frag_kfree_skb(qp->q.net, free_it);
}
}
/* 設置該skb的控制信息,即偏移值 */
FRAG_CB(skb)->offset = offset;
/* Insert this fragment in the chain of fragments. */
/* 插入報文,如果是最後一片則設置fragments_tail指針指向最後一片 */
skb->next = next;
if (!next)
qp->q.fragments_tail = skb;
if (prev)
prev->next = skb;
else
qp->q.fragments = skb;
dev = skb->dev;
if (dev) {
/* 記錄設備的索引同時清空skb的dev指針 */
qp->iif = dev->ifindex;
skb->dev = NULL;
}
/* 更新隊列的接收時間戳
* 更新隊列當前收到長度和,注意meat和len區別,前者保存當前已接受部分數據長度,
* 後者表示目前已知分片最大長度,當收到最後一個分片MF=0,就能夠得到原始報文長度
*/
qp->q.stamp = skb->tstamp;
qp->q.meat += skb->len;
qp->ecn |= ecn;
/* 增加分片內存所佔空間 */
atomic_add(skb->truesize, &qp->q.net->mem);
/* 設置標誌位 */
if (offset == 0)
qp->q.last_in |= INET_FRAG_FIRST_IN;
if (qp->q.last_in == (INET_FRAG_FIRST_IN | INET_FRAG_LAST_IN) &&
qp->q.meat == qp->q.len)
/* 如果報文已經收集齊,則調用ip_frag_reasm() 進行重組操作 */
return ip_frag_reasm(qp, prev, dev);
write_lock(&ip4_frags.lock);
/* 移到lru末尾 */
list_move_tail(&qp->q.lru_list, &qp->q.net->lru_list);
write_unlock(&ip4_frags.lock);
/* 分片還在繼續,返回EINPROGRESS */
return -EINPROGRESS;
err:
kfree_skb(skb);
return err;
}
如果分片報文的集齊了就會調用ip_frag_rasm來重組,來看下:
/* Add new segment to existing queue. */
/* 添加一個新的片段到分片隊列裏面 */
static int ip_frag_queue(struct ipq *qp, struct sk_buff *skb)
{
struct sk_buff *prev, *next;
struct net_device *dev;
int flags, offset;
int ihl, end;
int err = -ENOENT;
u8 ecn;
/* last_in標誌位已經置位,這時候再收到報文就不用處理了,
* 一種情況是重組已經完成,這時候又收到了報文,可能是重傳
* 當然,分片隊列被垃圾回收定時器回收的時候也會設置這個標誌位,
* 表示已廢棄。
*/
if (qp->q.last_in & INET_FRAG_COMPLETE)
goto err;
/* 下面這段描述摘自 http://blog.chinaunix.net/uid-23629988-id-3047513.html
* 關於ip_frag_too_far這個函數我還沒有分析清楚,日後搞明白了補上,:-)
* 歡迎懂得大神講一下
* 1. IPCB(skb)->flags只有在本機發送IPv4分片時被置位,那麼這裏的檢查應該是
* 預防收到本機自己發出的IP分片。
* 2. 關於ip_frag_too_far:該函數主要保證了來自同一個peer(相同的源地址)不
* 會佔用過多的IP分片隊列。
* 3. 前面兩個條件爲真時,調用ip_frag_reinit,重新初始化該隊列。出錯,那麼只
* 好kill掉這個隊列了。
*
*/
if (!(IPCB(skb)->flags & IPSKB_FRAG_COMPLETE) &&
unlikely(ip_frag_too_far(qp)) &&
unlikely(err = ip_frag_reinit(qp))) {
ipq_kill(qp);
goto err;
}
/* 獲取ip頭裏面的ecn標誌位 */
ecn = ip4_frag_ecn(ip_hdr(skb)->tos);
offset = ntohs(ip_hdr(skb)->frag_off);
/* 分片標誌位 */
flags = offset & ~IP_OFFSET;
offset &= IP_OFFSET;
/* 得到片偏移位置,相對於原始未分片報文,單位爲8字節 */
offset <<= 3; /* offset is in 8-byte chunks */
ihl = ip_hdrlen(skb);
/* Determine the position of this fragment. */
/* skb的長度減去IP頭就剩下數據部分長度,這個長度加上片偏移的長度
* 就得到了這段報文相對於原始報文的尾偏移
*/
end = offset + skb->len - ihl;
err = -EINVAL;
/* Is this the final fragment? */
/* 如果是最後的一片 */
if ((flags & IP_MF) == 0) {
/* If we already have some bits beyond end
* or have different end, the segment is corrupted.
*/
/*
* 既然是最後一片,尾偏移肯定要大於或者等於當前分片隊列的長度,不是的話就錯了
*
* 如果已經收到過最後分片(分片重傳)並且長度和當前skb所指向的尾偏移不一致,
* 出錯了
*/
if (end < qp->q.len ||
((qp->q.last_in & INET_FRAG_LAST_IN) && end != qp->q.len))
goto err;
/* 一切正常,設置last_in 標誌位,同時將分片隊列長度設置上
* 只有收到了最後一個分片報文才能夠得知完整的報文長度
*/
qp->q.last_in |= INET_FRAG_LAST_IN;
qp->q.len = end;
} else {
/* 如果不是最後一片,並且長度不是的倍數,就截取數據到的8倍數,
* 因爲數據被截取了,校驗和也失效了,這裏重置校驗和
*/
if (end&7) {
end &= ~7;
if (skb->ip_summed != CHECKSUM_UNNECESSARY)
skb->ip_summed = CHECKSUM_NONE;
}
if (end > qp->q.len) {
/* 數據的尾部超出分片隊列總長,如果已經收到了最後的分片,
說明出錯了,直接報錯。不是的話更新隊列長度就好。*/
/* Some bits beyond end -> corruption. */
if (qp->q.last_in & INET_FRAG_LAST_IN)
goto err;
qp->q.len = end;
}
}
/* 說明長度爲空,丟棄 */
if (end == offset)
goto err;
err = -ENOMEM;
/* 去掉IP頭部 */
if (pskb_pull(skb, ihl) == NULL)
goto err;
/* 只保留數據部分 */
err = pskb_trim_rcsum(skb, end - offset);
if (err)
goto err;
/* Find out which fragments are in front and at the back of us
* in the chain of fragments so far. We must know where to put
* this fragment, right?
*/
/* 如果是第一個分片報文則直接插入
* 如果上一個報文的偏移值小於當前偏移值則放在該報文後面即可
*/
prev = qp->q.fragments_tail;
if (!prev || FRAG_CB(prev)->offset < offset) {
next = NULL;
goto found;
}
/* 亂序到達的話找到它下面一個報文即可
* 這裏是遍歷分片列表,找到當前報文的後一個
*/
prev = NULL;
for (next = qp->q.fragments; next != NULL; next = next->next) {
if (FRAG_CB(next)->offset >= offset)
break; /* bingo! */
prev = next;
}
found:
/* We found where to put this one. Check for overlap with
* preceding fragment, and, if needed, align things so that
* any overlaps are eliminated.
* 這時候已經找到在分片隊列中的位置,需要和前後報文檢查看看是否有
* 數據重疊。
*/
if (prev) {
/* i等於與上個報文重疊部分數據長度,如果完全落在上個報文內部則報錯 */
int i = (FRAG_CB(prev)->offset + prev->len) - offset;
if (i > 0) {
/* 重疊的部分直接丟棄,end <= offset說明完全重疊 */
offset += i;
err = -EINVAL;
if (end <= offset)
goto err;
err = -ENOMEM;
/* 去掉重疊部分 */
if (!pskb_pull(skb, i))
goto err;
/* 數據有變更,重置校驗和 */
if (skb->ip_summed != CHECKSUM_UNNECESSARY)
skb->ip_summed = CHECKSUM_NONE;
}
}
err = -ENOMEM;
while (next && FRAG_CB(next)->offset < end) {
/* 與後面緊鄰的報文重疊部分數據長度 */
int i = end - FRAG_CB(next)->offset; /* overlap is 'i' bytes */
/* 如果重疊長度小於後面skb的長度,那麼只需要將next skb
* 的長度減去重疊部分即可,同時更新偏移值和校驗和
*/
if (i < next->len) {
/* Eat head of the next overlapped fragment
* and leave the loop. The next ones cannot overlap.
*/
if (!pskb_pull(next, i))
goto err;
FRAG_CB(next)->offset += i;
qp->q.meat -= i;
if (next->ip_summed != CHECKSUM_UNNECESSARY)
next->ip_summed = CHECKSUM_NONE;
break;
} else {
/* 走到這說明重疊長度大於next的長度,這時候next可以直接從隊列中
* 摘掉了。
*/
struct sk_buff *free_it = next;
/* Old fragment is completely overridden with
* new one drop it.
*/
next = next->next;
if (prev)
prev->next = next;
else
qp->q.fragments = next;
qp->q.meat -= free_it->len;
/* 從分片隊列釋放該skb */
frag_kfree_skb(qp->q.net, free_it);
}
}
/* 設置該skb的控制信息,即偏移值 */
FRAG_CB(skb)->offset = offset;
/* Insert this fragment in the chain of fragments. */
/* 插入報文,如果是最後一片則設置fragments_tail指針指向最後一片 */
skb->next = next;
if (!next)
qp->q.fragments_tail = skb;
if (prev)
prev->next = skb;
else
qp->q.fragments = skb;
dev = skb->dev;
if (dev) {
/* 記錄設備的索引同時清空skb的dev指針 */
qp->iif = dev->ifindex;
skb->dev = NULL;
}
/* 更新隊列的接收時間戳
* 更新隊列當前收到長度和,注意meat和len區別,前者保存當前已接受部分數據長度,
* 後者表示目前已知分片最大長度,當收到最後一個分片MF=0,就能夠得到原始報文長度
*/
qp->q.stamp = skb->tstamp;
qp->q.meat += skb->len;
qp->ecn |= ecn;
/* 增加分片內存所佔空間 */
atomic_add(skb->truesize, &qp->q.net->mem);
/* 設置標誌位 */
if (offset == 0)
qp->q.last_in |= INET_FRAG_FIRST_IN;
if (qp->q.last_in == (INET_FRAG_FIRST_IN | INET_FRAG_LAST_IN) &&
qp->q.meat == qp->q.len)
/* 如果報文已經收集齊,則調用ip_frag_reasm() 進行重組操作 */
return ip_frag_reasm(qp, prev, dev);
write_lock(&ip4_frags.lock);
/* 移到lru末尾 */
list_move_tail(&qp->q.lru_list, &qp->q.net->lru_list);
write_unlock(&ip4_frags.lock);
/* 分片還在繼續,返回EINPROGRESS */
return -EINPROGRESS;
err:
kfree_skb(skb);
return err;
}
/* Build a new IP datagram from all its fragments. */
/* 分片重組 */
static int ip_frag_reasm(struct ipq *qp, struct sk_buff *prev,
struct net_device *dev)
{
struct net *net = container_of(qp->q.net, struct net, ipv4.frags);
struct iphdr *iph;
struct sk_buff *fp, *head = qp->q.fragments;
int len;
int ihlen;
int err;
u8 ecn;
/* 重組之前首先將分片隊列從分片子系統隔離開 */
ipq_kill(qp);
/* 檢查ecn標誌位,0xff則丟棄該報文,之所以這麼做是因爲rfc文檔建議這麼做 */
ecn = ip4_frag_ecn_table[qp->ecn];
if (unlikely(ecn == 0xff)) {
err = -EINVAL;
goto out_fail;
}
/* Make the one we just received the head. */
/* 這一步是將最後接收到的skb指針指向分片隊列的首部接收完最後一片後重組完成
* 是要將skb傳遞給上層處理的。這一段代碼貌似複雜,多看幾遍就懂了,下面放上一個
* 簡要的圖。
*/
if (prev) {
head = prev->next;
fp = skb_clone(head, GFP_ATOMIC);
if (!fp)
goto out_nomem;
fp->next = head->next;
if (!fp->next)
qp->q.fragments_tail = fp;
prev->next = fp;
/* skb_morph 作用基本和skb_clone一致,這裏的作用是
* 將剛收到的指針指向分片隊列首部,fragments就是分片
* 首部。
*/
skb_morph(head, qp->q.fragments);
head->next = qp->q.fragments->next;
kfree_skb(qp->q.fragments);
qp->q.fragments = head;
}
WARN_ON(head == NULL);
WARN_ON(FRAG_CB(head)->offset != 0);
/* Allocate a new buffer for the datagram. */
ihlen = ip_hdrlen(head);
len = ihlen + qp->q.len;
/* ip報文最大65535字節,超過這個長度就報錯 */
err = -E2BIG;
if (len > 65535)
goto out_oversize;
/* Head of list must not be cloned. */
if (skb_cloned(head) && pskb_expand_head(head, 0, 0, GFP_ATOMIC))
goto out_nomem;
/* If the first fragment is fragmented itself, we split
* it to two chunks: the first with data and paged part
* and the second, holding only fragments. */
/*
*通常SKB數據區會由線性緩存和非線性緩存組成,超過MTU大小就要使用
* 另外的skb來存儲,這個部分放在skb_shinfo(head)->frag_list裏。
* 分片隊列重組完成後也是把原來的一個個分片放到skb_shinfo(head)->frag_list裏,
* 所以這裏爲了避免和head原有的frag_list弄混(如果head存在frag_list),將head的數據分爲
* 兩個部分,head存儲線性和非線性數據區,clone指向head的原有frag_list,同時再將分片隊列
* 裏的skb掛到clone後,這樣後續的上層處理就非常簡單。
*/
if (skb_has_frag_list(head)) {
struct sk_buff *clone;
int i, plen = 0;
/* 創建一個線性數據區長度爲0的skb */
if ((clone = alloc_skb(0, GFP_ATOMIC)) == NULL)
goto out_nomem;
clone->next = head->next;
head->next = clone;
/* 繼承head的frag_list */
skb_shinfo(clone)->frag_list = skb_shinfo(head)->frag_list;
/* 將head的frag_list指針 重置 */
skb_frag_list_init(head);
for (i = 0; i < skb_shinfo(head)->nr_frags; i++)
plen += skb_frag_size(&skb_shinfo(head)->frags[i]);
clone->len = clone->data_len = head->data_len - plen;
head->data_len -= clone->len;
head->len -= clone->len;
clone->csum = 0;
clone->ip_summed = head->ip_summed;
atomic_add(clone->truesize, &qp->q.net->mem);
}
/* 再將所有的分片掛到frag_list隊列上 */
skb_shinfo(head)->frag_list = head->next;
/* 指針指向傳輸層首部 */
skb_push(head, head->data - skb_network_header(head));
/* 處理校驗和 */
for (fp=head->next; fp; fp = fp->next) {
head->data_len += fp->len;
head->len += fp->len;
if (head->ip_summed != fp->ip_summed)
head->ip_summed = CHECKSUM_NONE;
else if (head->ip_summed == CHECKSUM_COMPLETE)
head->csum = csum_add(head->csum, fp->csum);
head->truesize += fp->truesize;
}
/* 重組完成,從分片佔用的系統內存中減去重組後大小 */
atomic_sub(head->truesize, &qp->q.net->mem);
head->next = NULL;
head->dev = dev;
head->tstamp = qp->q.stamp;
/* 重置IP頭 */
iph = ip_hdr(head);
iph->frag_off = 0;
iph->tot_len = htons(len);
iph->tos |= ecn;
IP_INC_STATS_BH(net, IPSTATS_MIB_REASMOKS);
qp->q.fragments = NULL;
qp->q.fragments_tail = NULL;
return 0;
out_nomem:
LIMIT_NETDEBUG(KERN_ERR pr_fmt("queue_glue: no memory for gluing queue %p\n"),
qp);
err = -ENOMEM;
goto out_fail;
out_oversize:
if (net_ratelimit())
pr_info("Oversized IP packet from %pI4\n", &qp->saddr);
out_fail:
IP_INC_STATS_BH(net, IPSTATS_MIB_REASMFAILS);
return err;
}
下面貼一張圖,主要處理是將收到的分片指針指向分片隊列首部,因爲重組完成後就會把重組好的報文還給協議棧繼續處理,這時候分片skb指針將由原先的skb分片指向重組skb首部
IPv4分片重組就是以上這些內容,代碼雖然很多但是邏輯不是很複雜,只要理解了分片隊列、垃圾回收隊列(lru隊列)的組織結構再結合具體的代碼分析就能夠搞清了。第一篇博客給的那張關於分片隊列、哈希表的、lru表的邏輯圖其實就是整個重組子系統的縮影,多看看那個。