huchuan2025/30-Resource/32-Work/Linux 网络数据包的接收和发送流程.md

18 KiB
Raw Blame History

数据包接收流程

为了简单起见,我们将描述在物理网卡上接收和发送 Linux 网络数据包的过程,以 UDP 数据包处理过程为例,并尽量忽略一些无关的细节。

从网卡到内存

众所周知,每个网络设备(网卡)都有一个驱动程序来工作,并且该驱动程序需要在内核启动时加载到内核中。从逻辑上看,驱动程序是负责连接网络设备和内核网络协议栈的中间模块。每当网络设备接收到一个新数据包时,它会触发一个中断,而相应的处理中断的程序正是加载到内核中的驱动程序。

下图详细展示了数据包如何从网络设备进入系统内存,并由内核中的驱动程序和网络协议栈处理的过程。

  1. 数据包进入物理网卡,如果目标地址不是该网络设备且该设备没有开启混杂模式,则数据包会被丢弃。

  2. 物理网卡通过DMA将数据包写入指定的内存地址,该地址由网卡驱动程序分配和初始化。

  3. 物理网卡通过硬件中断IRQ通知CPU有新数据包到达网卡并需要处理。

  4. 接下来CPU根据中断向量表调用已注册的中断函数该中断函数将调用驱动程序网卡驱动中的相应函数。

  5. 驱动程序首先禁用网卡的中断表示驱动已经知道内存中有数据并告诉物理网卡下次接收到数据包时直接写入内存不要再通知CPU以提高效率并避免CPU被不断中断。

  6. 启动一个软中断以继续处理数据包。这样做的原因是硬件中断处理程序在执行过程中不能被中断因此如果执行时间过长会导致CPU无法响应其他硬件中断所以内核引入了软中断使得硬件中断处理程序中耗时的部分可以转移到软中断处理程序中慢慢处理。

内核数据包处理

上一步中的网络设备驱动程序将通过触发内核网络模块中的软中断处理函数来处理数据包,内核处理数据包的过程如下图所示。

  1. 对于上一步中驱动程序发出的软中断,内核中的 ksoftirqd 进程会调用网络模块中相应的软中断处理函数,准确地说,这里调用的是net_rx_action函数。

  2. net_rx_action随后会调用网卡驱动程序中的poll函数,逐个处理数据包。

  3. poll函数会让驱动程序读取网卡写入内存的数据包,实际上,内存中数据包的格式只有驱动程序知道;

  4. 驱动程序将内存中的数据包转换为内核网络模块识别的skbsocket buffer格式然后调用[napi_gro_receive](https://zhida.zhihu.com/search?content_id=252497158&content_type=Article&match_order=1&q=napi_gro_receive&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3NDIzMTMwNzAsInEiOiJuYXBpX2dyb19yZWNlaXZlIiwiemhpZGFfc291cmNlIjoiZW50aXR5IiwiY29udGVudF9pZCI6MjUyNDk3MTU4LCJjb250ZW50X3R5cGUiOiJBcnRpY2xlIiwibWF0Y2hfb3JkZXIiOjEsInpkX3Rva2VuIjpudWxsfQ.tiI07qTKsomFW4GH4XjrAm_5OgFk2EU8bUAMoUqWKic&zhida_source=entity)函数。

  5. napi_gro_receive函数会处理GROGeneric Receive Offload相关的内容即合并可以合并的数据包从而只需调用一次协议栈然后判断是否启用了 RPSReceive Packet Steering如果启用了则会调用enqueue_to_backlog函数。

  6. enqueue_to_backlog函数会将数据包放入input_pkt_queue结构中并返回。

    注意:如果input_pkt_queue已满,数据包将被丢弃,该队列的大小可以通过net.core.netdev_max_backlog配置。

  7. 随后CPU 会在软中断上下文中处理其input_pkt_queue中的网络数据,实际上是通过调用__netif_receive_skb_core函数来完成。

  8. 如果未启用RPSnapi_gro_receive函数会直接调用__netif_receive_skb_core函数来处理网络数据包。

  9. 紧接着,如果存在类型为AF_PACKET的原始套接字raw socketCPU会将数据复制一份到该套接字中tcpdump 捕获的数据包就是这种数据包)。

  10. 将数据包传递给内核的 TCP/IP 协议栈进行处理。

