首页 > 代码库 > ip_append_data

ip_append_data

/*
 *    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;
}&nbsp;


ip_append_data