linux 内核 网络协议栈 流程基础分析

核心概念与数据结构

在了解流程之前,必须了解几个核心概念:

  1. struct sk_buff (Socket Buffer): 这是网络协议栈的“血液”。无论在协议栈的哪一层,数据包都以 sk_buff 的形式存在。它是一个复杂的数据结构,包含了指向数据包内容(header和payload)的指针,以及大量的元数据(如所属设备、协议类型、时间戳、校验和状态等)。
    • skb->data: 指向当前协议层头部的开始。随着数据包在协议栈中逐层处理(剥离头部),这个指针会向前移动。
    • skb->head, skb->end: 指向已分配数据区的开始和结束。
    • skb->mac_header, skb->network_header, skb->transport_header: 分别指向L2, L3, L4头的偏移量。
    • skb->dev: 指向与此包关联的网络设备 (struct net_device)。
    • skb->sk: 指向与此包关联的套接字 (struct sock)。
  2. struct net_device: 内核中代表一个网络接口(如 eth0)的数据结构。它包含了设备的状态、属性以及一组操作函数指针 net_device_ops,用于驱动与协议栈的交互。
  3. NAPI (New API): 一种高效处理网络包的机制,旨在解决传统纯中断方式在高流量下的“中断风暴”问题。它结合了中断和轮询,在有数据到达时通过中断触发,然后进入无中断的轮询模式(poll)批量收包,处理完后再重新开启中断。

第一部分:数据包接收流程 (从网卡到应用)

我们将以一个最常见的场景为例:一个以太网帧(包含了IP包,IP包内是TCP段)到达网卡。

阶段一:硬件与驱动层 (L1/L2) – 数据包的到来

  1. 物理接收: 网卡(NIC)在其物理层(L1)接收到电信号,解码成数字比特流,并根据以太网帧(L2)的格式(前导码、SFD)识别出一个完整的帧。
  2. DMA传输: 网卡通过DMA(Direct Memory Access)将接收到的帧数据直接写入到内存中的一个预先分配好的Ring Buffer(环形缓冲区)中。这个过程不需要CPU的参与。Ring Buffer是由网卡驱动在初始化时分配的。
  3. 触发中断: 数据写入完成后,NIC向CPU发起一个硬件中断(IRQ),通知CPU有新的数据包到达。
  4. 中断处理: CPU收到中断后,会暂停当前任务,跳转到内核预设的中断处理程序。
    • 中断处理程序首先会调用驱动注册的顶半部(Top Half)中断处理函数。这个函数的目标是 尽可能快地完成,通常只做少量工作,如禁用网卡的中断,然后调度软中断(SoftIRQ)来处理真正耗时的数据包。
    • 这里就是 NAPI 机制发挥作用的地方。

源码分析:中断处理与NAPI调度 (以Intel igb驱动为例)

  • 文件: drivers/net/ethernet/intel/igb/igb_main.c

驱动在初始化时会注册中断处理函数,例如 igb_msix_ringigb_intr

// drivers/net/ethernet/intel/igb/igb_main.c

static irqreturn_t igb_msix_ring(int irq, void *data)
{
    struct igb_q_vector *q_vector = data;

    /* Write register to clear interrupt */
    igb_write_itr(q_vector); // 清除中断状态,避免重复触发

    // 关键:这里并不直接处理数据包,而是调度NAPI
    if (likely(napi_schedule_prep(&q_vector->napi))) {
        __napi_schedule(&q_vector->napi); // 将该NAPI实例加入轮询列表,并触发软中断
    }

    return IRQ_HANDLED;
}
  • napi_schedule() 将此网卡队列的 napi_struct 挂入当前CPU的 softnet_data->poll_list 链表。
  • 它会触发一个 NET_RX_SOFTIRQ 类型的软中断。这意味着真正的数据包处理被推迟到软中断上下文中执行,从而大大缩短了硬件中断处理程序的执行时间。

阶段二:软中断与NAPI轮询 (L2 -> L3 的桥梁)

内核线程 ksoftirqd (每个CPU一个) 会周期性地检查待处理的软中断。当它发现 NET_RX_SOFTIRQ 被触发时,会执行 net_rx_action() 函数。

