目录
关键技术
Kernel 中的 sk_buff 与 sk_buffer
sk_buff 结构体是 Kernel 定义的一个用于描述 Frame 的数据结构。而 sk_buffer 的本质就是从 ZOME_DMA 中划分给某一个 NIC 的用于存放 sk_buff 实例的内存空间,sk_buffer 会在 NIC Driver 初始化的流程中分配。
当 Frame 到达 NIC 后,DMA Controller 就会将 Frame 的数据 Copy 到 sk_buffer 中的 sk_buff 实例,以此来完成 Frame => sk_buff 数据格式的封装。在后续的流程中,sk_buff 还会被从 ZONE_DMA Copy 到 Kernel Socket Receive Buffer 中,等待 Application 接收。
struct igb_ring {
...
union {
...
/* RX */
struct {
struct sk_buff *skb;
struct igb_rx_queue_stats rx_stats;
struct u64_stats_sync rx_syncp;
};
...
}
- 两者具有不同的特征和用途:Headers 包含了网络协议的元数据信息,而 Payload 包含了 Application 的业务信息。
- 两者具有不同的大小和格式:Headers 的格式通常是标准的,而且数据量比 Payload 小的多。
- 两者具有不同的处理逻辑:Headers 需要被快速识别、分析和校验,而 Payload 则需要被快速的传输和存储。
- 收包时:DMA Controller 搜索 Rx BDT,取出空闲的 DB Entry,将 Frame 存放到 Entry 指向的 sk_buff,修改 Entry 为 Ready。然后 DBT 指针下移一项。
- 发包时:DMA Controller 搜索 Tx BDT,取出状态为 Ready 的 DB Entry 所指向的 sk_buff 并转化为 Frame 发送出去。然后 DBT 指针下移一项。
-
硬中断(HW Interrupt):是一种 NIC 和 CPU 之间的通信机制,由 NIC 发出,让 CPU 能够及时掌握 NIC 发生的事件,并视乎于中断的类型来决定是否放下当前任务,尽快处理紧急的事件,例如:Frames 的到达。硬件中断的本质是一个 IRQ(中断请求信号)电信号。Kernel 为每个 NIC 分配了一个专属的 HW IRQ Number,以此来区分发出硬中断的设备类型。IRQ Number 又会映射到 Kernel ISR(中断服务路由列表)中的一个中断处理程序(通常由 NIC Driver 提供)。
-
软中断(SW Interrupt):同样具有由 NIC Driver 提供的软件中断处理程序,例如:收包处理函数。硬件中断处理程序只会处理关键性的、短时间内可以处理完的工作,剩余耗时较长工作,就交由软中断处理函数慢慢完成。
- NIC Driver 在初始化流程中,就会注册 NAPI 收包机制所必须的 poll() 轮询函数到 ksoftirqd(软中断处理)内核线程。
- Frame 到达 NIC;
- DMA Controller 写入 sk_buff;
- NIC Controller 发起硬中断,进入 NIC Driver 的 napi_schedule() 硬中断处理函数,然后将一个 napi_struct 结构体加入到 poll_queue(NAPI 软中断队列)中。此时 NIC Controller 立即禁用了硬中断,开始切换到 NAPI “轮训“ 收包工作模式。
- 再进入 raise_softirq_irqoff(NET_RX_SOFTIRQ) 软中断处理程序,唤醒 NAPI 子系统,新建 ksoftirqd 内核线程。
- ksoftirqd 线程调用 NIC Driver 注册的 poll() 函数。
- poll() 调用 netif_receive_skb() 开始从 sk_buffer 内存空间中读取 sk_buff,并将它传递给 TCP/IP 协议栈进行处理。
- 直到一段时间内没有 Frame 到达为止,关闭 ksoftirqd 内核线程,NIC Controller 重新切换到 NAPI “中断” 收包工作模式。
-
收包流程:
-
发包流程:
-
收包流程:
-
发包流程:
-
NIC Controller 接收到高低电信号,表示 Frame 到达。PHY 芯片首先将电信号转换为比特流,然后 MAC 芯片再将比特流转换为 Frame 格式并进行 CRC 校验,然后存入 Rx Queue。
-
DMA Controller 将 NIC Rx Queue 中的 Frame Copy 到 Kernel 的 Rx Ring 中的一个 Rx Desc 指向的 sk_buff 空间。
-
DMA Controller 更新相应的 BD Entry 状态为 Ready,并将 BDT 指针下移一项。
-
NIC Controller 给 CPU 的相关引脚上触发一个电压变化,触发硬中断。每个 IRQ(硬中断请求)都对应一个 IRQ Number,CPU 执行 IRQ Number 对应的硬中断处理程序。硬中断处理程序判断是否开启 NAPI。
-
如果开启了 NAPI 模式,那么 NIC Driver 执行 napi_schedule() 硬中断处理函数,然后将一个 napi 结构体加入到 poll_queue(NAPI 软中断队列)中。此时 NIC Controller 立即禁用了硬中断,开始切换到 “NAPI 轮训“ 收包工作模式。
-
再进入 raise_softirq_irqoff() 软中断处理程序,唤醒 NAPI 子系统,新建 ksoftirqd 内核线程。
-
ksoftirqd 线程调用 Net driver 注册的 linux/drivers/net/ethernet/intel/igb/igb_main.c poll() 函数。
sk_buff 的结构体定义如下图所示,它包含了一个 Frame 的 Interface(网络接口)、Protocol(协议类型)、Headers(协议头)指针、Data(业务数据)指针等信息。
值得留意的是,在 Kernel 中,一个 Frame 的 Headers 和 Payload 可能是分开存放到不同内存块种的。有以下几点原因:
分开存储和处理的方式,可以有效提高网络传输的效率和可靠性。
同时,sk_buff 是一个双向链表数据结构,支持链表操作。
Kernel 中的 Rx/Tx Ring
sk_buffer 是 Kernel 用于实际存储 sk_buff 的内存空间,而 Rx/Tx Ring 则是 Kernel 用于管理和使用 sk_buffer 内存空间的数据结构。在很多材料中会将 sk_buffer 和 Ring 统称为 Ring Buffer,但实际上两者间有本质的区别。
Ring 是高性能数据包处理场景中常见的一种数据结构,本质是一个首尾相连的 Queue。相较于传统的 FIFO Queue 数据结构,Ring Queue 可以避免频繁的内存分配和数据复制,从而提高传输效率。此外还具有缓存友好、易于并行处理等优势。
Kernel 将 sk_buffer 内存空间设计成一个首尾相连的 Ring。
但需要注意的是,在高速网络环境中,如果 CPU/Driver 的处理能力低于 NIC 带宽,那么 sk_buffer 空间就会溢满,NIC Rx Queue 中 Frames 就会堆积,直到 NIC Rx Queue 也溢满后,NIC 就会开始丢包。所以 CPU/Driver 的报文处理能力,和 NIC 的带宽往往需要匹配得上。
另外还需要注意的是,Rx/Tx Ring 中存储的是 sk_buff 的 Descriptor,而不是 sk_buff 本身,本质是一个指针,指向存储 sk_buff 的 sk_buffer 物理地址。所以 Ring 上的一个节点,又称为一个 Packet Descriptor。
Packet Descriptor 有 Ready 和 Used 这 2 种状态。初始时 Descriptor 指向一个预先分配好且是空的 sk_buff 空间,处在 Ready 状态。当有 Frame 到达时,DMA Controller 从 Rx Ring 中按顺序找到下一个 Ready 的 Descriptor,将 Frame 的数据 Copy 到该 Descriptor 指向的 sk_buff 空间中,最后标记为 Used 状态。
Buffer Descriptor Table
Rx/Tx Ring 的具体实现为一张 Buffer Descriptor Table(BDT)。
BDT 是一个 Table 数据结构,拥有多个 Entries,每条 Entry 都对应了 Rx/Tx Ring 中的一个 Rx/Tx Desc,它们记录了存放 sk_buff 的入口地址、长度以及状态标志位。
中断收包机制
值得注意的是,硬中断是 Kernel 调度优先级最高的任务类型之一,会进行抢占式调度,所以硬中断通常都伴随着任务切换,即:将 CPU 当前的任务切换到中断处理程序的上下文。
一次中断处理,首先需要将 CPU 的状态寄存器数据保存到虚拟内存空间中的堆栈,然后运行中断服务程序,最后再将状态寄存器数据从堆栈中夹在到 CPU。整个过程需要至少 300 个 CPU 时钟周期。并且在多核处理器计算平台中,每个 Core 都有可能执行硬件中断处理程序,所以还存在着跨 Core 处理要面对的 Cache 一致性流量的问题。
可见,大量的中断处理,尤其是硬件中断处理会非常消耗 CPU 资源。
NAPI 收包机制
为了解决中断收包机制效率低下的问题,Kernel 提出了 NAPI(New API)收包机制。
NAPI(New API)是一种 “中断 + 轮训” 收包机制,在高速网络场景中,只需要一次中断后,再通过轮询的方式进行收包,避免了多次中断的情况。相较于传统的单一中断(硬中断 + 软中断)收包的方式效率更高。
NAPI 的工作流程如下:
在具体的实现中,poll() 会轮训检查 BDT Entries 的状态,如果发现当前 BDT 指针指向的 Entry Ready,则将该 Entry 对应的 sk_buff 取出来,并恢复该 Entry 的空闲状态。
可见,和传统方式相比,NAPI 一次中断就可以接收多个包,因此可以减少硬件中断的次数。
内核协议栈收包/发包流程概览
内核协议栈收包流程详解
驱动程序层(数据链路层)
/**
* igb_poll - NAPI Rx polling callback
* @napi: napi polling structure
* @budget: count of how many packets we should handle
**/
static int igb_poll(struct napi_struct *napi, int budget)
{
...
if (q_vector->tx.ring)
clean_complete = igb_clean_tx_irq(q_vector, budget);
if (q_vector->rx.ring) {
int cleaned = igb_clean_rx_irq(q_vector, budget);
...
}
- 13
- 14
- 15
- poll() 调用 linux/net/core/dev.c netif_receive_skb() 开始从 sk_buffer 内存空间中收包,并将它传递给 TCP/IP 网络协议栈进行处理。
/**
* netif_receive_skb - process receive buffer from network
* @skb: buffer to process
*
* netif_receive_skb() is the main receive data processing function.
* It always succeeds. The buffer may be dropped during processing
* for congestion control or by the protocol layers.
*
* This function may only be called from softirq context and interrupts
* should be enabled.
*
* Return values (usually ignored):
* NET_RX_SUCCESS: no congestion
* NET_RX_DROP: packet was dropped
*/
int netif_receive_skb(struct sk_buff *skb)
{
int ret;
trace_netif_receive_skb_entry(skb);
ret = netif_receive_skb_internal(skb);
trace_netif_receive_skb_exit(ret);
return ret;
}
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 直到一段时间内没有 Frame 到达为止,关闭 ksoftirqd 内核线程,NIC Controller 重新切换到硬中断收包工作模式。
VLAN 协议族
Linux Bridge 子系统
网络协议层(L3 子系统)
- NIC Driver 调用 netif_receive_skb() 将 sk_buff 从 sk_buffer 中取出并交给 TCP/IP 协议栈处理的过程中,首先会根据 sk_buff 内层 Header 的 Protocol Type 选择相应的处理函数,如果是 IP 协议,则调用 ip_rcv() 进行处理。
- ip_rcv() 首先会解析 IP Header,根据 srcIP 和 dstIP 进入路由子系统处理流程。如果 dstIP 是本机地址,则根据内层 Header 的 Protocol Type 选择相应的传输层处理函数。UDP 对应 udp_rcv()、TCP 对应 tcp_rcv()、ICMP 对应 icmp_rcv()、IGMP 对应 igmp_rcv()。
ARP 子系统
IP 子系统
网络协议层(L4 子系统)
-
在使用 socket() 创建一个 TCP Socket 之后,Socket 对应的 Sock 结构体会被注册到一个 tcp_prot 全局变量中,并以 tcp_port 作为 Index。
-
当 tcp_rcv() 收到 sk_buff 之后,根据 TCP Header 中的 dstPort 字段索引到相应的 Sock。
-
然后将 sk_buff 加入到该 Sock 的 receive_queue 成员所指向的 Socket Receive Buffer 缓冲队列中,等待 Application 通过 read() 等 SCI 来进行读取。
TCP 子系统
协议接口层(BSD Socket 层)
当 Application 调用 read() 来进行读取时:
- 首先会根据 socket fd 查询 file inode,并从中得到相应的 Sock 结构体;
- 然后从 Sock 结构体成员 recieve_queue 指向的 Socket Receive Buffer 发起一次 CPU Copy;
- CPU 从用户模式转为内核模式,将数据包从 Kernel space Copy 到 User space。
收包流程总览
内核协议栈发包流程详解
以 UDP 数据报为例:
-
协议接口层:BSD socket 层的 sock_write() 会调用 INET socket 层的 inet_wirte()。INET socket 层会调用具体传输层协议的 write 函数,该函数是通过调用本层的 inet_send() 来实现的,inet_send() 的 UDP 协议对应的函数为 udp_write()。
-
L4 子系统:udp_write() 调用本层的 udp_sendto() 完成功能。udp_sendto() 完成 sk_buff 结构体相应的设置和 Header 的填写后会调用 udp_send() 来发送数据。而在 udp_send() 中,最后会调用 ip_queue_xmit() 将数据包下放的网络层。
-
L3 子系统:函数 ip_queue_xmit() 的功能是将数据包进行一系列复杂的操作,比如是检查数据包是否需要分片,是否是多播等一系列检查,最后调用 dev_queue_xmit() 发送数据。
-
驱动程序层:函数调用会调用具体设备提供的发送函数来发送数据包,e.g. hard_start_xmit(skb, dev)。具体设备的发送函数在协议栈初始化的时候已经设置了。