linux内核协议栈 IPv4之发送接口: ip

it2025-03-30  16

目录

1 ip_append_data 功能说明

1.1 关键点

1.2 struct cork

2 ip报文数据区构造 ip_append_data()

3 ip报文发送(构造 ip 首部) ip_push_pending_frames()

4 清除发送队列 ip_flush_pending_frames()


1 ip_append_data 功能说明

1.1 关键点

该函数的设计目标是将要发送的数据按照便于IP协议分段的方式组织成一个个skb,它并不负责实际的发送,调用者需要主动调用ip_push_pending_frames()进行实际的发送;调用者可以通过连续多次调用该函数将多个数据片段合并成一个大的IP报文。这里要注意区分 IP 报文和 IP 片段的不同,传输层可以通过该函数组织一个很大的 IP 报文,但是该函数会负责根据MTU将其分割成一个个的 skb,所以准确来讲,skb 是和 IP 片段一一对应的,那么一个skb的载荷部分当然不能超过MTU;在组织 skb 时,如果 skb 仅仅是一个IP片段并且不是IP报文的最后一个IP片段时,该IP片段的总长度(包括IP首部【协议规定首部的大小必须是32bit的整数倍】)必须是8字节的整数倍,这也是下面代码中 maxfraglen 的取值。至于为何有这样的规定,目前不是特别理解。如下图所示,ip_append_data()在组织skb过程中,有可能会遇到需要将skb_prev中最后尾部的若干个字节拷贝到下一个skb中的情况;

1.2 struct cork

inet_sock中的 cork 成员非常关键,它影响了多次连续的 ip_append_data() 调用过程中该函数的执行流程。

