内核版本:2.6.34
在前一篇”IP协议”中对报文接收时IP层的处理进行了分析,本篇分析将针对报文发送时IP层的处理。
传输层处理完后,会调用ip_push_pending_frames()将报文传递给IP层:
ip_push_pending_frames() -> ip_local_out() -> __ip_local_out()
在ip_push_pending_frames()中,会设置第一个IP分片的报头字段,tot_len和check不会设置。
int ip_local_out(struct sk_buff *skb)
{
int err;
err = __ip_local_out(skb);
if (likely(err == 1))
err = dst_output(skb);
return err;
}
__ip_local_out():设置IP报头字节总长度tot_len,校验和check。
iph->tot_len = htons(skb->len);
ip_send_check(iph);
最后调用dst_output()发送数据给IP层,dst_output()实际调用skb_dst(skb)->output(skb),skb_dst(skb)就是skb所对应的路由项。skb_dst(skb)指向的是路由项dst_entry,它的input在收到报文时赋值ip_local_deliver(),而output在发送报文时赋值ip_output()。
return nf_hook(PF_INET, NF_INET_LOCAL_OUT, skb, NULL, skb_dst(skb)->dev, dst_output);
在IP层的调用过程如下:
ip_output() -> ip_finish_output() -> ip_finish_output2() -> hh->hh_output()
在ip_output()中,设置了dev与协议号,从IP层往下,就是以dev驱动数据传输了。
skb->dev = dev;
skb->protocol = htons(ETH_P_IP);
在ip_finish_output()中,判断如果报文过大,则先调用ip_fragment()进行分片(后面会对这个函数进行分析),然后调用ip_finish_output2()发送。
if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb))
return ip_fragment(skb, ip_finish_output2);
else
return ip_finish_output2(skb);
情况一:ip_fragment()
ip_fragment()与ip_append_data()是IP层传送报文很重要的两个函数,弄清它们之间的关系很重要。
ip_append_data()是上层构造向IP层传送数据的skb使用的,它会根据MTU值对传送数据进行分片,后续分片链在第一个分片的frag_list上;如果设备支持SG,那么同一个分片内容(当分片内容是多次输入得到的)不一定在一个线性空间上,后续输入的分片内容存在分片的frags数组中。只有第一个分片才有frag_list,而每个分片都能拥有frags。由ip_append_data()构造好的skb大致如下图所示:
ip_fragments()字面意思是分片,但实际上分片工作已经由ip_append_data()完成了,它只在上层分片出现问题时重新进行分片。它的主要作用还是完成分片的后续工作。假设一个报文被分成了三份skb1, skb2, skb3,它们将独立的传递到网络上,但显然ip_append_data()得到的skb还不是独立的,skb1包含了整个报文的信息,分片报文也链在frag_list上;而skb2, skb3则缺少IP报头的信息,如分片的偏移,分片的标识,校验和等。ip_fragments()做的主要工作就是将skb拆分成能独立发送的报文。由ip_fragments()处理后的skb如图所示:
两张图只列出了IP报头tot_len字段的不同,其它诸如check, frag_list, frag_off等字段也是不同的。
先是对第一个分片的更新,让它脱离后续分片,成为独立包。frag_list置为空,当然frag_list得保存下来(到frag)中,后续分片要从frag_list中取出。更新skb_datalen和skb->len为第一个分片自身的值,在之前ip_append_data()处理后它是代表全部分片的值。ip报头的tot_len, frag_off和check分别设置。关于first_len的值,下面这张图可以清晰的解释(frags是支持SG的设备可能会出现的,不支持的话,skb->data_len=0):
frag = skb_shinfo(skb)->frag_list;
skb_frag_list_init(skb);
skb->data_len = first_len - skb_headlen(skb);
skb->truesize -= truesizes;
skb->len = first_len;
iph->tot_len = htons(first_len);
iph->frag_off = htons(IP_MF);
ip_send_check(iph);
下面是循环每个分片的代码,中间省略了每个分片的处理,这部分单独拿出来说明,frag是从skb中取出的skb_shinfo(skb)->frag_list。
for (;;) {
if (frag) {
…… // 分片处理
if (err || !frag)
break;
skb = frag;
frag = skb->next;
skb->next = NULL;
}
}
对于后续分片,要生成它的IP报头,设置好其中字段,这里根据分片的排列设置了片偏移iph->frag_off,以及偏移标识(前续分片打上IP_MF标签)。ip_copy_metadata()从前一个分片中拷贝些数据,比如pkt_type, protocol, dev, priority, mark, flags等。ip_options_fragment()处理分片的IP选项部分,因为很多选项只要第一个分片有就可以了,后续分片可以去除。
frag->ip_summed = CHECKSUM_NONE;
skb_reset_transport_header(frag);
__skb_push(frag, hlen);
skb_reset_network_header(frag);
memcpy(skb_network_header(frag), iph, hlen);
iph = ip_hdr(frag);
iph->tot_len = htons(frag->len);
ip_copy_metadata(frag, skb);
if (offset == 0)
ip_options_fragment(frag);
offset += skb->len - hlen;
iph->frag_off = htons(offset>>3);
if (frag->next != NULL)
iph->frag_off |= htons(IP_MF);
/* Ready, complete checksum */
ip_send_check(iph);
对于每一个分片,在处理完后,调用发送函数向下发送,这里output就是ip_finish_output2()。
err = output(skb);
情况二:ip_finish_output2()
调用相应发送函数发送给下一层。有关hh和neighbour参考”ARP模块”。
if (dst->hh)
return neigh_hh_output(dst->hh, skb);
else if (dst->neighbour)
return dst->neighbour->output(skb);
在创建邻居表项时neighbour->output()被赋值,比如收到arp报文,在arp_process() -> neigh_event_ns()中创建报文相应的邻居表项,而neigh->ops和neigh->output根据情况赋予不同的值。
if (dev->header_ops->cache)
neigh->ops = &arp_hh_ops;
else
neigh->ops = &arp_generic_ops;
if (neigh->nud_state&NUD_VALID)
neigh->output = neigh->ops->connected_output;
else
neigh->output = neigh->ops->output;
邻居表项创建后,相应的hh缓存项并没有创建,当向邻居表项中的主机发送报文时,先调用neigh->output(),假设neigh->ops被赋值arp_generiv_ops,则neigh->output= neigh_resolve_output,而在neigh_resolve_output()函数中,会创建hh缓存项,其中hh->output= dev_queue_xmit()。
所以,无论哪种情况,hh->output还是neigh->output,最终都是调用dev_queue_xmit()向下层传送报文的。这也是IP层下传送报文的统一方式-dev_queue_xmit()。虽然调用接口相同,但IP层下的各个协议模块都是有设备的概念的,因此每个模块的设备都不相同,在每个模块中都会更换skb->dev为下层的设备,而dev_queue_xmit()最终使用的是skb->dev特定的函数进行发送的,这样实现了各模块的接口一致。
dev_queue_xmit() 发送函数
skb_needs_linearize()判断是否要对报文进行线性处理,如果需要,它返回1,由__skb_linearize()完成线性处理。线性处理就是将报文的所有内容放到线性地址空间,不能有分片的存在。在发送报文时,ip_append_data()对过长的报文进行了分片frag_list,多次添加时使用了SG特性frags(如果支持)。skb_needs_linearize()就是判断设备能否处理ip_append_data()所做的分片工作。判断条件很简单:skb有分片即frag_list,但设备不支持分片NETIF_F_FRAGLIST;skb应用了SG但设备不支持NETIF_F_SG或者是有一个分片在highmem中。最后的线性化函数__skb_linearize()也很简单,它调用__pskb_pull_tail(skb, skb->data_len),data_len就是非线性空间的长度,__pskb_pull_taill会将这部分数据拷贝到skb->data,从而完成线性化。明显看到,不支持分片的设备在做线性化处理时会多一次数据拷贝操作。
if (skb_needs_linearize(skb, dev) && __skb_linearize(skb))
goto out_kfree_skb;
ip_summed==CHECKSUM_PARTIAL表示协议栈并没有计算完校验和,只计算了IP头,伪头等,将传输层的数据部分留给了硬件进行计算。dev_can_checksum()判断设备是否能计算校验和,如果不能的话,则skb_checksum_help()软件的计算校验和。
if (skb->ip_summed == CHECKSUM_PARTIAL) {
skb_set_transport_header(skb, skb->csum_start - skb_headroom(skb));
if (!dev_can_checksum(dev, skb) && skb_checksum_help(skb))
goto out_kfree_skb;
}
每个设备在创建时都会新建传送队列,dev->_tx。以B4401网卡创建为例,alloc_etherdev()创建的队列_tx数为1,即单队列的,dev_pick_tx()取出这个队列dev->_tx[0] -> txq中。其它支持多队列的网卡会根据skb->sk_tx_queue_mapping来选择_tx队列。
txq = dev_pick_tx(dev, skb);
q = rcu_dereference_bh(txq->qdisc);
支持queue discipline(队列排序)会由q->enqueue和q->dequeue来管理队列,发送报文。支持的网卡设备则由其后的代码来处理报文发送。B4401不支持,其q->enqueue为空。
if (q->enqueue) {
rc = __dev_xmit_skb(skb, q, dev, txq);
goto out;
}
下面是不支持qdisc的网卡设备发送数据的代码段:dev->falgs & IFF_UP判断网卡是否UP状态,netif_tx_queue_stopped()判断传送队列是否在运行状态。两者满足的话,调用dev_hard_start_xmit()向下传输报文。dev_xmit_complete()检查传输结果。
if (dev->flags & IFF_UP) {
……
if (!netif_tx_queue_stopped(txq)) {
rc = dev_hard_start_xmit(skb, dev, txq);
if (dev_xmit_complete(rc)) {
HARD_TX_UNLOCK(dev, txq);
goto out;
}
}
……
}
dev_hard_start_xmit()核心语句如下,ops->nod_start_xmit()调用设备skb->dev特定的发送操作将skb向下传送,紧接检查发送值rc,更新发送状态计数。如果此时dev指向vlan设备,则ops->ndo_start_xmit()指向vlan_dev_hard_start_xmit(),它生成vlan报文,更换skb->dev,更新计数,再次调用dev_queue_xmit();如果此时dev指向网卡设备(如b4401),则ops->ndo_start_xmit()指向b44_start_xmit(),它会将数据发送物理介质。
rc = ops->ndo_start_xmit(skb, dev);
if (rc == NETDEV_TX_OK)
txq_trans_update(txq);
简单总结下,在不支持QDISC的网卡上,从IP层向下的传输,循环的调用dev_queue_xmit()向下层传输报文,直到最后真正的网卡设备将数据发送到物理介质上,完成报文的发送。其循环调用的图示如下: