路由器数据转发原理

1.知识储备

1.1 TCP/IP四层模型
当前互联网上的应用都是基于TCP/IP协议来运作的,其分层的模型非常有利于互联网应用的开发。对于TCP/IP四层模型来讲,其中第二、三层(网间层和传输层)是由内核进行处理,第四层由应用程序进行处理。
TCP/IP四层模型内核部分提供通用的接口和详细信息以供应用程序调用,使应用程序开发者专注于应用层的实现,而不必把过多精力放在底层处理上。

TCP/IP四层模型分别完成以下的功能:
第一层:网络接口层
包括用于协作IP数据在已有网络介质上传输的协议。如定义地址解析协议(ARP/RARP),提供TCP/IP协议的数据结构和实际物理硬件之间的接口。
第二层:网间层
本层包含IP协议负责的数据包封装、寻址和路由。同时还包含ICMP(网络控制管理报文协议)用来提供网络诊断功能,以使得在不可靠的IP协议上得以辅助可靠的服务。
第三层:传输层
它提供端到端的通信服务,其中TCP协议提供可靠的数据流传输服务,UDP提供不可靠的数据流传输服务。
第四层:应用层
应用层包括FTP、HTTP、Telnet、SMTP等服务。

1.2 LINUX网络协议栈数据结构sk_buff简介
sk_buff是linux网络协议栈最重要的数据结构。大部分内核网络模块都是直接操作sk_buff(减少内存拷贝次数),如路由子系统,防火墙系统netfilter,流量控制系统tc等等。

数据包在内核中经历的过程,实际上就是sk_buff结构中的变量不断的被读取和赋值的过程,真实的数据并不会被拷贝。例如数据包到达IP层,去掉MAC层头部,实际的操作是在MAC层函数处理结束时,只是把skb->data指针指向MAC层协议头的末尾(即IP包头的开始),并不会真的把MAC层协议头的内存清除,没有产生内存的申请与释放操作,只是一个指针的重新赋值,大大的提高了处理效率。

1.3 sk_buff结构

struct sk_buff {  
    /* These two members must be first. */  
    struct sk_buff      *next;  
    struct sk_buff      *prev;  
  
//表示拥有此sk_buff的socket,主要是被4层用到,若http的80端口,若此sk_buff只在转发中用到,即源地址和目的地址都不是本机地址,这个指针是NULL  
    struct sock     *sk;  
//表示这个skb被接收的时间。  
    ktime_t         tstamp;  
//这个表示一个网络设备,当skb为输出时它表示skb将要输出的设备,当接收时,它表示输入设备。要注意,这个设备有可能会是虚拟设备如PPP接口(在3层以上看来)  
    struct net_device   *dev;  
//这里表示目的入口,最终生成的IP数据报路由形态,进入的数据包通过路由查询,如果发现是投递到本地的数据,则把该值赋值为ip_local_deliver,如果是转发的数据,则把该值赋值ip_forward,该头部还包含其余路由相关信息  
    struct  dst_entry  *dst;  
#ifdef CONFIG_XFRM 
// 用于IPSEC,记录安全转换信息
    struct  sec_path    *sp;  
#endif  
//这个域很重要,我们下面会详细说明。这里只需要知道这个域是保存每层的控制信息的就够了。  
    char            cb[48];  
//这个长度表示当前的skb中的数据的长度,这个长度即包括buf中的数据也包括切片的数据,也就是保存在skb_shared_info中的数据。这个值是会随着从一层到另一层而改变的。如在ip层时代表ip层的长度,在数据链路层代表数据链路层的长度
    unsigned int    len,  
//用于数据包被分片存储时,也就是skb_shared_info中的长度。不同于ip分片,只是在内存中用不连续的空间存储数据包
                data_len;  
//这个长度表示mac头的长度(2层的头的长度)  
    __u16       mac_len,  
//这个主要用于clone的时候,它表示clone的skb的头的长度。  
                hdr_len;  
  
//接下来是校验相关的域,以太网校对和  
    union {  
        __wsum      csum;  
        struct {  
            __u16   csum_start;  
            __u16   csum_offset;  
        };  
    };  
//优先级,主要用于QOS,流量控制模块根据这个域来对数据包进行分类,priority的值根据ip包头TOS字段计算得来,以决定调度策略。  
    __u32       priority;  
    kmemcheck_bitfield_begin(flags1);  
//接下来是一些标志位。  
//首先是是否可以本地切片的标志。  
    __u8        local_df:1,  
//为1说明头可能被clone。  
                cloned:1,  
//这个表示校验相关的一个标记,表示硬件驱动是否为我们已经进行了校验(前面的blog有介绍)  
                ip_summed:2,  
//这个域如果为1,则说明这个skb的头域指针已经分配完毕,因此这个时候计算头的长度只需要head和data的差就可以了。  
                nohdr:1,  
//这个域用于记录连接跟踪状态,连接跟踪处理函数resolve_normal_ct会对其进行赋值,NAT模块根据此状态来决定是使用连接跟踪记录还是查询NAT表进行地址转换,共有五种状态
> IP_CT_ESTABLISHED, 	
> IP_CT_RELATED, 	
> IP_CT_NEW, 	
> IP_CT_IS_REPLY,
> IP_CT_NUMBER = IP_CT_IS_REPLY * 21*/

