由於4層協議實現複雜度的不對稱性,導致3層協議實現也不易統一,換句話說就是同樣的3層協議比如IP要為不同的4層協議提供不同的實現,這是因為我們熟知的4層協議分為流和資料報兩種類型,流式協議比如tcp在4層就處理了大量的邏輯,比如分段等等,而資料報協議比如 udp卻不處理這些,因此當它們被交付到3層的時候,針對於分段來講,3層邏輯對tcp需要作的事就很少了,而對udp就要有大量的工作要做,這就導致了對於tcp來說,只需要調用簡單的ip_queue_xmit即可,而對於udp來說,就需要調用更複雜的 udp_push_pending_frames。從名稱上看,pending一詞表明,該函數並不是即時調用的,可能4層協議邏輯儘可能多的將udp資料報填充之後再發送到3層的,事實確實是這樣的,然而對於tcp來講也有pending一說,意義是一樣的。總之,udp的3層實現更複雜一些,理由就是它的4層實現太簡單,而3層的複雜邏輯比如分區是怎麼也逃不掉的。
udp的4層發送函數是udp_sendmsg,它進一步又調用了:
err = ip_append_data(sk, ip_generic_getfrag, msg->msg_iov, ulen,
sizeof(struct udphdr), &ipc, rt,
corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);
if (err)
udp_flush_pending_frames(sk);
else if (!corkreq)
err = udp_push_pending_frames(sk, up);
可見,調用udp_push_pending_frames將資料報發往3層是有條件的,要麼應用程式層強制即時發送不緩衝(通過setsockopt的CORK命令),要麼緩衝已經滿了,事實就是,如果你不強制,那麼就請尊重核心的決定,任何事情總要有個預設方案的。下面看一下ip_append_data的實現邏輯,它非常複雜,複雜之處在於它協助3層實現了很多它力所能及或者說是舉手之勞的事情,那就是預分區操作。雖然它不負責3層的ip分區這件事,但是它卻可以做一些事情使得接下來總逃不過的ip分區更加容易,更加有效率,ip_append_data函數首先將使用者傳進的資料按照尋找到的路由出口mtu分成一個個的小段,然後將這些小段組合,組合的方式根據是否啟用分散/聚集IO有兩種方式,如果不啟用分散/聚集IO,那麼將所有的小段串連成一個鏈表,如果啟用了,那麼就將第二個到最後一個的片段植入到skb的skb_shinfo(skb)->frags數組中。這隻是大體上的流程,細節上稍微複雜一些,在分配記憶體的時候,該函數根據路由出口考慮到了2層的協議頭,它預留了協議頭大小的空間,並且如果啟用了分散/聚集IO的話,它將不再為每一個mtu大小的資料(加上頭)分配一個skb,而是將後續的資料填充到當前skb的 frags數組中,這樣在最終網卡發送的時候,只要將這些frags映射到裝置空間就可以了。
現在有一個問題,udp_sendmsg中為何要將ip_append_data和udp_push_pending_frames分開呢?實際上這增加了應用程式對底層的控制,udp套結字有一個UDP_CORK的選項,在這個選項置為1的情況下,應用程式層的資料是不會被發出去的,只有在這個選項置為0的時候才會發送資料,這就實現了累積的發送,同時這個特性會影響到ip_append_data的記憶體配置,ip_append_data本質上就是幫ip層忙的,如果UDP_CORK為1的話,ip_append_data的falg參數將會加上MSG_MORE,這樣在分配記憶體的時候就會分配一個mtu的大小而不僅僅是當前資料的大小,因為它知道馬上還會有資料來,即使當前的資料長度沒有一個mtu的大小,接下來的資料還是可以使用剩餘空間的,但是如果啟用了分散/聚集IO的話就不能這樣了,因為分散 /聚集IO的本質就是“呆在原地”:
if ((flags & MSG_MORE) && !(rt->u.dst.dev->features&NETIF_F_SG))
alloclen = maxfraglen;
另外的一個特性是,如果路由出口網卡啟用了分散/聚集IO,那麼就不是往skb的剩餘空間塞資料了,而是往page的剩餘空間塞資料:
if (!(rt->u.dst.dev->features&NETIF_F_SG)) {
unsigned int off;
off = skb->len;
if (getfrag(from, skb_put(skb, copy), offset, copy, off, skb) < 0) {
...
}
} else {
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;
if (page && (left = PAGE_SIZE - off) > 0) {
if (page != frag->page) {
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) {
page = alloc_pages(sk->sk_allocation, 0);
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];
skb->truesize += PAGE_SIZE;
atomic_add(PAGE_SIZE, &sk->sk_wmem_alloc);
}
...
if (getfrag(from, page_address(frag->page)+frag->page_offset+frag->size, offset, copy, skb->len, skb) < 0)
...
分散/聚集IO儘可能的不拷貝資料,它儘可能的將資料集中在整個頁面內部。
分離了ip_append_data和udp_push_pending_frames,應用程式可以多次調用ip_append_data,然後一次性發送,以此可以控制資料收發的響應速度和輸送量。對於tcp來講,它也有一個TCP_CORK選項,然而這個cork和udp的意義不同,tcp的cork並沒有強制性,也就是說就算你設定了cork,資料在一定條件下也是會自動發出去的,道理在於,首先tcp的實現要遵從它的協議標準,然後再考慮效率最佳化和應用程式定製,而cork就是為了最佳化和定製而產生的,因此它也就是只能在毫無串連和任何控制機制的udp協議上實施專制和獨裁。