DPDK簡單example的閱讀——l2fwd

從大四開始嚷嚷著要學(xué)DPDK,一直沒有靜下心來看源碼,拖了兩年到現(xiàn)在才開始鉆研DPDK的簡單應(yīng)用。二層轉(zhuǎn)發(fā)是DPDK數(shù)據(jù)報(bào)處理應(yīng)用里一個(gè)比較簡單的example,代碼只有幾百行,全部看懂也大約只要半天時(shí)間。

在計(jì)算機(jī)網(wǎng)絡(luò)中,二層是鏈路層,是以太網(wǎng)所在的層,識別的是設(shè)備端口的MAC地址。DPDK作為用戶態(tài)驅(qū)動,主要的目的也就是不需要讓報(bào)文經(jīng)過操作系統(tǒng)協(xié)議棧而能實(shí)現(xiàn)快速的轉(zhuǎn)發(fā)功能。網(wǎng)卡驅(qū)動在二層上的作用就是根據(jù)設(shè)定的目的端口,轉(zhuǎn)發(fā)報(bào)文到目的端口。

l2fwd的運(yùn)行效果如下:
將兩臺機(jī)器用網(wǎng)線相連,一臺用pktgen發(fā)送數(shù)據(jù),一臺用l2fwd轉(zhuǎn)發(fā)數(shù)據(jù),l2fwd的運(yùn)行界面:

l2fwd運(yùn)行界面.png

由于只開了一個(gè)端口轉(zhuǎn)發(fā),所以在l2fwd的默認(rèn)規(guī)則下,就是單個(gè)port自己收自己發(fā),發(fā)送的報(bào)文數(shù)量和接收的一樣多。

程序的主要流程如下:

l2fwd主流程圖.png

每個(gè)邏輯核在任務(wù)分發(fā)后會執(zhí)行如下的循環(huán),直到退出:

從線程循環(huán).png

其中打印時(shí)間片在命令行參數(shù)中是可以自己設(shè)置的。

1.解析命令行參數(shù)

DPDK的命令行參數(shù)包括:EAL參數(shù)和程序自身的參數(shù),之間用“--”隔開。比如說,運(yùn)行l(wèi)2fwd時(shí),輸入命令
./l2fwd -c 0x3 -n 4 -- -p 3 -q 1
其中-c和-n就是EAL參數(shù),后面的-p和-q就是程序自帶的參數(shù)
所以在代碼中,解析命令行參數(shù),也分了兩步,先解析的是EAL參數(shù)

ret = rte_eal_init(argc, argv);
    if (ret < 0)
        rte_exit(EXIT_FAILURE, "Invalid EAL arguments\n");
    argc -= ret;
    argv += ret;

rte_eal_init不僅有解析命令行參數(shù)的作用,以及一系列很復(fù)雜的環(huán)境的初始化,詳見前一篇。當(dāng)解析完了EAL的參數(shù)之后,argc減去EAL參數(shù)的個(gè)數(shù)同時(shí)argv后移這么多位,這樣就能保證后面解析程序參數(shù)的時(shí)候跳過了前面的EAL參數(shù)。

ret = l2fwd_parse_args(argc, argv);
    if (ret < 0)
        rte_exit(EXIT_FAILURE, "Invalid L2FWD arguments\n");

(其實(shí)真正做到分割的原因是系統(tǒng)函數(shù)getopt以及getopt_long,這些處理命令行參數(shù)的函數(shù),處理到“--”時(shí)就會停止,所以這一機(jī)制可以被用來做多段參數(shù))

2.創(chuàng)建內(nèi)存池

由于DPDK在使用前需要分配大頁,所以實(shí)際創(chuàng)建內(nèi)存池時(shí)就是從這些已分配的大頁中創(chuàng)建。

l2fwd_pktmbuf_pool = rte_pktmbuf_pool_create("mbuf_pool", NB_MBUF,
        MEMPOOL_CACHE_SIZE, 0, RTE_MBUF_DEFAULT_BUF_SIZE,
        rte_socket_id());

其中參數(shù)包括cache_size、priv_size、data_room_size,以及在哪個(gè)socket上分配。這里的socket不是網(wǎng)絡(luò)中的套接字,而是numa架構(gòu)的socket。
numa架構(gòu)是多核技術(shù)發(fā)展的產(chǎn)物,在傳統(tǒng)結(jié)構(gòu)上,每個(gè)處理器都是通過系統(tǒng)總線訪問內(nèi)存,訪存時(shí)間開銷一致。而numa架構(gòu)里,每個(gè)socket上有數(shù)個(gè)node,每個(gè)node又包括數(shù)個(gè)core。每個(gè)socket有自己的內(nèi)存,每個(gè)socket里的處理器訪問自己內(nèi)存的速度最快,訪問其他socket的內(nèi)存則比較慢,如下圖所示[1]。因此我們在創(chuàng)建緩沖區(qū)的時(shí)候就需要充分考慮到內(nèi)存位置對性能的影響。