                nfctinfo:3;  
  
//pkt_type主要是表示数据包的类型,比如多播,单播,回环等等。  
> *#define PACKET_HOST        0       /* To us        */
> #define PACKET_BROADCAST    1       /* To all       */
> #define PACKET_MULTICAST    2       /* To group     */
> #define PACKET_OTHERHOST    3       /* To someone else      */
> #define PACKET_OUTGOING     4       /* Outgoing of any type */*

    __u8            pkt_type:3,  
//这个域是一个clone标记。主要是在fast clone中被设置,我们后面讲到fast clone时会详细介绍这个域。  
                fclone:2,  
//ipvs拥有的域。  
                ipvs_property:1,  
//这个域应该是udp使用的一个域。表示只是查看数据。  
                peeked:1,  
//netfilter使用的域。是一个trace 标记  
                nf_trace:1;  
//二层设备的角度所看到的协议。典型的协议包括IP,IPV6,ARP,IPX,PPP, 802.1Q等,设备驱动用于通知上层调用哪个协议处理函数*/。  
    __be16          protocol:16;  
    kmemcheck_bitfield_end(flags1);  
//skb的析构函数,一般都是设置为sock_rfree或者sock_wfree.  
    void            (*destructor)(struct sk_buff *skb);  
  
//netfilter相关的域。/* nfct,连接跟踪引用计数器,连接跟踪处理函数resolve_normal_ct会对其进行赋值*/  
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)  
    struct nf_conntrack *nfct;  
    struct sk_buff      *nfct_reasm;  
#endif  
#ifdef CONFIG_BRIDGE_NETFILTER  
    struct nf_bridge_info   *nf_bridge;  
#endif  
  
//数据包由哪个接口到达  
    int         iif;  
  
//流量控制的相关域。  
#ifdef CONFIG_NET_SCHED  
    __u16           tc_index;   /* traffic control index */  
#ifdef CONFIG_NET_CLS_ACT  
    __u16           tc_verd;    /* traffic control verdict */  
#endif  
#endif  
  
    kmemcheck_bitfield_begin(flags2);  
//多队列设备的映射,也就是说映射到那个队列。  
    __u16           queue_mapping:16;  
#ifdef CONFIG_IPV6_NDISC_NODETYPE  
    __u8            ndisc_nodetype:2;  
#endif  
    kmemcheck_bitfield_end(flags2);  
  
    /* 0/14 bit hole */  
  
#ifdef CONFIG_NET_DMA  
    dma_cookie_t        dma_cookie;  
