/* * ip_append_data() and ip_append_page() can make one large IP datagram * from many pieces of data. Each pieces will be holded on the socket * until ip_push_pending_frames() is called. Each piece can be a page * or non-page data. * * Not only UDP, other transport protocols - e.g. raw sockets - can use * this interface potentially. * * LATER: length must be adjusted by pad at tail, when it is required. */ int ip_append_data(struct sock *sk, int getfrag(void *from, char *to, int offset, int len, int odd, struct sk_buff *skb), void *from, int length, int transhdrlen, struct ipcm_cookie *ipc, struct rtable *rt, unsigned int flags) { struct inet_sock *inet = inet_sk(sk); struct sk_buff *skb; struct ip_options *opt = NULL; int hh_len; int exthdrlen; int mtu; int copy; int err; int offset = 0; unsigned int maxfraglen, fragheaderlen; int csummode = CHECKSUM_NONE; //.若指定了MSG_PROBE標誌則返回,這個標誌在之前記得在manpage send中有說明,如今怎麼又找不到了。。 if (flags&MSG_PROBE) return 0; /*.檢測本段數據是否是此ip包的第一個分片,若是是第一個分片則處理ip選項 (從sk->sk_write_queue檢測,這個隊列用來儲存一個ip包的全部分片,若是爲空則說明此段數據爲第一個分片) (1)是第一個分片:從函數參數中獲取一些信息保存在cork中,cork是一個緩存通常用來存儲些首部信息,之後的分片再進來就能夠直接使用裏面的信息,由於對於同一個ip包來講,這些信息都是相同的。從傳入的ipc中獲取ip選項而且將選項緩存在sock->cork.opt中,以便後來的分片以及ip_push_pending_frames使用。將參數rt的目標路由項也保存在cork.dst中,從路由項中獲取mtu存入cork.fragsize。若是exthdrlen(IPSec首部的長度)不爲0,則將參數length和transhdrlen都加上exthdrlen,算是爲exthdr預留空間。 (2)不是第一個分片:從cork中獲取到剛纔保存的信息而且從cork.dst中讀取到目標路由項,ip選項,mtu。同時將transhdrlen和exthdrlen都清0(由於L4首部和IPSec首部都只存在於第一個分片中)*/ if (skb_queue_empty(&sk->sk_write_queue)) { /* setup for corking.*/ opt = ipc->opt; if (opt) { if (inet->cork.opt == NULL) { inet->cork.opt = kmalloc(sizeof(struct ip_options) + 40, sk->sk_allocation); if (unlikely(inet->cork.opt == NULL)) return -ENOBUFS; } memcpy(inet->cork.opt, opt, sizeof(struct ip_options)+opt->optlen); inet->cork.flags |= IPCORK_OPT; inet->cork.addr = ipc->addr; } dst_hold(&rt->u.dst); inet->cork.fragsize = mtu = inet->pmtudisc == IP_PMTUDISC_PROBE ? rt->u.dst.dev->mtu : dst_mtu(rt->u.dst.path); inet->cork.dst = &rt->u.dst; inet->cork.length = 0; //sk_sndmsg_page指向當前正在使用的頁面,sk_sndmsg_off是新數據應該存放的偏移。 //有新的數據就繼續放入這個頁面的這個偏移,放不下了就再分配一個而且將sk_sndmsg_page指向它 sk->sk_sndmsg_page = NULL; sk->sk_sndmsg_off = 0; if ((exthdrlen = rt->u.dst.header_len) != 0) { length += exthdrlen; transhdrlen += exthdrlen; } } else { rt = (struct rtable *)inet->cork.dst; if (inet->cork.flags & IPCORK_OPT) opt = inet->cork.opt; transhdrlen = 0; exthdrlen = 0; mtu = inet->cork.fragsize; } //根據目標路由項的發送設備計算出的須要爲L2首部預留的最大長度 hh_len = LL_RESERVED_SPACE(rt->u.dst.dev); //ip首部加上選項的總長度 fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0); //一個分片的最大長度,注意分片的數據部分的長度必須八字節對齊 maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen; //一個ip封包的最大長度爲64k(0xffff),若是cork.length(累積的全部ip分片總長度)+length(當前分片長度)超過0xffff則出錯 if (inet->cork.length + length > 0xFFFF - fragheaderlen) { ip_local_error(sk, EMSGSIZE, rt->rt_dst, inet->dport, mtu-exthdrlen); return -EMSGSIZE; } /* transhdrlen > 0 means that this is the first fragment and we wish * it won't be fragmented in the future.*/ //若是知足如下條件則將csummode賦值爲CHECKSUM_PARTIAL(做用尚不明白): //1.transhdrlen不爲0(說明是第一個分片); //2.length+fragheaderlen<=mtu說明當前的ip包能夠整個發出去,不需分片; //3.目標設備特性有NETIF_F_V4_CSUM(支持L4層的硬件校驗和); //4.exthdrlen等於0(沒有IPSec首部) if (transhdrlen && length + fragheaderlen <= mtu && rt->u.dst.dev->features & NETIF_F_V4_CSUM && !exthdrlen) csummode = CHECKSUM_PARTIAL; //從這裏能夠看出cork.length存儲的是當前ip封包的總長度 inet->cork.length += length; if (((length> mtu) || !skb_queue_empty(&sk->sk_write_queue)) && (sk->sk_protocol == IPPROTO_UDP) && (rt->u.dst.dev->features & NETIF_F_UFO)) { err = ip_ufo_append_data(sk, getfrag, from, length, hh_len, fragheaderlen, transhdrlen, mtu, flags); if (err) goto error; return 0; } /* So, what's going on in the loop below? * We use calculated fragment length to generate chained skb, * each of segments is IP fragment ready for sending to network after * adding appropriate IP header.*/ //從sk->sk_write_queue取出末尾元素,賦給skb,若爲空,則說明此爲第一個分片,直接跳到alloc_new_skb if ((skb = skb_peek_tail(&sk->sk_write_queue)) == NULL) goto alloc_new_skb; //主循環將數據拷貝入緩衝區,若是不夠的話隨時能夠分配新的skb或者page,直到length==0就是拷貝完成 while (length > 0) { /* Check if the remaining data fits into current packet. */ /*計算本次循環容許拷貝的數據量,copy = mtu - skb->len(注意這時skb是上一個使用過的skb,這裏是看看給上一個skb分配的buff還有沒有剩餘空間)。若是剩餘空間小於待拷貝數據量(copy<length),則令copy = maxfraglen - skb->len(須要注意mtu和maxfraglen的區別,maxfraglen是知足數據部分8字節對齊的狀況下的最大報文長度,而mtu無須知足8字節對齊,所以maxfraglen的範圍在[mtu-7,mtu]。這裏是判斷上一個skb中有沒有須要移動到本次skb中的數據,由於上一個skb可能沒有8字節對齊,這時就要移動一部分數據到這個skb)。這裏可能產生非8字節對齊的狀況。這時copy三種狀況: (1)copy>0,說明上一個skb還有必定空間,數據能夠拷貝到上一個skb中去。 (2)copy=0.說明上一個skb恰好填滿,這時須要從新分配skb填入新數據。 (3)copy<0,說明上一個skb已經填滿,可是末尾有些數據不是8字節對齊,須要拷貝到新分配的skb中去。 對於第一種狀況,跳過度配skb的步驟,直接向上一個skb拷貝數據,後兩種狀況則須要從新分配skb。*/ copy = mtu - skb->len; if (copy < length) copy = maxfraglen - skb->len; if (copy <= 0) { char *data; //指向當前拷貝數據的目的地址 unsigned int datalen; //本次循環所需拷貝的數據長度,不包括ip頭 unsigned int fraglen; //此ip分片的長度,包括ip頭、ip選項和數據,通常等於datalen+fragheaderlen unsigned int fraggap; //須要從上一個skb尾部移動到新skb開頭的數據長度,爲了保證ip分片的8字節對齊 unsigned int alloclen; //須要分配的skb數據部分長度(除去L2頭部,實際分配時還要加上L2首部長度), //若是支持S/G則須要多少就恰好分配多少,若不支持S/G或者有MSG_MORE則分配mtu大小 struct sk_buff *skb_prev; //老是指向前一個skb,若是這時正在處理第一個skb,則它爲空指針 alloc_new_skb: skb_prev = skb; if (skb_prev) //檢查有多少數據須要從前一個skb移動到新skb來保證8字節對齊 fraggap = skb_prev->len - maxfraglen; else fraggap = 0; /* If remaining data exceeds the mtu, * we know we need more fragment(s). */ /*先令datalen = length + fraggap,爲待拷貝數據總長度, 若是大於一分片所能容納的數據 mtu - fragheaderlen,則令datalen=maxfraglen - fragheaderlen, 注意這裏也可能產生8字節不對齊的狀況,就是datalen雖然不大於mtu-fragheaderlen可是大於了maxfraglen-fragheaderlen, 能夠看出這種狀況下最後一個分片彷佛沒有遵照8字節對齊。 (附:通過實測,ip分片的最後一個分片容許不8字節對齊,由於offset是8字節對齊, 因此只要求非最後分片的報文長度是8字節對齊,所以這裏若是能直接放下全部數據就能夠不檢測對齊)。*/ datalen = length + fraggap; if (datalen > mtu - fragheaderlen) datalen = maxfraglen - fragheaderlen; fraglen = datalen + fragheaderlen; //若是用戶還要輸入數據(MSG_MORE)而且網卡不支持S/G的話,令alloclen=mtu, //算是爲後來的數據預先準備空間。不然只令alloclen = datalen + fragheaderlen。 if ((flags & MSG_MORE) && !(rt->u.dst.dev->features&NETIF_F_SG)) alloclen = mtu; else alloclen = datalen + fragheaderlen; /* The last fragment gets additional space at tail. * Note, with MSG_MORE we overallocate on fragments, * because we have no idea what fragment will be * the last.*/ /*若是當前分片是最後一個分片(datalen == length + fraggap),那麼alloclen還要加上rt->u.dst.trailer_len, 註釋中寫道最後一個分片須要在末尾留出一些空間,猜想多是爲某種協議的尾部預留空間。 我的以爲這裏只能判斷是當前發送請求的最後一個分片,不能判斷是否是整個ip封包的最後分片, 徹底有可能當前指定了MSG_MORE,後面還要來數據),*/ if (datalen == length + fraggap) alloclen += rt->u.dst.trailer_len; //若是當前是第一個分片(transhdrlen!=0),那麼調用sock_alloc_send_skb; //不然調用skb = sock_wmalloc(sk,alloclen + hh_len + 15, 1,sk->sk_allocation); //從參數能夠看出,分配的buf長度爲ip首部長度加上數據長度加上尾部長度(這三個包含在alloclen)加上l2首部長度, //另外加上15估計是爲了爲某種對齊預留空間。 if (transhdrlen) { skb = sock_alloc_send_skb(sk, alloclen + hh_len + 15, (flags & MSG_DONTWAIT), &err); } else { skb = NULL; if (atomic_read(&sk->sk_wmem_alloc) <= 2 * sk->sk_sndbuf) skb = sock_wmalloc(sk, alloclen + hh_len + 15, 1, sk->sk_allocation); if (unlikely(skb == NULL)) err = -ENOBUFS; } if (skb == NULL) goto error; /* * Fill in the control structures */ /*對剛分配的skb進行初始化,操做包括: ip_summed設置爲csummode(此時可能爲CHECKSUM_PARTIAL或CHECKSUM_NONE)。 校驗和skb->csum設爲0,在頭部預留hh_len空間給L2首部。 接下來在中間留出fraglen大小的數據部分用來存放ip數據,同時將局部變量data指向skb傳輸層(包括IPSec)的開頭, 將skb->network_header指向skb->data+exthdrlen,將skb->transport_header指向skb->network_header + fragheaderlen。*/ skb->ip_summed = csummode; skb->csum = 0; skb_reserve(skb, hh_len); /*Find where to start putting bytes. */ data = skb_put(skb, fraglen); skb_set_network_header(skb, exthdrlen); skb->transport_header = (skb->network_header + fragheaderlen); data += fragheaderlen; //若是fraggap不爲0,說明有些數據須要從上一個skb拷貝到當前skb, //這時進行拷貝而且從新計算上一個skb和本skb的ip校驗和,同時將data指針後移越過剛拷貝的fraggap數據 if (fraggap) { skb->csum = skb_copy_and_csum_bits( skb_prev, maxfraglen, data + transhdrlen, fraggap, 0); skb_prev->csum = csum_sub(skb_prev->csum, skb->csum); data += fraggap; pskb_trim_unique(skb_prev, maxfraglen); } //從新計算copy,看起來應該是L4的負載部分長度,不知道爲什麼要跳過傳輸層首部, //若是copy>0說明傳輸層有數據須要拷貝,調用getfrag將L4數據拷貝到skb中去, //這裏getfrag能夠參考ip_generic_getfrag,注意這裏的getfrag的from參數有多是用戶空間的指針 copy = datalen - transhdrlen - fraggap; if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0) { err = -EFAULT; kfree_skb(skb); goto error; } //offset爲還沒有拷貝的用戶數據的起始地址偏移 //這裏copy並不等於datalen - fraggap,參考上面幾行,copy = datalen - transhdrlen - fraggap //能夠看到getfrag的第二個參數也跳過了transhdrlen。從參數from來看,明明拷貝了L4首部,可是長度卻沒計算L4首部,不知爲什麼 offset += copy; length -= datalen - fraggap; transhdrlen = 0; exthdrlen = 0; csummode = CHECKSUM_NONE; /* * Put the packet on the pending queue. */ __skb_queue_tail(&sk->sk_write_queue, skb); continue; } //處理copy>=length也就是說上一個skb的空間末尾剩餘空間大於帶拷貝數據的狀況, //這種狀況無須新分配skb,而能夠直接使用上一個skb末尾的空閒空間 if (copy > length) copy = length; //下面根據發送設備是否支持S/G而分爲兩條路, if (!(rt->u.dst.dev->features&NETIF_F_SG)) { //不支持S/G說明上一個skb必定沒有使用frags分片保存數據,而是所有保存在skb的主buf中, //所以能夠直接調用getfrag將數據拷貝到上一個skb的剩餘空間處。 unsigned int off; off = skb->len; if (getfrag(from, skb_put(skb, copy), offset, copy, off, skb) < 0) { __skb_trim(skb, off); err = -EFAULT; goto error; } } else { //支持S/G說明上一個skb是分frags存放的,而上一個skb的數據小於mtu,則能夠向上一個skb添加數據, //對於支持SG的網卡來講,這時不必定有剩餘空間,要看上一個分配的page還有沒有空位,若是沒有空位就要從新分配page存放新數據 //首先從skb_shared_info中取得nr_frags,而後在從skb_shared_info中取得第nr_frags-1(即最後一個)frag, //而後從sk->sk_sndmsg_page取得緩存在sock中的頁面,這個緩存頁是用來保存最近使用過的頁面的, //再從sk->sk_sndmsg_off取得該緩存頁面空閒空間的偏移指針 int i = skb_shinfo(skb)->nr_frags; skb_frag_t *frag = &skb_shinfo(skb)->frags[i-1]; struct page *page = sk->sk_sndmsg_page; int off = sk->sk_sndmsg_off; unsigned int left; //若是緩存頁面存在而且頁面上的剩餘空間大於0: if (page && (left = PAGE_SIZE - off) > 0) { //當left>0 //判斷待拷貝數據(copy)是否大於剩餘空間(left),若不大於則令copy=left。 //而後看sock中的緩存頁(page)是否等於skb_shared_info中的最後一個頁面, //若不等(這種狀況爲何會發生?)則將page引用計數+1而且添加到skb_shared_info的frags末尾,frag指向新frag if (copy >= left) copy = left; if (page != frag->page) { if (i == MAX_SKB_FRAGS) { err = -EMSGSIZE; goto error; } get_page(page); skb_fill_page_desc(skb, i, page, sk->sk_sndmsg_off, 0); frag = &skb_shinfo(skb)->frags[i]; } } else if (i < MAX_SKB_FRAGS) { //當left==0且i<MAX_SKB_FRAGS,這時會從新分配一個頁面而且添加到sock的緩存頁以及skb_shared_info的frags末尾,frag指向新frag if (copy > PAGE_SIZE) copy = PAGE_SIZE; page = alloc_pages(sk->sk_allocation, 0); if (page == NULL) { err = -ENOMEM; goto error; } sk->sk_sndmsg_page = page; sk->sk_sndmsg_off = 0; skb_fill_page_desc(skb, i, page, 0, 0); frag = &skb_shinfo(skb)->frags[i]; } else { //left==0而且i>=MAX_SKB_FRAGS,出錯返回-EMSGSIZE err = -EMSGSIZE; goto error; } //通過以上處理應該都已經有空間了,雖然不必定能容納下全部數據,這時調用getfrag將數據拷貝入目標頁面 if (getfrag(from, page_address(frag->page)+frag->page_offset+frag->size, offset, copy, skb->len, skb) < 0) { err = -EFAULT; goto error; } //拷貝完成,調整指針,所有加上copy大小 sk->sk_sndmsg_off += copy; frag->size += copy; skb->len += copy; skb->data_len += copy; skb->truesize += copy; atomic_add(copy, &sk->sk_wmem_alloc); } offset += copy; length -= copy; } return 0; error: inet->cork.length -= length; IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTDISCARDS); return err; }