源码分析:net_rx_action 和 NAPI poll

  • 文件: net/core/dev.c

net_rx_action() 是所有网络接收软中断的入口。

// net/core/dev.c

static __latent_entropy void net_rx_action(struct softirq_action *h)
{
    struct softnet_data *sd = this_cpu_ptr(&softnet_data);
    unsigned long time_limit = jiffies + 2;
    int budget = netdev_budget; // 定义了单次轮询处理包数量的预算
    LIST_HEAD(list);
    LIST_HEAD(repoll);

    // ...

    while (!list_empty(&sd->poll_list)) { // 遍历 poll_list 链表
        struct napi_struct *n;
        // ...
        n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);
        // ...

        // 调用驱动注册的 poll 方法
        work = n->poll(n, weight);
    }
    // ...
}
  • net_rx_action 会遍历 poll_list,并调用每个 napi_struct 中注册的 poll 函数。这个poll函数是网卡驱动自己实现的,比如 igb 驱动的 igb_poll
  • 文件: drivers/net/ethernet/intel/igb/igb_main.c

igb_poll 是收包的核心,它会批量地从Ring Buffer中取出数据包,封装成sk_buff,然后送往协议栈上层。

// drivers/net/ethernet/intel/igb/igb_main.c

static int igb_poll(struct napi_struct *napi, int budget)
{
    // ...
    int cleaned = 0;

    while (cleaned < budget) {
        // ... 从 Ring Buffer 中取出描述符

        // 分配一个新的 skb 用于接收数据
        skb = igb_alloc_rx_skb(rx_ring, dma);
        if (!skb)
            break;

        // 将网卡DMA内存中的数据(或其引用)与skb关联起来
        // ...

        // 将数据包传递给上层协议栈
        napi_gro_receive(&rx_ring->q_vector->napi, skb);
        cleaned++;
    }
    // ...
    return cleaned;
}
  • napi_gro_receive() 是一个重要的函数。它会尝试进行 GRO (Generic Receive Offload),即如果连续收到多个属于同一数据流的小包,它会尝试在驱动层将它们聚合成一个大的sk_buff,从而减少上层协议栈处理的次数,提升性能。
  • 最终,数据包会通过 netif_receive_skb() 或其变体被送入协议栈的核心接收函数。

阶段三:网络层处理 (L3 – IP协议)

netif_receive_skb_core() 是协议栈处理的入口。它的主要任务是:根据以太网帧头中的协议类型字段(ethhdr->h_proto),将sk_buff分发给正确的网络层协议处理函数(如IP、ARP、IPv6)。

源码分析: __netif_receive_skb_coreip_rcv

  • 文件: net/core/dev.c
// net/core/dev.c

static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
    // ...
    // 从以太网头中提取协议类型 (例如 0x0800 代表 IPv4)
    type = skb->protocol; 

    // ptype_base 是一个哈希表,存储了所有L3协议处理函数的钩子
    // key 是协议类型 (eth_type_trans),value 是处理函数(ip_rcv, arp_rcv等)
    list_for_each_entry_rcu(pt, &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
        if (pt->type == type) {
            // 找到匹配的协议,调用其处理函数
            pt->func(skb, skb->dev, pt, orig_dev); 
            // ...
        }
    }
    // ...
}
  • 对于IPv4报文,pt->func 就是 ip_rcv
  • 文件: net/ipv4/ip_input.c

ip_rcv 是IPv4协议的入口点。它负责对IP包进行初步的校验和处理。

// net/ipv4/ip_input.c

int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
    struct iphdr *iph;

    // ... 各种检查
    iph = ip_hdr(skb);

    if (iph->ihl < 5 || iph->version != 4) // 检查IP头长度和版本
        goto inhdr_error;

    if (ip_fast_csum((u8 *)iph, iph->ihl)) // 检查IP头校验和
        goto inhdr_error;

    // ...

    // 关键:调用 ip_rcv_finish 进行路由决策
    return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
                   net, NULL, skb, dev, NULL,
                   ip_rcv_finish);
}
  • NF_HOOK 是 Netfilter 的钩子点,允许防火墙等模块在这里对数据包进行处理。如果包未被丢弃,最终会调用 ip_rcv_finish
  • ip_rcv_finish 的核心任务是进行路由决策:这个包是发给本机的,还是需要转发出去?
    • 它会调用 ip_route_input_noref() 查找路由表。
    • 查找结果会存放在 skb->dst (struct dst_entry) 中。
    • 如果目标地址是本机,skb->dst->input 会被设置为 ip_local_deliver
    • 如果需要转发,则设置为 ip_forward