#endif  
#ifdef CONFIG_NETWORK_SECMARK  
    __u32           secmark;  
#endif  
//* mark数据包标记,记得iptables的mark操作吗?即是对此参数进行赋值*/。  
    __u32           mark;  
  
//vlan的控制tag。  
    __u16           vlan_tci;  
/*当接收一个包时,处理n层协议头的函数从其下层(n-1层)收到一个缓冲区,它的 skb->data指向n层协议的头。处理n层协议的函数把本层的指针初始化为skb->data,这个指针(data指针)的值会在处理下一层协议时改变 (skb->data将被初始化成缓冲区里的其他地址)。在处理n层协议的函数结束时,在把包传递给n+1层的处理函数前,它会把skb-> data指针指向n层协议头的末尾,这正好是n+1层协议的协议头,同时len减去这个值。
比如:网卡驱动程序收到一个UDP数据报后,它创建一个结构体struct sk_buff,把收到的数据全部拷贝到sk_buff->data指向的空间,然后,把skb->mac_header指向data,此时,数据报的开始位置是一个以太网头,所以skb->mac.raw指向链路层的以太网头。然后进入链路层处理,链路层处理后,把sk_buff->data指向链路层头部的末尾(恰好是IP层头部的开始),同时len减去这个值,这样,在逻辑上,skb已经不包含以太网头了,但通过skb->mac.raw还能找到它。这就是我们通常所说的,IP数据报被收到后,在链路层被剥去以太网头. */  
//传输层的头  
    sk_buff_data_t      transport_header;  
//网络层的头  
    sk_buff_data_t      network_header;  
//链路层的头。  
    sk_buff_data_t      mac_header;  
//表示缓冲区和数据部分的边界。在每一层申请缓冲区时,它会分配比协议头或协议数据大的空间。head和end指向缓冲区的头部和尾部,而data和 tail指向实际数据的头部和尾部。每一层会在head和data之间填充协议头,或者在tail和end之间添加新的协议数据。 
    sk_buff_data_t      tail;  
    sk_buff_data_t      end;  
    unsigned char       *head,  
                *data;  
//这是缓冲区的总长度,包括sk_buff结构和数据部分。如果申请一个len字节的缓冲区,alloc_skb函数会把它初始化成len+sizeof(sk_buff)。当skb->len变化时,这个变量也会变化  
    unsigned int        truesize;  
//引用计数,用于计算有多少实体引用了这个sk_buff缓冲区。它的主要用途是防止释放sk_buff后,还有其他实体引用这个sk_buff。,通常使用函数 skb_get和kfree_skb来操作这个变量
    atomic_t        users;  
}; 

2.路由器数据转发原理
2.1 网卡接收数据包到内存
网卡需要有驱动才能工作,驱动是加载到内核中的模块,负责衔接网卡和内核的网络模块,驱动在加载的时候将自己注册进网络模块,当相应的网卡收到数据包时,网络模块会调用相应的驱动程序处理数据。
在这里插入图片描述

1: 数据包从外面的网络进入物理网卡。如果目的地址不是该网卡以及不是链路层的以太网帧,且该网卡没有开启混杂模式,该包会被网卡丢弃。
2: 网卡将数据包通过DMA(或DMA)的方式写入到指定的内存地址,该地址由网卡驱动分配并初始化。注: 老的网卡可能不支持DMA,不过新的网卡一般都支持,具体多少划给DMA使用,不同的计算机体系有所不同,很多体系全部内存都可用。
3: 网卡通过硬件中断(IRQ)通知CPU,告诉它有数据来了
4: CPU根据中断表,调用已经注册的中断函数,这个中断函数会调到驱动程序(NIC Driver)中相应的函数
5: 驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知CPU了,这样可以提高效率,避免CPU不停的被中断。
6: 启动软中断。这步结束后,硬件中断处理函数就结束返回了。由于硬中断处理程序执行的过程中不能被中断,所以如果它执行时间过长,会导致CPU没法响应其它硬件的中断,于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理。

