根据《深入理解LINUX网络技术内幕》描述,四层使用IP层输出主要分为两大类处理。函数分别为ip_append_data、ip_push_pending_frames的组合发送,以及ip_queue_xmit的发送。
当前仅对这两种输出情况进行分析。
其ip_append_data、ip_push_pending_frames组合最常用于UDP报文发送,此时ip_append_data
会根据路径MTU将待发送的UDP报文分为多块,以方便后续的IP分片,但此时数据并
不发送出去,则是通过调用ip_push_pending_frames才进行数据发送。
ip_queue_xmit常用于TCP报文发送,因为TCP已经对报文的大小进行控制,所以ip_queue_xmit中就不用在考虑报文分块的处理,相对比较简单。
一、ip_append_data、ip_push_pending_frames的组合发送
1、ip_append_data
inet_sock *inet = inet_sk(sk);
//上层仅仅进行探测,并不真正发送数据。
if (flags&MSG_PROBE)
return 0;
//当前套接口的发送队列为空,先暂存一些信息
if (skb_queue_empty(&sk->sk_write_queue))
//如果含有待发送的ip选项,则先存储到ip套接对象中
opt = ipc->opt;
if (opt)
inet->cork.flags |= IPCORK_OPT;
inet->cork.addr = ipc->addr;
dst_hold(&rt->u.dst);
//存储路径mtu及路由条目
inet->cork.fragsize = mtu = dst_mtu(rt->u.dst.path);
inet->cork.rt = rt;
inet->cork.length = 0;
sk->sk_sndmsg_page = NULL;
sk->sk_sndmsg_off = 0;
//存在扩展头
if ((exthdrlen = rt->u.dst.header_len) != 0)
length += exthdrlen;
transhdrlen += exthdrlen;
else
rt = inet->cork.rt;
if (inet->cork.flags & IPCORK_OPT)
opt = inet->cork.opt;
transhdrlen = 0;
exthdrlen = 0;
mtu = inet->cork.fragsize;
//二层硬件头长度
hh_len = LL_RESERVED_SPACE(rt->u.dst.dev);
//计算ip头部总长度
fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0);
//计算在mtu限制下,除去ip头部,还可以填充的ip负载长度
//这里会进行8个字节的对齐,是因为IP的分段偏移字段单位是8字节。
maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen;
//如果总长度会超出ip报头中总长度字段可容纳的大小,则返回错误。
if (inet->cork.length + length > 0xFFFF - fragheaderlen)
ip_local_error(sk, EMSGSIZE, rt->rt_dst, inet->dport, mtu-exthdrlen);
return -EMSGSIZE;
//如果是第一个报文,同时报文总长度小于MTU,同时硬件支持校验和,同时没
//有其它扩展头,则标记使用硬件校验和。
if (transhdrlen && length + fragheaderlen <= mtu &&
rt->u.dst.dev->features & NETIF_F_ALL_CSUM && !exthdrlen)
csummode = CHECKSUM_PARTIAL;
//记载接收队列中已经积累的负载长度,不含头部
inet->cork.length += length;
//如果当前报文长度大于mtu,同时当前是UDP报文,当前设备驱动支持UDP类型
//的GSO特性。
if (((length > mtu) && (sk->sk_protocol == IPPROTO_UDP)) &&
(rt->u.dst.dev->features & NETIF_F_UFO))
ip_ufo_append_data(sk, getfrag, from, length, hh_len,fragheaderlen, transhdrlen, mtu,
flags);
//当前发送列表是空的。
if ((skb = skb_peek_tail(&sk->sk_write_queue)) == NULL)
//分配一个新的skb,含硬件头、IP头、UDP头,这个20是预留?
skb = sock_alloc_send_skb(sk,hh_len + fragheaderlen + transhdrlen + 20,
(flags & MSG_DONTWAIT), &err);
//将skb的head到data之间预留硬件头长度。当前data指向IP头位置。
skb_reserve(skb, hh_len);
//将skb的tail移到UDP的负载位置
skb_put(skb,fragheaderlen + transhdrlen);
//记载三、四层头的位置
skb->nh.raw = skb->data;
skb->h.raw = skb->data + fragheaderlen;
//记载需要硬件来校验
skb->ip_summed = CHECKSUM_PARTIAL;
skb->csum = 0;
sk->sk_sndmsg_off = 0;
//将新收到的UDP负载添加到skb的frags数组中,该数组每个成员都是
//一个内存页,在硬件设备支持分散/聚合处理时,就可以把这些零散的
//内存页一次性交给硬件发送。
skb_append_datato_frags(sk,skb, getfrag, from,(length - transhdrlen));
do
//只能存放64K
frg_cnt = skb_shinfo(skb)->nr_frags;
if (frg_cnt >= MAX_SKB_FRAGS)
return -EFAULT;
page = alloc_pages(sk->sk_allocation, 0);
//在套接口对象中记载当前使用的页及偏移,方便下次使用。
sk->sk_sndmsg_page = page;
sk->sk_sndmsg_off = 0;
//初始化skb中的frag数组成员
skb_fill_page_desc(skb, frg_cnt, page, 0, 0);
frag = &skb_shinfo(skb)->frags[i];
frag->page = page; //新建的页地址
frag->page_offset = off; //0
frag->size = size; //0
skb_shinfo(skb)->nr_frags = i + 1; //更新frags数组长度
skb->truesize += PAGE_SIZE;
//更新skb总的内存用量
atomic_add(PAGE_SIZE, &sk->sk_wmem_alloc);
frg_cnt = skb_shinfo(skb)->nr_frags;
frag = &skb_shinfo(skb)->frags[frg_cnt - 1];
//left为当前页还可用的空间,如果当前页的空间已经不够用,则
//先copy先设置为left长度,否则copy为需要复制的整个数据长度
left = PAGE_SIZE - frag->page_offset;
copy = (length > left)? left : length;
//getfrag是传入的回调函数参数,该回调函数由不同上层协议调用时
//传入,主要用于将用户空间的数据复制到当前内存页中。该回调
//暂不进行分析。
getfrag(from, (page_address(frag->page) +
frag->page_offset + frag->size),offset, copy, 0, skb);
//数据更新
sk->sk_sndmsg_off += copy;
frag->size += copy;
skb->len += copy;
skb->data_len += copy;
offset += copy;
length -= copy;
//当还有待处理的数据时,则循环继续处理。
while (length > 0);
//这里有BUG,因为只有上面是新建的skb才应该加到队列中,否则
//原来的已经存在,不应该再次加入队列,通过查看当前3.4的内核
//发现新版内核已经解决了这个BUG,将这里的代码放到了上面
//if skb == NULL的判断内部了。
skb_shinfo(skb)->gso_size = mtu - fragheaderlen;
skb_shinfo(skb)->gso_type = SKB_GSO_UDP;
__skb_queue_tail(&sk->sk_write_queue, skb);
return 0;
//输出队列为空,直接跳过前面检测
if ((skb = skb_peek_tail(&sk->sk_write_queue)) == NULL)
goto alloc_new_skb;
while (length > 0)
//当前mtu限制下,输出队列中最后一个skb还可填充的空间。
copy = mtu - skb->len;
//如果copy大小容纳不下当数据长度,则copy先设置为最后一个skb可填充的
//大小。
if (copy < length)
copy = maxfraglen - skb->len;
//最后一个skb没有一点空间了,则需要分配一个新的skb
if (copy <= 0)
skb_prev = skb; //先前的skb,下面链接需要使用
alloc_new_skb:
if (skb_prev)
//fraggap可能是个负数,这是因为IP报头中分段的偏移量的单位是8个
//字节,如果上一个skb剩余空间已经不能8字节对齐,则上一个skb的
//多余的数据就会复制到当前新的skb中。如下所示:
//maxfraglen是8字节对齐的,buf尾部是7个字节,则maxfraglen会
//截断。此时skb_prev->len减去maxfraglen就去出现-7。
//------------------|--------|
//|----已使用--- | 7byte |
//------------------|--------|
fraggap = skb_prev->len - maxfraglen;
else
fraggap = 0;
//待处理的新的数据长度。
datalen = length + fraggap;
//如果数据长度大于一片限制的长度,则先复制一片
if (datalen > mtu - fragheaderlen)
datalen = maxfraglen - fragheaderlen;
fraglen = datalen + fragheaderlen;
//如果上层传入MORE标记,则表示上层马上会很新的包传下来,此时如果
//硬件不支持分散/聚合,则分配大小设置为mtu,通常mtu会大于等于
//datalen+fragheaderlen。
if ((flags & MSG_MORE) &&!(rt->u.dst.dev->features&NETIF_F_SG))
alloclen = mtu;
else
alloclen = datalen + fragheaderlen;
//如果到达数据的最后,当前目的对象还有扩冲的尾部信息,则多分配尾
//部信息的空间。
if (datalen == length + fraggap)
alloclen += rt->u.dst.trailer_len;
//第一个报文
if (transhdrlen)
//创建一个skb,同时判断套接口是否有错误、判断套接口已经关闭、
//判断发送BUF已经满等处理。
skb = sock_alloc_send_skb(sk,alloclen + hh_len + 15,
(flags & MSG_DONTWAIT), &err);
//非第一个报文,则仅需要分配skb即可
else
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;
skb->ip_summed = csummode;
skb->csum = 0;
//skb的data和tail下移,给硬件头留出空间。
skb_reserve(skb, hh_len);
//skb的tail下移到负载的尾部,data指向ip头之后
data = skb_put(skb, fraglen);
//nh.raw指向ip头,h.raw指向四层头的位置
skb->nh.raw = data + exthdrlen;
data += fragheaderlen;
skb->h.raw = data + exthdrlen;
//上面为了8个字节对齐,如果前一个skb不是8字节对齐,则会把前一个
//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;
//因为前一个skb不够8字节对齐的多余数据已经复制到新的
//skb中,所以这里调整前一个skb的tail值,使得指向有效数据
//位置。
pskb_trim_unique(skb_prev, maxfraglen);
//getfrag是传入的回调函数参数,该回调函数由不同上层协议调用时传入,主
//要用于将用户空间的数据复制到当前内存页中。该回调暂不进行分析。
copy = datalen - transhdrlen - fraggap;
if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0)
err = -EFAULT;
kfree_skb(skb);
goto error;
//新分配的skb已经完成数据复制。
offset += copy;
length -= datalen - fraggap;
transhdrlen = 0;
exthdrlen = 0;
csummode = CHECKSUM_NONE;
//将新分配的skb加入到套接字的发送队列中
__skb_queue_tail(&sk->sk_write_queue, skb);
continue;
if (copy > length)
copy = length;
//当前硬件不支持分散/聚合特性,则将数据复制到当前skb的buf中
if (!(rt->u.dst.dev->features&NETIF_F_SG))
off = skb->len;
if (getfrag(from, skb_put(skb, copy),offset, copy, off, skb) < 0)
__skb_trim(skb, off);
err = -EFAULT;
goto error;
//否则如果硬件支持分散/聚合功能,则将数据复制到skb的frags页中,这样
//可以提高分配效率。因为多次调用ip_append_data时,如果一页没有被填
//满,则一直可以使用当前页进行数据复制,同时加入到页中的数据,每次仅
//仅需要复制四层负载数据就可以了,如果不支持该特性,则新建的skb还需要
//多复制IP头。但请不要将分散/聚合与IP分段搞混了,如果已经到达IP分段
//的长度,再来的数据不能再放置到frags页中,必须新分配skb,并进行IP头、
//数据处理。
else
//获取上次未使用完成页地址及偏移量,一页不仅可以单个skb的数据存放,
//还可以用于多个skb的数据存放。即使大家把页指针都指向唯一的页,但
//因为有偏移量,所以大家之间可以区分开。
//|------------| |-----------|
//| skb_1 | | skb_2 |
//|------------|------> |--------| <-------------
// | page |
// ---------
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;
//当前套接口中记载的最后使用页地存在
if (page && (left = PAGE_SIZE - off) > 0)
if (copy >= left)
copy = left;
//套接口中记载的页与当前待处理的frags数组最后使用的页不相同
//则需要重新分配一页。
if (page != frag->page)
get_page(page);
//分配一个新的frags数组成员,并将页信息填充到新的frag对象中
//注意这里i在上面赋值为nr_frags,即frags数组当前长度,C代码
//从0开始索引,所以这里i就是一个未使用的新frag对象。
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); //分配新页
//套接字记载最后使用的页
sk->sk_sndmsg_page = page;
sk->sk_sndmsg_off = 0;
//将页关联到新的frag对象中
skb_fill_page_desc(skb, i, page, 0, 0);
frag = &skb_shinfo(skb)->frags[i];
//记载当前skb的大小,及内存使用量
skb->truesize += PAGE_SIZE;
atomic_add(PAGE_SIZE, &sk->sk_wmem_alloc);
//页分配数已经到了上限
else
err = -EMSGSIZE;
goto error;
//将四层负载数据使用传入的函数指针复制到当前页的空余位置
getfrag(from, page_address(frag->page)+frag->page_offset+frag->size, offset,
copy, skb->len, skb)
sk->sk_sndmsg_off += copy;
frag->size += copy;
skb->len += copy;
skb->data_len += copy;
offset += copy;
length -= copy;
2、ip_push_pending_frames
ip_push_pending_frames
//从套接口的发送队列头部取出一个skb,注意sk_write_queue仅仅是链表的哨岗
//节点,所以是取sk_write_queue->next数据。
skb = __skb_dequeue(&sk->sk_write_queue)
tail_skb = &(skb_shinfo(skb)->frag_list);
//将skb的data上拉到ip头的位置
if (skb->data < skb->nh.raw)
__skb_pull(skb, skb->nh.raw - skb->data);
//如果套接口的发送列表还有数据,则将后续的skb串接到上面第一个取出的skb
//的frag_list上,这里要注意frag_list与frags数组是不同的,frags数组每个成员
//都在当前skb的负载数据,是和当前skg构成一个完整的报文,而frag_list中每
//个成员都是独立的报文(二层、三层头、数据),frag_list仅仅是用于将发送队
//列中所有skb串联到上面第一个取出的skb后,这样通过第一个skb就可以把
//所有报文skb都访问到。
while ((tmp_skb = __skb_dequeue(&sk->sk_write_queue)) != NULL)
__skb_pull(tmp_skb, skb->h.raw - skb->nh.raw);
*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;
//如果用户设置路径MTU检测方式不是总是检测,将本地分片标记设置到套接口上
//,后续在准备进行分片时,如果ip头设置为DF不能分片标记,但本地local_df为
//true,则本地完成分片。
if (inet->pmtudisc != IP_PMTUDISC_DO)
skb->local_df = 1;
//如果当前路径MTU检测功能设置为总是进行,或者当前报文长度小于路径MTU大小
//并且不希望进行分片,则在IP头中标记不能分片。
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;
//如果当前是多播,则设置TTL为多播长度(通常为1),否则设置为单播长度。
if (rt->rt_type == RTN_MULTICAST)
ttl = inet->mc_ttl;
else
ttl = ip_select_ttl(inet, &rt->u.dst);
//设置版本及IP头长度。
iph = (struct iphdr *)skb->data;
iph->version = 4;
iph->ihl = 5;
//进行ip选项设置
if (opt)
iph->ihl += opt->optlen>>2;
ip_options_build(skb, opt, inet->cork.addr, rt, 0);
//其它IP头字段的填充
iph->tos = inet->tos;
iph->tot_len = htons(skb->len);
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
//校验和
ip_send_check(iph);;
skb->priority = sk->sk_priority;
skb->dst = dst_clone(&rt->u.dst);
//暂时先不分析netfilter框架,假设没有被拦截,则调用dst_output,dst_output在
//下面已经分析,这里不再重复分析。
NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL,skb->dst->dev, dst_output);
//当前一个完整的报文已经发出,则清除套接口中存储的临时数据
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;
二、ip_queue_xmit
sock *sk = skb->sk;
inet_sock *inet = inet_sk(sk);
ip_options *opt = inet->opt;
//已经含有目地条目,则直接跳到后面进行处理。
rt = (struct rtable *) skb->dst;
if (rt != NULL)
goto packet_routed;
//检测该套接口之前是否含有sk_dst_cache,当没有过时,则直接使用该目地条目。
rt = (struct rtable *)__sk_dst_check(sk, 0);
//没有目的对象,需要路由查找
if (rt == NULL)
//获取套接口中设置的目的地址
daddr = inet->daddr;
//如果含有严格路由的IP选项,则使用该选项中的远端地址做为目的地址。
if(opt && opt->srr)
daddr = opt->faddr;
security_sk_classify_flow(sk, &fl);
//路由选路处理,暂不分析。
ip_route_output_flow(&rt, &fl, sk, 0);
sk_setup_caps(sk, &rt->u.dst);
//将当前获取到的远端条目设置到套接口的缓存中
//sk->sk_dst_cache = dst;
__sk_dst_set(sk, dst);
//保存设备能力集到套接口中
sk->sk_route_caps = dst->dev->features;
//如果驱动支持GSO特性处理,则加上NETIF_F_GSO_MASK表示所有
//的GSO子协议(如TCP、UDP等)都能处理。
if (sk->sk_route_caps & NETIF_F_GSO)
sk->sk_route_caps |= NETIF_F_GSO_MASK;
//检测当前套接口处理的协议类型是否匹配当前驱动支持的GSO子协议
if (sk_can_gso(sk))
if (dst->header_len)
//如果有扩展头部,则不能使用GSO特性,所以需要去除
sk->sk_route_caps &= ~NETIF_F_GSO_MASK;
else
//GSO特性是需要设备硬件必须支持分散聚合功能及校验和功能
sk->sk_route_caps |= NETIF_F_SG | NETIF_F_HW_CSUM;
//增加目地对象的引用计数
skb->dst = dst_clone(&rt->u.dst);
atomic_inc(&dst->__refcnt)
packet_routed:
//如果含有严格路由选项,但当前目的地和网关不同,则丢弃。
if (opt && opt->is_strictroute && rt->rt_dst != rt->rt_gateway)
goto no_route;
//把skb->data提升到ip头字段的位置
iph = (struct iphdr *) skb_push(skb, sizeof(struct iphdr) + (opt ? opt->optlen : 0));
//填充协议版本、IP头长度、TOS字段
*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));
//填充总长度
iph->tot_len = htons(skb->len);
//如果套接口设置了路径MTU发现设置总是执行则不能进行分片
//如果套接口设置了路径MTU发现设置每次路由都进行处理,同时当前目地对象还没有索
//定。则也不能进行分片。
if (ip_dont_fragment(sk, &rt->u.dst) && !ipfragok)
//不能进行分片,则需要在IP头部将分片标记设置为DF
iph->frag_off = htons(IP_DF);
else
iph->frag_off = 0; //允许分片
//设置IP头其它字段
iph->ttl = ip_select_ttl(inet, &rt->u.dst);
iph->protocol = sk->sk_protocol;
iph->saddr = rt->rt_src;
iph->daddr = rt->rt_dst;
skb->nh.iph = iph;
//如果有IP选项,则进行填充
if (opt && opt->optlen)
iph->ihl += opt->optlen >> 2;
ip_options_build(skb, opt, inet->daddr, rt, 0);
//设置IP头ID字段
ip_select_ident_more(iph, &rt->u.dst, sk,(skb_shinfo(skb)->gso_segs ?: 1) - 1);
//设置校验和
ip_send_check(iph);
//设置优先级
skb->priority = sk->sk_priority;
//进行netfileter的LOCAL_OUT链处理,这里不分析netfileter,假设没有被
//netfilter拦截,则会触发dst_output调用。
//dst_output
// skb->dst->output(skb);
//这里dsp->output的回调是在ip_route_output_flow中设置的,假设当前发送的目的地
//是单播,则output回调被设置为ip_output
NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev,dst_output);
-------------------------------------------------------------------------------------------------------------------
ip_output
net_device *dev = skb->dst->dev;
skb->dev = dev; //输出设备
skb->protocol = htons(ETH_P_IP);
//进行netfileter的POST_ROUTING链处理,这里不分析netfileter,假设没有被
//netfilter拦截,则会触发ip_finish_output调用。
NF_HOOK_COND(PF_INET, NF_IP_POST_ROUTING, skb, NULL, dev,ip_finish_output,
!(IPCB(skb)->flags & IPSKB_REROUTED));
-------------------------------------------------------------------------------------------------------------------
ip_finish_output
//如果当前报文大于路径MTU,同时当前报文并不使用GSO特性处理。则需要进行
//分片,否则直接调用ip_finish_output2继续处理输出。支持GSO特性通常是把分片
//的处理拖延到最后网卡驱动中处理,以后再单独分析IP报的分片和组装。目前
//假设不需要分片。
if (skb->len > dst_mtu(skb->dst) && !skb_is_gso(skb))
ip_fragment(skb, ip_finish_output2);
else
ip_finish_output2(skb);
-------------------------------------------------------------------------------------------------------------------
ip_finish_output2
dst_entry *dst = skb->dst;
net_device *dev = dst->dev;
hh_len = LL_RESERVED_SPACE(dev);
//如果当前skb的head到data之间的空间已经不能放下二层数据包头,则需要重新
//分配一块空间。
if (unlikely(skb_headroom(skb) < hh_len && dev->hard_header))
skb2 = skb_realloc_headroom(skb, LL_RESERVED_SPACE(dev));
if (skb->sk)
skb_set_owner_w(skb2, skb->sk);
kfree_skb(skb);
skb = skb2;
//如果已经含有硬件缓存头,则直接填充硬件头,没有硬件缓存头则需要调用邻居
//模块去解析处理,以后再分析这块。
if (dst->hh)
neigh_hh_output(dst->hh, skb);
else if (dst->neighbour)
dst->neighbour->output(skb);