Linux RX报文处理全流程解析

Linux网络协议栈接收(RX)报文处理流程是一个从硬件中断到应用层Socket的复杂过程,其核心在于通过软中断和NAPI机制在中断上下文和内核进程上下文之间取得平衡,以实现高吞吐量。整个过程可以概括为:硬件中断触发 -> 软中断调度 -> 协议栈逐层解析 -> 交付Socket队列

以下将分阶段详细解析其源码级流程,并辅以关键数据结构和函数调用链说明。

一、 核心数据结构与处理模型

在深入流程前,需理解两个核心概念:

  1. sk_buff (Socket Buffer) :贯穿整个网络栈的统一报文容器 。它通过head, data, tail, end指针管理报文数据和各层协议头,并携带元数据(如网络设备dev、协议protocol、路由信息skb->rtable)。
  2. NAPI (New API) :现代网卡驱动使用的混合中断与轮询 机制。其核心是struct napi_struct,它包含一个待处理报文队列(poll_list)和轮询函数(poll)。NAPI在高速流量下禁用中断,转为内核主动轮询,大幅减少中断开销。

二、 RX报文处理详细流程

阶段1:网卡接收与中断处理(硬中断上下文)

  1. DMA与描述符环 :网卡通过DMA将接收到的数据帧直接写入内核预留的环形缓冲区(Ring Buffer),并更新接收描述符(包含帧地址、长度、状态)。
  2. 硬件中断触发:当网卡收到帧、DMA完成或描述符环达到一定水位时,触发硬件中断(IRQ)。
  3. 中断处理函数 :内核调用网卡驱动注册的中断处理函数(如e1000_intr)。该函数的核心任务是:
    • 禁用该网卡进一步的中断,避免中断风暴。
    • 调度NAPI轮询 ,将设备的napi_struct加入到当前CPU的poll_list中。
    • 触发软中断 NET_RX_SOFTIRQ,通知内核在稍后的软中断上下文中进行报文处理。
c 复制代码
// 以E1000驱动为例的中断处理函数简化逻辑
static irqreturn_t e1000_intr(int irq, void *data) {
    struct net_device *netdev = data;
    struct e1000_adapter *adapter = netdev_priv(netdev);
    struct e1000_hw *hw = &adapter->hw;

    // 1. 读取中断原因寄存器,确认是接收中断
    u32 icr = er32(ICR);
    if (!(icr & E1000_ICR_RXT0))
        return IRQ_NONE; // 不是接收中断,直接返回

    // 2. 禁用该网卡的中断,防止新的中断干扰
    e1000_irq_disable(adapter);

    // 3. 调度NAPI轮询。`&adapter->napi`是设备的napi_struct
    if (likely(napi_schedule_prep(&adapter->napi))) {
        __napi_schedule(&adapter->napi); // 将napi加入当前CPU的poll_list
    }

    return IRQ_HANDLED;
}
// 此函数执行在中断上下文,必须快速返回。

阶段2:软中断与NAPI轮询(软中断上下文)