我们假设包是发给本机的,所以接下来会调用 ip_local_deliver

阶段四:传输层处理 (L4 – TCP/UDP协议)

ip_local_deliver 的任务是剥离IP头,然后根据IP头中的协议字段(iph->protocol),将包分发给正确的传输层协议处理函数(如TCP、UDP、ICMP)。

源码分析: ip_local_delivertcp_v4_rcv

  • 文件: net/ipv4/ip_input.c
// net/ipv4/ip_input.c

int ip_local_deliver(struct sk_buff *skb)
{
    // ...
    // skb->dst->input 指向 ip_local_deliver
    return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
                   net, NULL, skb, skb->dev, NULL,
                   ip_local_deliver_finish);
}

static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    // ...
    // 从IP头获取L4协议号
    int protocol = ip_hdr(skb)->protocol;

    // inet_protos 是一个数组,存储了传输层协议的处理句柄
    // 例如,IPPROTO_TCP -> tcp_protocol, IPPROTO_UDP -> udp_protocol
    const struct net_protocol *ipprot;
    ipprot = rcu_dereference(inet_protos[protocol]);
    if (ipprot) {
        // 调用传输层协议的处理函数,例如 tcp_v4_rcv
        ret = ipprot->handler(skb); 
    }
    // ...
}
  • 对于TCP报文,ipprot->handler 就是 tcp_v4_rcv
  • 文件: net/ipv4/tcp_ipv4.c

tcp_v4_rcv 是TCP协议栈的入口。这是整个流程中最复杂的部分之一。其核心任务是:根据 四元组(源IP,目的IP,源端口,目的端口) 找到对应的 socket

// net/ipv4/tcp_ipv4.c

int tcp_v4_rcv(struct sk_buff *skb)
{
    // ...

    // 查找与此skb匹配的socket
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
                           th->dest, &refcounted);

    if (!sk) // 如果没找到已建立的连接
        goto no_tcp_socket;

process:
    // 如果是 ESTABLISHED 状态的连接
    if (sk->sk_state == TCP_ESTABLISHED) { 
        // ...
        // 调用 tcp_rcv_established 处理数据
        if (tcp_rcv_established(sk, skb, th, len)) {
            // ...
        }
        return 0;
    }

    // 如果是 LISTEN 状态,说明是新的连接请求(SYN)
    if (sk->sk_state == TCP_LISTEN) { 
        // ...
        // 处理SYN包,创建新的socket等
        struct sock *nsk = tcp_v4_syn_recv_sock(sk, skb, th, len);
        // ...
    }
    // ...
}
  • 找到 sock 后,数据包会根据TCP状态机的状态进行处理。我们假设这是一个已建立连接的数据包。
  • tcp_rcv_established() 会处理TCP序列号、ACK、滑动窗口等,然后将有效数据 (payload) 添加到 socket的接收队列 (sk->sk_receive_queue) 中。
  • 最后,它会调用 sk_data_ready()

阶段五:Socket层与用户空间

sk_data_ready() 的作用是唤醒正在等待该 socket 上数据的用户进程。

当用户程序调用 read()recvfrom() 等系统调用来读取数据时,如果 socket 的接收队列为空,进程会进入睡眠状态,并被放入 socket 的等待队列 (wq) 中。

源码分析:tcp_recvmsg 和数据拷贝

  1. 用户调用 recv(sockfd, buf, len, 0)
  2. 进入内核态,调用链为:sys_recv -> sock_recvmsg -> inet_recvmsg -> tcp_recvmsg
  • 文件: net/ipv4/tcp.c
// net/ipv4/tcp.c

