前言
在我的Rust入门及实战系列文章中已经说明, Rust是一门内存安全的高性能编程语言,从它的这些优秀特性来看,就是一门专为系统开发而诞生的语言。至于很多使用Rust来进行web开发的行为,不能说它们不好,只能说是杀鸡焉用牛刀耳。
本系列的文章旨在为大家介绍一个新的专用于开发eBPF程序的Rust框架:Aya。Aya 是 Rust 中的一个 eBPF(Extended Berkeley Packet Filter)库,提供了用 Rust 语言编写、加载和运行 eBPF 程序的能力。eBPF 是一种强大的技术,用于在 Linux 内核中安全地运行沙盒程序,常用于网络编程、性能监控和安全增强。我们在后面的文章中将对eBPF进行更加详细和深入的介绍。
Linux内核网络基础
如果你还不了解什么是eBPF, 那么其实从它的名称中便可见一斑,从Packet Filter
可以看出,这显然是用于包的处理的一门技术。它通过在Linux的内核中的不同挂载点,加入一个隔离可控的二进制程序,来达到我们想要为内核增加功能处理网络包的目的。既然是在内核的网络处理流程中动手脚,那么在开始之前,我们有必要对Linux内核原本的网络处理流程有基本的认知, 否则,如果直接开始eBPF的编写,那么我们很可能会变成狗拿刺猬,无从下手。
网络模型概览
众所周知,网络模型的的划分有不同的方法, 最流行的莫过于经典的OSI七层网络模型, TCP/IP四层网络模型,还有综合两者而成的五层网络模型。从理解内核网络工作原理的角度出发,我们选择五层网络模型进行接下来的探究:
注: 在Linux系统中,内核源码的位置位于/usr/src/
目录下的对应内核命名的目录下,例如我使用的azure虚拟机上,内核代码的目录为/usr/src/linux-headers-5.15.0-1052-azure
。 后文提到的内核代码相关的目录都是以此为根的相对路径。
Linux内核在收到一个网络数据包后,首先会由网卡驱动程序(相关内核代码位于drivers/net/ethernet
中)先进行处理, 然后回交由内核中处理协议栈相关的代码进行处理(相关内核代码位于kernel/
和net/
中),处理完成后的结果,再由socket
提供接口,供用户空间的应用层程序访问。
网络中断处理原理
那么从网卡收到数据包,是怎么传递给内核进行处理的呢,这里就要谈到中断处理了。 从硬件的角度来看,当一个网卡收到数据包时, 它会进行以下两件事情:
- 将数据包以DMA(Direct Memory Access)的方式, 将收到的数据帧存放到内存的环形缓冲区中(Buffer Ring);
- 向CPU的引脚施加一个电压变化,向CPU表明现在有一个数据来了,需要处理。
上述这种通过向CPU引脚施加电压的硬件操作,被称为硬中断。那么CPU此时就会对收到的数据包进行处理,那么如何处理呢?网络包如果一直不停的到来,而对网络包的处理往往又是复杂和耗时的,如果CPU每收到一个数据包都对它进行处理完成后再干别的工作,就会导致CPU的占用率过高,而无法对其他的硬中断进行响应了,比如鼠标键盘等设备发起的硬中断请求。
软中断注册
因此,当CPU收到一个网卡发来的硬中断时,它会告诉网卡驱动程序: “你先去内存登记一下待办事项吧”,于是网卡驱动程序会在内存中标记一个变量,表示这里有一个网络包需要人手来处理了。这个在内存中设置标志的操作,就被称为软中断。
上述过程的图示如下:
软中断处理
内核驱动程序处理
在我们的Linux启动后,内核中会运行一个进程ksoftirqd
, 它的职责就是检测内存中是否有软中断需要处理, 一旦检测到这是一个网络驱动注册的软中断,就会调用网卡驱动中的poll
函数,从内存的环形缓冲区中将网络数据包收下来并进行处理, 这个过程的图示如下:
其中, 网卡驱动程序中的igb_poll()
函数会从内存的环形缓冲区中将完整的网络数据包取出来,然后调用igb_clean_rx_irq()
函数进行处理,这些处理包括:
- 校验收到的数据格式是否是一个合法的网络包;
- 将收到的数据包格式化成
skb
,解析timestamp, VLAN id, protocol等字段信息
内核协议栈处理
在驱动程序对数据包进行处理后,处理完的数据将被发送到内核的协议栈进行处理,在进入协议栈之前,内核中存在一个GRO
引擎,它的作用是把一些小的网络包合成一个大的网络包,一次性发给协议栈进行处理,目的是减少传送给协议栈的包数量,这有助于减少 CPU 的使用量。
如上图所示,在数据包进入协议栈后:
- 首先会调用
netif_receive_skb()
函数,其中会辨别数据包的网络层协议,根据网络层协议调用不同的函数; - 例如判断得到这个数据包是个IP包,则会接下来调用
ip_rcv()
函数,在其中又会判断它的传输层协议,根据其是TCP
还是UDP
而调用不同的函数; - 例如判断得到这是一个TCP包,那么将继续调用
tcp_rcv()
函数对数据进行处理; - 处理完成后的数据可以供用户通过
socket
访问;
小结
网络模块在Linux内核中及其复杂,上面的介绍以尽可能简单明了的方式描述了一个数据包从网卡收到它开始,如何被内核进行处理的整个过程。其中包含了CPU硬中断,ksoftiqrd
线程,软中断处理,网卡驱动对数据包的处理,skb
的创建, 网络协议栈对数据包的处理等过程。
本文没有涉及用户空间应用程序从socket
取包的过程,这涉及到recvfrom
系统调用,也是一个比较复杂的话题。本系列文章旨在介绍Rust开发eBPF程序,只关注内核对网络包的处理流程,因此用户空间取包不在我们的关注范围内。在了解了网络包在内核中的处理流程之后,对于后需eBPF程序的挂载点,我们应当会有更清晰的认识。