为了清晰起见,我们以UDP数据包为例,描述一个数据包在物理网卡上的接收与发送过程,并尽量忽略一些无关的细节。
数据包接收流程
从网卡到内存
每个网络设备(网卡)都需要一个驱动程序来工作,该驱动会在内核启动时被加载。从逻辑上看,驱动程序是连接网络设备和内核网络协议栈的桥梁。当网卡接收到一个新数据包时,它会触发一个硬件中断,而处理这个中断的程序,正是加载到内核中的网卡驱动。
下图详细展示了数据包从网络设备进入系统内存,并由驱动和协议栈处理的过程。

图1:数据包从网卡DMA到内存,再通过硬中断和软中断触发内核处理的完整流程。
- 数据包进入物理网卡。如果数据包的目标地址不是该网卡,且网卡未开启混杂模式,数据包会被直接丢弃。
- 物理网卡通过 DMA 技术,将数据包直接写入由驱动程序预先分配好的内存缓冲区。
- 数据写入完成后,网卡通过硬件中断(IRQ)通知CPU有新数据包需要处理。
- CPU根据中断向量表,调用驱动程序注册的中断处理函数。
- 驱动首先禁用网卡的中断。这相当于告诉网卡:“我知道数据来了,下次收到包直接放内存就好,暂时别通知我”,以此避免CPU被频繁中断,提升效率。
- 驱动程序启动一个软中断(soft IRQ),将实际的数据包处理工作移交出去。这是因为硬件中断处理必须快速完成,耗时操作会阻塞其他中断,所以内核引入了软中断机制来异步处理这些任务。
内核数据包处理
上一步中驱动程序触发的软中断,会由内核的网络模块接手。具体处理流程如下图所示。

图2:内核软中断处理进程 ksoftirqd 调用驱动程序poll函数,并将数据包送入协议栈的路径。
- 内核中的
ksoftirqd 进程会调用网络模块的软中断处理函数 net_rx_action。
net_rx_action 接着调用网卡驱动中的 poll 函数,开始逐个处理数据包。
- 驱动中的
poll 函数读取网卡写入内存的原始数据,并将其转换为内核网络模块能识别的标准格式——skb(socket buffer)。
- 驱动程序调用
napi_gro_receive 函数。该函数会处理GRO(Generic Receive Offload),将可能的小包合并,然后判断是否启用了RPS(Receive Packet Steering)。
- 如果启用了RPS,会调用
enqueue_to_backlog 函数,将数据包放入 input_pkt_queue 队列。注意:如果此队列已满(大小由 net.core.netdev_max_backlog 控制),数据包将被丢弃。
- 随后,CPU在软中断上下文中处理自己队列里的网络数据,调用
__netif_receive_skb_core 函数。
- 如果未启用RPS,
napi_gro_receive 会直接调用 __netif_receive_skb_core。
__netif_receive_skb_core 会检查是否存在 AF_PACKET 类型的原始套接字(例如 tcpdump 使用的套接字)。如果有,它会将数据包复制一份给这些套接字。
- 最后,数据包被传递给内核的 TCP/IP协议栈 进行处理。
当内存中所有数据包都处理完毕(poll 函数完成),驱动程序会重新启用网卡的硬件中断,等待下一次数据到达的通知。
内核网络协议栈
此时,进入协议栈的数据包是网络层(第3层)的数据包,因此会先经过IP层,再传递到传输层。
IP网络层

图3:IP层对数据包进行路由决策,区分是发往本地还是需要转发的完整流程。
ip_rcv 是IP层的入口函数。它进行基本检查后,会调用Netfilter钩子 NF_INET_PRE_ROUTING 链上的处理函数(可通过iptables配置)。
- 路由决策:判断目标IP是否是本机IP。如果不是,且系统未开启IP转发,则丢弃包;否则,进入
ip_forward 函数进行转发处理。
ip_forward 会先经过 NF_INET_FORWARD 钩子链,然后调用 dst_output_sk 函数进入发送流程。
- 如果目标IP是本机,则调用
ip_local_deliver 函数。该函数先经过 NF_INET_LOCAL_IN 钩子链,然后将数据包传递给传输层。
传输层(UDP)

