Linux收发包

Linux接收包的过程

1. 网卡接收网络包

  • DMA 技术:网卡接收到网络包后,会通过 DMA(直接内存访问)技术将数据包直接写入到内存中的 Ring Buffer(环形缓冲区),而不需要 CPU 的参与。
  • 通知操作系统:网卡需要通知操作系统有新的网络包到达。最简单的方式是触发硬件中断。但是频繁的中断开销太大,所以 Linux 内核引入了 NAPI(New API)机制。

2. NAPI 机制

2.1 NAPI过程
  • 网卡接收网络包:网卡通过 DMA 将网络包写入 Ring Buffer。
  • 触发硬中断:网卡触发硬件中断,通知 CPU 有数据到达。
  • 硬中断处理函数执行
static irqreturn_t my_netdev_interrupt(int irq, void *dev_id)
{
    struct net_device *dev = dev_id;
    struct my_netdev_priv *priv = netdev_priv(dev);

    /* 1. 屏蔽中断 */
    disable_irq_nosync(irq);

    /* 2. 检查中断状态 */
    if (!check_interrupt_status(priv)) {
        /* 如果不是我们关心的中断,恢复中断并返回 */
        enable_irq(irq);
        return IRQ_NONE;
    }

    /* 3. 标记软中断 */
    napi_schedule(&priv->napi);

    /* 4. 恢复中断 */
    enable_irq(irq);

    return IRQ_HANDLED;
}
其步骤为:
- 屏蔽中断。
- 标记软中断。
- 恢复中断。
  • 软中断处理函数执行
    • 内核中的 ksoftirqd 线程专门负责软中断的处理,当 ksoftirqd 内核线程收到软中断后,就会来轮询处理数据。
    • ksoftirqd 线程会从从 Ring Buffer 中读取网络包。
    • 将获取的网络包交给网络协议栈进行逐层处理解析协议。
  • 轮询模式结束:当 Ring Buffer 中的数据被处理完毕后,网络驱动会重新启用网卡的中断,等待下一个网络包的到来。
  • 当第一个网络包到达时,网卡会触发一个硬件中断,通知 CPU 有数据到达。硬件中断会唤醒网络包处理程序(通常是内核中的网络驱动代码)。此时,CPU 会从当前任务切换到中断处理程序。
2.2 硬中断和软中断的分工
  • 硬中断
    • 由网卡触发,通知 CPU 有网络包到达。
    • 硬中断处理函数的工作非常简单:屏蔽中断、发起软中断、恢复中断。
    • 硬中断的执行时间非常短,对 CPU 的消耗较小。
  • 软中断
    • 软中断是由硬中断触发的,用于处理耗时的任务,比如从 Ring Buffer 中读取网络包、解析协议、将数据传递给上层应用程序等。
    • 软中断的执行时间较长,但它是异步的,不会阻塞 CPU 的其他任务。
2.3 硬中断和软中断对 CPU 的消耗
  • 硬中断:由于硬中断处理函数的工作非常简单,它的 CPU 消耗非常低。
  • 软中断:软中断的 CPU 消耗较高,因为需要处理网络包的具体内容。但是,软中断是异步的,CPU 可以在处理软中断的同时执行其他任务。

NAPI 的优势:通过减少硬中断的次数,NAPI 机制显著降低了 CPU 的负载,尤其是在高性能网络场景下。

3. 逐层解析

  • 首先,会先进入到网络接口层,在这一层会检查报文的合法性,如果不合法则丢弃,合法则会找出该网络包的上层协议的类型,比如是 IPv4 还是 IPv6,接着再去掉帧头和帧尾,然后交给网络层。
  • 到了网络层,则取出 IP 包,判断网络包下一步的走向,比如是交给上层处理还是转发出去。当确认这个网络包要发送给本机后,就会从 IP 头里看看上一层协议的类型是 TCP 还是 UDP,接着去掉 IP 头,然后交给传输层。
  • 传输层取出 TCP 头或 UDP 头,根据四元组「源 IP、源端口、目的 IP、目的端口」作为标识,找出对应的 Socket,并把数据放到 Socket 的接收缓冲区。
  • 最后,应用层程序调用 Socket 接口,将内核的 Socket 接收缓冲区的数据「拷贝」到应用层的缓冲区,然后唤醒用户进程。

Linux发送包的过程

