linux协议栈 IPv4之发送过程中的分段处理ip_fragment()
目錄
1 分片位置
2 ip_fragment()代碼分析
1 分片位置
分段是網(wǎng)絡層的一個重要任務,網(wǎng)絡層需要對兩方面的IP數(shù)據(jù)包進行分段:
這兩種數(shù)據(jù)包的長度如果超過了出口設備的MTU(或者PMTU),則網(wǎng)絡層必須先對數(shù)據(jù)包進行分段,使其適配出口設備的MTU。
IPv4使用 ip_fragment() 處理分段,在設計時,要求該函數(shù)能夠處理所有的情況,但是在實現(xiàn)過程中,充分考慮了實際可能的情況,對某些場景的處理進行了優(yōu)化,下面分情況介紹。
對于本機發(fā)送的數(shù)據(jù)包,TCP在組織skb數(shù)據(jù)時,本身就會考慮MTU的限制,它會盡可能的保證每個skb攜帶的數(shù)據(jù)不會超過MTU,就是為了避免網(wǎng)絡層再進行分段,因為分段對TCP性能的影響較大。因為TCP就幫忙做了很多事情,所以對于TCP發(fā)送場景,應該是很少有機會執(zhí)行分片的。考慮UDP,它并不會向TCP一樣保證skb長度,但是由于UDP往往是調(diào)用ip_append_data()組織skb數(shù)據(jù)的,該函數(shù)在組織skb過程中,會將屬于同一個IP報文的所有分片都組織成skb列表(非第一個分片都放在第一個分片skb的frag_list中),這樣網(wǎng)絡層在執(zhí)行分片時將會節(jié)省很多工作量。
對于轉(zhuǎn)發(fā)的數(shù)據(jù)包,則無法向本地發(fā)送一樣,提前做很多的工作,網(wǎng)絡層必須依靠自己來兼容所有可能的情況。同樣的,天有不測風云,對于一些特殊的異常場景,本機發(fā)送的數(shù)據(jù)包也有可能并沒有按照預期情況組織,這時網(wǎng)絡層也要能夠兼容處理。
綜上,網(wǎng)絡層在實現(xiàn)分段時,設計了快速路徑和慢速路徑兩個流程來分別對應上面的兩種情況。通常本地產(chǎn)生的UDP大包都會調(diào)用 ip_push_pending_frames 將發(fā)包隊列的skb整合到?frag_list?上,走快速路徑分發(fā)出去。
2 ip_fragment()代碼分析
/** This IP datagram is too large to be sent in one piece. Break it up into* smaller pieces (each of size equal to IP header plus* a block of the data of the original IP data part) that will yet fit in a* single device frame, and queue such a frame for sending.*/ int ip_fragment(struct sk_buff *skb, int (*output)(struct sk_buff*)) {struct iphdr *iph;int raw = 0;int ptr;struct net_device *dev;struct sk_buff *skb2;unsigned int mtu, hlen, left, len, ll_rs, pad;int offset;__be16 not_last_frag;struct rtable *rt = (struct rtable*)skb->dst;int err = 0;dev = rt->u.dst.dev;/** Point into the IP datagram header.*/iph = ip_hdr(skb);// 一旦進入該函數(shù),說明skb過大,需要進行IP分片,但是又設置了DF標記或者本身是抑制分片的,// 那么發(fā)送失敗,向源端發(fā)送ICMP報文,這里主要是為forward數(shù)據(jù)所判斷。if (unlikely((iph->frag_off & htons(IP_DF)) && !skb->local_df)) {IP_INC_STATS(IPSTATS_MIB_FRAGFAILS);icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, htonl(ip_skb_dst_mtu(skb)));kfree_skb(skb);return -EMSGSIZE;}// hlen保存IP首部長度hlen = iph->ihl * 4;// mtu代表每個IP片段能夠容納的L4載荷,所以需要在MTU基礎上去掉IP首部的開銷mtu = dst_mtu(&rt->u.dst) - hlen;// 設置IPCB(skb)->flags |= IPSKB_FRAG_COMPLETE;/* When frag_list is given, use it. First, check its validity:* some transformers could create wrong frag_list or break existing* one, it is not prohibited. In this case fall back to copying.** LATER: this step can be merged to real generation of fragments,* we can switch to copy when see the first bad fragment.*//** 如果4層將數(shù)據(jù)包分片了,那么就會把這些數(shù)據(jù)包放到skb的frag_list鏈表中,* 因此這里首先先判斷frag_list鏈表是否為空,為空的話將會進行slow 分片*/if (skb_shinfo(skb)->frag_list) {struct sk_buff *frag;/** 取得第一個數(shù)據(jù)報的len.當sk_write_queue隊列被flush后,* 除了第一個切好包的另外的包都會加入到frag_list中(接口ip_push_pending_frames),* 而這里需要得到的第一個包(也就是本身這個sk_buff)的長度。* first_len是第一個skb中所有數(shù)據(jù)的總長度,包括線性緩沖區(qū)和frags[]數(shù)組*/int first_len = skb_pagelen(skb);int truesizes = 0;/** 接下來的判斷都是為了確定能進行fast分片。分片不能被共享,* 這是因為在fast path 中,需要加給每個分片不同的ip頭(而并* 不會復制每個分片)。因此在fast path中是不可接受的。而在* slow path中,就算有共享也無所謂,因為他會復制每一個分片,* 使用一個新的buff。 *//** 判斷第一個包長度是否符合一些限制(包括mtu,mf位等一些限制).* 如果第一個數(shù)據(jù)報的len沒有包含mtu的大小這里之所以要把第一個* 切好片的數(shù)據(jù)包單獨拿出來檢測,是因為一些域是第一個包所獨有* 的(比如IP_MF要為1)。這里由于這個mtu是不包括hlen的mtu,因此* 需要減去一個hlen。 */// 1. 條件1說明高層協(xié)議切割的skb的長度還是太長了;// 2. 第一個片段長度必須是8字節(jié)對齊的(IP片段偏移量是8字節(jié)對齊決定的);// 3. 偏移量在下面的分段過程中才會進行設置,這里不應該有值;// 4. skb不能是被共享的,因為快速路徑上不會進行skb拷貝,而是直接修改skb;// 上述4個條件有任何一個不滿足,那么就用慢速路徑完成分片if (first_len - hlen > mtu || ((first_len - hlen) & 7) ||(iph->frag_off & htons(IP_MF|IP_OFFSET)) || skb_cloned(skb)){goto slow_path;}// 遍歷frag_list列表,檢查是否所有的分片是否符合快速分片處理for (frag = skb_shinfo(skb)->frag_list; frag; frag = frag->next) {// 這4個條件和前面對第一個片段的檢查類似if (frag->len > mtu || ((frag->len & 7) && frag->next) || skb_headroom(frag) < hlen)goto slow_path;if (skb_shared(frag))goto slow_path;BUG_ON(frag->sk);// 設置skb的屬主if (skb->sk) {sock_hold(skb->sk);frag->sk = skb->sk;frag->destructor = sock_wfree;truesizes += frag->truesize;}}// 對第一個分片的特殊處理err = 0;offset = 0;frag = skb_shinfo(skb)->frag_list;skb_shinfo(skb)->frag_list = NULL;skb->data_len = first_len - skb_headlen(skb);skb->truesize -= truesizes;skb->len = first_len;iph->tot_len = htons(first_len);iph->frag_off = htons(IP_MF);ip_send_check(iph);// 循環(huán)處理后面所有的分片for (;;) {if (frag) {frag->ip_summed = CHECKSUM_NONE;skb_reset_transport_header(frag);__skb_push(frag, hlen);skb_reset_network_header(frag);// 拷貝IP首部memcpy(skb_network_header(frag), iph, hlen);iph = ip_hdr(frag);iph->tot_len = htons(frag->len);ip_copy_metadata(frag, skb);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);}// 繼續(xù)發(fā)送過程,先發(fā)送首片ip的skb自己,后面繼續(xù)發(fā)送frag。err = output(skb);if (!err)IP_INC_STATS(IPSTATS_MIB_FRAGCREATES);if (err || !frag)break;// 繼續(xù)處理下一個分片skb = frag;frag = skb->next;skb->next = NULL;}// 一切正常,分片過程從這里返回if (err == 0) {IP_INC_STATS(IPSTATS_MIB_FRAGOKS);return 0;}// 分片或者發(fā)送過程失敗了,釋放所有的skb分片while (frag) {skb = frag->next;kfree_skb(frag);frag = skb;}// 快速路徑結束IP_INC_STATS(IPSTATS_MIB_FRAGFAILS);return err;}//end of (skb_shinfo(skb)->frag_list)slow_path:// left保存整個IP報文中剩余需要分段的報文長度,在下面分段過程中會逐漸減小,// 直到為0說明分段過程結束left = skb->len - hlen; /* Space per frame */// ptr指向L4載荷的偏移,初始值指向L4報文的開頭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*/// L2的特殊使用,處理橋接、VLAN、PPPOE相關MTU,這里認為pad=0即可pad = nf_bridge_pad(skb);// link layer reserved space,即鏈路層保留長度,是指應該在skb線性緩沖區(qū)的首部// 應該為L2保留的長度,主要包括mac層首部ll_rs = LL_RESERVED_SPACE_EXTRA(rt->u.dst.dev, pad);mtu -= pad;// offset為偏移量,不包括DF、MF標記offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;// 顧名思義,標識是否是IP報文的最后一個片段,因為最后一個片段的MF標記是0not_last_frag = iph->frag_off & htons(IP_MF);// 循環(huán)創(chuàng)建新的skb2,然后拷貝數(shù)據(jù),完成分段(left==0)while (left > 0) {// 調(diào)整len為本輪循環(huán)分片中能夠容納的L4報文載荷長度len = left;/* IF: it doesn't fit, use 'mtu' - the data space left */if (len > mtu)len = mtu;// IP報文首部的片偏移字段格式?jīng)Q定了非最后一個IP片段的偏移量必須是8字節(jié)對齊的if (len < left) {len &= ~7;}// 分配skb2,長度包括三部分:len代表的L4報文部分;hlen代表的IP報文首部;ll_rs代表的L2報文首部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;}// 首先設置skb2中的各個字段ip_copy_metadata(skb2, skb);skb_reserve(skb2, ll_rs);skb_put(skb2, len + hlen);skb_reset_network_header(skb2);skb2->transport_header = skb2->network_header + hlen;// 設置owner,內(nèi)存的消耗將會記錄到owner的賬上if (skb->sk)skb_set_owner_w(skb2, skb->sk);// 先從源skb的線性緩沖區(qū)將IP報文首部拷貝到skb2的線性緩沖區(qū),因為內(nèi)存上是連續(xù)的,// 所以直接使用memcpy拷貝即可skb_copy_from_linear_data(skb, skb_network_header(skb2), hlen);// 下面的數(shù)據(jù)拷貝就復雜了一些,因為慢速路徑要能夠處理任何可能的skb的數(shù)據(jù)組織方式。// skb_copy_bits()將從skb->data開始偏移的ptr位置開始拷貝數(shù)據(jù),共拷貝len字節(jié)到// skb2傳輸層開始的位置,注意這里在處理ptr偏移會考慮頁緩沖區(qū)和frag_list兩種情況if (skb_copy_bits(skb, ptr, skb_transport_header(skb2), len))BUG();// 分段完成,left減去1個片段的長度left -= len;// 填充IP首部,主要是選項和偏移字段iph = ip_hdr(skb2);iph->frag_off = htons((offset >> 3));// ip_options_fragment()會在第一個IP片段的基礎上將后面片段不需要的IP選項刪除,// 這樣后面的IP片段直接繼承即可(前面已經(jīng)拷貝),無需重新設定選項,所以這里只// 在第一個片段分片過程中調(diào)用ip_options_fragment()if (offset == 0)ip_options_fragment(skb);//不是最后一個包,因此設置mf位 if (left > 0 || not_last_frag)iph->frag_off |= htons(IP_MF);// 更新ptr和offset,為下一個分片做好準備ptr += len;offset += len;// IP首部的total字段表示的是IP片段的長度iph->tot_len = htons(len + hlen);// 計算IP首部校驗和ip_send_check(iph);// 調(diào)用發(fā)送接口繼續(xù)發(fā)送過程err = output(skb2);if (err)goto fail;IP_INC_STATS(IPSTATS_MIB_FRAGCREATES);}// 原有的IP報文都已經(jīng)被分割成一個個新的skb,所以處理結束后,原來的skb需要釋放kfree_skb(skb);IP_INC_STATS(IPSTATS_MIB_FRAGOKS);return err; fail:kfree_skb(skb);IP_INC_STATS(IPSTATS_MIB_FRAGFAILS);return err; }?
總結
以上是生活随笔為你收集整理的linux协议栈 IPv4之发送过程中的分段处理ip_fragment()的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java 兑换码生成_java兑换码生成
- 下一篇: linux 重置bios密码忘记,Win