ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리눅스 커널] CPU overview
    Linux/kernel 2023. 8. 3. 02:32

    글의 참고

    - http://www.wowotech.net/pm_subsystem/cpu_core_pm_overview.html

    - https://zhuanlan.zhihu.com/p/536776611


    글의 전제

    - 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다. 나도 모르기 때문에 나중에 알아봐야 할 내용이라는 뜻이다.

    - 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다. 그러므로, 밑 줄 처친 글이 이해가 안간다면 링크를 따라서 관련 내용을 공부하자.


    글의 내용

    1. 소개

    • SMP(Symmetric Multi-Processing)가 유행하기 한참 전 리눅스 커널의 전원 관리는 주로 외부 장치에 집중되었으며, CPU 코어와 관련된 것은 기껏해야 CPU idle 정도 였다. 그러나 SMP가 대중화됨에 따라 시스템에서 사용할 수 있는 CPU 코어가 점점 더 많아지고 이러한 코어의 주파수가 점점 높아지고 처리 능력이 점점 더 강해짐에 따라 전력 소비 또한 점점 증가하고 있다.따라서 CPU 코어와 관련된 전원 관리는 시스템 설계에서 필수적인 부분이 되며, 이와 관련된 사항는 다음과 같다.
    1) 하드웨어 비용이 점점 낮아지고 있다.

    2) 마케팅의 속임수.

    3) 소프트웨어 설계자의 부주의(비대해진 안드로이드가 대표적인 예)으로 인해 소프트웨어 효율성이 떨어지고 하드웨어 리소스가 심각하게 낭비된다. 그래서 몇 줄의 코드를 최적화하는 것이 몇 개의 cpu 코어를 추가하는 것보다 더 어려울 정도이다.
    • 이러한 맥락에서 CPU 코어의 전원 관리 논리는 매우 간단하다. 시스템 부하에 따라 "과도한 CPU 성능"을 끄고 사용자 요구를 충족한다는 전제하에 CPU의 전력 소비를 최대한 줄입니다. 그러나 CPU를 매우 세세하게 컨트롤하는 것은 쉽지 않다. 그래서, 현재 CPU 코어의 전원 관리 기능은 크게 아래 2가지 방법으로 구현된다.
      1. SMP 시스템에서 CPU core를 동적으로 On/Off 한다(이 글에서 중점적으로 설명하는 기능). - cpu hotplug
      2. CPU 작동 중 CPU core의 전압과 주파수를 동적으로 조정한다(다른 기사에서 별도로 분석함). - cpufreq(DVFS)
    • 이 글에서는 ARM64를 통해 Linux kernel CPU core 관련 전원 관리 설계를 소개한다.

     

     

    2. 기능 설명

    • Linux kernel에서 CPU core 관련 전원 관리의 구현은 단순한 전원 관리 동작이 아니라 시스템 초기화, CPU 토폴로지, 프로세스 스케줄링, CPU hotplug, 메모리 hotplug 및 기타 지식 포인트를 포함합니다. 주로 다음과 같은 기능을 수행합니다.
    1) 시스템 부팅 시, CPU core의 초기화, 정보 획득 등.

    2) 시스템 부팅 시, CPU core의 부팅(enable).

    3) 시스템 실행 중에 현재 부하에 따라 일부 CPU 코어는 성능과 전력 소비 사이의 균형을 맞추기 위해 동적으로 활성화/비활성화한다.

    4) CPU 코어에 대한 핫플러그 지원. 이른바 핫플러그는 시스템 작동 중에 CPU 코어를 동적으로(물리적 또는 논리적으로) 늘리거나 줄일 수 있음을 의미합니다.

    5) 시스템 작동 중 CPU idle 관리(자세한 내용은 "Linux cpuidle framework 시리즈" 참조).

    6) 시스템 작동 중 CPU 코어의 전압과 주파수는 성능과 전력 소비 사이의 균형을 맞추기 위해 현재 부하에 따라 동적으로 조정된다.

     

    3. 소프트웨어 아키텍처

    • Linux kernel은 위의 기능을 구현하기 위해 다음과 같은 소프트웨어 프레임워크를 추상화했습니다.

    출처 - http://www.wowotech.net/pm_subsystem/cpu_core_pm_overview.html

     

    • 소프트웨어 프레임워크는 arch-dependent와 arch-independent의 두 부분으로 구성됩니다.
    • ARM64의 경우 arch-dependent 부분은 'arch\arm64\kernel'에 위치하며 다음을 포함한 플랫폼 관련 제어 작업을 제공한다.
    1) CPU 정보 획득(cpuinfo)

    2) CPU 토폴로지 획득(cputopology)

    3)기본 CPU 작업(init, disable 등)의 구현, cpu ops(ARM32에서 smp ops 형태로 존재함)

    4) SMP 관련 초기화(smp)

     

    • arch-independent는 다음을 포함하여 플랫폼에 독립적인 추상화 구현을 담당합니다.
    1)CPU control 모듈은 기본이 되는 플랫폼과 관련된 구현 세부 내용을 보호하고 시스템 시작 및 프로세스 스케줄링과 같은 모듈에서 호출할 수 있는 CPU 제어(활성화, 비활성화 등)를 위한 통합 API를 제공해야 한다

    2) CPU subsystem driver - CPU hotplug 관련 기능을 사용자 공간에 제공

    3) cpuidle - CPU idle 관련 로직 처리

    4) cpufreq - CPU 주파수 조정 관련 로직 처리

    등이 있다.

     

     

     

    - CPU ID

    " SMP 환경에서는 다수의 CPU 가 존재한다. 이 때, 각 CPU 를 구별하는 방법이 뭘까? 여러 가지 방법이 있겠지만, arm64 에서는 각 CPU 에게 CPU ID 를 할당한다. 이 CPU ID 는 각 CPU 의 MPIDR 레지스터에 저장되어 있다(AArch32 에서는 `MPDIR`, AArch64 에서는 `MPIDR_E1` 레지스터).

     


    ARM Architecture Reference Manual ARMv8, for ARMv8-A architecture profile

     

     

    " arm64 에서는 boot CPU ID 와 secondary CPU ID 를 획득하는 방식이 다르다. 우리는 먼저 boot CPU 부터 알아본다.

    1. boot CPU ID : 직접 MPIDR_EL1 레지스터를 읽음
    2. secondary CPU ID : 디바이스 트리의 reg 프로퍼티에 읽음

     

     

     

    1. Boot CPU ID

    " 각 CPU 제조사마다 CPU ID 를 지정하는 방식이 모두 다르다. 가장 편하게 0 부터 할당하는 곳도 있지만, arm64 처럼 디바이스 트리의 CPU ID 와 MPIDR_EL1의 0xFF_00FF_FFFF 을 & 연산해서 CPU ID 를 만드는 곳도 있다. 그러나, OS 는 이런 하드웨어를 추상화하는 소프트웨어다. 그렇기 때문에, CPU ID 도 통일된 방식으로 접근하고 싶어한다. 그래서 물리적 CPU ID 를 논리적 CPU ID 로 매핑하는 `cpu_logical_map` 함수를 제공한다.

    // arch/arm64/kernel/setup.c - v6.5
    u64 __cpu_logical_map[NR_CPUS] = { [0 ... NR_CPUS-1] = INVALID_HWID };
    // arch/arm64/include/asm/smp.h - v6.5
    /*
     * Logical CPU mapping.
     */
    extern u64 __cpu_logical_map[NR_CPUS];
    extern u64 cpu_logical_map(unsigned int cpu);
    
    static inline void set_cpu_logical_map(unsigned int cpu, u64 hwid)
    {
    	__cpu_logical_map[cpu] = hwid;
    }
    // arch/arm64/kernel/setup.c - v6.5
    u64 cpu_logical_map(unsigned int cpu)
    {
    	return __cpu_logical_map[cpu];
    }

     

     

    " 위에 코드를 보면 알겠지만, 매핑이 되지 않은 CPU 는 `INVALID_HWID` 를 반환하도록 되어있다. 그런데, 논리적 ID 를 어떻게 물리적 ID 를 매핑시킬까? 간단하다. ID 를 가공하거나 변환하는 것은 아니다. 단지 배열의 인덱스를 CPU 번호로 매핑하고 __cpu_logical_map 배열안에 실제 각 제조사의 CPU ID 를 저장한다. 

     

     

    " 그리고, 리눅스 커널에서 boot CPU 는 반드시 논리적 0 번에 매핑되어야 한다. 나머지 물리적 CPU ID 들은 모두 디바이스 트리에서 파싱되어 __cpu_logical_map 배열에 저장된다(인덱스 1번 이상부터 저장됨). smp_setup_processor_id 함수는 boot CPU ID 를 논리적 0 번에 매핑하는 함수다. 

    // arch/arm64/kernel/setup.c - v6.5
    void __init smp_setup_processor_id(void)
    {
    	u64 mpidr = read_cpuid_mpidr() & MPIDR_HWID_BITMASK;
    	set_cpu_logical_map(0, mpidr);
    
    	pr_info("Booting Linux on physical CPU 0x%010lx [0x%08x]\n",
    		(unsigned long)mpidr, read_cpuid_id());
    }
    // arch/arm64/include/asm/smp.h - v6.5
    static inline void set_cpu_logical_map(unsigned int cpu, u64 hwid)
    {
    	__cpu_logical_map[cpu] = hwid;
    }

     

     

    " 아마 2가지 의문점이 들 것이다.

    1. arm64 의 CPU ID 는 어떻게 만들어지는 건가 ?
    2. smp_setup_processor_id 함수를 호출하는게 boot CPU 라는 것을 어떻게 알 수 있나 ?

     

     

    1) `read_cpuid_mpdir() & MPIDR_HWID_BITMASK` 가 boot CPU ID 다. 그런데, 이 방식은 boot CPU ID 에만 사용되는 방식이다. 위에서도 언급했지만, secondary CPU ID 는 read_cpuid_mpdir 함수를 통해 MPDIR_EL1 레지스터를 읽는게 아닌, 디바이스 트리에서 CPU ID 를 파싱한다. read_cpuid_mpdir 함수는 `MPDIR_EL1` 레지스터를 읽는 함수다. 

    // arch/arm64/include/asm/cputype.h - v6.5
    static inline u64 __attribute_const__ read_cpuid_mpidr(void)

    {
        return read_cpuid(MPIDR_EL1);
    }

     

     

    " `MPDIR_EL1` 레지스터 값과 MPIDR_HWID_BITMASK 를 `&` 연산해서 boot CPU ID 로 할당하고 있다. 즉, MPIDR_HWID_BITMASK 는 CPU ID 로 사용될 비트들을 MASK 한다.

     

    2) smp_setup_processor_id 함수의 caller 는 start_kernel 함수다. start_kernel 함수는 boot CPU 만 호출하는 함수다. secondary CPU`s 들은 모두 secondary_start_kernel 함수를 호출한다.

     

     

     

    2. Secondary CPU ID

    " Secondary CPU ID 를 얻는 과정은 Boot CPU 처럼 정적이지 않다. 다른 디바이스들과 마찬가지로, arm64 도 디바이스 트리를 통해서 각 CPU 의 Hardware ID 를 설정할 수 있다. 그렇다면, 디바이스 트리의 어떤 노드가 CPU ID 를 결정할까? 바로, `reg` 프로퍼티다. 그런데, 아래 주소에서 눈 여겨볼 점이 있다. CPU 주소가 조금 이상하지 않나? 아래는 생략되어 있지만, CPU0 부터 나열해보면 0x0, 0x1, 0x100, 0x101, 0x10000, x10001, 0x10100, 0x10101 ... 이건 뭘까? arm64 에서는 CPU Affinity 라는 개념이 있다. 말 그대로, `친화도` 를 의미하는데 이 값은 CPU 마이그레이션에 필수적으로 사용되는 중요한 값이다. 이 내용은 이 글을 참고하도록 하자. 

    // https://www.kernel.org/doc/Documentation/devicetree/bindings/cpu/cpu-topology.txt
    cpus {
        #size-cells = <0>;
        #address-cells = <2>;
        ....
        
        	CPU6: cpu@10100 {
    		device_type = "cpu";
    		compatible = "arm,cortex-a57";
    		reg = <0x0 0x10100>;
    		enable-method = "spin-table";
    		cpu-release-addr = <0 0x20000000>;
    	};
    
    	CPU7: cpu@10101 {
    		device_type = "cpu";
    		compatible = "arm,cortex-a57";
    		reg = <0x0 0x10101>;
    		enable-method = "spin-table";
    		cpu-release-addr = <0 0x20000000>;
    	};
    
    	CPU8: cpu@100000000 {
    		device_type = "cpu";
    		compatible = "arm,cortex-a57";
    		reg = <0x1 0x0>;
    		enable-method = "spin-table";
    		cpu-release-addr = <0 0x20000000>;
    	};
    
    	CPU9: cpu@100000001 {
    		device_type = "cpu";
    		compatible = "arm,cortex-a57";
    		reg = <0x1 0x1>;
    		enable-method = "spin-table";
    		cpu-release-addr = <0 0x20000000>;
    	};
        ....
    }

     

     

    : 그렇다면, reg 프로퍼티에는 2개의 값이 정의되어 있는데, 2개 중에서 어떤 값이 CPU ID 일까? 이걸 알기 위해서는 of_parse_and_init_cpus 함수를 살펴보자.

     

    : of_parse_and_init_cpus 함수는 디바이스 트리에서 `cpus` 노드를 파싱해서 `reg` 프로퍼티의 값을 파싱한다. 

    // arch/arm64/kernel/smp.c - v6.5
    /*
     * Enumerate the possible CPU set from the device tree and build the
     * cpu logical map array containing MPIDR values related to logical
     * cpus. Assumes that cpu_logical_map(0) has already been initialized.
     */
    static void __init of_parse_and_init_cpus(void)
    {
    	struct device_node *dn;
    
    	for_each_of_cpu_node(dn) { // --- 1
    		u64 hwid = of_get_cpu_hwid(dn, 0); // --- 1
    
    		if (hwid & ~MPIDR_HWID_BITMASK) // --- 1
    			goto next;
    
    		if (is_mpidr_duplicate(cpu_count, hwid)) {
    			pr_err("%pOF: duplicate cpu reg properties in the DT\n",
    				dn);
    			goto next;
    		}
    
    		/*
    		 * The numbering scheme requires that the boot CPU
    		 * must be assigned logical id 0. Record it so that
    		 * the logical map built from DT is validated and can
    		 * be used.
    		 */
    		if (hwid == cpu_logical_map(0)) { // --- 2
    			if (bootcpu_valid) {
    				pr_err("%pOF: duplicate boot cpu reg property in DT\n",
    					dn);
    				goto next;
    			}
    
    			bootcpu_valid = true;
    			early_map_cpu_to_node(0, of_node_to_nid(dn));
    
    			/*
    			 * cpu_logical_map has already been
    			 * initialized and the boot cpu doesn't need
    			 * the enable-method so continue without
    			 * incrementing cpu.
    			 */
    			continue;
    		}
    
    		if (cpu_count >= NR_CPUS) // --- 3
    			goto next;
    
    		pr_debug("cpu logical map 0x%llx\n", hwid);
    		set_cpu_logical_map(cpu_count, hwid); // --- 3
    
    		early_map_cpu_to_node(cpu_count, of_node_to_nid(dn));
    next:
    		cpu_count++; // --- 3
    	}
    }
    1. for_each_of_cpu_node 함수는 매크로다. 이 매크로에서는 다시 `of_get_next_cpu_node` 함수를 호출하는데, 여기서 `/cpus` 노드를 찾는다. 만약, 존재하면 다시 노드 이름이 `cpu` 인 노드를 찾는다. 만약에, 노드 이름이 `cpu` 가 존재한다면 반환한다. 이렇게, 마지막 `cpu` 노드가 등장할 때까지 루프를 반복한다. 

    `of_get_cpu_hwid` 는 `reg` 프로퍼티를 읽어온다. 그리고, 읽어온 값을 MPIDR_HWID_BITMASK 와 비교하는데, 이 값은 아래와 같다. MPDIR_EL1[24:31] 는 reserved 이기 때문에 반드시 0 이여야 한다. arm64 의 HWID 가 24 ~ 31번째 비트가 1 이면 이건 에러다.

    // arch/arm64/include/asm/cputype.h - v6.5
    #define MPIDR_HWID_BITMASK UL(0xff00ffffff)

    결국, secondary CPU ID 도 아래의 AFF[0:4] 가 된다.


    2. 이 조건이 참이 된다는 것은 디바이스 트리에 boot CPU ID 정보를 작성해놓은 경우를 의미한다. 디바이스 트리에는 secondary CPU ID 에 대한 정보를 작성해놔야 한다. 왜냐면,  boot CPU ID 는 start_kernel -> smp_setup_processor_id 함수에서 이미 초기화 되기 때문이다.

    3. 여기서 cpu_count 는 논리적 CPU ID 가 된다. 이걸 물리적 CPU ID 인 `hwid` 에 매핑하기 위해, set_cpu_logical_map 함수를 호출하는 것이다. cpu_count 는 디바이스 트리에 작성된 `cpu` 노드 개수만큼 증가한다.

     

     

    : of_get_cpu_hwid 함수는 이름이 `cpu%d` 인 노드에서 `reg` 프로퍼티를 파싱한다. 그런데, of_parse_and_init_cpus 함수에서 `thread` 값에 0을 전달하기 때문에 `cell += ac * thread` 는 그냥 `cell` 이 된다.

    // drivers/of/cpu.c - v6.5
    /**
     * of_get_cpu_hwid - Get the hardware ID from a CPU device node
     *
     * @cpun: CPU number(logical index) for which device node is required
     * @thread: The local thread number to get the hardware ID for.
     *
     * Return: The hardware ID for the CPU node or ~0ULL if not found.
     */
    u64 of_get_cpu_hwid(struct device_node *cpun, unsigned int thread)
    {
    	const __be32 *cell;
    	int ac, len;
    
    	ac = of_n_addr_cells(cpun);
    	cell = of_get_property(cpun, "reg", &len);
    	if (!cell || !ac || ((sizeof(*cell) * ac * (thread + 1)) > len))
    		return ~0ULL;
    
    	cell += ac * thread;
    	return of_read_number(cell, ac);
    }

     

     

    : `of_n_addr_cells` 함수는 인자로 전달된 노드의 `#address-cells` 의 값을 반환한다. 즉, 여기서는 `2`가 된다. `of_get_property` 함수는 생각보다 많은 기능을 한다. 아래의 그림으로 설명을 대체한다.

     

     

     

     

    4. 소프트웨어 모듈의 기능 및 API 설명

    4.1 Kernel cpu control

    - kernel cpu control은 .\kernel\cpu.c'에 위치하며 arch-dependent한 세부 구현 사항들을 추상화하여 CPU 코어 제어를 위한 통합 API를 상위 소프트웨어(User space)에 제공한다. kernel cpu control의 주요 기능은 다음과 같습니다.

     

    1) CPU core를 possible, present, online, active의 4가지 상태로 추상화하고, bitmap의 형태로 모듈 내부에서 모든 CPU core의 상태를 유지한다. 그리고 CPU의 상태 조회 및 상태 수정을 위한 API를 cpumask의 형태로 다른 모듈에 제공합니다. 관련 API는 다음과 같습니다.

     1: /* kernel/cpu.c */
       2:  
       3: #ifdef CONFIG_INIT_ALL_POSSIBLE
       4: static DECLARE_BITMAP(cpu_possible_bits, CONFIG_NR_CPUS) __read_mostly
       5:         = CPU_BITS_ALL;
       6: #else
       7: static DECLARE_BITMAP(cpu_possible_bits, CONFIG_NR_CPUS) __read_mostly;
       8: #endif
       9: const struct cpumask *const cpu_possible_mask = to_cpumask(cpu_possible_bits); // 기가막힌 테크닉이다. static으로 선언한 변수를 const 변수에 참조시키고 있다. 이게 되나?
      10: EXPORT_SYMBOL(cpu_possible_mask);  
      11:  
      12: static DECLARE_BITMAP(cpu_online_bits, CONFIG_NR_CPUS) __read_mostly;
      13: const struct cpumask *const cpu_online_mask = to_cpumask(cpu_online_bits); // 기가막힌 테크닉이다. static으로 선언한 변수를 const 변수에 참조시키고 있다. 이게 되나?
      14: EXPORT_SYMBOL(cpu_online_mask);	
      15:  
      16: static DECLARE_BITMAP(cpu_present_bits, CONFIG_NR_CPUS) __read_mostly;
      17: const struct cpumask *const cpu_present_mask = to_cpumask(cpu_present_bits); // 기가막힌 테크닉이다. static으로 선언한 변수를 const 변수에 참조시키고 있다. 이게 되나?
      18: EXPORT_SYMBOL(cpu_present_mask);	
      19:  
      20: static DECLARE_BITMAP(cpu_active_bits, CONFIG_NR_CPUS) __read_mostly;
      21: const struct cpumask *const cpu_active_mask = to_cpumask(cpu_active_bits); // 기가막힌 테크닉이다. static으로 선언한 변수를 const 변수에 참조시키고 있다. 이게 되나?
      22: EXPORT_SYMBOL(cpu_active_mask);

     

     

    - 비트맵의 정의는 다음과 같다.

    #define DECLARE_BITMAP(name,bits)    unsigned long name[BITS_TO_LONGS(bits)]

     

     

    - DECLARE_BITMAP(name,bits) 함수를 통해 선언되는 변수는 본질적으로 long 배열이며 배열의 각 bit는 CPU core의 상태를 나타냅니다. 예를 들어 long의 길이가 64비트인 시스템에서 CPU core가 8개 있으면 길이가 1인 배열의 처음(하위를 의미하는 것 같은데 말이지) 8개 bit를 사용하여 8개 core의 상태를 나타낼 수 있다.

    - 여기에는 총 4가지 상태가 표시되어야 합니다. -> 내 추측이지만, 아래로 상태로 내려갈수록(possbile -> active) cpu 상태가 active 쪽에 가깝고, 올라올 수록(active -> possible) 그냥 시스템에서 cpu의 존재를 인식했다 정도의 느낌인 거 같다.

    1) cpu_possible_bits - 시스템에 포함될 수 있는 모든 CPU 코어는 시스템 초기화 시점에 이미 결정된다. ARM64의 경우 DTS에 있는 모든 올바른 포맷의 CPU core는 possible의 core에 속한다.

    2) cpu_present_bits - 시스템에서 사용 가능한 모든 CPU core(online을 갖춘 조건, 구체적으로 하위 코드에 의해 결정됨)가 모든 possible의 core가 present되는 것은 아니다. CPU hotplug를 지원하는 형태에 대해 present core는 동적으로 변경될 수 있습니다.

    3) cpu_online_bits - 시스템의 모든 실행 상태에 있는 CPU 코어를 의미.

    4) cpu_active_bits - active process가 실행 중인 CPU 코어를 의미.

    - 또한 비트맵을 사용하여 이 4가지 상태를 나타내는 동시에 외부 인터페이스를 제공하기 위해 4개의 cpumask도 제공됩니다. cpumask의 본질도 bitmap(한 층 더 패키지화한 것)인데, 다만 kernel은 CPU 번호 단위로 cpumask를 조작할 수 있는 몇 가지 편리한 API를 제공하고 있다(구체적으로 include/linux/cpumask.h 참조).

    - 참고 1
    - 여기에는 constant 변수에 대한 고전적인 예가 있는데, 한 번 가볍게 알아보자.
    - CPU 코어의 이러한 상태가 매우 중요한 상태라는 것은 의심의 여지가 없으므로 커널은 외부에서만 읽고(CPU control 제외) 내부적으로 쓰기 가능(writeable)하기를 바랍니다. 이걸 구현 하는 방법은 무엇일까? 4개의 static의 bitmap 변수를 내부적으로 사용하여 writeable하도록 교묘하게 설계되어 있습니다. 외부적으로는 4개의 constant 타입의 cpumask 포인터를 사용하므로 readonly입니다. 외부 모듈이 read할 경우, 레이어 변환을 통해 static의 bitmap에서 실제 값을 가져옵니다. -> 위에 to_cpumask() 쪽 소스 참고하자.

     

     

    - 위에서 말한 외부로 공개되는 cpumask는 API를 통해서 조작이 가능하다고 한 게 있다. 아래는 cpumask를 조작할 수 있는 API들이다. -> 참고하는 글에서 아래 함수들에 대한 설명이 없다. 내가 직접 찾아봐야 할 듯 하다.

    1: /* include/linux/cpumask.h */
       2:  
       3: #define num_online_cpus()       cpumask_weight(cpu_online_mask)
       4: #define num_possible_cpus()     cpumask_weight(cpu_possible_mask)
       5: #define num_present_cpus()      cpumask_weight(cpu_present_mask)
       6: #define num_active_cpus()       cpumask_weight(cpu_active_mask)
       7: #define cpu_online(cpu)         cpumask_test_cpu((cpu), cpu_online_mask)
       8: #define cpu_possible(cpu)       cpumask_test_cpu((cpu), cpu_possible_mask)
       9: #define cpu_present(cpu)        cpumask_test_cpu((cpu), cpu_present_mask)
      10: #define cpu_active(cpu)         cpumask_test_cpu((cpu), cpu_active_mask)
      11:  
      12:  
      13: #define for_each_possible_cpu(cpu) for_each_cpu((cpu), cpu_possible_mask)
      14: #define for_each_online_cpu(cpu)   for_each_cpu((cpu), cpu_online_mask)
      15: #define for_each_present_cpu(cpu)  for_each_cpu((cpu), cpu_present_mask)
      16:  
      17: /* Wrappers for arch boot code to manipulate normally-constant masks */
      18: void set_cpu_possible(unsigned int cpu, bool possible);
      19: void set_cpu_present(unsigned int cpu, bool present);
      20: void set_cpu_online(unsigned int cpu, bool online);
      21: void set_cpu_active(unsigned int cpu, bool active);
      22: void init_cpu_present(const struct cpumask *src);
      23: void init_cpu_possible(const struct cpumask *src);
      24: void init_cpu_online(const struct cpumask *src);

     

     

    2) CPU core의 up/down 동작 및 up/down 시 notifier 메커니즘 제공

    - 흔히 CPU core up이란 어떤 CPU 코어를 '실행'하는 것이다. 실행이란 무엇일까요? 단일 코어 CPU의 작동을 기억해 보면, CPU core가 지정된 메모리 주소에서 실행되도록 하는 것입니다. 따라서 이 기능은 SMP 시스템에서만 유효합니다(CONFIG_SMP가 활성화됨). -> 일단 cpu_up() 함수 자체가 CPU hotplug 관련 기능인가? cpu가 아예 down 되어있는 애를 up 시키는 건지, 혹은 offline인 애를 online으로 만드건지를 모르겠네... 그리고 cpu core up SMP에서만 유효한다는 것은 뭔 말인지는 알 것 같다. SMP가 아니라면, 즉, AMP라면 하나의 프로세서만 높은 권한을 갖고 OS 레벨의 동작 할 것이고, 나머지 프로세서는 모두 유저 레벨로 동작할 것이다. 즉, 여러 프로세서가 동일한 메모리에 접근하는 레이스 컨디션같은 상황이 있을 수 없다. 그러므로, 부팅 시점에서도 모든 CPU들을 On 해도 된다. 

    - CPU core down은 CPU 코어가 장면을 저장하도록(번역이 엉망이라 좀 이상한데, 아마 느낌상 `현재 상태를 저장한 다음` 일 것으로 보인다) 한 다음(나중에 다시 실행할 수 있음) 명령 가져오기를 중지하는 것입니다. CPU 핫플러그 기능이 활성화된 경우에만 유효합니다(CONFIG_HOTPLUG_CPU). 이 두 기능에 해당하는 API는 다음과 같습니다.

      1: /* include/linux/cpu.h */
       2:  
       3: int cpu_up(unsigned int cpu);
       4:  
       5: int cpu_down(unsigned int cpu);

     

     

    3) SMP PM 관련 동작 제공

    - system suspend 과정에서 noboot-CPU는 비활성화되고, sysem resume 과정에서 다시 활성화(복구)된다.

    1: #ifdef CONFIG_PM_SLEEP_SMP
       2: extern int disable_nonboot_cpus(void);
       3: extern void enable_nonboot_cpus(void);
       4: #else /* !CONFIG_PM_SLEEP_SMP */
       5: static inline int disable_nonboot_cpus(void) { return 0; }
       6: static inline void enable_nonboot_cpus(void) {}
       7: #endif /* !CONFIG_PM_SLEEP_SMP */

     

    4.2 cpu subsystem driver

    - cpu subsystem driver는 "drivers/base/cpu.c"에 위치하며 device model 관점에서 보면, CPU core 를 추상화하고 sysfs를 통한 CPU core 상태 조회 및 hotplug 제어와 같은 인터페이스를 제공합니다. 세부 사항은 다음과 같습니다.

     

    1) 'bus'라는 이름의 subsystem을 등록한다(sysfs에서 디렉토리는 '/sys/devices/system/cpu/'이다).

     

    2) struct cpu를 사용하여 CPU core device를 추상화한다('include/linux/cpu.h' 참조).

    3) device model 관점에서 CPU core device의 register/unregister와 같은 인터페이스를 제공하고, 시스템 초기화 시 CPU core의 개수에 따라 이들 device를 kernel에 등록한다. 그와 동시에 kernel 구성에 따라 해당 CPU attribute를 등록합니다.

    1: extern int register_cpu(struct cpu *cpu, int num);
       2: extern struct device *get_cpu_device(unsigned cpu);
       3: extern bool cpu_is_hotpluggable(unsigned cpu);
       4: extern bool arch_match_cpu_phys_id(int cpu, u64 phys_id);
       5: extern bool arch_find_n_match_cpu_physical_id(struct device_node *cpun,
       6:                                               int cpu, unsigned int *thread);
       7:  
       8: extern int cpu_add_dev_attr(struct device_attribute *attr);
       9: extern void cpu_remove_dev_attr(struct device_attribute *attr);
      10:  
      11: extern int cpu_add_dev_attr_group(struct attribute_group *attrs);
      12: extern void cpu_remove_dev_attr_group(struct attribute_group *attrs);
      13:  
      14: #ifdef CONFIG_HOTPLUG_CPU
      15: extern void unregister_cpu(struct cpu *cpu);
      16: extern ssize_t arch_cpu_probe(const char *, size_t);
      17: extern ssize_t arch_cpu_release(const char *, size_t);
      18: #endif

    - sysfs에 대한 자세한 내용은 아래 내용을 참고.

     

    # ls /sys/devices/system/cpu/
    
    autoplug/  cpu2/      cpuidle/   offline    power/    
    
    cpu0/      cpu3/      kernel_max online     present

    - 자세한 내용은 추후 글에서 자세히 설명한다.

     

     

    4.3 smp

    : smp는 'arch/arm64/kernel/smp.c'에 위치하며, 아키텍처 종속적 부분과 독립적인 부분에 중간 매개체 역할을 담당하며, 주로 두 가지 기능을 제공합니다.

     

    1. 아키텍처 종속적인 `SMP 초기화`, `CPU core` 관련 기능을 지원한다.
    2. `Inter-Processor Interrupts(arm 에서는 SGI라고 한다)` 관련 기능을 지원한다.

     

    : SMP 초기화 작업은 주로 DTS에서 CPU core 정보를 파싱하고 필요한 정보 및 작업 기능 세트를 얻는 역할을 하며 smp_init_cpus 인터페이스에 의해 구현되고 setup_arch(arch\arm64\kernel\setup.c)에서 호출됩니다.

    1: /* arch/arm64/include/asm/smp.h */
       2:  
       3: /*
       4:  * Called from the secondary holding pen, this is the secondary CPU entry point.
       5:  */
       6: asmlinkage void secondary_start_kernel(void);
       7:  
       8: /*
       9:  * Initial data for bringing up a secondary CPU.
      10:  */
      11: struct secondary_data {
      12:         void *stack;
      13: };
      14: extern struct secondary_data secondary_data;
      15: extern void secondary_entry(void);
      16:  
      17: extern void arch_send_call_function_single_ipi(int cpu);
      18: extern void arch_send_call_function_ipi_mask(const struct cpumask *mask);
      19:  
      20: extern int __cpu_disable(void);
      21:  
      22: extern void __cpu_die(unsigned int cpu);
      23: extern void cpu_die(void);

    - secondary_start_kernel, secondary_entry는 noboot CPU의 entry point다. 자세한 내용은 나중에 설명한다.

    - __cpu_disable, __cpu_die, cpu_die 등의 함수는 특정 CPU core를 비활성화하는 역할을 하며, 하드웨어를 직접 조작하지 않고 하위 수준의 cpu_ops를 통해 특정 CPU core를 제어한다.

     

    4.4 cpu ops

    : SMP 아키텍처의 복잡성으로 인해, ARM64의 경우 가상화와 같은 보안 기능이 포함되기 때문에 ARM은 CPU core의 up/down과 같은 전원 관리 작업을 캡슐화합니다(예: secure mode에서 권한이 있는 OS 코드는 이와 상호 작용합니다). ARM64에서 앞에서 말한 캡슐화는 다음과 같이 struct cpu_operations 구조체를 통해 반영됩니다.

    // arch/arm64/include/asm/cpu_ops.h - v6.5
    /**
     * struct cpu_operations - Callback operations for hotplugging CPUs.
     *
     * @name:	Name of the property as appears in a devicetree cpu node's
     *		enable-method property. On systems booting with ACPI, @name
     *		identifies the struct cpu_operations entry corresponding to
     *		the boot protocol specified in the ACPI MADT table.
     * @cpu_init:	Reads any data necessary for a specific enable-method for a
     *		proposed logical id.
     * @cpu_prepare: Early one-time preparation step for a cpu. If there is a
     *		mechanism for doing so, tests whether it is possible to boot
     *		the given CPU.
     * @cpu_boot:	Boots a cpu into the kernel.
     * @cpu_postboot: Optionally, perform any post-boot cleanup or necessary
     *		synchronisation. Called from the cpu being booted.
     * @cpu_can_disable: Determines whether a CPU can be disabled based on
     *		mechanism-specific information.
     * @cpu_disable: Prepares a cpu to die. May fail for some mechanism-specific
     * 		reason, which will cause the hot unplug to be aborted. Called
     * 		from the cpu to be killed.
     * @cpu_die:	Makes a cpu leave the kernel. Must not fail. Called from the
     *		cpu being killed.
     * @cpu_kill:  Ensures a cpu has left the kernel. Called from another cpu.
     */
    struct cpu_operations {
    	const char	*name;
    	int		(*cpu_init)(unsigned int);
    	int		(*cpu_prepare)(unsigned int);
    	int		(*cpu_boot)(unsigned int);
    	void		(*cpu_postboot)(void);
    #ifdef CONFIG_HOTPLUG_CPU
    	bool		(*cpu_can_disable)(unsigned int cpu);
    	int		(*cpu_disable)(unsigned int cpu);
    	void		(*cpu_die)(unsigned int cpu);
    	int		(*cpu_kill)(unsigned int cpu);
    #endif
    };

    : `struct cpu_operations` 구조체는 기본적으로 디바이스 트리와 같이 연동된다. 디바이스 트리에 `cpu` 노드를 찾아서, 해당 노드를 파싱해서 콜백 함수들을 채우게 된다.  

     

     

    : 여기에서는 PSCI를 예로 들어 이러한 인터페이스의 의미를 간단히 알아봅니다

    1) cpu_boot - Boots a cpu into the kernel. 사실은 시작함수(secondary_entry)의 물리적 주소를 CPU core에 주어 실행하게 되는데, 구체적으로는 spec을 봐야 합니다.

    2) cpu_disable - Prepares a cpu to die.

    3) cpu_die - Makes a cpu leave the kernel.

    4) cpu_suspend - Suspends a cpu and saves the required context.

     

     

    4.5 cpu topology

    - 이 글에서 SMP, CPU core와 같은 많은 용어들을 언급하고 있어 해당 독자들이 잘 이해하지 못하고 있을 수 있으다. 이는 CPU의 토폴로지와 관련이 있으며, 프로세스 스케줄링, CPUfreq와 같은 모듈의 경우에는 구체적인 토폴로지에 따른 정책이 필요할 수 있다. 

     

    - ARM64의 토폴로지는 "./arch/arm64/include/asm/topology.h"에 있습니다.

     1: struct cpu_topology {
       2:         int thread_id;
       3:         int core_id;
       4:         int cluster_id;
       5:         cpumask_t thread_sibling;
       6:         cpumask_t core_sibling;
       7: };
       8:  
       9: extern struct cpu_topology cpu_topology[NR_CPUS];
      10:  
      11: #define topology_physical_package_id(cpu)       (cpu_topology[cpu].cluster_id)
      12: #define topology_core_id(cpu)           (cpu_topology[cpu].core_id)
      13: #define topology_core_cpumask(cpu)      (&cpu_topology[cpu].core_sibling)
      14: #define topology_thread_cpumask(cpu)    (&cpu_topology[cpu].thread_sibling)
      15:  
      16: void init_cpu_topology(void);
      17: void store_cpu_topology(unsigned int cpuid);
      18: const struct cpumask *cpu_coregroup_mask(int cpu);

    - topology의 구현은 'arch/arm64/kernel/topology.c'에 위치하며 시스템 초기화 시 boot cpu가 DTS를 읽고 각 CPU core의 struct cpu_topology 변수를 채우는 역할을 한다. 동시에 이 파일은 CPU 코어의 package id, core id 등과 같은 CPU core를 실행하는 정보를 얻기 위한 몇 가지 일반적인 매크로 정의를 제공합니다.

     

    4.6 cpu info

    : cpuinfo는 'arch/arm64/kernel/cpuinfo.c'에 위치하며 초기화시에, 레지스터에서 ARM CPU core어와 관련된 정보를 읽고 나중에 사용할 수 있도록 struct cpuinfo_arm64 유형의 변수에 캐싱한다. `struct cpuinfo_arm64` 구조체 하나는 ARM Core 한 개의 모든 레지스터 정보를 나타낸다고 보면된다.

    // arch/arm64/kernel/cpuinfo.c - v6.5
    /*
     * In case the boot CPU is hotpluggable, we record its initial state and
     * current state separately. Certain system registers may contain different
     * values depending on configuration at or after reset.
     */
    DEFINE_PER_CPU(struct cpuinfo_arm64, cpu_data);
    static struct cpuinfo_arm64 boot_cpu_data;
    
    // arch/arm64/include/asm/cpu.h - v6.5
    struct cpuinfo_arm64 {
    	struct cpu	cpu;
    	struct kobject	kobj;
    	u64		reg_ctr;
    	u64		reg_cntfrq;
    	u64		reg_dczid;
    	u64		reg_midr;
    	u64		reg_revidr;
    	u64		reg_gmid;
    	u64		reg_smidr;
    
    	u64		reg_id_aa64dfr0;
    	u64		reg_id_aa64dfr1;
    	u64		reg_id_aa64isar0;
    	u64		reg_id_aa64isar1;
    	u64		reg_id_aa64isar2;
    	u64		reg_id_aa64mmfr0;
    	u64		reg_id_aa64mmfr1;
    	u64		reg_id_aa64mmfr2;
    	u64		reg_id_aa64mmfr3;
    	u64		reg_id_aa64pfr0;
    	u64		reg_id_aa64pfr1;
    	u64		reg_id_aa64zfr0;
    	u64		reg_id_aa64smfr0;
    
    	struct cpuinfo_32bit	aarch32;
    
    	/* pseudo-ZCR for recording maximum ZCR_EL1 LEN value: */
    	u64		reg_zcr;
    
    	/* pseudo-SMCR for recording maximum SMCR_EL1 LEN value: */
    	u64		reg_smcr;
    };

     

Designed by Tistory.