2.2 网卡驱动程序接收以太网帧
在这里插入图片描述
(针对NAPI设备)
7: 内核中的ksoftirqd进程专门负责软中断的处理,当它收到软中断后,就会调用相应软中断所对应的处理函数,对于上面第6步中是网卡驱动模块抛出的软中断,ksoftirqd会调用网络模块的net_rx_action函数
8: net_rx_action调用网卡驱动里的poll函数来一个一个的处理数据包
9: 在pool函数中,驱动会一个接一个的读取网卡写到内存中的数据包,内存中数据包的格式只有驱动知道
10: 驱动程序将内存中的数据包转换成内核网络模块能识别的skb格式,然后调用napi_gro_receive函数
11: napi_gro_receive会处理GRO相关的内容,也就是将可以合并的数据包进行合并,这样就只需要调用一次协议栈。然后判断是否开启了RPS(多cpu的负载均衡),如果开启了,将会调用enqueue_to_backlog
12: 在enqueue_to_backlog函数中,会将数据包放入CPU的softnet_data结构体的input_pkt_queue中,然后返回,如果input_pkt_queue满了的话,该数据包将会被丢弃,queue的大小可以通过net.core.netdev_max_backlog来配置
13: CPU会接着在自己的软中断上下文中处理自己input_pkt_queue里的网络数据(调用__netif_receive_skb_core)
14: 如果没开启RPS,napi_gro_receive会直接调用__netif_receive_skb_core
15: 看是不是有AF_PACKET类型的socket(也就是我们常说的原始套接字),如果有的话,拷贝一份数据给它。tcpdump抓包就是抓的这里的包。
16: 调用协议栈相应的函数,将数据包交给协议栈处理,通常即IP协议栈。
17: 待内存中的所有数据包被处理完成后(即poll函数执行完成),启用网卡的硬中断,这样下次网卡再收到数据的时候就会通知CPU

linux旧的协议找收包方式(非NAPI设备):
netif_rx函数补充说明:
与协议无关的、网络设备均支持的通用网络接收处理函数 netif_rx(skb) 。
netif_rx() 函数作用是将报文加入到队列中,如果队列满,则会丢弃此分组,等待CPU处理,如果是多核CPU,则会存在多个队列,每个CPU维护一个队列。
netif_rx(skb)处理流程图
在这里插入图片描述

-> queue(skb进入队列)
-> netif_receive_skb(二三层的经典接口函数,开始内核协议栈之旅,对包进行处理如vlan报头处理等,按照注册的协议进行回调,准备将包交付给上层协议栈,报文走向ip/arp/rarp,如下面的步骤))
-> bond
-> packet_type_all: deliver_skb(当有抓包程序抓包时会注册到内核,此时分发数据包给抓包程序)
-> bridge(内核编译开Bridge_config,则将该数据包让网桥函数来处理,否则handle_bridge定义为空操作)
/是一个IP包,调用ip_rcv处理/
-> packet_type(IPV4)->func == ip_rcv

