当前位置: 首页 > 科技观察

Linux网络-数据包接收流程

时间:2023-03-12 12:32:58 科技观察

本文将逐步介绍Linux系统中数据包是如何从网卡传到流程的。如果英文没有问题,强烈推荐阅读下面参考资料中的两篇文章,里面介绍的比较详细。本文只讨论以太网的物理网卡,不涉及虚拟设备,以一个UDP包接收过程为例。本例中列出的函数调用关系来自内核3.13.0。如果你的内核不是这个版本,函数名和相关路径可能不同,但背后的原理应该是一样的(或者有细微差别)。网卡到内存网卡需要驱动才能工作。驱动是加载到内核中的一个模块,负责连接网卡和内核的网络模块。当驱动程序被加载时,它会将自己注册到网络模块中。当相应的网卡收到数据包后,网络模块会调用相应的驱动程序来处理数据。下图展示了一个数据包(packet)是如何进入内存并被内核的网络模块处理的:+-----+||Memroy+------+1||2DMA+--------+--------+--------+--------+|数据包|-------->|NIC|------------>|数据包|数据包|数据包|...|+------+||+------+--------+--------+--------+||<--------++-----+||+--------------+||3|RaiseIRQ|DisableIRQ|5|||↓|+-----++--------+||RunIRQhandler|||CPU|---------------->|网卡驱动|||4||+-----++-------------+|6|RaisesoftIRQ|↓1:数据包从外网进入物理网卡。如果目的地址不是网卡,且网卡没有开启混杂模式,则报文会被网卡丢弃。2:网卡通过DMA将数据包写入指定内存地址,由网卡驱动分配初始化。注意:较旧的NIC可能不支持DMA,但较新的NIC通常支持。3:网卡通过硬件中断(IRQ)通知CPU,告诉它有数据来了4:CPU根据中断表调用注册的中断函数,这个中断函数会被调用到相应的函数中驱动程序(NICDriver)5:驱动程序首先禁止网卡的中断,也就是说驱动程序已经知道内存中有数据,并告诉网卡下次直接将数据包写入内存,并且不再通知CPU,这样可以提高效率,避免CPU不停的被中断。6:启动软中断。这一步结束后,硬件中断处理函数返回。由于硬中断处理程序的执行不能被打断,如果执行时间过长,CPU将无法响应其他硬件中断,所以内核引入了软中断,可以去掉耗时的部分硬中断处理程序。移到软中断处理函数中慢慢处理。内核网络模块的软中断会触发内核网络模块中的软中断处理函数,后续流程如下+-----+14||+----------->|网卡|||||EnableIRQ+-----+||+------------+Memroy||Read+------+--------+--------+--------++---------------->|NICDriver|<--------------------|数据包|数据包|数据包|...||||9+------+------+-------+--------+|+------------+|||skbPoll|8RaisesoftIRQ|6+---------------+||10||↓↓+----------------+调用+------------++----------------+|net_rx_action|<------|ksoftirqd||napi_gro_receive|+----------------+7+----------++----------------+||11↓+------------------------+12+--------------------+|__netif_receive_skb_core|----------->|packettaps(AF_PACKET)|+----------------------------++-----------------------+||13↓+----------------+|protocollayers|+------------------+7:内核中的ksoftirqd进程专门负责软中断的处理。当它接收到软中断时,会调用相应软中断对应的处理函数。对于上面的第6步,是网卡驱动模块抛出的软中断,ksoftirqd会调用网络模块的net_rx_action函数8:net_rx_action调用网卡驱动中的poll函数,一个一个处理数据包9:在pool函数中,驱动会将网卡写入的数据包一个一个读入内存。只有驱动程序知道内存中数据包的格式。10:驱动将内存中的数据包转换成内核网络模块可以识别格式的skb,然后调用napi_gro_receive函数11:napi_gro_receive会处理GRO相关的内容,即合并可以合并的数据包,所以即只需要调用一次协议栈,然后调用__netif_receive_skb_core12:看是否有AF_PACKET类型的套接字(也就是我们常说的原始套接字),如果有,就复制一份数据到里面tcpdump在此处捕获数据包。13:调用协议栈的相应函数,将数据包交给协议栈处理。14:当内存中的数据包全部处理完毕(即poll函数执行完毕),使能网卡的硬中断,这样下次网卡收到数据时,通知CPU协议栈IP层,??因为是UDP包。所以第一步会进入IP层,然后逐级向下调整函数:||↓promiscuousmode&&+------+PACKET_OTHERHOST(setbydriver)+-----------------+|ip_rcv|---------------------------------------->|dropthispacket|+--------++----------------+||↓+----------------------+|NF_INET_PRE_ROUTING|+--------------------+||↓+--------+||enabledipforword+----------++----------------+|路由|-------------------->|ip_forward|------>|NF_INET_FOWARD|||+------------++---------------++--------+||||destinationIPislocal↓↓+----------------++-------------------+|dst_output_sk||ip_local_deliver|+----------------++----------------+||↓+----------------+|NF_INET_LOCAL_IN|+----------------+||↓+-----------+|UDPlayer|+------------+ip_rcv:ip_rcv函数是IP模块的入口函数。在这个函数中,***thing是直接丢弃垃圾数据包(目的mac地址不是当前网卡,但是因为网卡是混杂模式所以接收到的),然后调用注册的函数NF_INET_PRE_ROUTING在NF_INET_PRE_ROUTING上:netfilter放在协议栈中的钩子可以通过iptables注入一些数据包处理函数来修改或丢弃数据包。如果数据包没有被丢弃,会继续往下走路由:路由,如果目的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_filter|+--------------------++------------+||↓+---------------+|__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中可以像tcpdump一样在每个socket上定义过滤器,不满足条件的数据包会被丢弃discarded)__skb_queue_tail:将数据包放在socket接收队列的尾部执行过程是在软中断的上下文中进行的。socket应用层接收数据一般有两种方式。一种是recvfrom函数阻塞在那里等待数据的到来。在这种情况下,当socket收到通知时,recvfrom会被唤醒。,然后读取接收队列的数据;另一种是通过epoll或者select监听对应的socket,当收到通知时,调用recvfrom函数读取接收队列的数据。在这两种情况下,都可以正常接收相应的数据包。结语了解数据包的接收过程,可以帮助我们弄清楚在什么地方可以监控和修改数据包,以及在什么情况下数据包可能被丢弃。为我们处理网络问题提供了一些参考,了解了netfilter中对应的hooksiptables的位置,有助于理解iptables的用法,也有助于我们更好的理解Linux下的网络虚拟设备。在接下来的几篇文章中,我们将介绍Linux下的网络虚拟设备和iptables。