int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
        int flags, int *addr_len)
{
    // ...
    // skb_peek() 从接收队列的头部查看skb,但并不取出
    skb = skb_peek(&sk->sk_receive_queue);

    while(skb) {
        // ...

        // copy_datagram_iovec() 将数据从内核空间的skb拷贝到用户空间的buffer (msg->msg_iov)
        err = skb_copy_datagram_iovec(skb, offset, msg->msg_iov, used);

        // ...
    }

    // 如果没有数据并且是非阻塞模式,直接返回EAGAIN
    if (!copied) {
        if (nonblock)
            return -EAGAIN;

        // 阻塞模式下,进入睡眠等待,直到被 sk_data_ready 唤醒
        sk_wait_data(sk, &timeo, &last);
    }
    // ...
}
  • tcp_recvmsgsk->sk_receive_queue 中取出 sk_buff
  • 使用 skb_copy_datagram_iovecsk_buff 中的数据负载安全地拷贝到用户程序提供的缓冲区。
  • 至此,数据包的接收之旅圆满完成。

第二部分:数据包发送流程 (从应用到网卡)

发送过程在概念上是接收过程的逆序,但实现细节有所不同。

阶段一:用户空间与Socket层

  1. 系统调用: 用户程序调用 send(), write(), sendto() 等系统调用。
  2. 进入内核: 陷入内核态,调用链 sys_send -> sock_sendmsg -> inet_sendmsg -> tcp_sendmsg

源码分析:tcp_sendmsg

  • 文件: net/ipv4/tcp.c

tcp_sendmsg 的主要工作是:将用户数据拷贝到内核空间,并将其挂到 socket的发送队列 (sk->sk_write_queue)。

// net/ipv4/tcp.c
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
    // ...
    while (msg_data_left(msg)) { // 循环处理用户数据
        // ...
        // skb_copy_to_page_nocache() 将用户数据拷贝到内核页
        // 内核会分配skb,并将数据组织起来
        err = skb_copy_to_page_nocache(sk, &msg->msg_iter, skb,
                                       pfrag, copied);
        // ...

        // 将包含数据的 skb 添加到发送队列
        __skb_queue_tail(&sk->sk_write_queue, skb);
    }
    // ...

    // 尝试推送数据包,触发发送流程
    __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
    return copied;
}

阶段二:传输层 (L4 – TCP)

__tcp_push_pending_frames 会检查是否可以发送数据(根据Nagle算法、拥塞窗口等)。如果可以,它会调用 tcp_write_xmit 来构建TCP段。

源码分析:tcp_write_xmit

  • 文件: net/ipv4/tcp.c

该函数会从 sk_write_queue 取出 sk_buff,为其添加TCP头部,并计算校验和。

// net/ipv4/tcp.c
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle)
{
    // ...
    while ((skb = tcp_send_head(sk))) { // 获取发送队列的第一个skb
        // ...
        // tcp_transmit_skb() 是实际构建和发送skb的核心函数
        tcp_transmit_skb(sk, skb, 1, gfp);
    }
    // ...
}

// net/ipv4/tcp_output.c
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask)
{
    // ...
    // 构建TCP头部
    th = (struct tcphdr *)skb_push(skb, tcp_header_len);
    // ...填充TCP头字段: 端口、序列号、标志位等...

    // 计算TCP校验和(如果硬件不支持校验和卸载)
    // ...

    // 将包交给IP层发送
    err = icsk->icsk_af_ops->queue_xmit(sk, skb, &fl);
    return err;
}
  • icsk->icsk_af_ops->queue_xmit 对于IPv4来说,就是 ip_queue_xmit

阶段三:网络层 (L3 – IP)

ip_queue_xmit 负责添加IP头部。它会从socket上缓存的路由信息 (dst_entry) 中获取下一跳地址等信息。

源码分析:ip_queue_xmitip_output

  • 文件: net/ipv4/ip_output.c
// net/ipv4/ip_output.c
int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
    // ...
    // sk_setup_caps() 检查 GSO/TSO 等硬件卸载能力
    // TSO (TCP Segmentation Offload) 允许TCP层构建一个大的TCP包,由网卡来切分成小的TCP segment

    // ...
    // 调用 dst_output,它最终会指向 ip_output
    return dst_output(net, sk, skb);
}