图4:UDP层查找对应套接字,并通过过滤器检查后将数据包放入接收队列的流程。
udp_rcv 是UDP层的入口函数。它首先调用 __udp4_lib_lookup_skb,根据目标IP和端口查找对应的套接字。若未找到,则丢弃数据包。
sock_queue_rcv_skb 函数检查套接字接收缓存是否已满,并调用 sk_filter 执行可能的BPF过滤器程序。如果缓存满或过滤未通过,数据包将被丢弃。
__skb_queue_tail 将数据包放入套接字的接收队列末尾。
- 调用
sk_data_ready 函数通知套接字:有数据就绪,等待应用层读取。
注意:以上所有过程均在软中断上下文中执行。
数据包的发送流程
逻辑上,发送是接收的逆过程。我们同样以通过物理网卡发送UDP数据包为例。
应用层

图5:应用程序通过socket和sendto系统调用,将数据传递给内核传输层的初始步骤。
- 应用调用
socket(...) 创建套接字,并初始化相关操作函数。
- 应用调用
sendto(sock, ...) 发送数据,该调用会进入内核的 inet_sendmsg 函数。
inet_sendmsg 检查套接字是否绑定了源端口。如果没有,则调用 inet_autobind 自动分配一个。
inet_autobind 通过 get_port 函数获取一个可用端口。
传输层(UDP)

图6:UDP发送入口函数处理路由、构造skb并填充UDP头部的过程。
udp_sendmsg 是UDP发送的入口。它首先调用 ip_route_output_flow 获取路由信息(决定从哪个网卡、哪个源IP发出)。
ip_route_output_flow 根据路由表和目标IP确定出口设备和源IP。如果路由不可达,则返回错误。
- 然后调用
ip_make_skb 构造 skb 结构,并检查套接字发送缓存,若满则返回 ENOBUFS 错误。
- 最后调用
udp_send_skb 填充UDP头部和校验和,并将 skb 交给IP网络层。
IP网络层

图7:IP层设置包头、经过Netfilter钩子,并查询下一跳ARP地址的完整发送路径。
ip_send_skb 是IP层发送入口,它调用后续一系列函数。
__ip_local_out_sk 设置IP包头长度和校验和,然后经过 NF_INET_LOCAL_OUT Netfilter钩子链。
dst_output_sk 调用 ip_output 函数,将出口设备信息写入 skb,并经过 NF_INET_POST_ROUTING 钩子链(常用于SNAT)。
ip_finish_output 判断路由是否因NAT改变,若有变化则可能重新路由。
ip_finish_output2 根据目标IP在路由表中查找下一跳地址,并查询ARP表获取下一跳的MAC地址。若ARP表中无记录,则创建一个邻居项并触发ARP请求。
dst_neigh_output 用获取到的MAC地址填充 skb 的以太网头,然后调用 dev_queue_xmit 将数据包交给网络设备。
内核处理与设备驱动

图8:数据包经过流量控制(TC)队列,最终由网卡驱动程序发送出去的流程。
dev_queue_xmit 是内核将数据包交付给设备的入口。它首先获取设备的队列规则(qdisc)。如果是环回接口等无队列设备,则直接调用 dev_hard_start_xmit;否则,数据包会先经过流量控制(TC)模块进行排队、整形或过滤。若队列满,数据包可能被丢弃。
dev_hard_start_xmit 将 skb 复制一份给“数据包探针”(供 tcpdump 等抓包工具使用),然后调用网卡驱动注册的 ndo_start_xmit 函数。如果发送失败,会触发 NET_TX_SOFTIRQ 软中断稍后重试。
ndo_start_xmit 是具体的网卡驱动发送函数。此后,任务完全交给网卡驱动:
- 驱动将
skb 放入网卡的发送队列。
- 通知网卡开始发送数据包。
- 网卡发送完成后,向CPU发送一个中断。
- 驱动在中断处理程序中清理已发送的
skb。
总结
通过梳理Linux网络数据包的收发全路径,我们可以清晰地知道在哪个环节可以监控或修改数据包,以及在哪些情况下数据包可能被丢弃。特别是理解Netfilter各个钩子(HOOK)的位置,能帮助我们更透彻地掌握iptables的工作原理,并对Linux下的各种网络虚拟设备有更深的认识。
下图是一张更为综合的Netfilter/Iptables数据包流向全景图,涵盖了接收、转发、发送等所有路径,可以帮助我们建立全局观。

图9:展示数据包经过Netfilter五个钩子点的完整路径,包括路由决策、桥接和ARP处理。
值得注意的是,tcpdump 等抓包工具的抓包点,分别位于Ingress(接收路径)之前和Egress(发送路径)之后。理解整个内核网络栈的流程,是进行高性能网络监控与调试的基础。
参考
原文来源