对io数据面线程进行绑核是spdk实现对io加速的重要一个手段之一。现在的cpu基本都是多核的,线程执行时如果没有特别指定,默认都为RR模式(即时间片轮转),内核会根据当前cpu上负载情况将线程调度到cpu负载低的核上。
有两种方式可设置cpu的绑核情况,一种是在spdk初始化时通过配置文件传入,另外一种是通过spdk的入参进行配置。
配置文件传入方式,入参-c /file/,
spdk入参传入有两种,一种是-cpumask 0xxxxx(可参考spdk环境搭建), 一种-lcore (idx1, idx2, idx3...),该方式如下所示:
spdk进程的入参--cpumask可以配置cpu的绑核以及轮训核的数量,默认值为0x1。如果配置为0x7,那spdk的轮训核是就绑定在cpu0, cpu1,cpu2上,因为0x7的二进制为0111,其中的每一位表示一个cpu核,true表示该核为spdk的reactor核。(比如想要创建2个reactor,绑定cpu核6和cpu核7上,那对应的二进制为0x1100 0000, 16进制应该为0x50)
看下代码中是怎么实现的,首先main函数初始化时,将--cpumask传给spdk_app_start,spdk_app_start中将该参数解析。如果未设置,则默认值为SPDK_APP_DPDK_DEFAULT_CORE_MASK, 该宏值为0x1,即为cpu0核。
#define SPDK_APP_DPDK_DEFAULT_CORE_MASK "0x1" //默认绑核值
int spdk_app_start(struct spdk_app_opts *opts_user, spdk_msg_fn start_fn,
void *arg1)
{...
app_copy_opts(opts, opts_user, opts_user->opts_size);
if (!(opts->lcore_map || opts->reactor_mask)) {
opts->reactor_mask = SPDK_APP_DPDK_DEFAULT_CORE_MASK; //设置默认值
}
app_setup_env(opts); //初始化环境参数
...}
static int app_setup_env(struct spdk_app_opts *opts)
{...
env_opts.core_mask = opts->reactor_mask;
rc = spdk_env_init(&env_opts);
return rc;
...}
spdk_env_init接口调用到rte_eal_init,rte_eal_init为dpdk中抽象层的初始化接口。rte_eal_init中与cpu初始化的接口有以下几个:rte_eal_cpu_init, eal_parse_args,eal_plugins_init,rte_config_init。 最终将cpu信息存放在struct rte_config *config中,config为dpdk中缓存的全局配置信息。
-->spdk_app_start
-->app_copy_opts 将传入的参数缓存到 spdk_app_opts 结构体中
-->opts->reactor_mask = SPDK_APP_DPDK_DEFAULT_CORE_MASK;
-->app_setup_env
-->spdk_env_init
-->rte_eal_init
-->rte_eal_cpu_init
-->eal_parse_args-->eal_parse_common_option --> eal_parse_lcores
--> eal_create_runtime_dir --> eal_set_runtime_dir
-->eal_plugins_init
-->rte_config_init.....
-->eal_worker_thread_create RTE_LCORE_FOREACH_WORKER(i) 遍历所有reactor,创建reactor线程
-->rte_thread_set_affinity_by_id 将对应的线程id绑定到对应的cpu上
int rte_eal_cpu_init(void)
{
/* pointer to global configuration */
struct rte_config *config = rte_eal_get_configuration();
unsigned lcore_id;
unsigned count = 0;
unsigned int socket_id, prev_socket_id;
int lcore_to_socket_id[RTE_MAX_LCORE]; //RTE_MAX_LCORE宏值是在编译时的meson.build文件设置
//标记可用的物理cpu, 0 ~ RTE_MAX_LCORE
for (lcore_id = 0; lcore_id < RTE_MAX_LCORE; lcore_id++) {
socket_id = eal_cpu_socket_id(lcore_id);
lcore_to_socket_id[lcore_id] = socket_id;
if (eal_cpu_detected(lcore_id) == 0) {
config->lcore_role[lcore_id] = ROLE_OFF;
lcore_config[lcore_id].core_index = -1;
continue;
}
...
CPU_SET(lcore_id, &lcore_config[lcore_id].cpuset);
lcore_config[lcore_id].core_id = eal_cpu_core_id(lcore_id);
lcore_config[lcore_id].socket_id = socket_id;
count++;
}
//超过RTE_MAX_LCORE的标记为不可用 -1
for (; lcore_id < CPU_SETSIZE; lcore_id++) {
if (eal_cpu_detected(lcore_id) == 0)
continue;
eal_cpu_socket_id(lcore_id));
}
config->lcore_count = count;
qsort(lcore_to_socket_id, RTE_DIM(lcore_to_socket_id), sizeof(lcore_to_socket_id[0]), socket_id_cmp);
//标记要使用的numa数量和socket
prev_socket_id = -1;
config->numa_node_count = 0;
for (lcore_id = 0; lcore_id < RTE_MAX_LCORE; lcore_id++) {
socket_id = lcore_to_socket_id[lcore_id];
if (socket_id != prev_socket_id)
config->numa_nodes[config->numa_node_count++] = socket_id;
prev_socket_id = socket_id;
}
return 0;
}
#define SYS_CPU_DIR "/sys/devices/system/cpu/cpu%u"
#define NUMA_NODE_PATH "/sys/devices/system/node"
int eal_cpu_detected(unsigned lcore_id)
{
char path[PATH_MAX];
int len = snprintf(path, sizeof(path), SYS_CPU_DIR"/"CORE_ID_FILE, lcore_id);
if (len <= 0 || (unsigned)len >= sizeof(path))
return 0;
if (access(path, F_OK) != 0)
return 0;
return 1;
}
rte_eal_cpu_init接口会去寻找当前系统的三个信息:socket,numa,cpu_id,将该信息缓存在config内存配置文件和lcore_config中。struct lcore_config lcore_config[RTE_MAX_LCORE]在初始化结束之后会记录这cpu的详细信息。
以cpu_id为例进行分析,首先对RTE_MAX_LCORE内的cpu核进行遍历,每个cpu核都调用eal_cpu_detected接口判断,如果是Linux系统,eal_cpu_detected接口根据系统文件文件/sys/devices/system/cpu/cpu{$idx}是否存在决定,如果文件存在则标记该核可用,并且可用cpu数量count加一。类似的socket和numa也是这个处理逻辑,并且这种类似的逻辑在dpdk处理大页内存时也会用到。
eal_parse_args接口中与cpu绑核的有两部分,一部分是绑核相关的入参信息解析,一部分是运行时目录的设置。
cpu绑核作为普通入参,通过eal_parse_lcores接口进行解析-lcore (idx1, idx2, idx3...)类型的参数;
最终解析完的参数会保存到dpdk的struct rte_config rte_config全局变量中。
非主线程reactor是在rte_eal_init接口中实现的,其实绑核也是调用了pthread_setaffinity_np接口绑定对应的cpu。虽然该处将线程抛出,但实际并未真正运行,线程状态为WAIT。
这里也可以看出来,spdk设置绑核其实和c语言程序中直接调用pthread_setaffinity_np绑核是一样的效果。
int rte_eal_init(int argc, char **argv)
{...
if (rte_eal_cpu_init() < 0) {
}
...
RTE_LCORE_FOREACH_WORKER(i) {
lcore_config[i].state = WAIT; //线程状态设置为 WAIT等待
ret = eal_worker_thread_create(i); //创建reactor核
//修改线程名称, 由于reactor会再修改一次线程名称为reactor_%i, 所有这里设置无关紧要
snprintf(thread_name, sizeof(thread_name), "dpdk-worker%d", i);
rte_thread_set_name(lcore_config[i].thread_id, thread_name);
//绑定到对应cpu上
ret = rte_thread_set_affinity_by_id(lcore_config[i].thread_id,&lcore_config[i].cpuset);
...
}
后续在spdk_reactors_start接口中会唤醒所有的reactor。主线程的reactor_run直接在spdk_reactors_start中直接调用,非主线程reactor,通过spdk_env_thread_launch_pinned接口唤醒。 其实主线程reactor就是vhost进程,vhost进程在结束阶段修改线程名称为reactor,并进入轮训状态。
void spdk_reactors_start(void)
{
struct spdk_reactor *reactor;
uint32_t i, current_core;
int rc;
g_rusage_period = (CONTEXT_SWITCH_MONITOR_PERIOD * spdk_get_ticks_hz()) / SPDK_SEC_TO_USEC;
g_reactor_state = SPDK_REACTOR_STATE_RUNNING;
g_stopping_reactors = false;
current_core = spdk_env_get_current_core();
SPDK_ENV_FOREACH_CORE(i) {
if (i != current_core) {
reactor = spdk_reactor_get(i);
//非主线程reactor唤醒
rc = spdk_env_thread_launch_pinned(reactor->lcore, reactor_run, reactor);
}
spdk_cpuset_set_cpu(&g_reactor_core_mask, i, true);
}
reactor = spdk_reactor_get(current_core);
assert(reactor != NULL);
reactor_run(reactor); //主线程reactor运行
spdk_env_thread_wait_all();
g_reactor_state = SPDK_REACTOR_STATE_SHUTDOWN;
}
spdk绑核方式有三种: 配置文件, -cpumask 0xxxxx, 一种-lcore (idx1, idx2, idx3...)。
spdk绑核会先设置/解析参数,并结合物理机上的cpu状态设置,最终将信息设置到dpdk的全局公共config中,绑核和抛reactor就是根据里面的信息进行处理。
reactor线程的产生方式:主线程reactor是vhost进程转变的, 其余reactor是在dpdk的rte_eal_init接口中完成抛线程,绑核操作。
reactor创建之后并未直接开始工作,是在spdk_reactors_start中唤醒。