当前位置: 首页 > Linux

Linux网络-数据包接收流程

时间:2023-04-06 02:07:05 Linux

本文将逐步介绍Linux系统中数据包是如何从网卡传到流程的。如果英文没有问题,强烈推荐阅读下面参考资料中的两篇文章,里面介绍的比较详细。本文只讨论以太网的物理网卡,不涉及虚拟设备,以一个UDP包接收过程为例。本例中列出的函数调用关系来自内核3.13.0。如果你的内核不是这个版本,函数名和相关路径可能不同,但背后的原理应该是一样的(或者有细微差别)。网卡到内存网卡需要驱动才能工作。驱动是加载到内核中的一个模块,负责连接网卡和内核的网络模块。当驱动程序被加载时,它会将自己注册到网络模块中。当相应的网卡收到数据包后,网络模块会调用相应的驱动程序来处理数据。下图展示了一个数据包是如何进入内存并被内核的网络模块处理的:+-----+||内存+--------+1||2DMA+--------+--------+--------+--------+|数据包|-------->|网卡|------------>|数据包|数据包|数据包|......|+------+||+--------+--------+--------+------+||<--------++-----+||+--------------+||3|提高IRQ|禁用中断|5|||↓|+-----++-------------+||运行IRQ处理程序|||CPU|-------------------->|网卡驱动|||4||+-----++-------------+|6|提高软IRQ|↓1:数据包从外网进入物理网卡,如果目的地址不是网卡,网卡没有开启混杂模式,数据包会被网卡丢弃。2:网卡通过DMA将数据包写入指定内存地址,由网卡驱动分配初始化。注意:较旧的NIC可能不支持DMA,但较新的NIC通常支持。3:网卡通过硬件中断(IRQ)通知CPU,告诉它有数据来了4:CPU根据中断表调用注册的中断函数,这个中断函数会被调用到相应的函数中驱动程序(NICDriver)5:驱动程序首先禁止网卡的中断,也就是说驱动程序已经知道内存中有数据,并告诉网卡下次直接将数据包写入内存,并且不再通知CPU,这样可以提高效率,避免CPU不停的被中断。6:启动软中断。这一步结束后,硬件中断处理函数返回。由于硬中断处理程序的执行不能被打断,如果执行时间过长,CPU将无法响应其他硬件中断,所以内核引入了软中断,可以去掉耗时的部分硬中断处理程序。移到软中断处理函数中慢慢处理。内核网络模块的软中断会触发内核网络模块中的软中断处理函数,后续流程如下+-----+17||+------------>|网卡|||||启用IRQ+-----+||+------------+内存||阅读+--------+--------+-------+--------++-------------->|网卡驱动|<----------------------|数据包|数据包|数据包|......||||9+--------+--------+--------+--------+|+------------+|||skP滚|8提高软IRQ|6+----------------+||10||↓↓+-------------+调用+---------++----------------++--------------------+12+--------------------+|net_rx_action|<--------|ksoftirqd||napi_gro_receive|-------->|enqueue_to_backlog|----->|CPUinput_pkt_queue|+----------------+7+------------++----------------+11+--------------------++--------------------+||1314|+--------------------+↓↓+-------------------------+15+------------------------+|__netif_receive_skb_core|------------>|数据包水龙头(AF_PACKET)|+------------------------++----------------------+||16↓+----------------+|协议层|+----------------+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。如果启用,会调用enqueue_to_backlog12:在enqueue_to_backlog函数中,数据包会被放入CPU的softnet_data结构的input_pkt_queue中,然后返回。如果input_pkt_queue满了,数据包会被丢弃,队列的大小可以通过net.core.netdev_max_backlog进行配置13:CPU会在自己的软中断上下文中处理input_pkt_queue中的网络数据(调用__netif_receive_skb_core)14:如果没有开启RPS,napi_gro_receive会直接调用__netif_receive_skb_core15:检查是否有AF_PACKET类型的socket(也就是原来的socket),如果有,就复制一份数据给它。tcpdump在此处捕获数据包。16:调用协议栈相应函数,将数据包交给协议栈处理。17:当内存中的数据包全部处理完毕(即poll函数执行完毕),使能网卡的硬中断,这样下次网卡收到数据时,通知中央处理器。enqueue_to_backlog函数也会被netif_rx函数调用,netif_rx是lo设备发送数据包时调用的函数。由于协议栈的IP层是一个UDP包,所以第一步会进入IP层,然后逐级向下调整功能:||↓混杂模式&&+--------+PACKET_OTHERHOST(由驱动程序设置)+----------------+|ip_rcv|-------------------------------------->|丢弃这个数据包|+--------++------------------+||↓+--------------------+|NF_INET_PRE_ROUTING|+----------------------+||↓+--------+||启用ipforword+------------++----------------+|路由|-------------------->|ip_forward|-------->|NF_INET_FORWARD|||+------------++----------------++--------+||||目标IP是本地的↓↓+----------------++----------------+|dst_output_sk||ip_local_deliver|+----------------++----------------+||↓+------------------+|NF_INET_LOCAL_IN|+--------------------+||↓+------------+|UDP层|+------------+i??p_rcv:ip_rcv函数是IP模块的入口函数。在这个函数中,首先是将发送的垃圾数据包(目的mac地址不是当前网卡,但是因为网卡设置为混杂模式而接收到)直接丢弃,然后调用函数NF_INET_PRE_ROUTING注册onNF_INET_PRE_ROUTING:netfilter放在协议栈的钩子,可以通过iptables注入一些数据包处理函数来修改或丢弃数据包,如果数据包没有被丢弃,会继续往下走routing:路由,如果目的IP是不是本地IP,并且没有开启ip转发功能,那么数据包会被丢弃,如果开启了ip转发功能,那么会进入ip_forward函数ip_forward:ip_forward会先调用netfilter注册的NF_INET_FORWARD相关函数,如果数据包没有被丢弃,那么会继续调用dst_output_sk函数dst_output_sk:这个函数会调用IP层对应的函数发送数据包,与下一篇要介绍的发包流程的后半部分相同ip_local_deliver:如果在上面的路由过程中发现目的IP是本地IP,那么就会调用这个函数。在这个函数中,会先调用NF_INET_LOCAL_IN相关的钩子程序。如果通过,则将数据包向下发送到UDP层UDP层||↓+---------++---------------------+|udp_rcv|----------->|__udp4_lib_lookup_skb|+---------++---------------------+||↓+-----------------++---------+|sock_queue_rcv_skb|----->|sk_过滤器|+----------------------++------------+||↓+-----------------+|__skb_queue_tail|+----------------+||↓+----------------+|sk_data_ready|+------------+udp_rcv:udp_rcv函数是UDP模块的入口函数,会调用其他函数,主要是做一些必要的检查,其中一个重要的调用是__udp4_lib_lookup_skb,这个函数会根据目的IP和端口找到对应的socket。如果没有找到对应的socket,则丢弃该数据包,否则继续sock_queue_rcv_skb:主要做两件事,一是检查这个socket的接收缓冲区是否正确满,如果满了,则丢弃数据packet,然后调用sk_filter查看packet是否满足条件。如果在当前socket上设置了过滤器,而数据包不满足条件,数据包也会被丢弃(在Linux中,每个sock过滤器可以像tcpdump一样定义在et上,不符合条件的数据包将被丢弃)__skb_queue_tail:将数据包放在socket接收队列的尾部sk_data_ready,一个数据包处理完成,等待应用层程序读取。以上所有函数都是在软中断的上下文中执行的。socket应用层接收数据一般有两种方式。一种是recvfrom函数阻塞在那里等待数据的到来。一种情况,当socket收到通知后,recvfrom会被唤醒,然后读取接收队列的数据;另一种是通过epoll或者select监听对应的socket,当收到通知时,调用recvfrom函数从队列中读取Receive数据。在这两种情况下,都可以正常接收相应的数据包。结语了解数据包的接收过程,可以帮助我们弄清楚在什么地方可以监控和修改数据包,以及在什么情况下数据包可能被丢弃。为我们处理网络问题提供了一些参考,了解了netfilter中对应的hooksiptables的位置,有助于理解iptables的用法,也有助于我们更好的理解Linux下的网络虚拟设备。在接下来的几篇文章中,我们将介绍Linux下的网络虚拟设备和iptables。请参阅监视和调整Linux网络堆栈:接收数据图示指南以监视和调整Linux网络堆栈:接收DataNAPI