日期:2014-05-16  浏览次数:20847 次

TCP和UDP在网络层实现的不同--基于linux内核 --转

????? 由于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协议上实施专制和独裁。

?

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/dog250/archive/2010/10/13/5939241.aspx