dpdk线程亲和性

Linux对线程的亲和性是有支持的,在Linux内核中,所有线程都有一个相关的数据结构,称为task_count,这个结构中和亲和性有关的是cpus_allowed位掩码,这个位掩码由n位组成,n代码逻辑核心的个数。

Linux内核API提供了一些方法,让用户可以修改位掩码或者查看当前的位掩码。

sched_set_affinity();   //修改位掩码
sched_get_affinity();   //查看当前的位掩码

使用亲和性的原因是将线程和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:

  1. 单生产者/单消费者:不受影响,正常使用
  2. 多生产者/多消费者,且调度策略都是SCHED_OTHER(分时调度策略),可以使用,但是性能稍有影响
  3. 多生产者/多消费者,且调度策略都是SCHED_FIFO (实时调度策略,先到先服务)或者SCHED_RR(实时调度策略,时间片轮转 ),会死锁。

一个lcore初始化和执行任务分发的流程如下:

这里写图片描述

用户态初始化具体的流程如下:

  1. 主核启动main()
  2. rte_eal_init()进行初始化,主要包括内存、日志、PCI等方面的初始化工作,同时启动逻辑核线程
  3. pthread()在逻辑核上进行初始化,并处于等待状态
  4. 所有逻辑核都完成初始化后,主核进行后续初始化步骤,如初始化lib库和驱动
  5. 主核远程启动各个逻辑核上的应用实例初始化操作
  6. 主核启动各个核(主核和逻辑核)上的应用

简要介绍一下DPDK的EAL的初始化。

EAL初始化的代码一般在MASTER线程中执行,也就是main()函数中,作为启动整个DPDK底层配置的函数,这个函数要求被最先执行并且只可以被执行一次。

函数代码如下(位于./lib/librte_eal/bsdapp/eal/eal.c之中):