当内存中的所有数据包都处理完毕(poll函数执行完成重新启用网卡的硬件中断以便下次网卡再次接收到数据时通知CPU。

内核网络协议栈

此时,内核 TCP/IP 协议栈接收到的数据包实际上是第3层网络层的数据包所以数据包将首先到达 IP 网络层,然后再传递到传输层进行处理。

IP网络层

  • ip_rcv是 IP 网络层处理模块的入口函数,它首先确定数据包是否需要被丢弃(目标 MAC 地址不是当前网卡且网卡未设置为混杂模式),如果需要进一步处理,则调用在 netfilter 中注册的[NF_INET_PRE_ROUTING](https://zhida.zhihu.com/search?content_id=252497158&content_type=Article&match_order=1&q=NF_INET_PRE_ROUTING&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3NDIzMTMwNzAsInEiOiJORl9JTkVUX1BSRV9ST1VUSU5HIiwiemhpZGFfc291cmNlIjoiZW50aXR5IiwiY29udGVudF9pZCI6MjUyNDk3MTU4LCJjb250ZW50X3R5cGUiOiJBcnRpY2xlIiwibWF0Y2hfb3JkZXIiOjEsInpkX3Rva2VuIjpudWxsfQ.x1YIlAnehTkjllddwWzQAwUaqQcn_CEBplw0puyUJ3s&zhida_source=entity)链中的处理函数。

  • NF_INET_PRE_ROUTING是 netfilter 在协议栈中放置的一个钩子函数,通过 iptables 注入一些数据包处理函数来修改或丢弃数据包。如果数据包未被丢弃,将继续向下传递。

    netfilter 链中的处理逻辑,如NF_INET_PRE_ROUTING,可以通过 iptables 进行设置。

  • 路由处理:如果目标 IP 不是本地 IP 且未启用 IP 转发,则数据包将被丢弃;否则,数据包将传递到ip_forward函数进行转发处理。

  • ip_forward函数将首先调用 netfilter 在[NF_INET_FORWARD](https://zhida.zhihu.com/search?content_id=252497158&content_type=Article&match_order=1&q=NF_INET_FORWARD&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3NDIzMTMwNzAsInEiOiJORl9JTkVUX0ZPUldBUkQiLCJ6aGlkYV9zb3VyY2UiOiJlbnRpdHkiLCJjb250ZW50X2lkIjoyNTI0OTcxNTgsImNvbnRlbnRfdHlwZSI6IkFydGljbGUiLCJtYXRjaF9vcmRlciI6MSwiemRfdG9rZW4iOm51bGx9.ZWVB2LHebelQ9B-Np1mNpzQ4AQyA3H0VRcdfobNMgNM&zhida_source=entity)链中注册的处理函数,如果数据包未被丢弃,将继续调用dst_output_sk函数。

  • dst_output_sk函数将调用 IP 网络层的适当函数来发送数据包,此步骤的详细内容将在下一节关于发送数据包的部分中描述。

  • ip_local_deliver:如果上述路由处理发现目标 IP 是本地网卡的 IP则将调用ip_local_deliver函数,该函数首先调用[NF_INET_LOCAL_IN](https://zhida.zhihu.com/search?content_id=252497158&content_type=Article&match_order=1&q=NF_INET_LOCAL_IN&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3NDIzMTMwNzAsInEiOiJORl9JTkVUX0xPQ0FMX0lOIiwiemhpZGFfc291cmNlIjoiZW50aXR5IiwiY29udGVudF9pZCI6MjUyNDk3MTU4LCJjb250ZW50X3R5cGUiOiJBcnRpY2xlIiwibWF0Y2hfb3JkZXIiOjEsInpkX3Rva2VuIjpudWxsfQ.RxOHbmMBa9x3WswoKqN9OvauySvC-BGZ8r-KNbVZyp4&zhida_source=entity)链中相关的处理函数,如果通过,则将数据包传递到传输层。

传输层

  • udp_rcv函数是 UDP 处理层模块的入口函数,它首先调用__udp4_lib_lookup_skb函数,根据目标 IP 和端口查找对应的套接字(所谓的套接字基本上是由 IP+端口组成的结构)。如果未找到对应的套接字,则数据包将被丢弃;否则,继续处理。

  • sock_queue_rcv_skb函数首先检查套接字的接收缓存是否已满,如果已满则丢弃数据包;其次,它调用sk_filter检查数据包是否符合条件。如果当前套接字上设置了过滤器且数据包不符合条件,则数据包也会被丢弃。

sk_filter 函数是 Linux 内核中用于套接字过滤Socket Filtering的一个接口。它允许应用程序在数据包到达用户空间之前在内核空间对数据包进行过滤。这个函数的主要目的是执行与给定套接字关联的BPF过滤器程序。具体实现参考 net/core/filter.c 中的sk_filter_trim_cap函数。

  • __skb_queue_tail函数将数据包放入套接字的接收队列末尾。

  • sk_data_ready通知套接字数据包已准备好。

  • 调用sk_data_ready后,一个数据包处理完毕,等待应用层读取。

注意:上述所有执行过程都在软中断上下文中进行。

数据包的发送流程

从逻辑上讲Linux 网络数据包的发送过程与接收过程相反,因此,我们仍然以通过物理网卡发送 UDP 数据包为例进行说明。

应用层

应用层的开始是应用程序调用 Linux 网络接口创建套接字,下图详细展示了应用层如何构建套接字并将其发送到传输层。

  • 调用socket%28...%29来创建一个套接字结构并初始化相应的操作函数。

  • sendto%28sock, ...%29由应用层程序调用以开始发送数据包;此函数调用后面的inet_sendmsg函数。

  • inet_sendmsg此函数主要检查当前套接字是否绑定了源端口,如果没有,则调用inet_autobind函数分配一个端口,然后调用 UDP 层函数进行传输。

  • inet_autobind函数将调用get_port函数以获取一个可用的端口。

传输层

  • udp_sendmsg函数是 UDP 传输层模块发送数据包的入口点。该函数首先调用ip_route_output_flow函数获取路由信息(主要是源 IP 和网卡),然后调用ip_make_skb构造skb结构,最后将网卡信息与skb关联。

  • ip_route_output_flow函数主要处理路由信息,它将根据路由表和目标 IP 确定数据包应从哪个网络设备发送。如果套接字未绑定源 IP该函数还将根据路由表为其找到最合适的源 IP。如果套接字绑定了源 IP但根据路由表与该源 IP 对应的网卡无法到达目标地址,则数据包将被丢弃,并返回错误以表示发送失败。该函数最终将找到的网络设备和源 IP 填充到flowi4结构中,并将其返回给udp_sendmsg函数。

  • ip_make_skb函数使用分配的 IP 数据包头(包括源 IP 信息)构造skb数据包,并调用__ip_append_dat函数对数据包进行切片,检查套接字的发送缓存是否已耗尽,如果已耗尽则返回ENOBUFS错误消息。

  • udp_send_skb%28skb, fl4%29函数用 UDP 数据包头填充skb并处理校验和,然后将其传递给 IP 网络层中的相应函数。

IP 网络层

  • ip_send_skb是 IP 网络层模块发送数据包的入口函数,它实质上调用后面的一系列函数来发送网络层数据包。

  • __ip_local_out_sk函数用于设置 IP 数据包头的长度和校验和值,然后调用在NF_INET_LOCAL_OUT钩子链上注册的后续处理函数。

  • NF_INET_LOCAL_OUT是一个 netfilter 钩子门,可以通过 iptables 配置链上的处理函数;如果数据包未被丢弃,将继续沿链传递。

  • dst_output_sk函数根据skb内部的信息调用相应的输出函数ip_output

  • ip_output函数将前一层udp_sendmsg获取的网卡信息写入skb,然后调用在[NF_INET_POST_ROUTING](https://zhida.zhihu.com/search?content_id=252497158&content_type=Article&match_order=1&q=NF_INET_POST_ROUTING&zd_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ6aGlkYV9zZXJ2ZXIiLCJleHAiOjE3NDIzMTMwNzAsInEiOiJORl9JTkVUX1BPU1RfUk9VVElORyIsInpoaWRhX3NvdXJjZSI6ImVudGl0eSIsImNvbnRlbnRfaWQiOjI1MjQ5NzE1OCwiY29udGVudF90eXBlIjoiQXJ0aWNsZSIsIm1hdGNoX29yZGVyIjoxLCJ6ZF90b2tlbiI6bnVsbH0.NCFqIOnFlYtze0wrolgwpJYn67FzqGOWXhXkHEIe8rM&zhida_source=entity)钩子链上注册的处理函数。%2ANF_INET_POST_ROUTING是 netfilter 钩子链NF_INET_POST_ROUTING

  • NF_INET_POST_ROUTING是一个 netfilter 钩子门,可以通过 iptables 配置链上的处理函数在这个步骤中主要配置源地址转换SNAT这会导致此skb的路由信息发生变化。

  • ip_finish_output函数确定自上一步以来路由信息是否已发生变化,如果已变化,则需要再次调用dst_output_sk函数(当此函数再次被调用时,可能不会进入调用ip_output函数的分支,而是进入 netfilter 指定的输出函数,可能是xfrm4_transport_output),否则将继续传递。

  • ip_finish_output2函数根据目标 IP 在路由表中查找下一跳地址,然后调用__ipv4_neigh_lookup_noref函数在 ARP 表中查找下一跳的邻居信息,如果未找到,则调用__neigh_create函数构建一个空的邻居结构。

  • dst_neigh_output函数调用neigh_resolve_output函数获取邻居信息,并用其中的 MAC 地址填充skb,然后调用dev_queue_xmit函数发送数据包。

内核处理数据包

  • dev_queue_xmit函数是内核模块开始处理发送数据包的入口点此函数将首先获取设备的相应队列规则qdisc如果没有例如环回接口或 IP 隧道),则会直接调用dev_hard_start_xmit函数,否则数据包将通过流量控制%28TC%29模块进行处理。

  • 流量控制模块主要负责过滤和排序数据包,如果队列已满,数据包将被丢弃,详情请参阅:http://tldp.org/HOWTO/Traffic-Control-HOWTO/intro.html

  • dev_hard_start_xmit函数首先将skb的一个副本复制到“数据包探针”(tcpdump命令从这里获取数据包),然后调用ndo_start_xmit函数发送数据包。如果dev_hard_start_xmit函数返回错误,调用它的函数会将skb放置在某个位置,并抛出一个软中断NET_TX_SOFTIRQ到软中断处理函数net_tx_action,以便稍后重试处理。

  • ndo_start_xmit函数绑定到特定驱动程序处理发送数据的处理函数。

注意ndo_start_xmit函数将指向特定网卡驱动程序以发送数据包,在此步骤之后,发送数据包的任务将交给网络设备驱动程序,不同的网络设备驱动程序有不同的处理方式,但整体流程基本相同。

  • skb放入网卡的传输队列。

  • 通知网卡发送数据包。

  • 网卡发送完成后向 CPU 发送中断。

  • 收到中断后清理skb

Source

https://www.sobyte.net/post/2022-10/linux-net-snd-rcv/