numa架構(gòu)簡單示意圖.png

3.設(shè)置二層轉(zhuǎn)發(fā)目的端口

對每個(gè)端口,先初始化設(shè)置他們的目的端口都是0,然后用一個(gè)for循環(huán)來讓端口兩兩互為目的端口。例如:0號端口的目的端口是1,1號端口的目的端口是0;2號端口的目的端口是3,3號端口的目的端口是2……這里我們也可以修改成我們想要的轉(zhuǎn)發(fā)規(guī)則。

for (portid = 0; portid < nb_ports; portid++) {
        /* skip ports that are not enabled */
        if ((l2fwd_enabled_port_mask & (1 << portid)) == 0)
            continue;

        if (nb_ports_in_mask % 2) {
            l2fwd_dst_ports[portid] = last_port;
            l2fwd_dst_ports[last_port] = portid;
        }
        else
            last_port = portid;

        nb_ports_in_mask++;

        //獲取端口的名字、發(fā)送隊(duì)列、接收隊(duì)列等信息,主要就是填充每個(gè)端口的dev_info結(jié)構(gòu)體
        rte_eth_dev_info_get(portid, &dev_info);
    }

以及最后填充了一下每個(gè)端口的結(jié)構(gòu)體里有關(guān)該端口的各種信息

4.為每個(gè)端口分配邏輯核

我們在命令行輸入的參數(shù)有一個(gè)q,指的就是每個(gè)邏輯核最多可以用來處理幾個(gè)端口,在這里綁定核的時(shí)候,就會執(zhí)行這方面的檢查。while后面的語句是尋找一個(gè)可同的邏輯核,在不超過最大核數(shù)量(128)的基礎(chǔ)上,從0開始,看每個(gè)核是否超過了設(shè)置的每個(gè)核綁定幾個(gè)端口數(shù)限制,如果沒有當(dāng)前循環(huán)的這個(gè)端口就可以綁定該核。

for (portid = 0; portid < nb_ports; portid++) {
        ...
        while (rte_lcore_is_enabled(rx_lcore_id) == 0 ||
               lcore_queue_conf[rx_lcore_id].n_rx_port ==
               l2fwd_rx_queue_per_lcore) {
            rx_lcore_id++;
            if (rx_lcore_id >= RTE_MAX_LCORE)
                rte_exit(EXIT_FAILURE, "Not enough cores\n");
        }

        //綁定該核
        if (qconf != &lcore_queue_conf[rx_lcore_id])
            /* Assigned a new logical core in the loop above. */
            qconf = &lcore_queue_conf[rx_lcore_id];
        ...
    }

實(shí)際的綁定就是在這個(gè)核處理的端口列表中加上當(dāng)前這個(gè)端口,然后該核綁定的端口數(shù)加1。

5.初始化每個(gè)端口

其中fflush函數(shù)是清除緩沖區(qū)的作用,會強(qiáng)迫未寫入磁盤的內(nèi)容立即寫入。這部分比較簡單,直接上源碼。