int rte_eal_init(int argc, char **argv)
{
    int i, fctret, ret;
    pthread_t thread_id;    //DPDK底层以pthread实现
    static rte_atomic32_t run_once = RTE_ATOMIC32_INIT(0);  //操作静态局部变量run_once确保函数只执行一次
    char cpuset[RTE_CPU_AFFINITY_STR_LEN];
    char thread_name[RTE_MAX_THREAD_NAME_LEN];

    if (!rte_cpu_is_supported()) {  //检测CPU是否支持
        rte_eal_init_alert("unsupported cpu type.");
        rte_errno = ENOTSUP;
        return -1;
    }

    if (!rte_atomic32_test_and_set(&run_once)) {    //查看原子变量
        rte_eal_init_alert("already called initialization.");
        rte_errno = EALREADY;
        return -1;
    }

    thread_id = pthread_self(); //获得线程自身的ID

    eal_reset_internal_config(&internal_config);

    //初始化结构体struct internal_config,解析命令行参数,只处理“--log-level”,保存在internal_config.log_level
    eal_log_level_parse(argc, argv);    

    if (rte_eal_cpu_init() < 0) {   //赋值全局结构struct lcore_config,获取全局配置结构struct rte_config,初始指向全局变量early_mem_config,探索CPU并读取其CPU ID
        rte_eal_init_alert("Cannot detect lcores.");
        rte_errno = ENOTSUP;
        return -1;
    }

    //解析处理EAL的命令行参数,赋值struct internal_config结构的相关字段
    fctret = eal_parse_args(argc, argv);
    if (fctret < 0) {
        rte_eal_init_alert("Invalid 'command line' arguments.");
        rte_errno = EINVAL;
        rte_atomic32_clear(&run_once);
        return -1;
    }

    if (internal_config.no_hugetlbfs == 0 &&
            internal_config.process_type != RTE_PROC_SECONDARY &&
            eal_hugepage_info_init() < 0) { 
        /*赋值struct hugepage_info数组(internal_config.hugepage_info),遍历子目录,
        获取当前大小hugepage对应的structhugepage_info结构体,读取文件“/proc/mounts”,
        查找所有“hugetlbfs”,找到当前大小hugepage对应的挂载路径,打开hugepage目录文件,
        上文件锁,删除hugepage files,如“rtemap_xxx”,对struct internal_config结构中
        的各个大小的struct hugepage_info进行排序,从大到小,检查是否至少有一个有效的
        struct hugepage_info
        */
        rte_eal_init_alert("Cannot get hugepage information.");
        rte_errno = EACCES;
        rte_atomic32_clear(&run_once);
        return -1;
    }

        if (internal_config.memory == 0 && internal_config.force_sockets == 0) {
            if (internal_config.no_hugetlbfs)
                internal_config.memory = MEMSIZE_IF_NO_HUGE_PAGE;
            else
                internal_config.memory = eal_get_hugepage_mem_size();
            /*遍历struct hugepage_info,获取所有hugepage占用内存的总数,结果存放
            在internal_config.memory
            */
        }

        if (internal_config.vmware_tsc_map == 1) {
    #ifdef RTE_LIBRTE_EAL_VMWARE_TSC_MAP_SUPPORT
            rte_cycles_vmware_tsc_map = 1;
            RTE_LOG (DEBUG, EAL, "Using VMWARE TSC MAP, "
                    "you must have monitor_control.pseudo_perfctr = TRUE\n");
    #else
            RTE_LOG (WARNING, EAL, "Ignoring --vmware-tsc-map because "
                    "RTE_LIBRTE_EAL_VMWARE_TSC_MAP_SUPPORT is not set\n");
    #endif
        }

    rte_srand(rte_rdtsc()); //将当前时间作为种子,产生伪随机数序列

    rte_config_init();  
    /*主应用的情况:获取runtime配置文件路径,打开文件,
    上锁,mmap映射文件到内存,将early configuration structure(全局变量
    early_mem_config)拷贝到此内存中,rte_config.mem_config指向这块内存
    映射地址保存在rte_config.mem_config->mem_cfg_addr中,用于从应用将来
    映射到相同的地址*/
    /*从应用的情况:打开文件,mmap映射文件到内存,rte_config.mem_config指
    向映射的内存,如果struct rte_mem_config结构的magic成员没有被写成RTE_MAGIC,
    就继续等待,(主应用ready后会将struct rte_mem_config结构的magic成员写
    成RTE_MAGIC),前面mmap映射文件中获取主应用mmap的映射地址,munmap解除先
    前的映射,指定主应用映射地址重新执行mmap映射,如果最终映射地址和指定映射
    地址不一致,则出错退出,将rte_config.mem_config指向重新映射的内存*/

    //解锁hugepage目录(由前面的eal_hugepage_info_init函数加锁)
    if (rte_eal_memory_init() < 0) {
        rte_eal_init_alert("Cannot init memory\n");
        rte_errno = ENOMEM;
        return -1;
    }

    if (rte_eal_memzone_init() < 0) {
        rte_eal_init_alert("Cannot init memzone\n");
        rte_errno = ENODEV;
        return -1;
    }

    //遍历以全局变量rte_tailq_elem_head为头部的struct rte_tailq_elem结构tailq链表
    if (rte_eal_tailqs_init() < 0) {
        rte_eal_init_alert("Cannot init tail queues for objects\n");
        rte_errno = EFAULT;
        return -1;
    }

    //赋值全局的struct rte_intr_handle结构,调用timerfd_create函数创建定时器timer对象
    if (rte_eal_alarm_init() < 0) {
        rte_eal_init_alert("Cannot init interrupt-handling thread\n");
        /* rte_eal_alarm_init sets rte_errno on failure. */
        return -1;
    }
    /*
    处理中断的初始化,创建管道,创建线程等待中断,线程执行函数为eal_intr_thread_main
    线程运行循环,创建epoll文件描述符,并将接受中断的管道读端放到epoll中。
    */
    if (rte_eal_intr_init() < 0) {
        rte_eal_init_alert("Cannot init interrupt-handling thread\n");
        return -1;
    }

    //设定全局变量eal_timer_source为EAL_TIMER_TSC(TSC/HPET)
    if (rte_eal_timer_init() < 0) {
        rte_eal_init_alert("Cannot init HPET or TSC timers\n");
        rte_errno = ENOTSUP;
        return -1;
    }

    eal_check_mem_on_local_socket();    //获取masterlcore对应的numa socket

    /*
    EAL的“-d”选项可以指定需要载入的动态链接库,和要载入的库文件相关
    */
    if (eal_plugins_init() < 0)
        rte_eal_init_alert("Cannot init plugins\n");

    /*
    设置主线程的lcore_id
    */
    eal_thread_init_master(rte_config.master_lcore);
    //dump当前线程的CPU affinity
    ret = eal_thread_dump_affinity(cpuset, RTE_CPU_AFFINITY_STR_LEN);

    RTE_LOG(DEBUG, EAL, "Master lcore %u is ready (tid=%p;cpuset=[%s%s])\n",
        rte_config.master_lcore, thread_id, cpuset,
        ret == 0 ? "" : "...");

    if (eal_option_device_parse()) {
        rte_errno = ENODEV;
        return -1;
    }

    if (rte_bus_scan()) {
        rte_eal_init_alert("Cannot scan the buses for devices\n");
        rte_errno = ENODEV;
        return -1;
    }

    RTE_LCORE_FOREACH_SLAVE(i) {

        /*
         * create communication pipes between master thread
         * and children
         */
        if (pipe(lcore_config[i].pipe_master2slave) < 0)
            rte_panic("Cannot create pipe\n");
        if (pipe(lcore_config[i].pipe_slave2master) < 0)
            rte_panic("Cannot create pipe\n");

        lcore_config[i].state = WAIT;

        /* 创建主线程与子线程通信使用的pipe,设置子线程状态为WAIT,
        创建子线程,线程执行函数为eal_thread_loop,根据线程ID,获
        取当前线程的lcore_id,获取主线程向子线程通信所用管道,子线
        程向主线程通信所用管道,
        */
        ret = pthread_create(&lcore_config[i].thread_id, NULL,
                     eal_thread_loop, NULL);
        if (ret != 0)
            rte_panic("Cannot create thread\n");

        /* Set thread_name for aid in debugging. */
        snprintf(thread_name, RTE_MAX_THREAD_NAME_LEN,
                "lcore-slave-%d", i);
        rte_thread_setname(lcore_config[i].thread_id, thread_name);
    }

    /*
     * 为所有子线程启动一个函数,使得主线程知道所有子线程都已经准备完毕,等待子线程返回
     */
    rte_eal_mp_remote_launch(sync_func, NULL, SKIP_MASTER);
    rte_eal_mp_wait_lcore();

    /* initialize services so vdevs register service during bus_probe. */
    ret = rte_service_init();
    if (ret) {
        rte_eal_init_alert("rte_service_init() failed\n");
        rte_errno = ENOEXEC;
        return -1;
    }

    /* Probe all the buses and devices/drivers on them */
    if (rte_bus_probe()) {
        rte_eal_init_alert("Cannot probe devices\n");
        rte_errno = ENOTSUP;
        return -1;
    }

    /* initialize default service/lcore mappings and start running. Ignore
     * -ENOTSUP, as it indicates no service coremask passed to EAL.
     */
    /*如果是主应用,将全局内存配置struct rte_mem_config结构
    的magic成员写成RTE_MAGIC,表明主应用EAL初始化完成*/
    ret = rte_service_start_with_defaults();
    if (ret < 0 && ret != -ENOTSUP) {
        rte_errno = ENOEXEC;
        return -1;
    }

    rte_eal_mcfg_complete();

    return fctret;
}

对用于pthread的支持

除了使用DPDK提供的逻辑核之外,用户也可以将DPDK的执行上下文运行在任何用户自己创建的pthread中,但是部分库不支持(如timer、Mempool)。

你可能感兴趣的:(DPDK)