IP 层收发报文简要剖析5--ip报文发送2
Posted codestack
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了IP 层收发报文简要剖析5--ip报文发送2相关的知识,希望对你有一定的参考价值。
udp 发送ip段报文接口ip_append_data
ip_append_data 函数主要用来udp 套接字以及raw套接字发送报文的接口。在tcp中发送ack 以及rest段的ip_send_unicast_reply也会调用;其主要作用是将收到的大数据报文拆分成多个等于小于MTU的SKB,为网络层实现ip分片做准备。
ip_append_data 在udp tcp raw 套接字以及icmp 都有被调用到,因此复制数据时有时只需要复制传输层负载部分;此函数并不传输数据,只是将数据放在大小合适的一个缓冲区中,让后续的函数可以借此构成一些片段(必要的话)并进行传输。所以次函数并不建立或操作任何ip报头。要把数据报文显示的传输,需要调用ip_push_pending_frames(会处理ip报头)才可以。如果L4层想要快速的发送报文,每次调用ip_append_data后,就需要调用ip_push_pending_frames 但是此函数是为了L4可以可以将尽可能的把多一点的数据存暂时放在缓冲区里面(直到PMTU大小)。然后一次传输,这样效率更高。
tcp stcp 做了很多准备工作,而使得ip层处理的相对少,但是左边的raw ip 以及udp 等把所有的分段工作都留给了ip层
/* * 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. @transhdrlen :L4报头大小length:要传输的数量(包含L4报头和有效载荷) @ipc:正确发送封包的必须信息rtp:路由缓存 @sk:此次封包背后的套接字 @from:指向邋L4层的有效载荷 @flags: #define MSG_PROBE 0x10 /* Do not send. Only probe path f.e. for MTU #define MSG_DONTWAIT 0x40 /* Nonblocking io #define MSG_MORE 0x8000 /* Sender will send more 应用层使用 告诉L4层马上会有更多的其他传输 此标志会传输到L3 */ int ip_append_data(struct sock *sk, struct flowi4 *fl4, 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 **rtp, unsigned int flags) { struct inet_sock *inet = inet_sk(sk); int err; /*使用MSG_PROBE 并不会真正传输数据,而是进行路径MTU的探测*/ if (flags&MSG_PROBE) return 0; /* * 如果传输控制块的输出队列为空,则需要为传输控制块设置一些临时 * 信息。 */ if (skb_queue_empty(&sk->sk_write_queue)) { err = ip_setup_cork(sk, &inet->cork.base, ipc, rtp); if (err) return err; } else { /*队列不为空,则使用上次的路由,IP选项,以及分片长度 */ transhdrlen = 0;//传输层报头长度=0 } return __ip_append_data(sk, fl4, &sk->sk_write_queue, &inet->cork.base, sk_page_frag(sk), getfrag, from, length, transhdrlen, flags); }
2、
_ip_append_data
static int __ip_append_data(struct sock *sk, struct flowi4 *fl4, struct sk_buff_head *queue, struct inet_cork *cork, struct page_frag *pfrag, int getfrag(void *from, char *to, int offset, int len, int odd, struct sk_buff *skb), void *from, int length, int transhdrlen, unsigned int flags) { struct inet_sock *inet = inet_sk(sk); struct sk_buff *skb; struct ip_options *opt = cork->opt; int hh_len; int exthdrlen; int mtu; int copy; int err; int offset = 0; unsigned int maxfraglen, fragheaderlen, maxnonfragsize; int csummode = CHECKSUM_NONE; struct rtable *rt = (struct rtable *)cork->dst; u32 tskey = 0; /*这里skb有两种情况,如果队列为空, 则skb = NULL,否则为尾部skb的指针 */ skb = skb_peek_tail(queue); /*参考《understand linux network internal》图21-10*/ exthdrlen = !skb ? rt->dst.header_len : 0; mtu = cork->fragsize; if (cork->tx_flags & SKBTX_ANY_SW_TSTAMP && sk->sk_tsflags & SOF_TIMESTAMPING_OPT_ID) tskey = sk->sk_tskey++; hh_len = LL_RESERVED_SPACE(rt->dst.dev);/*链路层首部长度 */ /* * IP数据包的数据需4字节对齐,为加速计算直接将IP数据包的数据根据当前 * MTU 8字节对齐,然后重新得到用于分片的长度。 */ fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0);/* IP首部(包括IP选项)长度 */ maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen;/* 最大IP首部长度,注意对齐 */ maxnonfragsize = ip_sk_ignore_df(sk) ? 0xFFFF : mtu; /* * 如果输出的数据长度超出一个IP数据包能容纳的长度,则向输出该 *数据报的 套接字发送EMSGSIZE出错信息。 */ if (cork->length + length > maxnonfragsize - fragheaderlen) {/*一个IP数据包最大大小不能超过64K */ ip_local_error(sk, EMSGSIZE, fl4->daddr, inet->inet_dport, mtu - (opt ? opt->optlen : 0)); return -EMSGSIZE; } /* * transhdrlen > 0 means that this is the first fragment and we wish * it won‘t be fragmented in the future. */ /* * 如果IP数据包没有分片,且输出网络设备支持硬件执行校验和,则设置 * CHECKSUM_PARTIAL,表示由硬件来执行校验和。 */ if (transhdrlen && length + fragheaderlen <= mtu && rt->dst.dev->features & (NETIF_F_HW_CSUM | NETIF_F_IP_CSUM) && !(flags & MSG_MORE) && !exthdrlen)/*由硬件执行校验和计算 */ csummode = CHECKSUM_PARTIAL; cork->length += length;/*更新数据长度 */ /* 对于UDP报文,新加的数据长度大于MTU,并且需要进行分片,则需要 * 进行分片处理 * 这里相当于《understand linux network internel》图21-11最左边的那条支线 * 注意:这里需要加入判断skb是否为NULL*/ if (((length > mtu) || (skb && skb_is_gso(skb))) && (sk->sk_protocol == IPPROTO_UDP) && (rt->dst.dev->features & NETIF_F_UFO) && !rt->dst.header_len && (sk->sk_type == SOCK_DGRAM) && !sk->sk_no_check_tx) { err = ip_ufo_append_data(sk, queue, getfrag, from, length, hh_len, fragheaderlen, transhdrlen, maxfraglen, 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. */ if (!skb) goto alloc_new_skb; /* 参照《understand linux network internel》图21-11 * 主要可以分为4条支线,copy <= 0和copy > 0两种与是否设置NETIF_F_SG * 标志两种的组合。 * 这几种组合可以结合《understand linux network internel》图21-3~图21-6 * 来看。*/ while (length > 0) { /* Check if the remaining data fits into current packet. * 检测待发送数据是否能全部复制到最后一个SKB的剩余空间中。如果可以, * 则说明是IP分片中的上一个分片,可以不用4字节对齐,否则需要4字节 * 对齐,因此用8字节对齐后的MTU减去上一个SKB的数据长度,得到上一个 * SKB的剩余空间大小,也就是本次复制数据的长度. * 当本次复制数据的长度copy小于等于0时,说明上一个SKB已经填满或 * 空间不足8B,需要分配新的SKB。 * 当copy大于0时,说明上一个SKB有剩余空间,数据可以复制到该SKB中去。 *copy > 0 : 最后一个skb还有一些空余空间 * copy = 0 : 最后一个skb已经被填满 * copy < 0 : 有些数据必须从当前IP片段中删除移动到新的片段*/ copy = mtu - skb->len; if (copy < length) copy = maxfraglen - skb->len; /* * 如果上一个SKB已经填满或空间不足8B,或者不存在上一个SKB,则将数据复制到 * 新分配的SKB中去。 */ if (copy <= 0) { /* * 如果上一个SKB(通常是在调用ip_append_data()时, * 输出队列中最后一个SKB)中存在多余8字节对齐的MTU的数据, * 则这些数据需移动到当前SKB中,确保最后一个IP分片之外的 * 数据能够4字节对齐,因此需计算移动到当前SKB的数据长度。 */ char *data; unsigned int datalen; unsigned int fraglen; unsigned int fraggap; unsigned int alloclen; struct sk_buff *skb_prev; alloc_new_skb: skb_prev = skb; if (skb_prev)/*需要计算从上一个skb中复制到新的新的skb中的数据长度 */ fraggap = skb_prev->len - maxfraglen;/* 就是copy取反 */ else fraggap = 0; /* * If remaining data exceeds the mtu, * we know we need more fragment(s). */ /* * 如果剩余数据的长度超过MTU,则需要更多的分片。 */ /* * 计算需要复制到新SKB中的数据长度。因为如果前一个SKB * 还能容纳数据,则有一部分数据会复制到前一个SKB中。 */ datalen = length + fraggap; /* * 如果剩余的数据一个分片不够容纳,则根据MTU重新计算本次 * 可发送的数据长度。 */ if (datalen > mtu - fragheaderlen) datalen = maxfraglen - fragheaderlen; /* * 根据本次复制的数据长度以及IP首部长度,计算三层 * 首部及其数据的总长度 */ fraglen = datalen + fragheaderlen; /* * 如果后续还有数据要输出且网络设备不支持聚合分散I/O,则将 * MTU作为分配SKB的长度,使分片达到最长,为后续的数据 * 预备空间。否则按数据的长度(包括IP首部)分配SKB的空间 * 即可。 */ if ((flags & MSG_MORE) && !(rt->dst.dev->features&NETIF_F_SG)) alloclen = mtu; else alloclen = fraglen; alloclen += exthdrlen; /* 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. */ /* * 如果是最后一个分片,且是根据目的路由启用IPsec的情况, * 则可能需要多分配一些空间来支持IPsec。 */ if (datalen == length + fraggap) alloclen += rt->dst.trailer_len; /* * 根据是否存在传输层首部,确定用何种方法分配SKB。 * 如果存在传输层首部,则可以确定该分片为分片组中的 * 第一个分片,因此在分配SKB时需要考虑更多的情况,如 * 输出操作是否超时,传输层是否发生未处理的致命错误, * 发送通道是否已关闭等。当分片不是第一个分片时, * 则无需考虑以上情况 */ 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)) err = -ENOBUFS; } if (!skb) goto error; /* * Fill in the control structures */ /* * 填充用于校验的控制信息 */ skb->ip_summed = csummode;//设置校验位 skb->csum = 0; skb_reserve(skb, hh_len); /* * 为数据包预留用于存放二层首部、三层首部和数据的空间, * 并设置SKB中指向三层和四层的指针。 */ /* only the initial fragment is time stamped */ skb_shinfo(skb)->tx_flags = cork->tx_flags; cork->tx_flags = 0; skb_shinfo(skb)->tskey = tskey; tskey = 0; /* * Find where to start putting bytes. *///得到数据位置 data = skb_put(skb, fraglen + exthdrlen); /*预留L2,L3首部空间 */ skb_set_network_header(skb, exthdrlen);/*设置L3层的指针 */ //得到传输层的头部 skb->transport_header = (skb->network_header + fragheaderlen); data += fragheaderlen + exthdrlen; /* * 如果上一个SKB的数据超过8字节对齐MTU,则将超出数据和 * 传输层首部复制到当前SKB,重新计算校验和,并以8字节 * 对齐MTU为长度截取上一个SKB的数据。 */ if (fraggap) { /*填充原来的skb尾部的空间 */ 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 = datalen - transhdrlen - fraggap;//得到所需要拷贝的数据的大小 if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0) {//开始拷贝数据 err = -EFAULT; kfree_skb(skb); goto error; } offset += copy;/* 计算下次需要复制的数据长度*/ length -= datalen - fraggap; transhdrlen = 0; exthdrlen = 0; csummode = CHECKSUM_NONE; /* * Put the packet on the pending queue. */ __skb_queue_tail(queue, skb); /*将skb添加的尾部 */ continue; } if (copy > length) copy = length; if (!(rt->dst.dev->features&NETIF_F_SG)) { unsigned int off; /*不支持分散聚合,《understand linux netowrk internel》图21-11 中的分支,直接填充缓存*/ off = skb->len; if (getfrag(from, skb_put(skb, copy), offset, copy, off, skb) < 0) { __skb_trim(skb, off); err = -EFAULT; goto error; } } else { int i = skb_shinfo(skb)->nr_frags;
- //如果支持S/G I/O则开始进行相应操作
- ///i为当前已存储的个数。
err = -ENOMEM; if (!sk_page_frag_refill(sk, pfrag)) goto error; if (!skb_can_coalesce(skb, i, pfrag->page, pfrag->offset)) {/*已经分配了页面 */ err = -EMSGSIZE; if (i == MAX_SKB_FRAGS) goto error; //当剩余的空间不够放将要拷贝的数据时,则先将剩余的空间拷贝完毕。然后下次循环再进行拷贝剩下的。 __skb_fill_page_desc(skb, i, pfrag->page, pfrag->offset, 0); skb_shinfo(skb)->nr_frags = ++i; get_page(pfrag->page); } copy = min_t(int, copy, pfrag->size - pfrag->offset); if (getfrag(from, page_address(pfrag->page) + pfrag->offset, offset, copy, skb->len, skb) < 0) goto error_efault; pfrag->offset += copy; skb_frag_size_add(&skb_shinfo(skb)->frags[i - 1], 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_efault: err = -EFAULT; error: cork->length -= length; IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTDISCARDS); return err; } static int ip_setup_cork(struct sock *sk, struct inet_cork *cork, struct ipcm_cookie *ipc, struct rtable **rtp) { struct ip_options_rcu *opt; struct rtable *rt; /* * 如果传输控制块的输出队列为空,则需要为传输控制块设置一些临时 * 信息。 * 如果输出数据包中存在IP选项,则将IP选项信息复制到临时信息块中, * 并设置IPCORK_OPT,表示临时信息块中存在IP选项。由于存在IP选项, * 因此需要设置临时信息块中的目的地址,因为在IP选项中存在 * 源路由选项。 * 同时还设置了IP数据包分片大小,输出路由缓存、初始化当前发送 * 数据包中数据的长度(如果启用了IPsec,则还要加上IPsec首部的 * 长度)等。 */ /* * setup for corking. */ opt = ipc->opt; if (opt) { if (!cork->opt) { cork->opt = kmalloc(sizeof(struct ip_options) + 40, sk->sk_allocation); if (unlikely(!cork->opt)) return -ENOBUFS; } memcpy(cork->opt, &opt->opt, sizeof(struct ip_options) + opt->opt.optlen); cork->flags |= IPCORK_OPT; cork->addr = ipc->addr; } rt = *rtp; if (unlikely(!rt)) return -EFAULT; /* * We steal reference to this route, caller should not release it */ *rtp = NULL; cork->fragsize = ip_sk_use_pmtu(sk) ? dst_mtu(&rt->dst) : rt->dst.dev->mtu; cork->dst = &rt->dst; cork->length = 0; cork->ttl = ipc->ttl; cork->tos = ipc->tos; cork->priority = ipc->priority; cork->tx_flags = ipc->tx_flags; return 0; }
ip_append_page只被udp使用。tcp不使用ip_append_data和ip_push_pending_frams是因为它把一些逻辑放到tcp_sendmsg来实现了。因此相似的,0拷贝接口,tcp不使用ip_append_page是因为他在do_tcp_sendpage中实现了相同的逻辑。
报文转发示意图
此图来自(https://blog.csdn.net/lee244868149/article/details/77823276)
以上是关于IP 层收发报文简要剖析5--ip报文发送2的主要内容,如果未能解决你的问题,请参考以下文章
IP 层收发报文简要剖析2--ip报文的输入ip_local_deliver
IP 层收发报文简要剖析6--ip_forward 报文转发