for (portid = 0; portid < nb_ports; portid++) {

        ...

        //清除讀寫緩沖區(qū)
        fflush(stdout);
        //配置端口,將一些配置寫進(jìn)設(shè)備dev的一些字段,以及檢查設(shè)備支持什么類型的中斷、支持的包大小
        ret = rte_eth_dev_configure(portid, 1, 1, &port_conf);

        ...

        //獲取設(shè)備的MAC地址,寫在后一個(gè)參數(shù)里
        rte_eth_macaddr_get(portid,&l2fwd_ports_eth_addr[portid]);

        /* init one RX queue */
        //清除緩沖區(qū)
        fflush(stdout);
        //設(shè)置接收隊(duì)列
        ret = rte_eth_rx_queue_setup(portid, 0, nb_rxd,
                         rte_eth_dev_socket_id(portid),
                         NULL,
                         l2fwd_pktmbuf_pool);
        if (ret < 0)
            rte_exit(EXIT_FAILURE, "rte_eth_rx_queue_setup:err=%d, port=%u\n",
                  ret, (unsigned) portid);

        /* init one TX queue on each port */
        fflush(stdout);
        //設(shè)置發(fā)送隊(duì)列
        ret = rte_eth_tx_queue_setup(portid, 0, nb_txd,
                rte_eth_dev_socket_id(portid),
                NULL);
        if (ret < 0)
            rte_exit(EXIT_FAILURE, "rte_eth_tx_queue_setup:err=%d, port=%u\n",
                ret, (unsigned) portid);

        /* Initialize TX buffers */
        //每個(gè)端口分配接收緩沖區(qū),根據(jù)numa架構(gòu)的socket就近分配
        tx_buffer[portid] = rte_zmalloc_socket("tx_buffer",
                RTE_ETH_TX_BUFFER_SIZE(MAX_PKT_BURST), 0,
                rte_eth_dev_socket_id(portid));
        if (tx_buffer[portid] == NULL)
            rte_exit(EXIT_FAILURE, "Cannot allocate buffer for tx on port %u\n",
                    (unsigned) portid);

        //初始化接收緩沖區(qū)
        rte_eth_tx_buffer_init(tx_buffer[portid], MAX_PKT_BURST);

        //設(shè)置接收緩沖區(qū)的err_callback
        ret = rte_eth_tx_buffer_set_err_callback(tx_buffer[portid],
                rte_eth_tx_buffer_count_callback,
                &port_statistics[portid].dropped);
        if (ret < 0)
                rte_exit(EXIT_FAILURE, "Cannot set error callback for "
                        "tx buffer on port %u\n", (unsigned) portid);

        /* Start device */
        //啟用端口
        ret = rte_eth_dev_start(portid);
        if (ret < 0)
            rte_exit(EXIT_FAILURE, "rte_eth_dev_start:err=%d, port=%u\n",
                  ret, (unsigned) portid);

        printf("done: \n");

        rte_eth_promiscuous_enable(portid);
        //打印端口MAC地址
        printf("Port %u, MAC address: %02X:%02X:%02X:%02X:%02X:%02X\n\n",
                (unsigned) portid,
                l2fwd_ports_eth_addr[portid].addr_bytes[0],
                l2fwd_ports_eth_addr[portid].addr_bytes[1],
                l2fwd_ports_eth_addr[portid].addr_bytes[2],
                l2fwd_ports_eth_addr[portid].addr_bytes[3],
                l2fwd_ports_eth_addr[portid].addr_bytes[4],
                l2fwd_ports_eth_addr[portid].addr_bytes[5]);

        /* initialize port stats */
        //初始化端口數(shù)據(jù),就是后面要打印的,接收、發(fā)送、drop的包數(shù)
        memset(&port_statistics, 0, sizeof(port_statistics));
    }

6.任務(wù)分發(fā)

這里就是DPDK程序最熟悉的任務(wù)分發(fā)函數(shù)了,每個(gè)slave從線程啟動后運(yùn)行的函數(shù)是l2fwd_launch_one_lcore:

rte_eal_mp_remote_launch(l2fwd_launch_one_lcore, NULL, CALL_MASTER);
    RTE_LCORE_FOREACH_SLAVE(lcore_id) {
        if (rte_eal_wait_lcore(lcore_id) < 0) {
            ret = -1;
            break;
        }
    }

而l2fwd_launch_one_lcore實(shí)際上運(yùn)行的是l2fwd_main_loop,這就是上面說的從線程循環(huán)了

static int
l2fwd_launch_one_lcore(__attribute__((unused)) void *dummy)
{
    l2fwd_main_loop();
    return 0;
}

7.從線程循環(huán)

這部分就直接附上注釋的源碼吧,注釋有點(diǎn)調(diào)皮~

