内核版本: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 plaincopy
  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 plaincopy
  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 plaincopy
  1. skb->dev = dev;
  2. skb->protocol = htons(ETH_P_IP);

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

[cpp] view plaincopy
  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 plaincopy
  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 plaincopy
  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 plaincopy
  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 plaincopy
  1. err = output(skb);

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

[cpp] view plaincopy
  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 plaincopy
  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 plaincopy
  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 plaincopy
  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 plaincopy
  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 plaincopy
  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 plaincopy
  1. rc = ops->ndo_start_xmit(skb, dev);
  2. if (rc == NETDEV_TX_OK)
  3. txq_trans_update(txq);

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

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

  1. Linux内核分析 - 网络[八]:IP协议

    内核版本:2.6.34 这篇是关于IP层协议接收报文时的处理,重点说明了路由表的查找,以及IP分片重组. ip_rcv进入IP层报文接收函数       丢弃掉不是发往本机的报文,skb->pk ...

  2. Linux内核分析 - 网络[四补]:路由表补充

    内核版本:2.6.34       前篇路由表http://blog.csdn.net/qy532846454/article/details/6423496说明了路由表的结构及路由表的创建.下面是一 ...

  3. Linux内核分析 - 网络[五]:vlan协议-802.1q

    内核版本:2.6.34 802.1q 1. 注册vlan网络系统子空间, [cpp] view plaincopy err = register_pernet_subsys(&vlan_net ...

  4. linux内核分析 网络九,“Linux内核分析”实验报告(九)

    一 Linux内核分析博客简介及其索引 本次实验简单的分析了计算机如何进行工作,并通过简单的汇编实例进行解释分析 在本次实验中 通过听老师的视频分析,和自己的学习,初步了解了进程切换的原理.操作系统通 ...

  5. Linux内核分析——第八周学习笔记

    实验作业:进程调度时机跟踪分析进程调度与进程切换的过程 20135313吴子怡.北京电子科技学院 [第一部分]理解Linux系统中进程调度的时机 1.Linux的调度程序是一个叫schedule()的 ...

  6. Linux内核分析 - 网络[十四]:IP选项

    内核版本:2.6.34       在发送报文时,可以调用函数setsockopt()来设置相应的选项,本文主要分析IP选项的生成,发送以及接收所执行的流程,选取了LSRR为例子进行说明,主要分为选项 ...

  7. Linux内核分析 - 网络[十六]:TCP三次握手

    内核:2.6.34       TCP是应用最广泛的传输层协议,其提供了面向连接的.可靠的字节流服务,但也正是因为这些特性,使得TCP较之UDP异常复杂,还是分两部分[创建与使用]来进行分析.这篇主要 ...

  8. Linux内核分析 - 网络[十一]:ICMP模块

    内核版本:2.6.34 ICMP模块比较简单,要注意的是icmp的速率限制策略,向IP层传输数据ip_append_data()和ip_push_pending_frames(). 在net/ipv4 ...

  9. Linux内核分析 - 网络[四]:路由表

    路由表 在内核中存在路由表fib_table_hash和路由缓存表rt_hash_table.路由缓存表主要是为了加速路由的查找,每次路由查询都会先查找路由缓存,再查找路由表.这和cache是一个道理 ...

最新文章

  1. 关于Hibernate中No row with the given identifier exists问题的原因及解决
  2. node.js学习笔记1
  3. 【计算理论】计算复杂性 ( NP 类不同表述 | 团问题 | P 对 NP 问题 )
  4. 搭建elasticsearch+kibana+logstash+filebeat
  5. C语言补漏(1)--- char到int赋值的一个陷阱
  6. IDEA VM options调优
  7. 公司正式宣布创业失败!
  8. Bash Shell学习笔记三
  9. Uber 和通用拟开源自动驾驶可视化软件
  10. 企业服务器上病房床号修改,关于医院病房安放陪护床(共享陪护床)申请报告...
  11. RecyclerView通过notifyItemChanged方法更新item数据避免闪烁
  12. htc服务器更新系统,HTC U11刷机教程_HTC U11卡刷官方ruu升级更新系统
  13. 我想我是适合独处的人
  14. Vue3使用echarts教程
  15. 运用流体布局的html代码,div+css布局之流体浮动布局_html/css_WEB-ITnose
  16. 英语中常见的反义词组
  17. Flink1.12 文档
  18. error 系统错误 错误码10007_linux系统中socket错误码:eintr和eagain的处理方法
  19. java 从set取值_怎样从java集合类set中取出数据?
  20. Jetson Nano系列教程3-生死看淡,不服就干之GPIO

热门文章

  1. c语言可以利用数组处理批量数据库,C语言程序设计 利用数组处理批量数据.ppt...
  2. pandas Series 判断每个元素是否包含某个子串
  3. Python使一列数据总和为1
  4. python不相等的两个字符串的 if 条件判断为True
  5. php网页如何做出透明的效果,css+filter实现简单的图片透明效果
  6. mpvue中使用vant-weapp
  7. PipeCAD之管道标准库PipeStd(3)
  8. mysql zip 文件安装
  9. hibench测试出现问题--zookeeper
  10. [GCJ] Qualification Round 2017