因为这个函数实在比较长,也有些复杂,专门做些注释,防止以后自己忘了 其实结合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;
- }
|