• CPU知识学习汇总


    一、相关名词解释

    SMP:(Symmetric Multi-Processing)对称多处理,一个chip上集成多个核心
    SMT:(Simultaneous multithreading)同时多线程,一个核心上实现多个hardware context,以支持多线程。通过复制硬件寄存器状态等手段,同时执行多个线程。

    Node:某些Core之间,独享总线和memory,称作Node。Core只访问Node内的memory,因此可以减轻对总线和memory的带宽需求。但是有些场景下,Core会不可避免的访问其它Node的memory,这会造成很大的访问延迟。

    NUMA: (Non-uniform Memory Access)不一致内存访问,以内存访问的不一致性为代价,减轻对总线和memory的带宽需求。这种结构对进程调度算法的要求较高,尽量减少跨Node的内存访问次数,以提升系统性能。

    HMP:(Heterogeneous Multi-Processing)异构多处理,ARM的一种架构,在乎功耗的存在。HMP架构在一个chip中,封装两类ARM Core,一类为高性能Core(如Cortex-A15,也称作big core),一类为低性能Core(如Cortex-A7,也称作little core),因此HMP也称作big·little架构。
    还有big-middle-little架构。

    二、CPU拓扑

    1. CPU topology除了描述CPU的组成之外,其主要功能是向kernel调度器提供必要的信息,以便让它合理地分配任务,最终达到性能和功耗之间的平衡。

    CPU topology:Cluster-->Core-->Threads

    2.CPU拓扑框架

    -------------------------     ----------------------------  
    |  CPU topology driver  |     |    Task Scheduler etc.  | 
    -------------------------     ----------------------------
    ------------------------------------------------------- 
    |      Kernel general CPU topology       | 
    ----------------------------------------------------------
    ---------------------------------------------------------- 
    |      arch-dependent CPU topology       | 
    ---------------------------------------------------------- 

    Kernel general CPU topology位于"include/linux/topology.h”中,定义了获取系统CPU topology信息的标准接口。底层的arch-dependent CPU topology会根据平台的特性,实现kernel定义的那些接口。

    CPU topology信息有两个重要的使用场景:一是向用户提供当前的CPU信息(eg:lscpu),这是由CPU topology driver实现的;二是向调度器提供CPU core的信息,以便合理的调度任务。

    2.1 Kernel general CPU topology

    Kernel general CPU topology位于 include/linux/topology.h 中,主要以“#ifndef ... #define”类型的宏定义的形式提供API,其目的是:底层的arch-dependent CPU topology可以重新定义这些宏,只要底层有定义,则优先使用底层的,否则就使用Kernel general CPU topology中的默认API,主要包括:

    /* include/linux/topology.h */
    #ifndef topology_physical_package_id
    #define topology_physical_package_id(cpu)       ((void)(cpu), -1)
    #endif
    #ifndef topology_core_id
    #define topology_core_id(cpu)                   ((void)(cpu), 0)
    #endif
    #ifndef topology_thread_cpumask
    #define topology_thread_cpumask(cpu)            cpumask_of(cpu)
    #endif
    #ifndef topology_core_cpumask
    #define topology_core_cpumask(cpu)              cpumask_of(cpu)
    #endif
    
    #ifdef CONFIG_SCHED_SMT
    static inline const struct cpumask *cpu_smt_mask(int cpu)
    {
        return topology_thread_cpumask(cpu);
    }
    #endif
    
    static inline const struct cpumask *cpu_cpu_mask(int cpu)
    {
        return cpumask_of_node(cpu_to_node(cpu));
    }

    topology_physical_package_id:用于获取某个CPU的package ID,即socket(X86)或者cluster(ARM),具体意义依赖于具体平台的实现;
    topology_core_id:某个CPU的core ID。即第二章所描述的core,具体意义依赖于具体的平台实现;
    topology_thread_cpumask:获取和该CPU属于同一个core的所有CPU,通俗的讲,就是姐妹Thread;
    topology_core_cpumask:获取和该CPU属于同一个cluster的所有CPU;
    cpu_cpu_mask: 获取该CPU属于同一个Node的所有CPU;
    cpu_smt_mask: 用于SMT调度(CONFIG_SCHED_SMT)的一个封装,意义同topology_thread_cpumask。

    2.2 arch-dependent CPU topology

    位于“arch/arm64/include/asm/topology.h”和“arch/arm64/kernel/topology.c”中,主要负责ARM64平台相关的topology转换,包括:

    (1) 定义一个数据结构,以及基于该数据结构的变量,用于存储系统的CPU topology

    /* arch/arm64/include/asm/topology.h */
    struct cpu_topology {
        int thread_id;
        int core_id;
        int cluster_id;
        cpumask_t thread_sibling;
        cpumask_t core_sibling;
    };
    extern struct cpu_topology cpu_topology[NR_CPUS];

    cluster_id、core_id、thead_id描述了拓扑结构的三个层次,thread_sibling和core_sibling,保存了和该CPU位于相同级别(同一个core和同一个cluster)的所有姐妹CPU。系统中每个CPU(个数由NR_CPUS指定,是从OS的角度看的)都有一个struct cpu_topology变量,用于描述该CPU在整个topology中的地位。以数组的形式维护。

    (2)重定义CPU topology有关的宏定义

    /* arch/arm64/include/asm/topology.h */
    #define topology_physical_package_id(cpu)       (cpu_topology[cpu].cluster_id)
    #define topology_core_id(cpu)           (cpu_topology[cpu].core_id)
    #define topology_core_cpumask(cpu)      (&cpu_topology[cpu].core_sibling)
    #define topology_thread_cpumask(cpu)    (&cpu_topology[cpu].thread_sibling)

    实现比较简单,从该CPU对应的struct cpu_topology变量中取出指定的字段即可。

    (3)提供初始化并构建CPU topology的方法,以便在系统启动时调用

    /* arch/arm64/include/asm/topology.h */
    void init_cpu_topology(void);
    void store_cpu_topology(unsigned int cpuid);

    init_cpu_topology的调用路径是:kernel_init-->smp_prepare_cpus-->init_cpu_topology,主要完成如下任务:

    store_cpu_topology的调用路径是:kernel_init-->smp_prepare_cpus-->store_cpu_topology,在没有从DTS中成功获取CPU topology的情况下,从ARM64的MPIDR寄存器中读取topology信息。

    设备树中的cpu-map和clusterX描述了CPU的拓扑结构,具体可参考“Documentation/devicetree/bindings/arm/topology.txt”中的描述。

    2.3 CPU topology driver

    CPU topology driver位于“driversase opology.c”中,基于“include/linux/topology.h”所提供的API,以sysfs的形式,向用户空间提供获取CPU topology信息的接口,lscpu应用,就是基于该接口实现的。sysfs的格式可参考“Documentation/cputopology.txt”。

    /sys/devices/system/cpu/cpuX/topology/下

    physical_package_id: //就是此CPU位于的Cluster编号
    core_id: //在一个Cluster内此CPU的编号
    thread_siblings: //每个CPU核的位掩码,CPU0 CPU1 CPU2分别为0x1 0x2 0x4
    thread_siblings_list: //CPU是几这个就是几,CPU0就是0,CPU7就是7
    core_siblings: //每个Cluster内的CPU组成的位掩码,若四小核Cluster0就是0x0f,3中核就是0x70
    core_siblings_list: //每个Cluster内的CPU组成的数字加中画线显示,若4小核,3中核,1大核,小核的是0-3,中核就是4-6,大核就是7

    /sys/devices/system/cpu/下

    kernel_max 一共有多少个核,7
    kernel_max: 31
    offline: 2,4-31,32-63
    online: 0-1,3
    possible: 0-31
    present: 0-31

    3. CPU一共有4种状态需要表示:

    cpu_possible_bits,系统中包含的所有的可能的CPU core,在系统初始化的时候就已经确定。对于ARM64来说,DTS中所有格式正确的CPU core,都属于possible的core;
    
    cpu_present_bits,系统中所有可用的CPU core(具备online的条件,具体由底层代码决定),并不是所有possible的core都是present的。对于支持CPU hotplug的形态,present core可以动态改变;
    
    cpu_online_bits,系统中所有运行状态的CPU core(后面会详细说明这个状态的意义);
    
    cpu_active_bits,有active的进程正在运行的CPU core。

    三、Linux cpu ops

    1. 在SMP系统中,Linux kernel会在一个CPU(primary CPU)上完成启动操作。primary CPU启动完成后,再启动其它的CPU(secondary CPUs),这称作secondary CPU boot。一般是CPU0作为boot cpu。

    2. CPU(或SOC)中会集成一个ROM,ROM上有CPU厂商在出厂时固化的代码,这些代码会进行一些必要的初始化后,将CPU跳转到其它地址(例如0x20000000),这些地址一般是RAM或者NOR flash,用户代
    码可以存放在这些位置。

    3. 不同的CPU core可能有着不同的power domain,因而有可能单独上电。

    4. CPU hotplug
    hotplug功能,是在处理性能需求不高的情况下,从物理上关闭不需要的CPU core,并在需要的时候,将它们切换为online状态的一种手段。和cpuidle类似,cpu hotplug也是根据系统负荷,动态调整
    处理器性能,从而达到节省功耗的目的。

    hotplug与idle的区别:
    处于idle状态的CPU,对调度器来说是可见的,换句话说,调度器并不知道某个CPU是否处于idle状态,因此不需要对它们做特殊处理。而处于un-hotplugged状态CPU,对调度器是不可见,因此调度器必
    须做一些额外的处理,包括:主动移除CPU,并将该CPU上的中断等资源迁移到其它CPU上,同时进行必要的负载均衡;反之亦然。

    5. 每一个core掉电后,都要检查该core的sibling core是否都已掉电,如果是,则关闭cluster的供电。

    6. cpu ops

    对ARM64平台来说,kernel使用struct cpu_operations来抽象cpu ops

    struct cpu_operations {
        const char *name;
        int    (*cpu_init)(struct device_node *, unsigned int);
        int    (*cpu_init_idle)(struct device_node *, unsigned int);
        int    (*cpu_prepare)(unsigned int);
        int    (*cpu_boot)(unsigned int);
        void (*cpu_postboot)(void);
    #ifdef CONFIG_HOTPLUG_CPU
        int    (*cpu_disable)(unsigned int cpu);
        void (*cpu_die)(unsigned int cpu);
        int    (*cpu_kill)(unsigned int cpu);
    #endif
    #ifdef CONFIG_ARM64_CPU_SUSPEND
        int    (*cpu_suspend)(unsigned long);
    #endif
    };

    针对ARM64,kernel提供了两种可选的方法,smp spin table和psci,如下:

    static const struct cpu_operations *supported_cpu_ops[] __initconst = {
    #ifdef CONFIG_SMP
        &smp_spin_table_ops,
    #endif
        &cpu_psci_ops,
        NULL,
    };

    具体使用哪一个operation,是通过DTS中的“enable-method”域指定的,DTS格式如下:

    cpus {
        ...
            cpu@000 {
                ...
                enable-method = "psci";
                cpu-release-addr = <0x1 0x0000fff8>;
        };
        ...
    };

    系统初始化的时候,会根据DTS配置获取使用的operations(setup_arch-->cpu_read_bootcpu_ops-->cpu_read_ops),最终保存在一个cpu_ops数组(每个CPU一个)中,供SMP(arch/arm64/kernel/smp.c)使用,如下:

    /* arch/arm64/kernel/cpu_ops.c */
    const struct cpu_operations *cpu_ops[NR_CPUS];

     

    三、cpu control & hotplug

    1. kernel cpu control位于“./kernel/cpu.c”中,是一个承上启下的模块,负责屏蔽arch-dependent的实现细节,向上层软件提供控制CPU core的统一API(主要包括cpu_up/cpu_down等接口的实现)。

    2. cpu的四种状态

    kernel使用4个bitmap,来保存分别处于4种状态的CPU core:possible、present、active和online。

    /* include/linux/cpumask.h */
    cpu_possible_mask- has bit 'cpu' set iff cpu is populatable,在启动时就是固定的,作为CPU ID的集合,可理解为存在这个CPU资源。
    cpu_present_mask - has bit 'cpu' set iff cpu is populated,cpu_present_mask是动态的,表示当前插入了哪些CPU,可理解为被kernel接管。
    cpu_online_mask  - has bit 'cpu' set iff cpu available to scheduler,pu_online_mask是cpu_present_mask的动态子集,指示可用于调度的CPU。
    cpu_active_mask  - has bit 'cpu' set iff cpu available to migration,即是否对调度器可见

    如果启用了HOTPLUG,则将强制cpu_possible_mask设置为所有NR_CPUS位。

    2.1 possible CPU
    possible的CPUs,代表了系统中可被使用的所有的CPU,在boot阶段确定之后,就不会再修改。以ARM64为例,其初始化的过程如下:
    (1)系统上电后,boot CPU启动,执行start_kernel(init/main.c),并分别调用 boot_cpu_init 和 setup_arch 两个接口,进行possible CPU相关的初始化。
    (2)boot_cpu_init负责将当前的boot CPU放到possible CPU的bitmap中,同理,boot CPU也是present、oneline、active CPU。

    /* init/main.c */
    static void __init boot_cpu_init(void)
    {
        int cpu = smp_processor_id(); //用户获取当前CPU的ID
        /* Mark the boot cpu "present", "online" etc for SMP and UP case */
        set_cpu_online(cpu, true);
        set_cpu_active(cpu, true);
        set_cpu_present(cpu, true);
        set_cpu_possible(cpu, true);
    }

    2.2 present CPU
    start_kernel —> rest_init —> kernel_init(pid 1,init task) —> kernel_init_freeable -> smp_prepare_cpus”,轮询所有的possible CPU,如果某个CPU core满足具备相应的cpu_ops指针,cpu ops的.cpu_prepare回调成功,则调用set_cpu_present(),将其设置为present CPU。

    2.3 online CPU
    已经boot的CPU,会在 secondary_start_kernel 中,调用 set_cpu_online 接口,将其设置为online状态。反之,会在__cpu_disable中将其从online mask中清除。

    2.4 active CPU
    调度器需要监视 CPU hotplug 有关的每一个风吹草动。由于调度器和CPU控制两个独立的模块,kernel 通过 notifier 机制实现这一功能。每当系统的CPU资源有任何变动,kernel CPU control 模块就会通知调度器,调度器根据相应的event(CPU_DOWN_FAILED、CPU_DOWN_PREPARE等),调用set_cpu_active接口,将某个CPU添加到active mask或者移出active mask。这就是active CPU的意义。

    3. 对于支持CPU hotplug功能的平台来说,可以在系统启动后的任意时刻,关闭任意一个secondary CPU(对ARM平台来说,CPU0或者说boot CPU,是不可以被关闭的),并在需要的时候,再次打开它。

    4. 在kernel/cpu.c中,cpu_up 接口,只会在使能了 CONFIG_SMP 配置项(意味着是SMP系统)后才会提供。而cpu_down接口,则只会在使能了 CONFIG_HOTPLUG_CPU 配置项后才会提供。

    5. per-CPU 的idle线程

    boot CPU在执行初始化动作的时候,会通过“smp_init —> idle_threads_init —> idle_init”的调用,为每个CPU创建一个idle线程,如下:

    /* kernel/smpboot.c */
    static inline void idle_init(unsigned int cpu)
    {
        struct task_struct *tsk = per_cpu(idle_threads, cpu);
        if (!tsk) {
            tsk = fork_idle(cpu);
            if (IS_ERR(tsk))
                pr_err("SMP: fork_idle() failed for CPU %u
    ", cpu);
            else
            per_cpu(idle_threads, cpu) = tsk;
        }
    }

    该接口的本质是,为每个CPU fork一个idle thread(由struct task_struct结构表示),并保存在一个per-CPU的全局变量(idle_threads)中。此时,idle thread只是一个task结构,并没有执行。

    6. 打开和关闭CPU分析

    在当前kernel实现中,只支持通过sysfs的形式,关闭或打开CPU:

    echo 0 > /sys/devices/system/cpu/cpuX/online  # 关闭CPU
    echo 1 > /sys/devices/system/cpu/cpuX/online  # 打开CPU

    CPU online 的软件流程如下:

    echo 0 > /sys/devices/system/cpu/cpuX/online 
        online_store(drivers/base/core.c) 
            device_online(drivers/base/core.c) 
                cpu_subsys_online(drivers/base/cpu.c) 
                    cpu_up(kernel/cpu.c) 
                        _cpu_up(kernel/cpu.c) 

    (1) up前后,发送PREPARE、ONLINE、STARTING等notify,以便让关心者作出相应的动作,例如调度器、RCU、workqueue等模块,都需要关注CPU的hotplug动作,以便进行任务的重新分配等操作。

    (2) 执行Arch-specific相关的boot操作,将CPU boot起来,最终通过 secondary_start_kernel 接口,停留在per-CPU的idle线程上。

    _cpu_up 接口会在完成一些准备动作之后,调用平台相关的__cpu_up接口,由平台代码完成具体的up操作,如下:

    static int _cpu_up(unsigned int cpu, int tasks_frozen)
    {
        void *hcpu = (void *)(long)cpu;
        unsigned long mod = tasks_frozen ? CPU_TASKS_FROZEN : 0;
        struct task_struct *idle;
        cpu_hotplug_begin();
        idle = idle_thread_get(cpu);
        ret = smpboot_create_threads(cpu);
        ret = __cpu_notify(CPU_UP_PREPARE | mod, hcpu, -1, &nr_calls);
        ret = __cpu_up(cpu, idle);
        /* Wake the per cpu threads */
        smpboot_unpark_threads(cpu);
        /* Now call notifier in preparation. */
        cpu_notify(CPU_ONLINE | mod, hcpu);
    }

    准备动作包括:
    (1) 获取idle thread的task指针,该指针最终会以参数的形式传递给arch-specific代码。
    (2) 创建一个用于管理CPU hotplug动作的线程(smpboot_create_threads),该线程的具体意义,后面会再说明。
    (3) 发送CPU_UP_PREPARE notify。

    以ARM64为例,__cpu_up 的内部实现如下:

    /* arch/arm64/kernel/smp.c */
    int __cpu_up(unsigned int cpu, struct task_struct *idle)
    {
        int ret;
        /* We need to tell the secondary core where to find its stack and the page tables. */
        secondary_data.stack = task_stack_page(idle) + THREAD_START_SP;
        __flush_dcache_area(&secondary_data, sizeof(secondary_data));
        /* Now bring the CPU into our world. */
        ret = boot_secondary(cpu, idle);
        if (ret == 0) {
            /*
            * CPU was successfully started, wait for it to come online or
            * time out.
            */
            wait_for_completion_timeout(&cpu_running, msecs_to_jiffies(1000));
            cpu_online(cpu);
        } else {
            pr_err("CPU%u: failed to boot: %d
    ", cpu, ret);
        }
        secondary_data.stack = NULL;
        return ret;
    }

    该接口以 idle thread 的 task 指针为参数,完成如下动作:
    (1) 将idle线程的堆栈,保存在一个名称为 secondary_data 的全局变量中(这地方很重要,后面再介绍其中的奥妙)。
    (2) 执行 boot_secondary 接口,boot CPU,具体的流程。
    (3) boot_secondary 返回后,等待对应的CPU切换为online状态。

    secondary_startup 接口位于arch/arm64/kernel/head.S中,负责secondary CPU启动后的后期操作,如下:

    ENTRY(secondary_startup)
        /*
        * Common entry point for secondary CPUs.
        */
        mrs     x22, midr_el1                   // x22=cpuid
        mov     x0, x22
        bl      lookup_processor_type
        mov     x23, x0                         // x23=current cpu_table
        cbz     x23, __error_p                  // invalid processor (x23=0)?
    
        pgtbl   x25, x26, x28                   // x25=TTBR0, x26=TTBR1
        ldr     x12, [x23, #CPU_INFO_SETUP]
        add     x12, x12, x28                   // __virt_to_phys
        blr     x12                             // initialise processor
    
        ldr     x21, =secondary_data
        ldr     x27, =__secondary_switched      // address to jump to after enabling the MMU
        b       __enable_mmu
    ENDPROC(secondary_startup)
    
    ENTRY(__secondary_switched)
        ldr     x0, [x21]                       // get secondary_data.stack
        mov     sp, x0
        mov     x29, #0
        b       secondary_start_kernel
    ENDPROC(__secondary_switched)

    我们重点关注上面16~17行,以及21~26行的 __secondary_switched,__secondary_switched 会将保存在 secondary_data 全局变量中的堆栈取出,保存在该CPU的SP中,
    并跳转到 secondary_start_kernel 继续执行。

    CPU启动后,需要先配置好堆栈,才能进行后续的函数调用,这里使用的是该CPU idle thread的堆栈。看一下kernel中“current”指针(获取当前task结构的宏定义)的实现方法:

    #define current get_current()
    #define get_current() (current_thread_info()->task)
    
    static inline struct thread_info *current_thread_info(void)
    {
        register unsigned long sp asm ("sp");
        return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
    }

    通过CPU的SP指针,是可以获得CPU的当前task的。也就是说,当CPU SP被赋值为idle thread的堆栈的那一瞬间,当前的上下文已经是idle thread了!

    6. 另外,CPU hotplug 还受“maxcpus”命令行参数影响

    系统启动的时候,可以通过命令行参数“maxcpus”,告知kernel本次启动所使用的CPU个数,该个数可以小于等于possible CPU的个数。系统初始化时,只会把“maxcpus”所指定个数的CPU置为present状态
    Documentationcpu-hotplug.txt”文档是这样描述的:

    maxcpus=n    Restrict boot time cpus to n. Say if you have 4 cpus, using 
                 maxcpus=2 will only boot 2. You can choose to bring the 
                 other cpus later online, read FAQ's for more info.

    注:
    内核中经常有这样的函数,xxx、_xxx 或者 __xxx,区别是一个或者两个下划线,其中的含义是:
    xxx接口,通常需要由某个锁保护,一般提供给其它模块调用。它会直接调用_xxx接口;
    _xxx接口,则不需要保护,一般由模块内部在确保安全的情况下调用。有时,外部模块确信可行(不需要保护),也可能会直接调用;
    __xxx接口,一般提供给arch-dependent的软件层实现,比如这里的arch/arm64/kernel/xxx.c。
    理解这些含义后,会加快我们阅读代码的速度,另外,如果直接写代码,也尽量遵守这样的原则,以便使自己的代码更规范、更通用。

    参考:

    Linux CPU core的电源管理(1)_概述: http://www.wowotech.net/pm_subsystem/cpu_core_pm_overview.html

    Linux CPU core的电源管理(2)_cpu topology:http://www.wowotech.net/pm_subsystem/cpu_topology.html

    Linux CPU core的电源管理(5)_cpu control及cpu hotplug:http://www.wowotech.net/pm_subsystem/cpu_hotplug.html

  • 相关阅读:
    生活
    通俗易懂----尾递归
    。。。
    调用startActivityForResult,onActivityResult无响应的解决办法
    安卓官方ViewPager与android.support.design.widget.TabLayout双向交互联动切换 。
    1、面向对象以及winform的简单运用(开篇)
    15、C#基础整理(递归)
    14、C#基础整理(函数)
    13、C#基础整理(枚举)
    12、C#基础整理(结构体)
  • 原文地址:https://www.cnblogs.com/hellokitty2/p/12898962.html
Copyright © 2020-2023  润新知