CPU知識學習彙總【轉】

轉自:https://www.cnblogs.com/hellokitty2/p/12898962.html

一、相關名詞解釋

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章