硬件中断返回后,内核会在一个更安全、可延迟的软中断上下文中执行实际的报文处理。

  1. 软中断入口 :内核执行net_rx_action()函数来处理NET_RX_SOFTIRQ软中断。
  2. 轮询设备net_rx_action()循环遍历当前CPU的poll_list,对每个设备的NAPI结构调用其注册的poll()方法(如e1000_clean)。
  3. 从环缓冲区取包 :驱动的poll()函数执行以下操作:
    • 读取描述符环,确认哪些描述符已被DMA填充(即已收到包)。
    • 为每个收到的数据帧分配一个新的sk_buffskb)结构。
    • 将DMA缓冲区中的数据映射或拷贝到skb中。
    • skb递交给上层网络栈 ,通常通过napi_gro_receive()netif_receive_skb()函数。
    • 清理并回收描述符,将其重新交给网卡用于下一次DMA。
  4. 预算与时间限制net_rx_action()时间预算(netdev_budget时间限制 。它会持续处理报文,直到处理数量超过预算或执行时间过长,以确保软中断不会独占CPU。未处理完的设备会留在poll_list中,等待下次软中断调用。
c 复制代码
// net_rx_action 函数的核心逻辑简化
static void net_rx_action(struct softirq_action *h) {
    struct softnet_data *sd = &__get_cpu_var(softnet_data);
    unsigned long time_limit = jiffies + 2; // 时间限制
    int budget = netdev_budget; // 默认预算300
    LIST_HEAD(list);
    LIST_HEAD(repoll);

    // 将当前CPU的poll_list转移到本地list,避免并发问题
    list_splice_init(&sd->poll_list, &list);

    while (!list_empty(&list)) {
        struct napi_struct *n;
        int work, weight;

        n = list_first_entry(&list, struct napi_struct, poll_list);
        weight = n->weight;

        work = 0;
        if (test_bit(NAPI_STATE_SCHED, &n->state)) {
            // 调用设备驱动的poll方法
            work = n->poll(n, weight); // 例如 e1000_clean
            trace_napi_poll(n);
        }
        budget -= work;

        // 检查:1. poll函数消耗了全部权重(weight)? 2. 预算超了? 3. 时间到了?
        if (work < weight || budget <= 0 || time_after(jiffies, time_limit)) {
            // 将未处理完的设备放入repoll列表
            list_move_tail(&n->poll_list, &repoll);
        } else {
            // 该设备本轮处理完毕,从列表中移除
            list_del_init(&n->poll_list);
            // 如果设备需要重新启用中断,则启用之
            if (test_bit(NAPI_STATE_DISABLE, &n->state))
                ____napi_complete(n);
            else
                napi_complete(n);
        }
        if (budget <= 0 || time_after(jiffies, time_limit))
            break;
    }
    // 将需要重新轮询的设备重新加回sd->poll_list
    list_splice_tail(&repoll, &sd->poll_list);
    // 如果poll_list不为空,说明还有工作,再次触发软中断
    if (!list_empty(&sd->poll_list))
        __raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

阶段3:协议栈分发与处理(进程上下文)

skb通过netif_receive_skb()napi_gro_receive()(支持GRO卸载时)进入协议无关的接收路径

  1. RPS处理 :如果内核配置了RPS(Receive Packet Steering),可能会在此处将skb转移到其他CPU的 backlog 队列,以实现负载均衡。
  2. 协议分发__netif_receive_skb_core()是核心分发函数。它主要做两件事:
    • 处理Tap设备 :如果有Tap设备(如用于虚拟机或抓包)监听,会克隆一份skb发送给它们。
    • 协议类型分发 :根据skb->protocol字段(如ETH_P_IPETH_P_ARP),调用相应的协议处理函数 。这是通过遍历ptype_allptype_base哈希表实现的。
c 复制代码
// 协议分发核心逻辑示意 (net/core/dev.c)
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc) {
    // ... 省略RPS、Tap处理等 ...

    // 获取二层协议类型
    type = skb->protocol;

    // 遍历协议处理程序链表
    list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
        if (ptype->type == type && (ptype->dev == null_or_dev || ptype->dev == skb->dev)) {
            if (pt_prev)
                ret = deliver_skb(skb, pt_prev, orig_dev); // 交付给上一个协议处理程序
            pt_prev = ptype;
        }
    }
    // ... 交付最后一个匹配的协议处理程序 ...
}
// 对于IP报文,`ptype->func` 指向 `ip_rcv()` 函数。
  1. 网络层处理 :以IP协议为例,ip_rcv()函数接收skb

    • 完整性检查:校验IP头部校验和、版本、长度等。
    • 路由决策 :调用ip_rcv_finish() -> ip_route_input_noref()。路由子系统根据目标IP地址决定报文去向:
      • 本机接收:目标IP是本机IP,报文上传给传输层。
      • 转发:目标IP是其他主机,且本机开启了IP转发,报文进入转发路径。
      • 丢弃:不符合任何条件。
    • 本机接收路径 :对于本机报文,调用ip_local_deliver()。如果报文是分片,则在此进行重组(ip_defrag())。最后,根据IP头中的协议字段(如IPPROTO_TCP, IPPROTO_UDP),调用ipprot->handler,即传输层入口函数(如tcp_v4_rcv()udp_rcv())。
  2. 传输层处理 :以TCP为例,tcp_v4_rcv()函数处理。

    • 查找对应的struct sock(通过IP和端口号在哈希表中查找)。
    • 进行复杂的TCP状态机处理(序列号、ACK、窗口管理等)。
    • 排好序、已确认的数据 放入对应Socket的接收缓冲区sk->sk_receive_queue)。

