发送ip_append_data函数分析

因为这个函数实在比较长,也有些复杂,专门做些注释,防止以后自己忘了
其实结合ULNI的图很快就能看懂,但是那几张图实在懒得贴上来了,自己到该书的21章去找吧

看完了肯定会佩服作者用图说明问题的能力的,呵呵

首先简单介绍几个输入参数
sk:             要发送数据的sock结构
getfrag:        在把数据从from拷贝到新的buffer中时,根据L4协议的不同还有些不同的情况需要处理,比如tcp计算校验和等,为了所有L4都能使用ip_append_data函数,在这里加入getfrag作为参数来让上层协议决定自己如何拷贝。
from:           L4及以上层数据,包括L4头部,可能是来自用户空间的。注意这是一个struct iovec指针,而不是直接的数据指针
length:         from所指向的数据的长度(包括L4头部)
transhdrlen:    L4头部长度
ipc:            发送ip包所需要的信息,包括首部字段的值,选项等等
rt:             目标路由项,在ip_queue_xmit中,这一项是由自己确定的,此函数要求上层完成路由选择
flags:          send的最后一个参数,

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

差不多就这样吧,这个函数一般是udp使用

你可能感兴趣的:(struct,header,null,NetWork,protocols,Allocation)