tcp/ip 协议栈Linux源码分析三 IPv4分片报文重组分析三
生活随笔
收集整理的這篇文章主要介紹了
tcp/ip 协议栈Linux源码分析三 IPv4分片报文重组分析三
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
繼續上篇,上次講到了分片隊列的查找操作,剩下的就是分片隊列插入和重組兩個部分了,這個也是分片重組的關鍵部分。
將收到的分片插入到分片隊列是由函數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;elseqp->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;elseqp->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;elseqp->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;elseqp->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表的邏輯圖其實就是整個重組子系統的縮影,多看看那個。?
總結
以上是生活随笔為你收集整理的tcp/ip 协议栈Linux源码分析三 IPv4分片报文重组分析三的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 高收益保本理财产品可靠吗
- 下一篇: c语言命令行选项处理函数getopt和g