spdk的绑核实现流程

简介

        对io数据面线程进行绑核是spdk实现对io加速的重要一个手段之一。现在的cpu基本都是多核的,线程执行时如果没有特别指定,默认都为RR模式(即时间片轮转),内核会根据当前cpu上负载情况将线程调度到cpu负载低的核上。

spdk中CPU的绑核的方法

        有两种方式可设置cpu的绑核情况,一种是在spdk初始化时通过配置文件传入,另外一种是通过spdk的入参进行配置。

        配置文件传入方式,入参-c /file/,

        spdk入参传入有两种,一种是-cpumask 0xxxxx(可参考spdk环境搭建), 一种-lcore (idx1, idx2, idx3...),该方式如下所示:

spdk关于绑核的初始化

        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上

rte_eal_cpu_init分析

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 分析

eal_parse_args接口中与cpu绑核的有两部分,一部分是绑核相关的入参信息解析,一部分是运行时目录的设置。

cpu绑核作为普通入参,通过eal_parse_lcores接口进行解析-lcore (idx1, idx2, idx3...)类型的参数;

最终解析完的参数会保存到dpdk的struct rte_config rte_config全局变量中。

抛reactor线程并绑核

非主线程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中唤醒。

        

你可能感兴趣的:(spdk,服务器,运维,linux,云计算,后端)