阶段4:应用层读取

  1. 用户态进程调用read(), recv()等系统调用。
  2. 内核的sys_recv()陷入内核,最终调用到Socket对应的struct proto中的recvmsg方法(如tcp_recvmsg)。
  3. tcp_recvmsg()sock的接收队列(sk_receive_queue)中拷贝数据到用户态缓冲区。
  4. 如果接收队列为空,且Socket是阻塞的,则进程将进入睡眠状态,等待数据到来。

三、 关键机制:GRO与RPS

  1. GRO (Generic Receive Offload) :在napi_gro_receive()中,内核尝试将多个相似的数据包(如属于同一个TCP流)合并成一个大的skb,然后再提交给协议栈。这极大地减少了协议栈的处理开销,是提升吞吐量的关键技术。
  2. RPS (Receive Packet Steering) :纯软件实现的接收多队列。在netif_receive_skb()中,根据数据包的哈希值(如四元组)选择一个目标CPU,将skb排入该CPU的softnet_data->input_pkt_queue。随后通过进程间中断(IPI) 唤醒目标CPU的软中断来处理它。这可以将接收负载分摊到多个CPU核心,尤其对单队列网卡有益。

四、 流程图与总结

整个RX流程可以总结为下表所示的层级与上下文转换:

处理阶段 执行上下文 关键函数/动作 核心数据结构
硬件收包 硬件/DMA 网卡DMA到Ring Buffer 描述符环(Descriptor Ring)
中断触发 硬中断 e1000_intr() struct net_device, struct napi_struct
调度轮询 硬中断 napi_schedule(), 触发NET_RX_SOFTIRQ softnet_data->poll_list
NAPI轮询 软中断 net_rx_action() -> 驱动poll() (如e1000_clean) sk_buff
协议分发 软中断 netif_receive_skb() -> __netif_receive_skb_core() sk_buff (protocol字段)
网络层 软中断 ip_rcv() -> ip_local_deliver() sk_buff, 路由表
传输层 软中断 tcp_v4_rcv() / udp_rcv() struct sock, sk_receive_queue
Socket交付 进程上下文 tcp_recvmsg() 拷贝数据到用户空间 用户缓冲区

总结 :Linux网络RX路径是一个精心设计的分层、异步流水线 。它通过硬中断快速响应 ,通过NAPI和软中断批量处理 来平衡延迟与吞吐量,通过协议分发器 将报文准确送达各层处理函数,最终通过Socket接口 服务于应用程序。理解sk_buff的生命周期和NAPI的调度机制是剖析此流程的关键。


参考来源

相关推荐
小侯不躺平.1 小时前
C++ Boost库【2】 --stringalgo字符串算法
linux·c++·算法
夏乌_Wx2 小时前
计算机网络实践项目 | 云相册(文件互传与管理系统)
linux·计算机网络
用户805533698032 小时前
嵌入式Linux驱动开发——设备树语法与编译工具——读懂这张"藏宝图"
linux·嵌入式
原来是猿2 小时前
网络计算器:理解序列化与反序列化(下)
linux·开发语言·网络·网络协议·json·php
木木_王2 小时前
嵌入式学习 | STM32裸板驱动开发(Day01)入门学习笔记(超详细完整版|点灯实验 + 库函数代码 + 原理全解)
linux·驱动开发·笔记·stm32·学习
勤自省2 小时前
ROS2从入门到“重启解决”:21讲8~12章踩坑血泪史与核心总结
linux·开发语言·ubuntu·ssh·ros
原来是猿2 小时前
Linux守护进程(Daemon)完全指南:从原理到实战
linux·运维·服务器·网络·php
阡陌..3 小时前
如何使用samba为Linux设置一个局域网共享盘
linux·运维·服务器
霞姐聊IT3 小时前
三大并发技术—进程、线程和协程
linux·运维·网络·操作系统