dpdk之多进程client_server_mp源代码分析

首先浏览了下官网的介绍

1.运行程序

应用层参数:
-p 系统将要使用的端口的十六进制的掩码,如0x3(二进制0011)表示只是用端口0和端口1

-n 开启的客户端的进程数,处理从服务器接收到的数据包

mp_server运行参数:
./mp_server/build/mp_server -l 1-2 -n 4 – -p 3 -n 2
-l 1-2参数:服务器程序运行在core 1上,打印统计信息是在core 2上

mp_client运行参数:
./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
-l参数:两个客户端进程运行在core 3和core 4上。

2.流程分析和走读源代码

int
main(int argc, char *argv[])
{
    /* initialise the system 环境的初始化*/
    if (init(argc, argv) < 0 )
        return -1;
    RTE_LOG(INFO, APP, "Finished Process Init.\n");

    cl_rx_buf = calloc(num_clients, sizeof(cl_rx_buf[0]));

    /* clear statistics */
    clear_stats();

    /* put all other cores to sleep bar master */
    rte_eal_mp_remote_launch(sleep_lcore, NULL, SKIP_MASTER);

    //数据转发
    do_packet_forwarding();
    return 0;
}

初始化函数init
内部函数调用依次为:
1.rte_eal_init函数->eal环境初始化
2.rte_memzone_reserve->申请端口信息预留内存
3.parse_app_args->解析应用层参数
4.init_mbuf_pools->初始化内存池
5.init_port->针对每个端口去初始化端口
6.check_all_ports_link_status->检测端口的链路状态
7.init_shm_rings->初始化用于共享内存的ring
挑选init_port和init_shm_rings函数来讲解下流程,剩下的都是常规流程

init_port(uint8_t port_num)
{
    /* for port configuration all features are off by default */
    const struct rte_eth_conf port_conf = {
        .rxmode = {
            .mq_mode = ETH_MQ_RX_RSS
        }
    };
    const uint16_t rx_rings = 1, tx_rings = num_clients;
    const uint16_t rx_ring_size = RTE_MP_RX_DESC_DEFAULT;
    const uint16_t tx_ring_size = RTE_MP_TX_DESC_DEFAULT;

    uint16_t q;
    int retval;

    printf("Port %u init ... ", (unsigned)port_num);
    fflush(stdout);

    /* Standard DPDK port initialisation - config port, then set up
     * rx and tx rings */
    //配置端口属性,比如发送队列和接收队列
    if ((retval = rte_eth_dev_configure(port_num, rx_rings, tx_rings,
        &port_conf)) != 0)
        return retval;

    //创建接收队列
    for (q = 0; q < rx_rings; q++) {
        retval = rte_eth_rx_queue_setup(port_num, q, rx_ring_size,
                rte_eth_dev_socket_id(port_num),
                NULL, pktmbuf_pool);
        if (retval < 0) 
            return retval;
    }

    //创建发送队列
    for ( q = 0; q < tx_rings; q ++ ) {
        retval = rte_eth_tx_queue_setup(port_num, q, tx_ring_size,
                rte_eth_dev_socket_id(port_num),
                NULL);
        if (retval < 0) return retval;
    }

    //设置端口混杂模式
    rte_eth_promiscuous_enable(port_num);

    //启动网卡设备
    retval  = rte_eth_dev_start(port_num);
    if (retval < 0) return retval;

    printf( "done: \n");

    return 0;
}
init_shm_rings(void)
{
    unsigned i;
    unsigned socket_id;
    const char * q_name;
    const unsigned ringsize = CLIENT_QUEUE_RINGSIZE;

    //用于保存连接的client信息
    clients = rte_malloc("client details",
        sizeof(*clients) * num_clients, 0);
    if (clients == NULL)
        rte_exit(EXIT_FAILURE, "Cannot allocate memory for client program details\n");

    for (i = 0; i < num_clients; i++) {
        /* Create an RX queue for each client */
        socket_id = rte_socket_id();
        q_name = get_rx_queue_name(i);

        clients[i].rx_q = rte_ring_create(q_name,
                ringsize, socket_id,
                RING_F_SP_ENQ | RING_F_SC_DEQ ); /* single prod, single cons */
        if (clients[i].rx_q == NULL)
            rte_exit(EXIT_FAILURE, "Cannot create rx ring queue for client %u\n", i);
    }
    return 0;
}
clients = rte_malloc("client details",sizeof(*clients) * num_clients, 0);

