文章目录
这篇笔记分析了ip_append_data()函数的实现细节。
要点说明
看代码前,先来看看对该函数一些关键点进行说明:
- 该函数的设计目标是将要发送的数据封装成一个个方便IP层处理的skb,它并不负责实际的发送,调用者需要调用ip_push_pending_frames()进行实际的数据发送;
- 调用者可以通过连续多次调用该函数将多个数据片段封装成一个大的IP报文。这里要注意区分IP报文和IP片段的不同,传输层可以通过该函数组织一个很大的IP报文,但是该函数会根据MTU将该大报文进行分割,所以准确的来讲,skb是和IP片段是一一对应的,那么一个skb的载荷部分当然不能超过MTU;
- 由于IP首部的片段偏移字段只有13位,它要表示65535字节的报文大小,必须左移3位后使用,所以IP报文中的非最后一个片段的长度必须是8字节对齐的,否则片偏移字段将无法设置,这也是下面代码中maxfraglen的取值。
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) */
struct ipcm_cookie
该结构作为ip_append_data()的一个入参,让高层协议将一些控制信息传递给ip_append_data()。
struct ipcm_cookie
{
// IP地址,UDP调用ip_append_data()时传递的是目的地址
__be32 addr;
// 出口设备的网络设备索引
int oif;
// IP选项
struct ip_options *opt;
};
ip_append_data()
@getfrag(): 用于将待封装的数据拷贝到skb中。不同的L4层协议使用的拷贝函数可能不同;
@from: 待拷贝数据的起始地址,有getfrag()解释,大多数时候是用户态地址;
@length:待拷贝数据长度
@transhdrlen:L4层首部长度,对于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;
// 扩展首部,某些协议场景下,在IP首部和L4层首部之间需要增加扩展首部(如IPSec),该首部也
// 会占用MTU,因此在组织skb时需要考虑它。这里我们认为不启用IPSec
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;
// 下面的分支根据是否是首次调用ip_append_data(),确定局部变量opt、rt、mtu、exthdrlen的值
if (skb_queue_empty(&sk->sk_write_queue)) {
// 发送队列为空,说明是第一次调用该函数。因为调用者有可能后续还会很快再调用
// 该函数追加数据,所以这里将IP选项、路由等信息缓存到inet_sk->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.opt有效
inet->cork.flags |= IPCORK_OPT;
// 保存报文的目的IP地址
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中获取路由和IP选项信息
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层协议要求的首部预留空间长度,分配skb时会为L2/L3首部预留空间,
// 这样底层协议在处理skb时就不用重新分配内存并拷贝数据了
hh_len = LL_RESERVED_SPACE(rt->u.dst.dev);
// 每个IP片段都需要有IP首部,fragheaderlen就是每个IP片段的IP层首部长度,包括选项部分
fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0);
// 因为IP首部的片段偏移量只有13位,但是IP报文长度最大时65535,所以非最后一个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;
// 经过上面的判断后,已经可以确定当前IP报文是可以容纳待封装的length字节数据的
// (已封装数据inet->cork.length + 待封装数据length后依然小于IP层最大报文长度0xFFFF),
// 所以更新inet->cork.length
inet->cork.length += length;
// cond1: 发送的数据超过了MTU或者发送队列不为空(不是第一次调用ip_append_data)
// cond2: 发送的是UDP报文
// cond3: 设备支持UFO
// 上面三个条件都成立时,调用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
// fraggap的范围一定在[0, 8)内。因为前一个skb已经不是IP报文的最后一个片段了,
// 所以其IP片段总长度必须是8字节的对齐的。如果之前不是8字节对齐的,那么需要
// 将其末尾多余的几个字节拷贝到新的skb开头
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;
// 下面计算要分配的skb大小
// 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
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;
}
// skb分配失败,那么本次调用失败结束
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;
if (fraggap) {
// 将上一个skb末尾的几个字节数据拷贝到新的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);
}
// 调用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;
}
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片段链表中。要特别注意,
// frag_list中的片段剥掉了IP层首部,它们的长度信息都统计了到第一个skb中,这
// 样做的好处是为了方便后面的ip_fragment()处理
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协议内部的接口继续发送过程,主要是过Netfilter
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;
}
清除发送队列: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;
// 删除发送队列中的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;
}
}