int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
    struct net_device *dev = skb_dst(skb)->dev;
    // ...
    // 添加IP头
    iph = ip_hdr(skb);
    iph->version = 4;
    //... 填充IP头其他字段: ttl, protocol等...

    // 计算IP头校验和
    ip_send_check(iph);

    // ...
    // 将包交给邻居子系统(ARP)处理
    return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
                        net, sk, skb, NULL, dev,
                        ip_finish_output,
                        !(IPCB(skb)->flags & IPSKB_REROUTED));

}
  • ip_finish_outputPOST_ROUTING 之后的最终发送函数。

阶段四:邻居子系统与驱动层 (L2)

ip_finish_output 会调用 dst_neigh_output,它会查找下一跳IP地址对应的MAC地址(即查询ARP缓存)。

  1. ARP查询:
    • 如果ARP缓存中有对应的MAC地址,就用它来填充以太网头部。
    • 如果缓存中没有,系统会发送一个ARP请求,并将当前的sk_buff放入一个队列中等待。当ARP响应回来后,再将此sk_buff取出并发送。
  2. 排队与发送: dev_queue_xmit 是将sk_buff交给网络设备的通用接口。
    • 它会首先经过Linux的 Qdisc (Queueing Discipline),即流量控制和队列规则。
    • Qdisc决定了数据包的发送顺序(比如 pfifo_fast 队列)。

源码分析:dev_queue_xmit 和 驱动 ndo_start_xmit

  • 文件: net/core/dev.c
// net/core/dev.c
int dev_queue_xmit(struct sk_buff *skb, struct net_device *sb_dev)
{
    // ...
    // 获取设备的发送队列
    txq = netdev_get_tx_queue(dev, skb_get_queue_mapping(skb));

    // ...
    // 将 skb 送入 Qdisc
    rc = q->enqueue(skb, q, &to_free, &ret);
    // ...

    // __qdisc_run() 会最终触发发送
}
  • Qdisc处理完后,会调用网络设备驱动注册的发送函数,即 net_device_ops->ndo_start_xmit
  • 文件: drivers/net/ethernet/intel/igb/igb_main.c
// drivers/net/ethernet/intel/igb/igb_main.c
static netdev_tx_t igb_xmit_frame(struct sk_buff *skb,
                                  struct net_device *netdev)
{
    // ...

    // 将 skb 的数据映射到DMA可以访问的地址
    dma = dma_map_single(dev, skb->data, skb_headlen(skb), DMA_TO_DEVICE);

    // ...

    // 准备发送描述符,填入DMA地址和长度
    tx_desc->buffer_addr = cpu_to_le64(dma);

    // ...

    // 更新环形缓冲区的尾指针,这会“告诉”网卡有一个新的包需要发送
    writel(i, tx_ring->tail);

    // 网卡硬件会检测到指针变化,通过DMA从内存中读取数据,并将其发送到网络上
    return NETDEV_TX_OK;
}
  • 驱动程序将sk_buff信息填入发送 Ring Buffer,并更新硬件寄存器。
  • 网卡硬件接管,通过DMA读取数据,并将其按以太网帧格式发送出去。
  • 发送完成后,网卡可能会再次触发一个中断,通知驱动发送完成,以便驱动可以释放相关的sk_buff和Ring Buffer空间。

总结

Linux网络协议栈是一个高度模块化、分层清晰且充满优化的复杂系统。

  • 接收流程: 中断 -> NAPI轮询(igb_poll) -> netif_receive_skb -> ip_rcv -> ip_local_deliver -> tcp_v4_rcv -> socket接收队列 -> 用户程序recv()
  • 发送流程: 用户程序send() -> tcp_sendmsg -> socket发送队列 -> tcp_write_xmit -> ip_queue_xmit -> dev_queue_xmit -> 驱动ndo_start_xmit(igb_xmit_frame) -> 硬件发送

整个过程以sk_buff为核心载体,在各层之间传递,每层通过添加/剥离头部并调用下一层/上一层的函数指针来完成自己的任务。同时,像NAPI、GRO/TSO、Netfilter、Qdisc等机制穿插其中,共同构成了一个功能强大且性能卓越的网络处理框架。

本文版权归原作者zhaofujian所有,采用 CC BY-NC-ND 4.0 协议进行许可,转载请注明出处。

发表评论