struct inet_sock { ... struct { // 可取下面的IPCORK_OPT和IPCORK_ALLFRAG两个值的组合 unsigned int flags; // 记录一个IP片段可以容纳的数据量,其实就是mtu,之所以记录是为了不用每次都计算一遍 unsigned int fragsize; // 保存了IP选项和路由信息 struct ip_options *opt; struct rtable *rt; // 当前IP报文(注意不是IP片段)中已经放入的数据长度,初始化时为0 int length; /* Total length of all frames */ __be32 addr; struct flowi fl; } cork; }; #define IPCORK_OPT 1 /* ip-options has been held in ipcork.opt */ #define IPCORK_ALLFRAG 2 /* always fragment (for ipv6 for now) */

该结构作为ip_append_data()的一个入参,让高层协议将一些控制信息传递给ip_append_data()。

struct ipcm_cookie { // IP地址,UDP调用ip_append_data()时传递的是目的地址 __be32 addr; // 出口设备的网络设备索引 int oif; // IP选项 struct ip_options *opt; };

2 ip报文数据区构造 ip_append_data()

@getfrage()函数用于将L4的指定的数据拷贝到一个个的skb中;因为该函数会由多个L4协议公用,在执行拷贝时它们的动作时有所差异的(主要是校验和计算),所以这里作为参数由L4指定自己的拷贝函数 @from: 待拷贝数据的用户态起始地址 @length:待拷贝数据长度 @transhdrlen:传输层报文长度,对于UDP就是sizeof(struct udphdr) @ipc:临时的IP控制信息 @rtp:路由信息,调用者必须已经查询过路由 @flags:控制标记,这里我们关心的只有MSG_MORE 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 **rtp, unsigned int flags) { struct inet_sock *inet = inet_sk(sk); struct sk_buff *skb; struct ip_options *opt = NULL; int hh_len; // 扩展首部,由IPSec使用,这种协议是在普通的IP报文的基础上添加自己的头和尾, // 会占用MTU空间,因此在组织skb时需要考虑。这里我们认为不启用IPSec,即这个 // 变量为0,这种情况下MAC首部直接添加到在IP报文的前面 int exthdrlen; int mtu; int copy; int err; int offset = 0; unsigned int maxfraglen, fragheaderlen; int csummode = CHECKSUM_NONE; struct rtable *rt; // MSG_PROBE标记的作用是让用户态查询当前是否有数据可读, // 即只用于接收过程,发送过程指定该标记视为错误 if (flags & MSG_PROBE) return 0; // 下面的分支是为了确定局部变量opt、rt、mtu、exthdrlen的值 if (skb_queue_empty(&sk->sk_write_queue)) { // 如果发送队列为空,说明是第一次调用该函数。因为调用者有可能后续还会很快再调用 // 该函数继续追加数据,所以这里对IP选项、路由等信息进行缓存(即所谓的cork),这样 // 下次就不用重新分配了 // IP选项来自于调用者,而调用者是根据inet->opt(应用程序指定的IP选项)和路由项 // 确定ipc->opt的, 实际中IP选项用的极少,先忽略 opt = ipc->opt; if (opt) { // 选项会被缓存到inet->cork.opt中,这块内存在ip_cork_release中被释放 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); // 设置IPCORK_OPT标记,表示缓存内容有效 inet->cork.flags |= IPCORK_OPT; inet->cork.addr = ipc->addr; } // 路由信息必须由调用者通过ip_route_output_flow()提前查询到 rt = *rtp; if (unlikely(!rt)) return -EFAULT; /* * We steal reference to this route, caller should not release it */ *rtp = NULL; // cork.fragsize的作用是记录当前skb中还可以容纳多少字节数据, // 对于第一个skb, 其值当然是MTU了 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; // cork.length会记录cork期间,该IP数据报中已经包含了多少数据,对于第一个skb,肯定需要初始化为0 inet->cork.length = 0; // 支持S/G IO时会使用这两个字段,其作用见下文 sk->sk_sndmsg_page = NULL; sk->sk_sndmsg_off = 0; //显然,是否启用扩展首部是由路由项决定的。将扩展首部的长度算到传输层首部长度中是为了计算方便 if ((exthdrlen = rt->u.dst.header_len) != 0) { length += exthdrlen; transhdrlen += exthdrlen; } } else { // 发送队列不为空,说明已经不是第一次调用该函数了,那么所需的路由等信息已经被缓存到了inet->cork中 // 从crok中获取路由和选项信息 rt = (struct rtable *)inet->cork.dst; if (inet->cork.flags & IPCORK_OPT) opt = inet->cork.opt; // 由于多次调用ip_append_data()组织的数据属于一个IP报文,所以只有第一个IP片段才包含传输层首部, // 因此如果不是第一次调用,transhdrlen和exthdrlen就是0 transhdrlen = 0; exthdrlen = 0; mtu = inet->cork.fragsize; } // hh_len是L2的首部长度,分配内存时会为L2/L3首部预留空间,这样底层协议在处理时就 // 不用重新分配内存并移动数据了 hh_len = LL_RESERVED_SPACE(rt->u.dst.dev); // 每个IP片段都需要有IP首部,fragheaderlen就是IP层首部长度,包括选项部分 fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0); // 为了计算方便,这里IP片段要求载荷部分8字节对齐,所以maxfraglen就是最大的IP片段长度(包括IP首部) maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen; // 长度判断。由于IP数据报首部的total字段占16bit,所以一个IP报文的总长度(包括IP首部)最大 // 就是0xFFFF,所以如果多次调用ip_append_data(),使得总长度超过了该限定,那么发送失败 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. */ // 校验和相关,先忽略 if (transhdrlen && length + fragheaderlen <= mtu && rt->u.dst.dev->features & NETIF_F_V4_CSUM && !exthdrlen) csummode = CHECKSUM_PARTIAL; // 经过上面的判断后,已经可以确定传入的length字节数据可以在该IP报文中(主要是当前数据+ // lenght后依然小于IP层最大报文长度0xFFFF),所以累加该IP报文已经容纳的数据长度 inet->cork.length += length; // cond1: 发送的数据超过了MTU或者发送队列不为空(不是第一次调用ip_append_data) // cond2: 发送的是UDP报文 // cond3: 设备支持对UDP报文S/G IO处理 // 上面三个条件都成立时,调用ip_ufo_append_data()处理 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. */ // 取出发送队列尾部的skb,因为该skb当前容纳的数据可能还没有达到MTU, // 还可以继续填充数据;否则的话就得分配新的skb if ((skb = skb_peek_tail(&sk->sk_write_queue)) == NULL) goto alloc_new_skb; // 循环处理将length字节数据都安排到skb中,循环过程中会递减length while (length > 0) { /* Check if the remaining data fits into current packet. */ // copy代表本轮循环要拷贝的数据量,初始化为当前skb还可以容纳的数据量 copy = mtu - skb->len; // 当前skb不能容纳全部的剩余数据,说明当前skb不是最后一个IP片段, // 所以需要按照8字节对齐方式安排skb,故重新计算copy if (copy < length) copy = maxfraglen - skb->len; // 发现当前skb无剩余空间可以拷贝数据,那么需要分配一个新的skb if (copy <= 0) { char *data; unsigned int datalen; unsigned int fraglen; unsigned int fraggap; unsigned int alloclen; struct sk_buff *skb_prev; alloc_new_skb: // 这里需要分配skb,说明上一个skb肯定已经无法容纳更多数据,所以skb_prev->len一定是大于 // maxfraglen的,而且二者的差值一定在[0, 8)区间内 skb_prev = skb; if (skb_prev) fraggap = skb_prev->len - maxfraglen; else fraggap = 0; // datalen记录了该新的skb能够保存多少字节的L4数据 datalen = length + fraggap; if (datalen > mtu - fragheaderlen) datalen = maxfraglen - fragheaderlen; // fraglen记录了该新的skb的实际片段长度(在datalen的基础上+IP首部长度) fraglen = datalen + fragheaderlen; // cond1: 如果后续很快有数据到达,并且设备不支持S/G IO:那么最优的分配策略就是直接分配一个 // mtu大小的skb,这样后续的ip_append_data()调用可以直接使用,无需再次分配(当然,如果该skb // 剩余空间不足还是会继续分配的)。 // cond2:其它情况,只需要分配能够容纳当前数据的大小就好了 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. */ // 如果是最后一个片段,将可能存在的额外尾部加上,IPsec才需要 if (datalen == length + fraggap) alloclen += rt->u.dst.trailer_len; // 分配缓冲区,transhdrlen不为0表示是第一次调用ip_append_data(),即IP报文的第一个IP片段, // 第一次调用需要拷贝L4报文的首部,这时需要考虑更多的情况,所以分配函数不同 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; else /* only the initial fragment is time stamped */ ipc->shtx.flags = 0; } // 分配失败,那么本次调用失败结束 if (skb == NULL) goto error; // 初始化skb的一些字段 skb->ip_summed = csummode; skb->csum = 0; // 为L2预留头部空间 skb_reserve(skb, hh_len); *skb_tx(skb) = ipc->shtx; /* * 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中,需要以增量方式重新计算校验和 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); } // 调用getfrag()拷贝copy字节的数据到skb中 copy = datalen - transhdrlen - fraggap; if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0) { err = -EFAULT; kfree_skb(skb); goto error; } // 本次拷贝结束,更新偏移量,因为下一次拷贝有可能需要从offset处开始 offset += copy; // 递减总的待拷贝数据量 length -= datalen - fraggap; transhdrlen = 0; exthdrlen = 0; csummode = CHECKSUM_NONE; // 将新分配的skb放入到发送队列中 __skb_queue_tail(&sk->sk_write_queue, skb); continue; }// end of if (copy <= 0) // 重新调整待拷贝数据量 if (copy > length) copy = length; if (!(rt->u.dst.dev->features & NETIF_F_SG)) { // 设备不支持S/G IO的情况下只能将数据拷贝在线性缓冲区 unsigned int 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 IO时将数据拷贝到skb的frags数组中 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 (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) { 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 { // frags数组无法容纳更多的小数据段时,发送失败 err = -EMSGSIZE; goto error; } // 将数据拷贝到skb的页面缓冲区中 if (getfrag(from, page_address(frag->page)+frag->page_offset+frag->size, offset, copy, skb->len, skb) < 0) { err = -EFAULT; goto error; } 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: // 处理出错,将length从cork中减去 inet->cork.length -= length; IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTDISCARDS); return err; }

3 ip报文发送(构造 ip 首部) ip_push_pending_frames()

如注释所述,该函数将发送队列中的所有skb组织成一个IP报文发送出去。该函数核心逻辑如下;

将发送队列中的所有skb都组织成一个skb,队列中后续的skb都放入第一个skb共享结构中的frag_list中;填充IP首部后调用ip_local_out()继续发送; /* * Combined all pending IP fragments on the socket as one IP datagram * and push them out. */ int ip_push_pending_frames(struct sock *sk) { struct sk_buff *skb, *tmp_skb; struct sk_buff **tail_skb; struct inet_sock *inet = inet_sk(sk); struct ip_options *opt = NULL; struct rtable *rt = inet->cork.rt; struct iphdr *iph; __be16 df = 0; __u8 ttl; int err = 0; // 将第一个skb出队列,出队列失败说明队列为空 if ((skb = __skb_dequeue(&sk->sk_write_queue)) == NULL) goto out; // 获取第一个skb共享结构中的frag_list指针 tail_skb = &(skb_shinfo(skb)->frag_list); /* move skb->data to ip header from ext header */ if (skb->data < skb_network_header(skb)) __skb_pull(skb, skb_network_offset(skb)); // 将发送队列中的后续skb全部接到第一个skb的frag_list列表中 while ((tmp_skb = __skb_dequeue(&sk->sk_write_queue)) != NULL) { __skb_pull(tmp_skb, skb_network_header_len(skb)); *tail_skb = tmp_skb; tail_skb = &(tmp_skb->next); skb->len += tmp_skb->len; skb->data_len += tmp_skb->len; skb->truesize += tmp_skb->truesize; __sock_put(tmp_skb->sk); tmp_skb->destructor = NULL; tmp_skb->sk = NULL; } // 下面就是构造IP报文首部 /* Unless user demanded real pmtu discovery (IP_PMTUDISC_DO), we allow * to fragment the frame generated here. No matter, what transforms * how transforms change size of the packet, it will come out. */ // PMTU相关,先忽略 if (inet->pmtudisc < IP_PMTUDISC_DO) skb->local_df = 1; /* DF bit is set when we want to see DF on outgoing frames. * If local_df is set too, we still allow to fragment this frame * locally. */ if (inet->pmtudisc >= IP_PMTUDISC_DO || (skb->len <= dst_mtu(&rt->u.dst) && ip_dont_fragment(sk, &rt->u.dst))) df = htons(IP_DF); if (inet->cork.flags & IPCORK_OPT) opt = inet->cork.opt; if (rt->rt_type == RTN_MULTICAST) ttl = inet->mc_ttl; else ttl = ip_select_ttl(inet, &rt->u.dst); iph = (struct iphdr *)skb->data; iph->version = 4; iph->ihl = 5; if (opt) { iph->ihl += opt->optlen>>2; ip_options_build(skb, opt, inet->cork.addr, rt, 0); } iph->tos = inet->tos; iph->frag_off = df; ip_select_ident(iph, &rt->u.dst, sk); iph->ttl = ttl; iph->protocol = sk->sk_protocol; iph->saddr = rt->rt_src; iph->daddr = rt->rt_dst; skb->priority = sk->sk_priority; skb->mark = sk->sk_mark; skb->dst = dst_clone(&rt->u.dst); if (iph->protocol == IPPROTO_ICMP) icmp_out_count(((struct icmphdr *)skb_transport_header(skb))->type); // IP协议内部的接口继续发送过程,主要是过防火墙 err = ip_local_out(skb); if (err) { if (err > 0) err = inet->recverr ? net_xmit_errno(err) : 0; if (err) goto error; } out: ip_cork_release(inet); return err; error: IP_INC_STATS(IPSTATS_MIB_OUTDISCARDS); goto out; }

4 清除发送队列 ip_flush_pending_frames()

如果在 ip_append_data() 过程中遇到了异常,那么需要将之前已经构造成功并且放入发送队列中的skb全部清除,高层协议会调用ip_flush_pending_frames()完成这一动作。

/* * Throw away all pending data on the socket. */ void ip_flush_pending_frames(struct sock *sk) { struct sk_buff *skb; // 删除发送队列中的数据 while ((skb = __skb_dequeue_tail(&sk->sk_write_queue)) != NULL) kfree_skb(skb); // 由于处于pending状态时,inet_sk的cork字段保存了一些缓存信息,所以也需要清除 ip_cork_release(inet_sk(sk)); } static void ip_cork_release(struct inet_sock *inet) { // 主要是路由和IP选项 inet->cork.flags &= ~IPCORK_OPT; kfree(inet->cork.opt); inet->cork.opt = NULL; if (inet->cork.rt) { ip_rt_put(inet->cork.rt); inet->cork.rt = NULL; } }
最新回复(0)