Linux内核分析 - 网络[八补]:IP协议补充

内核版本: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不会设置。

[cpp]  view plain copy
  1. int ip_local_out(struct sk_buff *skb)  
  2. {  
  3.  int err;  
  4.  err = __ip_local_out(skb);  
  5.  if (likely(err == 1))  
  6.   err = dst_output(skb);  
  7.  return err;  
  8. }  

      __ip_local_out():设置IP报头字节总长度tot_len,校验和check。

[cpp]  view plain copy
  1. iph->tot_len = htons(skb->len);  
  2. 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驱动数据传输了。

[cpp]  view plain copy
  1. skb->dev = dev;  
  2. skb->protocol = htons(ETH_P_IP);  


      在ip_finish_output()中,判断如果报文过大,则先调用ip_fragment()进行分片(后面会对这个函数进行分析),然后调用ip_finish_output2()发送。

[cpp]  view plain copy
  1. if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb))  
  2.  return ip_fragment(skb, ip_finish_output2);  
  3. else  
  4.  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): 

 

[cpp]  view plain copy
  1. frag = skb_shinfo(skb)->frag_list;  
  2. skb_frag_list_init(skb);  
  3. skb->data_len = first_len - skb_headlen(skb);  
  4. skb->truesize -= truesizes;  
  5. skb->len = first_len;  
  6. iph->tot_len = htons(first_len);  
  7. iph->frag_off = htons(IP_MF);  
  8. ip_send_check(iph);  

      下面是循环每个分片的代码,中间省略了每个分片的处理,这部分单独拿出来说明,frag是从skb中取出的skb_shinfo(skb)->frag_list。

[cpp]  view plain copy
  1. for (;;) {  
  2.  if (frag) {  
  3.   …… // 分片处理  
  4.   if (err || !frag)  
  5.    break;  
  6.   skb = frag;  
  7.   frag = skb->next;  
  8.   skb->next = NULL;  
  9.  }  
  10. }  

      对于后续分片,要生成它的IP报头,设置好其中字段,这里根据分片的排列设置了片偏移iph->frag_off,以及偏移标识(前续分片打上IP_MF标签)。ip_copy_metadata()从前一个分片中拷贝些数据,比如pkt_type, protocol, dev, priority, mark, flags等。ip_options_fragment()处理分片的IP选项部分,因为很多选项只要第一个分片有就可以了,后续分片可以去除。

[cpp]  view plain copy
  1. frag->ip_summed = CHECKSUM_NONE;  
  2. skb_reset_transport_header(frag);  
  3. __skb_push(frag, hlen);  
  4. skb_reset_network_header(frag);  
  5. memcpy(skb_network_header(frag), iph, hlen);  
  6. iph = ip_hdr(frag);  
  7. iph->tot_len = htons(frag->len);  
  8. ip_copy_metadata(frag, skb);  
  9. if (offset == 0)  
  10.  ip_options_fragment(frag);  
  11. offset += skb->len - hlen;  
  12. iph->frag_off = htons(offset>>3);  
  13. if (frag->next != NULL)  
  14.  iph->frag_off |= htons(IP_MF);  
  15. /* Ready, complete checksum */  
  16. ip_send_check(iph);  

      对于每一个分片,在处理完后,调用发送函数向下发送,这里output就是ip_finish_output2()。

[cpp]  view plain copy
  1. err = output(skb);  

 

      情况二:ip_finish_output2()
      调用相应发送函数发送给下一层。有关hh和neighbour参考”ARP模块”。

[cpp]  view plain copy
  1. if (dst->hh)  
  2.  return neigh_hh_output(dst->hh, skb);  
  3. else if (dst->neighbour)  
  4.  return dst->neighbour->output(skb);  

      在创建邻居表项时neighbour->output()被赋值,比如收到arp报文,在arp_process() -> neigh_event_ns()中创建报文相应的邻居表项,而neigh->ops和neigh->output根据情况赋予不同的值。

[cpp]  view plain copy
  1. if (dev->header_ops->cache)  
  2.  neigh->ops = &arp_hh_ops;  
  3. else  
  4.  neigh->ops = &arp_generic_ops;  
  5. if (neigh->nud_state&NUD_VALID)  
  6.  neigh->output = neigh->ops->connected_output;  
  7. else  
  8.  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,从而完成线性化。明显看到,不支持分片的设备在做线性化处理时会多一次数据拷贝操作。

[cpp]  view plain copy
  1. if (skb_needs_linearize(skb, dev) && __skb_linearize(skb))  
  2.  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队列。

[cpp]  view plain copy
  1. txq = dev_pick_tx(dev, skb);  
  2. q = rcu_dereference_bh(txq->qdisc);  

      支持queue discipline(队列排序)会由q->enqueue和q->dequeue来管理队列,发送报文。支持的网卡设备则由其后的代码来处理报文发送。B4401不支持,其q->enqueue为空。

[cpp]  view plain copy
  1. if (q->enqueue) {  
  2.  rc = __dev_xmit_skb(skb, q, dev, txq);  
  3.  goto out;  
  4. }  

      下面是不支持qdisc的网卡设备发送数据的代码段:dev->falgs & IFF_UP判断网卡是否UP状态,netif_tx_queue_stopped()判断传送队列是否在运行状态。两者满足的话,调用dev_hard_start_xmit()向下传输报文。dev_xmit_complete()检查传输结果。

[cpp]  view plain copy
  1. if (dev->flags & IFF_UP) {  
  2.  ……  
  3.  if (!netif_tx_queue_stopped(txq)) {  
  4.   rc = dev_hard_start_xmit(skb, dev, txq);  
  5.   if (dev_xmit_complete(rc)) {  
  6.    HARD_TX_UNLOCK(dev, txq);  
  7.    goto out;  
  8.   }  
  9.  }  
  10.  ……  
  11. }  

      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(),它会将数据发送物理介质。

[cpp]  view plain copy
  1. rc = ops->ndo_start_xmit(skb, dev);  
  2. if (rc == NETDEV_TX_OK)  
  3.  txq_trans_update(txq);  

      简单总结下,在不支持QDISC的网卡上,从IP层向下的传输,循环的调用dev_queue_xmit()向下层传输报文,直到最后真正的网卡设备将数据发送到物理介质上,完成报文的发送。其循环调用的图示如下:

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值