内存管理部分 04 - DPDK的多进程
内存管理 - 4. 多进程支持
DPDK 的设计目标是提升网络数据包的处理性能。多进程支持使得多个 DPDK 进程能够协作执行任务,从而提高并行处理能力,减少资源竞争,并且能够处理更大规模的网络流量。DPDK有两种不同的进程类型,一种是主进程,一种是从进程;
主进程可以进行初始化,对内存有完全的权限;
从进程不能初始化共享内存,但是可以附加到初始化的共享内存,并在其中创建对象。
为了支持这两种类型以及后续更多的进程设置,EAL增加了两个命令行参数:
--proc-type
:用于指定给定进程实例为主进程或从进程的 DPDK 实例。
--file-prefix
:允许不希望协作的进程使用不同的内存区域。
共享内存
DPDK的主进程在启动的时候会在内存记录内存映射文件,包括巨页、映射的虚拟地址、内存通道数等,从进程可以从文件读取,EAL在从进程重建相同的内存配置,所有的内存区域在进程间共享,指向这些内存的指针用于不同的进程操作者部分内存。
如果主进程使用了 --legacy-mem
或 --single-file-segments
选项启动,则从进程必须使用相同的选项启动。
EAL 还支持一种自动检测模式(通过设置 EAL 的 --proc-type=auto
标志),即如果主进程已经在运行,DPDK 进程将作为从进程启动。
部署模型
对于DPDK的多进程模型,有4种部署方法:对称进程模型、非对称进程模型、独立的DPDK程序并行、多个独立的DPDK应用组并行;
对称进程模型:所有进程执行相同的任务,类似于多线程应用。第一个进程作为主进程启动,其他进程作为从进程启动,执行相同的主循环函数。这种模型适用于负载均衡和高并发任务的处理。
非对称进程模型:只有一个主进程负责管理任务(如负载均衡),而多个从进程执行实际的工作。通常主进程会将任务分发给从进程,典型应用如客户端/服务器架构。这种模型通常在需要负载分配和集中管理的场景中使用。
独立的 DPDK 应用并行运行:允许多个 DPDK 应用程序在同一台机器上独立运行。每个应用都可以通过 --file-prefix
参数配置自己的内存区域,避免进程之间的内存冲突。使用 -m
或 --socket-mem
参数,可以限制每个进程的内存使用,防止内存溢出。需要注意的是,独立进程不能共享网络端口,因此每个进程必须保证使用的端口不会互相冲突。
多个独立 DPDK 应用组并排运行:除了独立的 DPDK 应用外,还可以同时运行多个 DPDK 应用组。这种情况下,从进程必须与主进程使用相同的 --file-prefix
参数,以确保共享内存的正确映射。
因为DPDK中线程是和核心绑定的,所以也不会被调度,相较于多线程模型,DPDK的多进程模型起到的是在资源隔离、网卡隔离方面的作用。
多进程限制
DPDK多进程存在一些限制:
巨页内存映射一致性问题:Linux的安全机制ASLR(地址空间布局随机化)会导致每次进程分配的虚拟地址映射不一致(本意是为了提高Linux系统的防攻击能力),它会导致DPDK多进程启动的时候得到的虚拟地址映射不一致,如果使用DPDK多进程,需要关闭ALSR,这会降低系统的安全性;
核心掩码/核心列表限制:在多进程模型下每个进程需有使用不同的核心掩码(CPU核心编号),核心列表(corelist)。如果绑定了相同的核心,会导致内存损坏。
中断交付问题:中断只能在主进程触发,如果需要传播,需要自己实现。
跨进程使用函数指针的问题:不支持跨多个进程使用函数指针,尤其是在不同编译二进制文件之间使用。由于一个进程中的函数位置可能与另一个进程中的函数位置不同,这会导致 librte_hash
库在多进程实例中无法正常工作,因为该库内部使用了指向哈希函数的指针。
建议解决方案:接调用哈希计算函数,并使用 rte_hash_add_with_hash()
/ rte_hash_lookup_with_hash()
函数,而不是那些内部进行哈希的函数(如 rte_hash_add()
/ rte_hash_lookup()
)。
HPET 定时器问题:
限制:根据所使用的硬件和 DPDK 进程的数量,可能无法在每个 DPDK 实例中都使用 HPET 定时器。Linux 用户空间可用的最低 HPET 比较器数量可能只有一个,这意味着只有第一个主 DPDK 进程实例可以打开并映射 /dev/hpet
。如果需要的 DPDK 进程数量超过了可用的 HPET 比较器数量,则必须使用 TSC(时间戳计数器)作为所有进程的时间源,而不是使用 HPET。
解决方案:在多进程应用程序中,若使用多个 DPDK 进程且可用的 HPET 比较器不足,TSC 可以作为替代的时间源,但这可能影响时间精度和同步。
进程间通信
DPDK 提供了一个 本地的 IPC(进程间通信)API,该 API 主要用于主进程与从进程之间交换短消息。它并不旨在实现高性能的通信,而是提供了一种方便的通用方法,用于在进程间交换简单的消息。
IPC API 支持的通信模式:
- 从进程到主进程的单播消息(Unicast):
- 从进程可以向主进程发送消息,但消息仅会被主进程接收。
- 主进程向所有从进程广播消息(Broadcast):
- 主进程可以向所有从进程广播消息,所有的从进程都会收到此消息。
注意:DPDK 不支持主进程与从进程之间或从进程之间的单播通信。
可用的通信类型:
DPDK IPC API 提供了三种类型的通信:
- 消息(Message):
- 这种类型的消息不期望收到响应,主要用于简单的通知机制。
- 同步请求(Synchronous request):
- 发送请求后,发送方会等待对方的响应。这种请求是双向通信,发送方会阻塞直到收到响应。
- 异步请求(Asynchronous request):
- 发送请求后,发送方不会立即等待响应,而是通过回调函数来处理响应或超时。
4.4.1 注册接收消息
在接收任何消息之前,必须注册一个回调函数。这是通过调用 rte_mp_action_register()
来完成的。该函数接受一个唯一的回调名称和一个回调函数的指针,该回调函数将在收到与此名称匹配的消息时被触发。
如果不再希望接收某个特定回调函数的消息,可以调用 rte_mp_action_unregister()
函数来注销该回调。
4.4.2 发送消息
要发送消息,首先需要填充一个 rte_mp_msg
描述符。需要填充的字段如下:
- name:消息名称。该名称必须与接收方的回调名称匹配。
- param:消息数据(最大 256 字节)。
- len_param:消息数据的长度。
- fds:需要传递的文件描述符(最多 8 个 fd)。
- num_fds:发送的文件描述符数量。
填充好结构后,调用 rte_mp_sendmsg()
函数发送消息:
- 如果从主进程发送消息,则消息会发送给所有从进程。
- 如果从从进程发送消息,则消息会发送给主进程。
该函数会返回一个值,指示消息发送是否成功。
4.4.3 发送请求
发送请求时需要等待对方的回复,因此请求可能会阻塞较长时间。发送请求的过程如下:
- 首先需要填充
rte_mp_msg
描述符,并指定一个 超时时间,如果超时未收到回复,则停止等待。 - 对于同步请求,必须创建一个
rte_mp_reply
描述符来存储响应。字段如下:- nb_sent:发送的请求数量(即在请求时有多少对等进程处于活动状态)。
- nb_received:接收到的响应数量(即有多少活动进程已回复)。
- msgs:指向存储所有响应的内存区域。响应的顺序是未定义的。
对于异步请求,需要提供一个 回调函数指针,该回调函数会在请求超时或收到所有响应时被调用。
警告:如果异步请求超时,回调函数将由 EAL 中断线程调用,而不是 IPC 线程。这可能会影响 DPDK 触发另一个中断事件(如警报)的能力。
4.4.4 接收和响应消息
- 要接收消息,首先必须使用
rte_mp_action_register()
注册回调函数。回调函数的名称必须与发送方的rte_mp_msg
描述符中的名称字段匹配,这样消息才能正确传递,回调函数会被触发。 - 如果需要响应消息,必须构造一个新的
rte_mp_msg
描述符,并通过rte_mp_reply()
函数发送响应,响应会传递给正确的请求者。
警告:在处理请求回调时,仅返回一个值并不会自动发送响应。即使在出现错误的情况下,也必须显式地发送响应,否则请求方会在等待响应时超时。
4.4.5 其他注意事项
- 递归请求不被支持:由于底层的 IPC 实现是单线程的,因此递归请求(即在响应另一个请求时发送请求)不被支持。
- 消息与请求的混合使用:发送消息时不会涉及 IPC 线程,因此在处理其他消息或请求时,可以发送消息。而请求会涉及 IPC 线程,需要谨慎处理。
- 内存分配与 IPC 不能混用:内部使用 IPC 的内存子系统不允许在内存相关的回调中使用 IPC,也不允许在 IPC 回调中分配/释放内存,这样做可能导致死锁。
- 超时与请求响应:如果回调处理时间过长,可能导致请求方超时,因此需要在请求方设置合适的超时时间。
- 消息超时:如果某些消息超时,
rte_mp_reply
描述符中的nb_sent
和nb_received
字段的值可能不匹配,用户需要根据具体情况处理此类情况。