2.2 接收IPV4报文ip_rcv函数
ip_rcv函数先进行一些例行检查,判断是否发给其他主机,IPV4版本号是否正确,长度是否正确等等。
紧接着进入netfilter,进行钩子函数的处理,如进入netfilter ->PREROUTING链处理,nat;mangle;conntrack在此注册了钩子函数。
最后进行如下的操作处理:
ip_conntrack_defrag(对分片包重组); ip_conntrack_in (判断数据包是否已在连接跟踪表中,如果不在,则为数据包初始化ip_conntrack 且赋值,并记录连接跟踪状态;
dnat操作(目的地址转换);
mark操作(skb->mark,在后续的处理过程中可以使用此mark标记对数据包进行分类,如制定策略路由,TC流量队列整形)等等*/
-> NF_HOOK(PREROUTING)
->ip_rcv_finish
/进入路由查找流程/
-> ip_route_input
/路由缓存查找,如果命中则直接赋值给skb->dst = (struct dst_entry)rth;*/
-> ip_route_input_cached
/缓存没有命中则进入路由表查找(此处包含策略路由,主路由,多路径选择等等的查找),如果命中则创建新的缓存,
赋值rth->u.dst.input = ip_forward/ ip_local_deliver, rth->u.dst.output=ip_output等等,
注意:路由查找的最终表示形态为(struct dst_entry), 最后都要进行skb->dst = rth赋值操作,路由缓存即是在 dst_entry 封装一层rtable结构
/
-> ip_route_input_slow
-> ip_mkroute_input
-> __mkroute_input
dst->input = (ip_forward/ ip_local_deliver)
dst->output = ip_output
-> dst_input
-> LOCAL_IN: dst->input == ip_local_deliver
-> NF_HOOK(NF_INPUT)
-> ip_local_deliver_finish
-> ipprot->handler(tcp, udp, icmp …)
-> FORWARD: dst->input == ip_forward

转发:
在ip_forward,对普通数据包并没有做过多复杂的操作,只是进行一些必要检查,如pkt_type类型判断(是否发往本地),是否符合分片条件?多播判断处理,设置优先级等等,此处会用IP包头TOS字段计算优先级,并赋值给skb->priority,如果是ipsec包则进入ipsec处理*/
ip_forward
/如果是ipsec则进入ipsec 处理/
-> xfrm4_route_forward (net/xfrm.h, get xfrm_dst)
-> xfrm_route_forward
-> __xfrm_route_forward
-> xfrm_lookup
-> xfrm_find_bundle
-> afinfo->find_bundle == __xfrm4_find_bundle
-> xfrm_bundle_create
-> afinfo->bundle_create == __xfrm4_bundle_create
tunnel mode
-> xfrm_dst_lookup
-> afinfo->dst_lookup == xfrm4_dst_lookup
-> __ip_route_output_key
-> dst_list: dst->list=policy_bundles, policy->bundles = dst

普通数据包进入netfilter Forward链处理,mangle表;filter表在此注册了钩子函数,我们的路由器绝大部分过滤操作即在此完成(如端口过滤,URL过滤,MAC地址过滤等等*/
-> NF_HOOK(NF_FORWARD)
-> ip_forward_finish
在前面的数据接收部分(ip_rcv),经过ip_route_input时,路由处理函数已经skb->dst->output赋值为ip_output
-> dst_output = ip_output

发送Ip_output
dst_output: dst_list循环
/如果是ipsec则进入xfrm处理/
-> dst->output == xfrm_dst->output == xfrm4_output == xfrm4_state_afinfo->output
-> NF_HOOK(POSTROUTING)
-> xfrm4_output_finish
-> gso :
-> xfrm4_output_finish2
-> xfrm4_output_one
-> mode->output
-> type->output
-> skb->dst=dst_pop(skb->dst)
-> nf_hook(NF_OUTPUT)
-> !dst->xfrm
-> dst_output
-> nf_hook(POSTROUTING)
-> dst->output == ip_output
/普通数据包直接进入netfilter POSTROUTING链处理,
Mangle 标记数据包(此处不常用)
NAT(nf_nat_out ;nf_nat_adjust )进行源地址转换,更新连接跟踪记录,重新计算效验值)
连接跟踪( ipv4_confirm写入连接跟踪链表)在此注册了钩子函数)
/
-> NF_HOOK(POSTROUTING)
/会再次进行分片检查/
-> ip_finish_output
-> ip_finish_output2
/在arp里, hh_output会被初始化为dev_queue_xmit ,二层头部在此添加进去/
-> hh_output == dev_queue_xmit
接下来就是流量控制进行队列整形,然后由驱动程序发送数据……………

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

渐行渐远962

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值