zoukankan      html  css  js  c++  java
  • linux IPv4报文处理浅析

    在《linux网络报文接收发送浅析》一文中介绍了数据链路层关于网络报文的处理。
    对于接收到的报文,如果不被丢弃、不被网桥转发,会调用netif_receive_skb()提交给IP层;
    而对于IP层向外发送的报文,则通过调用dev_queue_xmit()提交给数据链路层。

    本文就以netif_receive_skb()和dev_queue_xmit()为起始,简要介绍一下报文在IP层的处理过程。
    先来一张图:

    报文接收(图中橙色箭头所指)

     

    netif_receive_skb()
    对每一种已注册的协议类型调用deliver_skb(),从而调用到其packet_type->func()函数。
    对于IP协议,会调用到ip_rcv()。

    ip_rcv()
    IP报头检查,调用ip_fast_csum()检查校验和。
    netfilter:NF_INET_PRE_ROUTING。
    调用ip_rcv_finish()。

    ip_rcv_finish()
    调用ip_route_input_noref()查找路由表,其结果会决定报文的去向。
    调用ip_rcv_options()处理IP选项。
    调用dst_input(),从而调用到rtable.dst_entry->input()。根据路由不同,主要有ip_forward(转发)、ip_local_deliver(接收)两种取值。

    ip_local_deliver()
    如果存在分片,调用ip_defrag()完成分片重组。如果分片暂未到齐则直接返回。
    netfilter:NF_INET_LOCAL_IN。
    调用ip_local_deliver_finish()。

    ip_local_deliver_finish()
    调用raw_local_deliver()试图按raw socket方式(直接收发IP报文的socket)递交给上层应用,递交成功则返回。
    按ip_hdr->protocol取出相应L4协议,调用net_protocol.handler(),从而将报文提交给传输层。主要有的L4协议handler有icmp_rcv()、udp_rcv()、tcp_v4_rcv()、等。


    报文发送(图中紫色箭头所指)

     

    传输层在发送报文时,会调用到IP层的接口。如:ip_queue_xmit()、ip_append_data()/ip_append_page()+ip_push_pending_frames()、ip_local_out()、等。
    这些函数最终会调用到ip_local_out()。
    在此之前,报文会被构造好。可能由传输层的代码自己构造、也可能通过调用ip_append_data()这样的辅助函数来构造。
    具体构造报文的细节就不细说了,引用ULNI上的一张图,直接看结果:


    struct sock是跟应用创建的socket相对应的结构,其中的sk_write_queue指向待发送的IP报文分片队列,每个分片由一个struct sk_buff来表示。
    由于网络节点存在MTU,也就是最大传输单元,一次提交给数据链路层的报文不能太大(如1500字节),所以过大的IP报文需要分片后发送。
    注意,只有第一个分片有L4的报头,因为对于L4协议来说,这些分片组装成的整体才是一个报文。
    当然,最好的情况是没有分片,也就是L4协议总是发送尺寸小于MTU的报文。

    struct sk_buff中有一组head、data、tail、end这样的指针,指向需要发送的报文buffer。
    struct sk_buff之后会紧跟一个struct skb_shared_info结构。属于同一个分片的报文数据可能分散于多个碎片中,其中的第一个碎片由上述head、data等指针指向,后续碎片则由struct skb_shared_info来指示。
    为什么要有多个碎片呢?
    一方面,因为上层发送数据有可能就是一小片一小片的发送的,比如传送文件,每次读128字节并发送。而这些碎片可以在同一个IP报文中发送,只要总和不超过MTU。
    另一方面,很多硬件支持这样的由多个碎片构成的缓冲区。如果某些硬件不支持的话,那构造sk_buff的时候就只能把每次提交的数据都拷贝到同一块连续的buffer中,这样可能就会多一次拷贝。
    当然,就算硬件支持多个碎片,数据拷贝可能也无法避免。比如说当数据源来自于用户空间的时候,就必定存在用户空间到内核空间的一次拷贝(因为用户指定的地址可能存在错误,不能直接提交给网卡)。而在类似于sendfile这样的系统调用中,数据源本身就在内核空间,则可能做到真正的零拷贝。
    ip_append_data()/ip_append_page()就是完成sk_buff构造的辅助函数,调用它往struct sock中塞数据,其内部会控制是否应该分配新的struct sk_buff作为分片、或者数据是否应该作为碎片放入struct skb_shared_info结构。

     

    ip_local_out()
    netfilter:NF_INET_LOCAL_OUT。
    调用dst_output(),从而调用到ip_output()。

    ip_output()
    netfilter:NF_INET_POST_ROUTING。
    调用ip_finish_output()。

    ip_finish_output()
    调用ip_fragment()对报文做分片后发送,或直接调用ip_finish_output2()发送。

    ip_finish_output2()
    调用邻居子系统neigh_output(),最终由邻居子系统调用dev_queue_xmit()发送报文。


    报文转发(图中绿色箭头所指)

     

    对于接收到的报文,如果路由子系统认为应该转发,则dst_input()会调用到ip_forward()。

    ip_forward()
    处理IP选项、递减ttl。
    netfilter:NF_INET_FORWARD。
    调用ip_forward_finish()。

    ip_forward_finish()
    调用ip_forward_options()处理IP选项。
    调用dst_output(),从而调用到ip_output()。


    其他


    在上述流程中,有几个点再额外说明一下:


    netfilter:IP报文的处理流程中共有PRE_ROUTING、LOCAL_IN、FORWARD、LOCAL_OUT、POST_ROUTING这五个HOOK点。用户可以通过配置netfilter,在这几个节点上添加一些规则,实现对特定IP报文的干预。


    route:路由子系统。决定IP报文下一步应该发送到哪个IP地址上(或者自己接收)。这个目的IP地址所对应的主机必定是与本机直接相连、或通过交换机相连的(也就是说,两台机器之前的通信不需要路由器转发,两台机器是“邻居”)。

    举个简单的例子,路由表有如下配置:

    Destination     Gateway         Genmask         Flags Metric Ref    Use Iface

    10.20.150.0     *               255.255.255.0   U     0      0        0 eth0

    default         10.20.150.254   0.0.0.0         UG    0      0        0 eth0

    那么,目的地是10.20.150.0/24这个子网的报文,直接进行转发(目的主机和本机直接就是邻居);而目的地是其他地址的报文,发往默认网关10.20.150.254,由它来继续转发。(注意,默认网关10.20.150.254也是本机的邻居。)


    neighbour:邻居子系统。路由子系统确定了报文要发送到的IP地址,而在将报文提交给数据链路层之前,还需要知道目的主机的Mac地址。这就是邻居子系统干的事情。

    简单来说,一台机器通过在子网中广播一个ARP报文,来询问目的IP地址的Mac地址是什么。比如目的IP地址是10.20.150.133,本机会广播"Who is 10.20.150.133/24"的ARP报文。像交换机这样的数据链路层设备会将ARP报文转发,从而让每一个邻居都收到。当10.20.150.133收到询问后,会向本机回复其Mac地址。

    然后在此基础上,邻居子系统会实现一定的缓存策略,不会对于每个报文件都这么询问一下。


  • 相关阅读:
    单独使用mybatis创建多数据源
    【mybatis-oracle】批量插入、批量删除以及xml文件大于号 小于号处理
    让只连了内网某台机器A的机器B能上外网
    python类静态函数和变量,heapq自定义优先级队列
    git 的基础
    Java线程池
    ansible 管理工具
    理解容器跨主机通信一
    Java多线程的实现方式二
    单机容器网络
  • 原文地址:https://www.cnblogs.com/wangfengju/p/6173077.html
Copyright © 2011-2022 走看看