Linux對線程的親和性是有支援的,在Linux核心中,所有線程都有一個相關的資料結構,稱為task_count,這個結構中和親和性有關的是cpus_allowed位掩碼,這個位掩碼由n位組成,n代碼邏輯核心的個數。
Linux核心API提供了一些方法,讓使用者可以修改位掩碼或者檢視目前的位掩碼。
sched_setaffinity(); //修改位掩碼,主要事用來綁定程序
sched_getaffinity(); //檢視目前的位掩碼,檢視程序的親和性
pthread_setaffinity_np();//主要用來綁定線程
pthread_getaffinity_np();//檢視線程的親和性
使用親和性的原因是将線程和CPU綁定可以提高CPU cache的命中率,進而減少記憶體通路損耗,提高程式的速度。多核體系的CPU,實體核上的線程來回切換,會導緻L1/L2 cache命中率的下降,如果将線程和核心綁定的話,線程會一直在指定的核心上跑,不會被作業系統排程到别的核上,線程之間互相不幹擾完成工作,節省了作業系統來回排程的時間。同時NUMA架構下,如果作業系統排程線程的時候,跨越了NUMA節點,将會導緻大量的L3 cache的丢失。這樣NUMA使用CPU綁定的時候,每個核心可以更專注的處理一件事情,資源被充分的利用了。
DPDK通過把線程綁定到邏輯核的方法來避免跨核任務中的切換開銷,但是對于綁定運作的目前邏輯核,仍可能發生線程切換,若進一步減少其他任務對于某個特定任務的影響,在親和性的基礎上更進一步,可以采用把邏輯核從核心排程系統剝離的方法。
DPDK的多線程
DPDK的線程基于pthread接口建立(DPDK線程其實就是普通的pthread),屬于搶占式線程模型,受核心排程支配,DPDK通過在多核裝置上建立多個線程,每個線程綁定到單獨的核上,減少線程排程的開銷,以提高性能。
DPDK的線程可以屬于控制線程,也可以作為資料線程。在DPDK的一些示例中,控制線程一般當頂到MASTER核(一般用來跑主線程)上,接收使用者配置,并傳遞配置參數給資料線程等;資料線程分布在不同的SLAVE核上處理資料包。
EAL中的lcore
DPDK的lcore指的是EAL線程,本質是基于pthread封裝實作的,lcore建立函數為:
rte_eal_remote_launch();
在每個EAL pthread中,有一個TLS(Thread Local Storage)稱為_lcore_id,當使用DPDk的EAL ‘-c’參數指定核心掩碼的時候,EAL pehread生成相應個數lcore并預設是1:1親和到對應的CPU邏輯核,_lcore_id和CPU ID是一緻的。
下面簡要介紹DPDK中lcore的初始化及執行任務的注冊
初始化
所有DPDK程式中,main()函數執行的第一個DPDK API一定是int rte_eal_init(int argc, char **argv);,這個函數是整個DPDK的初始化函數,在這個函數中會執行lcore的初始化。
初始化的接口為rte_eal_cpu_init(),這個函數讀取/sys/devices/system/cpu/cpuX下的相關資訊,确定目前系統有哪些CPU核,以及每個核心屬于哪個CPU Socket。
接下來是eal_parse_args()函數,解析-c參數,确認可用的CPU核心,并設定第一個核心為MASTER核。
然後主核為每一個SLAVE核建立線程,并調用eal_thread_set_affinity()綁定CPU。線程的執行體是eal_thread_loop()。eal_thread_loop()主體是一個while死循環,調用不同子產品注冊的回調函數。
注冊
不同的注冊子產品調用rte_eal_mp_remote_launch(),将自己的回調函數注冊到lcore_config[].f中,例子(來源于example/distributor)
rte_eal_remote_launch((lcore_function_t *)lcore_distributor, p, lcore_id);
lcore的親和性
DPDK除了-c參數,還有一個–lcore(-l)參數是指定CPU核的親和性的,這個參數講一個lcore ID組綁定到一個CPU ID組,這樣邏輯核和線程其實不是完全1:1對應的,這是為了滿足網絡流量潮汐現象時刻,可以更加靈活的處理資料包。
lcore可以親和到一個CPU或一組CPU集合,使得在運作時調整具體某個CPU承載lcore成為可能。
多個lcore也可以親和到同個核,這裡要注意的是,同一個核上多個可搶占式的任務排程涉及非搶占式的庫時,會有一定的限制,例如非搶占式無鎖rte_ring:
- 單生産者/單消費者:不受影響,正常使用
- 多生産者/多消費者,且排程政策都是SCHED_OTHER(分時排程政策),可以使用,但是性能稍有影響
- 多生産者/多消費者,且排程政策都是SCHED_FIFO (實時排程政策,先到先服務)或者SCHED_RR(實時排程政策,時間片輪轉 ),會死鎖。
一個lcore初始化和執行任務分發的流程如下:
使用者态初始化具體的流程如下:
- 主核啟動main()
- rte_eal_init()進行初始化,主要包括記憶體、日志、PCI等方面的初始化工作,同時啟動邏輯核線程
- pthread()在邏輯核上進行初始化,并處于等待狀态
- 所有邏輯核都完成初始化後,主核進行後續初始化步驟,如初始化lib庫和驅動
- 主核遠端啟動各個邏輯核上的應用執行個體初始化操作
- 主核啟動各個核(主核和邏輯核)上的應用
CPU的綁定
先将主線程綁定在master核上,然後通過主線程建立線程池,通過線程池進行配置設定副線程到slave核上。在這之前首先要擷取CPU的核數量等資訊,将這些資訊存放到全局的結構體中。
int
rte_eal_init(int argc, char **argv)
{
。。。。。。
thread_id = pthread_self();
if (rte_eal_log_early_init() < 0)
rte_panic("Cannot init early logs\n");
if (rte_eal_cpu_init() < 0)//指派全局結構struct lcore_config,擷取全局配置結構struct rte_config,初始指向全局變量early_mem_config,探索CPU并讀取其CPU ID
rte_panic("Cannot detect lcores\n");
。。。。。。。。
eal_thread_init_master(rte_config.master_lcore);//綁定CPU,綁定到master核上
ret = eal_thread_dump_affinity(cpuset, RTE_CPU_AFFINITY_STR_LEN);//檢視CPU綁定情況
。。。。。。。。。。
ret = pthread_create(&lcore_config[i].thread_id, NULL,
eal_thread_loop, NULL);//啟動線程池中的副線程,為每個lcore建立一個線程,綁定到slave核上
if (ret != 0)
rte_panic("Cannot create thread\n");
}
以上可以清楚的看見dpdk在初始化的時候綁定cpu的大緻過程。
首先看一下 rte_eal_cpu_init()函數:
/*解析/sys/device /system/cpu以獲得機器上的實體和邏輯處理器的數量。該函數将填充cpu_info結構。*/
int
rte_eal_cpu_init(void)
{
/* pointer to global configuration */
struct rte_config *config = rte_eal_get_configuration();//擷取全局結構體指針
unsigned lcore_id;
unsigned count = 0;
/*
* 設定邏輯核心的最大集合,檢測正在運作的邏輯核心的子集并預設啟用它們
*/
for (lcore_id = 0; lcore_id < RTE_MAX_LCORE; lcore_id++) {
/* init cpuset for per lcore config */
CPU_ZERO(&lcore_config[lcore_id].cpuset);
/* 在1:1映射中,記錄相關的cpu檢測狀态 */
lcore_config[lcore_id].detected = cpu_detected(lcore_id);//檢測CPU狀态
if (lcore_config[lcore_id].detected == 0) {
config->lcore_role[lcore_id] = ROLE_OFF;
continue;
}
//映射到cpu id
CPU_SET(lcore_id, &lcore_config[lcore_id].cpuset);
/* 檢測cpu核啟動 */
config->lcore_role[lcore_id] = ROLE_RTE;
lcore_config[lcore_id].core_id = cpu_core_id(lcore_id);
lcore_config[lcore_id].socket_id = eal_cpu_socket_id(lcore_id);
if (lcore_config[lcore_id].socket_id >= RTE_MAX_NUMA_NODES)
#ifdef RTE_EAL_ALLOW_INV_SOCKET_ID
lcore_config[lcore_id].socket_id = 0;
#else
rte_panic("Socket ID (%u) is greater than "
"RTE_MAX_NUMA_NODES (%d)\n",
lcore_config[lcore_id].socket_id, RTE_MAX_NUMA_NODES);
#endif
RTE_LOG(DEBUG, EAL, "Detected lcore %u as core %u on socket %u\n",
lcore_id,
lcore_config[lcore_id].core_id,
lcore_config[lcore_id].socket_id);
count ++;
}
/* 設定EAL配置的啟用邏輯核心的計數 */
config->lcore_count = count;
RTE_LOG(DEBUG, EAL, "Support maximum %u logical core(s) by configuration.\n",
RTE_MAX_LCORE);
RTE_LOG(DEBUG, EAL, "Detected %u lcore(s)\n", config->lcore_count);
return 0;
}
在這個函數中,并沒有綁定具體線程,隻是将cpu中的核給預設啟動起來,并且放入到全局結構體中,我們後面會用到這個結構體來進行綁定線程。
eal_thread_init_master()函數:
void eal_thread_init_master(unsigned lcore_id)
{
/* set the lcore ID in per-lcore memory area */
RTE_PER_LCORE(_lcore_id) = lcore_id;
/* set CPU affinity */
if (eal_thread_set_affinity() < 0)
rte_panic("cannot set affinity\n");
}
。。。
int
rte_thread_set_affinity(rte_cpuset_t *cpusetp)
{
int s;
unsigned lcore_id;
pthread_t tid;
tid = pthread_self();
s = pthread_setaffinity_np(tid, sizeof(rte_cpuset_t), cpusetp);
if (s != 0) {
RTE_LOG(ERR, EAL, "pthread_setaffinity_np failed\n");
return -1;
}
/* store socket_id in TLS for quick access */
RTE_PER_LCORE(_socket_id) =
eal_cpuset_socket_id(cpusetp);
/* store cpuset in TLS for quick access */
memmove(&RTE_PER_LCORE(_cpuset), cpusetp,
sizeof(rte_cpuset_t));
lcore_id = rte_lcore_id();
if (lcore_id != (unsigned)LCORE_ID_ANY) {
/* EAL thread will update lcore_config
線程更新,這塊代碼為何自己覆寫自己,不應該啊*/
lcore_config[lcore_id].socket_id = RTE_PER_LCORE(_socket_id);
memmove(&lcore_config[lcore_id].cpuset, cpusetp,
sizeof(rte_cpuset_t));
}
return 0;
}
這個地方我們就清楚的看見将主線程綁定到master核上了,可以看見傳的參數就是master核的id,然後綁定master核更新cpu核結構體 資訊。
eal_thread_loop()函數:
從上面代碼中可以看見是主線程建立了一個副線程來進行的。接下來我們就看看這個線程中eal_thread_loop到底幹了些什麼。
__attribute__((noreturn)) void *
eal_thread_loop(__attribute__((unused)) void *arg)
{
char c;
int n, ret;
unsigned lcore_id;
pthread_t thread_id;
int m2s, s2m;
char cpuset[RTE_CPU_AFFINITY_STR_LEN];
thread_id = pthread_self();
/* retrieve our lcore_id from the configuration structure */
//循環查找目前線程的id所在的核
RTE_LCORE_FOREACH_SLAVE(lcore_id) {
if (thread_id == lcore_config[lcore_id].thread_id)
break;
}
if (lcore_id == RTE_MAX_LCORE)
rte_panic("cannot retrieve lcore id\n");
m2s = lcore_config[lcore_id].pipe_master2slave[0];
s2m = lcore_config[lcore_id].pipe_slave2master[1];
/* set the lcore ID in per-lcore memory area */
RTE_PER_LCORE(_lcore_id) = lcore_id;
/* set CPU affinity */
//設定目前線程CPU的親和性
if (eal_thread_set_affinity() < 0)
rte_panic("cannot set affinity\n");
ret = eal_thread_dump_affinity(cpuset, RTE_CPU_AFFINITY_STR_LEN);
RTE_LOG(DEBUG, EAL, "lcore %u is ready (tid=%x;cpuset=[%s%s])\n",
lcore_id, (int)thread_id, cpuset, ret == 0 ? "" : "...");
/* read on our pipe to get commands */
//下面說是等待讀取管道上的指令,這個地方其實在等待一個有趣的東西(執行函數)
while (1) {
void *fct_arg;
/* wait command */
do {
n = read(m2s, &c, 1);
} while (n < 0 && errno == EINTR);
if (n <= 0)
rte_panic("cannot read on configuration pipe\n");
lcore_config[lcore_id].state = RUNNING;
/* send ack */
n = 0;
while (n == 0 || (n < 0 && errno == EINTR))
n = write(s2m, &c, 1);
if (n < 0)
rte_panic("cannot write on configuration pipe\n");
if (lcore_config[lcore_id].f == NULL)
rte_panic("NULL function pointer\n");
/* call the function and store the return value */
fct_arg = lcore_config[lcore_id].arg;
ret = lcore_config[lcore_id].f(fct_arg);
lcore_config[lcore_id].ret = ret;
rte_wmb();
lcore_config[lcore_id].state = FINISHED;
}
/* never reached */
/* pthread_exit(NULL); */
/* return NULL; */
}
這個函數非常的有趣,再綁定的時候還是調用的上面綁定函數,綁定好了之後,讓這個線程處于等待狀态,等待什麼呢?其實這個地方再等待配置設定執行函數過來,也就是說,這個核需要給他配置設定一個任務他才去執行。
總結
dapk設定cpu親和性大大的提高了效率,減少了線程之間的切換。這是一個值得學習的地方。下面就說一下線程遷移問題。
遷移線程
在普通程序的load_balance過程中,如果負載不均衡,目前CPU會試圖從最繁忙的run_queue中pull幾個程序到自己的run_queue來。
但是如果程序遷移失敗呢?當失敗達到一定次數的時候,核心會試圖讓目标CPU主動push幾個程序過來,這個過程叫做active_load_balance。這裡的“一定次數”也是跟排程域的層次有關的,越低層次,則“一定次數”的值越小,越容易觸發active_load_balance。
這裡需要先解釋一下,為什麼load_balance的過程中遷移程序會失敗呢?最繁忙run_queue中的程序,如果符合以下限制,則不能遷移:
1、程序的CPU親和力限制了它不能在目前CPU上運作;
2、程序正在目标CPU上運作(正在運作的程序顯然是不能直接遷移的);
(此外,如果程序在目标CPU上前一次運作的時間距離目前時間很小,那麼該程序被cache的資料可能還有很多未被淘汰,則稱該程序的cache還是熱的。對于cache熱的程序,也盡量不要遷移它們。但是在滿足觸發active_load_balance的條件之前,還是會先試圖遷移它們。)
對于CPU親和力有限制的程序(限制1),即使active_load_balance被觸發,目标CPU也不能把它push過來。是以,實際上,觸發active_load_balance的目的是要嘗試把當時正在目标CPU上運作的那個程序弄過來(針對限制2)。
在每個CPU上都會運作一個遷移線程,active_load_balance要做的事情就是喚醒目标CPU上的遷移線程,讓它執行active_load_balance的回調函數。在這個回調函數中嘗試把原先因為正在運作而未能遷移的那個程序push過來。為什麼load_balance的時候不能遷移,active_load_balance的回調函數中就可以了呢?因為這個回調函數是運作在目标CPU的遷移線程上的。一個CPU在同一時刻隻能運作一個程序,既然這個遷移線程正在運作,那麼期望被遷移的那個程序肯定不是正在被執行的,限制2被打破。
當然,在active_load_balance被觸發,到回調函數在目标CPU上被執行之間,目标CPU上的TASK_RUNNING狀态的程序可能發生一些變化,是以回調函數發起遷移的程序未必就隻有之前因為限制2而未能被遷移的那一個,可能更多,也可能一個沒有。
部分參考:https://blog.csdn.net/u012630961/article/details/80918682