1. 应用程序发送数据包的流程

1.1 用户态到内核态的切换

应用程序调用 send()write() 等 Socket 接口发送数据。由于这是系统调用,程序会从用户态切换到内核态,进入内核的 Socket 层。

1.2 分配 sk_buff 结构体

内核会分配一个 sk_buff 结构体(简称 skb),用于描述网络包。sk_buff 是 Linux 内核中用于管理网络包的核心数据结构,它包含了网络包的数据、元信息以及协议头的指针。

1.3 拷贝用户数据

将用户态的数据拷贝到 sk_buff 的数据缓冲区中。这个缓冲区通常位于内核的内存区域。

1.4 加入发送缓冲区

sk_buff 加入到 Socket 的发送缓冲区中,等待协议栈处理。

2. 协议栈处理 sk_buff

2.1 从发送缓冲区取出 sk_buff

网络协议栈从 Socket 的发送缓冲区中取出 sk_buff,并按照 TCP/IP 协议栈从上到下逐层处理。

2.2 TCP 层的处理
  • 如果使用 TCP 协议,内核会拷贝一个新的 sk_buff 副本。这是因为 TCP 是可靠传输协议,支持丢失重传。在收到对方的 ACK 之前,原始的 sk_buff 不能被释放。每次调用网卡发送时,实际传递的是 sk_buff 的副本,等收到 ACK 后再真正删除原始的 sk_buff
  • 填充 TCP 头:在 sk_buff 的数据缓冲区前预留空间,填充 TCP 协议头(如源端口、目标端口、序列号、确认号等)。
2.3 IP 层的处理
  • 填充 IP 头:在 sk_buff 的数据缓冲区前预留空间,填充 IP 协议头(如源 IP 地址、目标 IP 地址、TTL 等)。
  • 进行路由查找,确定数据包的下一跳地址。
2.4 数据链路层的处理
  • 填充以太网头:在 sk_buff 的数据缓冲区前预留空间,填充以太网协议头(如源 MAC 地址、目标 MAC 地址等)。
  • sk_buff 传递给网卡驱动程序。

3. sk_buff 的设计原理

3.1 为什么只用 sk_buff 一个结构体?

协议栈是分层结构,每一层都需要添加或移除协议头。如果每一层都用一个单独的结构体,在层之间传递数据时会发生多次拷贝,这会大大降低 CPU 效率。为了避免拷贝,Linux 内核使用 sk_buff 一个结构体来描述所有的网络包。

3.2 sk_buff 如何实现零拷贝?
  • 接收数据时:从网卡驱动开始,通过协议栈层层上传数据包。通过增加 skb->data 的值,逐步剥离协议头(如以太网头、IP 头、TCP 头)。
  • 发送数据时:创建 sk_buff 时,数据缓冲区的头部预留足够的空间。通过减少 skb->data 的值,逐步添加协议头(如 TCP 头、IP 头、以太网头)。
3.3 sk_buff 的结构

sk_buff 的核心字段包括:

  • data:指向当前协议层的数据起始位置。
  • head:指向数据缓冲区的起始位置。
  • tail:指向数据缓冲区的结束位置。
  • len:数据包的长度。
  • protocol:协议类型(如 TCP、UDP、IP 等)。
  • nextprev:用于将 sk_buff 链接到链表中。

4. 网卡发送数据包

4.1 传递给网卡驱动

协议栈处理完成后,将 sk_buff 传递给网卡驱动程序。网卡驱动程序会将 sk_buff 中的数据包通过 DMA 技术直接写入网卡的发送队列。

4.2 释放 sk_buff
  • 如果使用 TCP 协议,原始的 sk_buff 会保留在发送缓冲区中,直到收到对方的 ACK。
  • 如果使用 UDP 协议,sk_buff 会在发送完成后立即释放。

5. 总结

  • sk_buff 的作用sk_buff 是 Linux 内核中用于管理网络包的核心数据结构,它通过调整 data 指针实现零拷贝,避免了协议栈层之间的数据拷贝。
  • 协议栈的处理流程:从上到下逐层处理 sk_buff,填充协议头(TCP、IP、以太网头),最终传递给网卡驱动程序。
  • TCP 的重传机制:TCP 协议会保留原始的 sk_buff,直到收到对方的 ACK,确保数据的可靠传输。
    请添加图片描述

参考资料:

参考资料

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