從大四開始嚷嚷著要學(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)行界面:
由于只開了一個(gè)端口轉(zhuǎn)發(fā),所以在l2fwd的默認(rèn)規(guī)則下,就是單個(gè)port自己收自己發(fā),發(fā)送的報(bào)文數(shù)量和接收的一樣多。
程序的主要流程如下:
每個(gè)邏輯核在任務(wù)分發(fā)后會執(zhí)行如下的循環(huán),直到退出:
其中打印時(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)存位置對性能的影響。
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,讓程序自然退出,這樣程序就來得及完成最后退出之前的操作。