首先申请了clients数组,用于保存每个客户端的信息,如客户端用于通信的rte_ring,客户端标识client_id,收包数,以及丢包数。

q_name = get_rx_queue_name(i);

使用get_rx_queue_name()函数构造“接收队列”的字符串标识。

clients[i].rx_q = rte_ring_create(q_name,
                ringsize, socket_id,
                RING_F_SP_ENQ | RING_F_SC_DEQ );

创建用于client和server通信的ring,ring的类型是单生产者单消费者。

此时init函数,初始化过程先告一段落。

数据转发函数do_packet_forwarding

do_packet_forwarding(void)
{
    unsigned port_num = 0; /* indexes the port[] array */

    for (;;) {
        struct rte_mbuf *buf[PACKET_READ_SIZE];
        uint16_t rx_count;

        /* read a port 读取端口上的数据*/
        rx_count = rte_eth_rx_burst(ports->id[port_num], 0, \
                buf, PACKET_READ_SIZE);

        /*统计数据报数量*/
        ports->rx_stats.rx[port_num] += rx_count;

        /* Now process the NIC packets read */
        if (likely(rx_count > 0))
            //处理网卡上读取到的数据包
            process_packets(port_num, buf, rx_count);

    /* 移动到下一个端口发送数据,如果端口等于num_ports,则从0开始*/
        if (++port_num == ports->num_ports)
            port_num = 0;
    }
}

流程是rte_eth_rx_burst函数从端口接收数据包,调用process_packets处理读取的数据包,而且是采用轮询端口的方式来处理数据包的哦。

进入process_packets函数中

process_packets(uint32_t port_num __rte_unused,
        struct rte_mbuf *pkts[], uint16_t rx_count)
{
    uint16_t i;
    uint8_t client = 0;

    for (i = 0; i < rx_count; i++) {
        enqueue_rx_packet(client, pkts[i]);

        //将数据包分依次发给不同的客户端
        if (++client == num_clients)
            client = 0;
    }

    for (i = 0; i < num_clients; i++)
        flush_rx_queue(i);
}

第一步:将数据包通过enqueue_rx_packet函数发送给不同的客户端,采用的方式是轮询。

enqueue_rx_packet(uint8_t client, struct rte_mbuf *buf)
{
    cl_rx_buf[client].buffer[cl_rx_buf[client].count++] = buf;
}

转定义看下cl_rx_buf变量的类型

/* One buffer per client rx queue - dynamically allocate array */
static struct client_rx_buf *cl_rx_buf;

struct client_rx_buf {
    struct rte_mbuf *buffer[PACKET_READ_SIZE];
    uint16_t count;
};

client_rx_buf的buffer是个数组,数组的长度是PACKET_READ_SIZE(32),每个元素的类型是rte_mbuf*.

现在好好分析下enqueue_rx_packet函数的这条语句,
cl_rx_buf是个结构体数组,数组的下标是client的序号。
cl_rx_buf[client]表示指定client的结构体信息
cl_rx_buf[client].count++ 表示指定客户端使用buffer[PACKET_READ_SIZE]数组中的哪一个元素。

第二步:调用rte_ring_enqueue_bulk函数将数据入队客户端的队列(cl->rx_q标识的队列)

flush_rx_queue(uint16_t client)
{
    uint16_t j;
    struct client *cl;

    if (cl_rx_buf[client].count == 0)
        return;

    cl = &clients[client];
    if (rte_ring_enqueue_bulk(cl->rx_q, (void **)cl_rx_buf[client].buffer,
            cl_rx_buf[client].count) != 0)
    {
        for (j = 0; j < cl_rx_buf[client].count; j++)
            rte_pktmbuf_free(cl_rx_buf[client].buffer[j]);

        cl->stats.rx_drop += cl_rx_buf[client].count;
    }
    else
        cl->stats.rx += cl_rx_buf[client].count;

    cl_rx_buf[client].count = 0;
}

发送成功则统计每个客户端的发包数,发送失败则统计丢包数。

