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位于“drivers\base\topology.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\n", 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\n", 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状态
Documentation\cpu-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

 

posted on 2020-05-16 10:10  Hello-World3  阅读(2592)  评论(0编辑  收藏  举报

导航