28. Multi-process Sample Application
本章描述了在DPDK中多进程示例应用程序。
28.1. Example Applications
28.1.1. Building the Sample Applications
多进程示例应用程序的构建方式与其他示例应用程序相同,在“DPDK Getting Started Guide”中有文档记录。
要编译示例应用程序,请参阅” Compiling the Sample Applications.“。
应用程序位于multi_process子目录中。
!Note
如果需要构建一个特定的多进程应用程序,最终的make命令可以只在该应用程序的目录中运行,而不需要在多进程目录最上层目录运行。
(前提是环境库已经编译好,环境变量设置好了)
28.1.2. Basic Multi-process Example
DPDK版本中的示例/ simple_mp文件夹中包含一个基本的示例应用程序,用来演示两个DPDK进程如何使用队列和内存池来共享信息。
28.1.2.1. Running the Application
要运行应用程序,在一个终端中启动一个simple_mp进程,在coremask / corelist参数中至少设置两个核,如下:
./build/simple_mp -l 0-1 -n 4 --proc-type=primary
对于第一个DPDK进程,proc-type参数可以忽略或设置为”auto“,因为所有DPDK进程将默认一个主实例,这意味着它们可以控制hugepage共享内存区域。该进程应该成功启动,并显示一个命令提示符如下:
$ ./build/simple_mp -l 0-1 -n 4 --proc-type=primary
EAL: coremask set to 3
EAL: Detected lcore 0 on socket 0
EAL: Detected lcore 1 on socket 0
EAL: Detected lcore 2 on socket 0
EAL: Detected lcore 3 on socket 0
...
EAL: Requesting 2 pages of size 1073741824
EAL: Requesting 768 pages of size 2097152
EAL: Ask a virtual area of 0x40000000 bytes
EAL: Virtual area found at 0x7ff200000000 (size = 0x40000000)
...
EAL: check igb_uio module
EAL: check module finished
EAL: Master core 0 is ready (tid=54e41820)
EAL: Core 1 is ready (tid=53b32700)
Starting core 1
simple_mp >
为了运行同一二进制文件的副进程和主进程通信,coremask/corelist参数至少得设置两个核:
./build/simple_mp -l 2-3 -n 4 --proc-type=secondary
在运行如上所示的副进程时,proc-type参数也可以被指定为auto。但是,省略该参数将导致该进程启动时尝试作为一个主进程而不是副过程。
(上面的意思就是副进程启动时只能设置proc-type参数为secondary,auto,不能省略)
一旦正确地指定了进程类型,进程就会正常启动,显示和主进程初始化时相同的大量状态消息。同样,您将看到一个命令提示符。
当两个进程都运行时,可以使用send命令在它们之间发送消息。在任何阶段,任何进程都可以使用quit命令退出。
EAL: Master core 10 is ready (tid=b5f89820) EAL: Master core 8 is ready (tid=864a3820)
EAL: Core 11 is ready (tid=84ffe700) EAL: Core 9 is ready (tid=85995700)
Starting core 11 Starting core 9
simple_mp > send hello_secondary simple_mp > core 9: Received 'hello_secondary'
simple_mp > core 11: Received 'hello_primary' simple_mp > send hello_primary
simple_mp > quit simple_mp > quit
!Note
如果主进程被终止,副进程也必须的终止,然后在主进程启动之后再启动。这是必要的,因为主进程在启动的时候将清除并重置共享内存区域,从而使副进程的指针失效。可以在不影响主进程的情况下停止和重启副进程。
28.1.2.2. How the Application Works
这个示例应用程序的核心是在共享内存中使用两个队列和一个内存池。这三个对象是由主进程在启动时创建的,副进程不能在内存中创建对象,因为它不能保留内存区域,副进程在启动时使用查找函数关联到这些对象上。
if (rte_eal_process_type() == RTE_PROC_PRIMARY){
send_ring = rte_ring_create(_PRI_2_SEC, ring_size, SOCKET0, flags);
recv_ring = rte_ring_create(_SEC_2_PRI, ring_size, SOCKET0, flags);
message_pool = rte_mempool_create(_MSG_POOL, pool_size, string_size, pool_cache, priv_data_sz, NULL, NULL, NULL, NULL, SOCKET0, flags);
} else {
recv_ring = rte_ring_lookup(_PRI_2_SEC);
send_ring = rte_ring_lookup(_SEC_2_PRI);
message_pool = rte_mempool_lookup(_MSG_POOL);
}
注意,在主进程中创建send_ring的参数名字_PRI_2_SEC,在副进程中是作为recv_ring的参数的。
(目的就是主进程发数据,副进程收数据,副进程发数据,主进程收数据)
一旦这些ring和内存池在主进程和副进程中都可用,应用程序就会启动两个线程分别用于发送和接收消息。接收线程简单地将接收ring上的任何消息提出队列,打印它们,并将消息使用的缓冲区空间释放回内存池。发送线程利用用户在交互命令行中输入的内容生成消息准备发送。一旦用户发出了发送命令,就会从内存池中分配一个缓冲区,并填充消息内容,然后发送到适当的rte_ring上的队列。
28.1.3. Symmetric Multi-process Example
DPDK多进程支持的第二个示例演示了一组进程如何并行运行,每个进程执行相同的包处理操作。(由于每个进程在功能上与其他进程是相同的,所以我们将其称为对称多处理,以区别于不对称的多处理——如下一个示例中所看到的客户机-服务器模式,在这里,不同的进程执行不同的任务,通过协作以形成一个包处理系统)下图显示了通过应用程序的数据流,使用了两个进程。
根据上图所示, 每个进程接收指定接口的数据包。RSS用于将每个端口上的接收数据包分发到不同的硬件RX队列上。每个进程在每个端口上读取各自的指定RX队列,因此不必和其他进程竞争RX队列。类似地,每个进程在每个端口上有各自的TX队列以发送数据包。
28.1.3.1. Running the Application
与simple_mp示例一样,symmetric_mp程序的第一个实例必须以主进程的形式运行,像其他一些DPDK应用程序一样,EAL库也提供了一些特定的参数。这些额外的参数是:
- -p <portmask>,其中,portmask是一个十六进制位掩码,用于在系统上使用的端口。例如:-p 3 仅使用端口0和1。
- –num-procs <N>,其中N是将会并行运行执行分组处理的symmetric_mp进程的总个数。该参数用于在每个网络端口上配置接收队列的适当数量。
- –proc-id <n> 其中n为0 <= n < N(进程的数量,上面指定的进程个数)的数值。这将标识正在运行的symmetric_mp进程,以便每个进程可以在每个网络端口上读取唯一的接收队列。
第二个symmetric_mp进程也必须指定的这些参数,前两个参数必须和主进程的参数相同,否则会返回错误。
例如,要运行一组4个symmetric_mp实例,在核1 - 4上运行,在端口0和1之间执行二层数据包转发,可以使用以下命令(假设以root身份运行):
# ./build/symmetric_mp -l 1 -n 4 --proc-type=auto -- -p 3 --num-procs=4 --proc-id=0
# ./build/symmetric_mp -l 2 -n 4 --proc-type=auto -- -p 3 --num-procs=4 --proc-id=1
# ./build/symmetric_mp -l 3 -n 4 --proc-type=auto -- -p 3 --num-procs=4 --proc-id=2
# ./build/symmetric_mp -l 4 -n 4 --proc-type=auto -- -p 3 --num-procs=4 --proc-id=3
!Note
在上面的示例中,可以显式指定进程类型为主或副,而不用auto。当使用auto时,第一个进程运行时会创建所有进程所需的所有内存结构——不管它的proc-id是0、1、2或3。
!Note
对于对称的多进程示例,因为所有进程都以相同的方式工作,一旦hugepage共享内存和网络端口被初始化,如果主进程挂掉,不必重新启动所有其它进程。相反,通过在命令行中显式地将proc-type设置为副进程,可以重新启动该进程。(所有后续启动的实例也都需要显式指定,因为自动检测将检测到没有主进程正在运行,因此尝试重新初始化共享内存。)
28.1.3.2. How the Application Works
主副进程中的初始化工作大部分是相同的,调用rte_eal_init()、1g和10g网卡驱动程序初始化,然后调用rte_pci_probe()函数。此后,初始化的完成取决于该进程是否被配置为主进程或是副进程。
在主进程中,为数据包mbufs创建了一个内存池,并初始化了要使用的网络端口——通过命令行上传递的num-procs参数来确定每个端口的RX和TX队列的数量。被初始化的网络端口的结构存储在共享内存中,副进程在初始化时可以访问。
if (num_ports & 1)
rte_exit(EXIT_FAILURE, "Application must use an even number of ports\n");
for(i = 0; i < num_ports; i++){
if(proc_type == RTE_PROC_PRIMARY)
if (smp_port_init(ports[i], mp, (uint16_t)num_procs) < 0)
rte_exit(EXIT_FAILURE, "Error initializing ports\n");
}
在副进程中,不会初始化网络端口,而是使用主进程导出的端口信息,副进程通过端口信息访问每个网络端口的硬件和软件ring。类似地,通过内存池的名称进行查找,可以访问mbufs的内存池:mp = (proc_type == RTE_PROC_SECONDARY) ? rte_mempool_lookup(_SMP_MBUF_POOL) : rte_mempool_create(_SMP_MBUF_POOL, NB_MBUFS, MBUF_SIZE, ... )
一旦这个初始化完成,每个进程的主循环开始运行(主进程和副进程的主循环函数是完全相同的)——每个进程从每个端口的对应于它的proc - id参数的队列上读取数据,写入数据到对应的输出端口队列上。
28.1.4. Client-Server Multi-process Example
第三个DPDK多进程应用示例中演示了如何使用客户机-服务器模型的多进程设计来进行数据包处理。在本例中,单个服务器进程接收来自网络端口的数据包,并通过轮询的方式将数据包分发到一组客户机进程中,它们执行实际的包处理。在这种情况下,客户端应用程序只是通过将每个包发送到不同的网络端口来执行二层转发。
下图显示了通过应用程序的数据流,使用了两个客户机进程。
28.1.4.1. Running the Application
服务器进程必须先运行以便初始化客户进程所使用的全部内存结构。除EAL参数外,具体应用参数为:
- -p <portmask >,其中,portmask是一个十六进制位掩码,用于在系统上使用的端口。例如:-p 3 仅使用端口0和1。
- -n <num-clients>,num-client参数是处理接收来自服务器的数据包的客户机进程的数量。
# ./mp_server/build/mp_server -l 1-2 -n 4 -- -p 3 -n 2
# ./mp_client/build/mp_client -l 3 -n 4 --proc-type=auto -- -n 0
# ./mp_client/build/mp_client -l 4 -n 4 --proc-type=auto -- -n 1
!Note
- 主进程启动并初始化一个变量,在创建了从属进程之后,它将不会被更改。这种情况下就可以了。
- 在创建了从属进程之后,主进程或从进程核需要更改一个变量,但是其他进程不需要知道更改。这种情况也可以。
- 在创建了从属进程之后,主进程或从属进程需要改变一个变量。与此同时,其他一个或多个进程需要知晓改变。在这种情况下,不能使用全局变量和静态变量来共享信息。需要另一种沟通机制。一个没有锁保护的简单方法可以是由rte_malloc或memzone分配的堆缓冲区提供。
在讨论恢复机制之前,有必要知道在先前的一个从属实例已经退出,一个新的从属实例可以运行之前需要什么。
- 为每个从属进程保留一个资源列表。在一个从属进程运行之前,主进程应该准备一个资源列表。在退出后,主进程可以删除已分配的资源并创建新的资源,或者重新初始化新实例所使用的资源。
- 为从属进程退出建立通知机制。在特定的从属进程退出后,应该通知主进程,然后帮助创建一个新的从属进程。该机制是在 Master-slave Process Models提供的。
- 在相关进程之间使用同步机制。主进程应该有能力停止或杀死从属进程,这个从属进程依赖于另一个进程,但另一个进程已经退出了。然后,在退出的从属进程的新实例开始运行之后,依赖项可以从一开始就恢复或生成。该示例将一个STOP命令发送到依赖于退出进程的从属进程,然后它们将退出。此后,主进程为已退出的从属进程创建新实例。
#./build/l2fwd_fork -l 2-4 -n 4 -- -p 3 -f
这个例子提供了另一个- f 选项来指定浮动进程的启动。如果没有指定,这个示例将使用固定核方式来执行L2转发任务。
#ps -fe | grep l2fwd_fork
root 5136 4843 29 11:11 pts/1 00:00:05 ./build/l2fwd_fork
root 5145 5136 98 11:11 pts/1 00:00:11 ./build/l2fwd_fork
root 5146 5136 98 11:11 pts/1 00:00:11 ./build/l2fwd_fork
然后,杀掉一个从属进程:
#kill -9 5145
1或2秒后,检查从属是否已经恢复:
#ps -fe | grep l2fwd_fork
root 5136 4843 3 11:11 pts/1 00:00:06 ./build/l2fwd_fork
root 5247 5136 99 11:14 pts/1 00:00:01 ./build/l2fwd_fork
root 5248 5136 99 11:14 pts/1 00:00:01 ./build/l2fwd_fork
还可以监视静态流量生成器,以查看是否恢复了从属进程。
static int
l2fwd_malloc_shared_struct(void)
{
port_statistics = rte_zmalloc("port_stat", sizeof(struct l2fwd_port_statistics) * RTE_MAX_ETHPORTS, 0);
if (port_statistics == NULL)
return -1;
/* allocate mapping_id array */
if (float_proc) {
int i;
mapping_id = rte_malloc("mapping_id", sizeof(unsigned) * RTE_MAX_LCORE, 0);
if (mapping_id == NULL)
return -1;
for (i = 0 ;i < RTE_MAX_LCORE; i++)
mapping_id[i] = INVALID_MAPPING_ID;
}
return 0;
}
for (portid = 0; portid < nb_ports; portid++) {
/* skip ports that are not enabled */
if ((l2fwd_enabled_port_mask & (1 << portid)) == 0)
continue;
/* Find pair ports' lcores */
find_lcore = find_pair_lcore = 0;
pair_port = l2fwd_dst_ports[portid];
for (i = 0; i < RTE_MAX_LCORE; i++) {
if (!rte_lcore_is_enabled(i))
continue;
for (j = 0; j < lcore_queue_conf[i].n_rx_port;j++) {
if (lcore_queue_conf[i].rx_port_list[j] == portid) {
lcore = i;
find_lcore = 1;
break;
}
if (lcore_queue_conf[i].rx_port_list[j] == pair_port) {
pair_lcore = i;
find_pair_lcore = 1;
break;
}
}
if (find_lcore && find_pair_lcore)
break;
}
if (!find_lcore || !find_pair_lcore)
rte_exit(EXIT_FAILURE, "Not find port=%d pair\\n", portid);
printf("lcore %u and %u paired\\n", lcore, pair_lcore);
lcore_resource[lcore].pair_id = pair_lcore;
lcore_resource[pair_lcore].pair_id = lcore;
}
for (i = 0; i < RTE_MAX_LCORE; i++) {
if (lcore_resource[i].enabled) {
/* Create ring for master and slave communication */
ret = create_ms_ring(i);
if (ret != 0)
rte_exit(EXIT_FAILURE, "Create ring for lcore=%u failed",i);
if (flib_register_slave_exit_notify(i,slave_exit_cb) != 0)
rte_exit(EXIT_FAILURE, "Register master_trace_slave_exit failed");
}
}
while (1) {
sleep(1);
cur_tsc = rte_rdtsc();
diff_tsc = cur_tsc - prev_tsc;
/* if timer is enabled */
if (timer_period > 0) {
/* advance the timer */
timer_tsc += diff_tsc;
/* if timer has reached its timeout */
if (unlikely(timer_tsc >= (uint64_t) timer_period)) {
print_stats();
/* reset the timer */
timer_tsc = 0;
}
}
prev_tsc = cur_tsc;
/* Check any slave need restart or recreate */
rte_spinlock_lock(&res_lock);
for (i = 0; i < RTE_MAX_LCORE; i++) {
struct lcore_resource_struct *res = &lcore_resource[i];
struct lcore_resource_struct *pair = &lcore_resource[res->pair_id];
/* If find slave exited, try to reset pair */
if (res->enabled && res->flags && pair->enabled) {
if (!pair->flags) {
master_sendcmd_with_ack(pair->lcore_id, CMD_STOP);
rte_spinlock_unlock(&res_lock);
sleep(1);
rte_spinlock_lock(&res_lock);
if (pair->flags)
continue;
}
if (reset_pair(res->lcore_id, pair->lcore_id) != 0)
rte_exit(EXIT_FAILURE, "failed to reset slave");
res->flags = 0;
pair->flags = 0;
}
}
rte_spinlock_unlock(&res_lock);
}
static int
l2fwd_launch_one_lcore( attribute ((unused)) void *dummy)
{
unsigned lcore_id = rte_lcore_id();
if (float_proc) {
unsigned flcore_id;
/* Change it to floating process, also change it's lcore_id */
clear_cpu_affinity();
RTE_PER_LCORE(_lcore_id) = 0;
/* Get a lcore_id */
if (flib_assign_lcore_id() < 0 ) {
printf("flib_assign_lcore_id failed\n");
return -1;
}
flcore_id = rte_lcore_id();
/* Set mapping id, so master can return it after slave exited */
mapping_id[lcore_id] = flcore_id;
printf("Org lcore_id = %u, cur lcore_id = %u\n",lcore_id, flcore_id);
remapping_slave_resource(lcore_id, flcore_id);
}
l2fwd_main_loop();
/* return lcore_id before return */
if (float_proc) {
flib_free_lcore_id(rte_lcore_id());
mapping_id[lcore_id] = INVALID_MAPPING_ID;
}
return 0;
}