客户端代码分析
1、rte_eal_init函数,eal环境初始化
2、parse_app_args函数,解析应用层参数,解析-n 客户端个数
3、使用rte_ring_lookup函数去查询服务器创建的ring,函数参数是get_rx_queue_name获取的。
4、使用rte_mempool_lookup函数去查询服务器创建的mempool内存池。名称是”MProc_pktmbuf_pool”
5、使用rte_memzone_lookup函数去查询服务器创建的memzone,名称是”MProc_port_info”,通过ports = mz->addr;获取到端口信息。
6、configure_output_ports函数

configure_output_ports(const struct port_info *ports)
{
    int i;
    if (ports->num_ports > RTE_MAX_ETHPORTS)
        rte_exit(EXIT_FAILURE, "Too many ethernet ports. RTE_MAX_ETHPORTS = %u\n",
                (unsigned)RTE_MAX_ETHPORTS);
    //遍历端口,比如p1=0   p2 = 1,映射输出端口和输入端口的关系
    for (i = 0; i < ports->num_ports - 1; i+=2){
        uint8_t p1 = ports->id[i];
        uint8_t p2 = ports->id[i+1];
        output_ports[p1] = p2;
        output_ports[p2] = p1;

        //配置发送缓冲区
        configure_tx_buffer(p1, MBQ_CAPACITY);
        configure_tx_buffer(p2, MBQ_CAPACITY);

    }
}

进入configure_tx_buffer函数中

static void
configure_tx_buffer(uint8_t port_id, uint16_t size)
{
    int ret;

    /* Initialize TX buffers */
    tx_buffer[port_id] = rte_zmalloc_socket("tx_buffer",
            RTE_ETH_TX_BUFFER_SIZE(size), 0,
            rte_eth_dev_socket_id(port_id));
    if (tx_buffer[port_id] == NULL)
        rte_exit(EXIT_FAILURE, "Cannot allocate buffer for tx on port %u\n",
                (unsigned) port_id);

    rte_eth_tx_buffer_init(tx_buffer[port_id], size);

    ret = rte_eth_tx_buffer_set_err_callback(tx_buffer[port_id],
            flush_tx_error_callback, (void *)(intptr_t)port_id);
    if (ret < 0)
            rte_exit(EXIT_FAILURE, "Cannot set error callback for "
                    "tx buffer on port %u\n", (unsigned) port_id);
}

解析下

rte_zmalloc_socket函数
void* rte_zmalloc_socket ( const char * type, size_t size, unsigned
align, int socket ) type:一个字符串,去标识申请的对象类型,方便日后调试,如“tx_buffer”
size:申请的长度(以字节为单位) align:内存对齐方式,此处填写0
socket:在哪个socket-id上申请内存,socket-id理论上和物理cpu一一对应。rte_eth_dev_socket_id(uint8_t
port_id)函数返回port_id标识的以太网设备所对应的socketId。

rte_eth_tx_buffer_init函数 int rte_eth_tx_buffer_init ( struct
rte_eth_dev_tx_buffer * buffer, uint16_t size ) 功能:初始化发送缓冲区
buffer:被初始化的发送缓冲区 size:缓冲区大小

返回值:成功则返回指向申请对象的指针。失败则返回NULL值

rte_eth_tx_buffer_set_err_callback函数 设置发送缓冲区的失败回调函数

继续分析代码
下面是个while循环一直从ring无锁队列中弹出数据rte_ring_dequeue_bulk

rx_pkts = (uint16_t)RTE_MIN(rte_ring_count(rx_ring), PKT_READ_SIZE);
rte_ring_count(rx_ring)表示ring中元素的个数,将ring中元素的个数和PKT_READ_SIZE取最小值。

接收到数据包后调用handle_packet函数

static void
handle_packet(struct rte_mbuf *buf)
{
    int sent;
    const uint8_t in_port = buf->port;
    const uint8_t out_port = output_ports[in_port];
    struct rte_eth_dev_tx_buffer *buffer = tx_buffer[out_port];

    sent = rte_eth_tx_buffer(out_port, client_id, buffer, buf);
    if (sent)
        tx_stats->tx[out_port] += sent;

}

将数据包从发送端口发送出去。

分析完毕,如有不清晰的地方,请留言或者加qq1078285863

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值