static void
l2fwd_main_loop(void)
{
    
    ...

    //獲取自己的lcore_id
    lcore_id = rte_lcore_id();
    qconf = &lcore_queue_conf[lcore_id];

    //分配后多余的lcore,無事可做,orz
    if (qconf->n_rx_port == 0) {
        RTE_LOG(INFO, L2FWD, "lcore %u has nothing to do\n", lcore_id);
        return;
    }

    //有事做的核,很開心的進(jìn)入了主循環(huán)~
    RTE_LOG(INFO, L2FWD, "entering main loop on lcore %u\n", lcore_id);

    ...

    //直到發(fā)生了強(qiáng)制退出,在這里就是ctrl+c或者kill了這個(gè)進(jìn)程
    while (!force_quit) {

        cur_tsc = rte_rdtsc();

        /*
         * TX burst queue drain
         */
        //計(jì)算時(shí)間片
        diff_tsc = cur_tsc - prev_tsc;
        //過了100us,把發(fā)送buffer里的報(bào)文發(fā)出去
        if (unlikely(diff_tsc > drain_tsc)) {

            for (i = 0; i < qconf->n_rx_port; i++) {

                portid = l2fwd_dst_ports[qconf->rx_port_list[i]];
                buffer = tx_buffer[portid];

                sent = rte_eth_tx_buffer_flush(portid, 0, buffer);
                if (sent)
                    port_statistics[portid].tx += sent;

            }

            //到了時(shí)間片了打印各端口的數(shù)據(jù)
            /* 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 >= timer_period)) {

                    /* do this only on master core */
                    //打印讓master主線程來做
                    if (lcore_id == rte_get_master_lcore()) {
                        print_stats();
                        /* reset the timer */
                        timer_tsc = 0;
                    }
                }
            }

            prev_tsc = cur_tsc;
        }

        /*
         * Read packet from RX queues
         */
        //沒有到發(fā)送時(shí)間片的話,讀接收隊(duì)列里的報(bào)文
        for (i = 0; i < qconf->n_rx_port; i++) {

            portid = qconf->rx_port_list[i];
            nb_rx = rte_eth_rx_burst((uint8_t) portid, 0,
                         pkts_burst, MAX_PKT_BURST);

            //計(jì)數(shù),收到的報(bào)文數(shù)
            port_statistics[portid].rx += nb_rx;

            for (j = 0; j < nb_rx; j++) {
                m = pkts_burst[j];
                rte_prefetch0(rte_pktmbuf_mtod(m, void *));
                //updating mac地址以及目的端口發(fā)送buffer滿了的話,嘗試發(fā)送
                l2fwd_simple_forward(m, portid);
            }
        }
    }
}

值得注意的小操作

程序中強(qiáng)制退出,是自己寫的一個(gè)信號量,包括兩種操作,即在ctrl+c或者kill了這個(gè)進(jìn)程的時(shí)候,會觸發(fā):

static void
signal_handler(int signum)
{
    if (signum == SIGINT || signum == SIGTERM) {
        printf("\n\nSignal %d received, preparing to exit...\n",
                signum);
        force_quit = true;
    }
}

signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);

signal_handler函數(shù)第一個(gè)參數(shù)signum:指明了所要處理的信號類型,它可以取除了SIGKILL和SIGSTOP外的任何一種信號。  
第二個(gè)參數(shù)handler:描述了與信號關(guān)聯(lián)的動作,它可以取以下三種值:
1. SIG_IGN
這個(gè)符號表示忽略該信號。
2. SIG_DFL  
這個(gè)符號表示恢復(fù)對信號的系統(tǒng)默認(rèn)處理。不寫此處理函數(shù)默認(rèn)也是執(zhí)行系統(tǒng)默認(rèn)操作。
3. sighandler_t類型的函數(shù)指針
此函數(shù)必須在signal()被調(diào)用前申明,handler中為這個(gè)函數(shù)的名字。當(dāng)接收到一個(gè)類型為sig的信號時(shí),就執(zhí)行handler 所指定的函數(shù)。(int)signum是傳遞給它的唯一參數(shù)。執(zhí)行了signal()調(diào)用后,進(jìn)程只要接收到類型為sig的信號,不管其正在執(zhí)行程序的哪一部分,就立即執(zhí)行func()函數(shù)。當(dāng)func()函數(shù)執(zhí)行結(jié)束后,控制權(quán)返回進(jìn)程被中斷的那一點(diǎn)繼續(xù)執(zhí)行。

在該函數(shù)中就是第三種,所以當(dāng)我們退出是ctrl+c不是直接將進(jìn)程殺死,而是會將force_quit置為true,讓程序自然退出,這樣程序就來得及完成最后退出之前的操作。

圖引用:
[1].http://www.cnblogs.com/cenalulu/p/4358802.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 1. 簡介 本文檔包含DPDK軟件安裝和配置的相關(guān)說明。旨在幫助用戶快速啟動和運(yùn)行軟件。文檔主要描述了在Linux...
    半天妖閱讀 17,996評論 0 22
  • 3. 環(huán)境抽象層 環(huán)境抽象層(Environment Abstraction Layer,下文簡稱EAL)是對操作...
    希爾哥哥s閱讀 4,768評論 0 1
  • linux資料總章2.1 1.0寫的不好抱歉 但是2.0已經(jīng)改了很多 但是錯(cuò)誤還是無法避免 以后資料會慢慢更新 大...
    數(shù)據(jù)革命閱讀 12,239評論 2 33
  • iOS面試小貼士 ———————————————回答好下面的足夠了------------------------...
    不言不愛閱讀 2,014評論 0 7
  • 已經(jīng)冬天了 冬天是危險(xiǎn)的。她已經(jīng)不想再出門,因?yàn)橥饷娴娘L(fēng),是危險(xiǎn)的,外面的空氣也是危險(xiǎn)的。 太冷了。 她一整個(gè)冬天...
    永之_閱讀 252評論 0 0