smp_setup_processor_id()

<kernel v5.10>

리눅스는 물리 cpu 번호(id)를 사용하지 않고 로지컬 cpu 번호(id)를 사용하여 관리한다. 현재 부트된 물리 cpu를 로지컬 cpu id 0번으로 배치하여 사용한다.

  • DTB를 사용하는 경우 setup_arch() -> arm_dt_init_cpu_maps() 함수에서 로지컬 cpu id가 재조정된다.

 

NR_CPUS

  • 가능한 CPU의 최대 개수로 configuration 할 때 static하게 정해지는 값.
    • nr_cpu_ids
      • 런타임에 사용하는 cpu의 수
      • 런타임에 설정되기 전까지는 NR_CPUS 값과 동일하다.
  • 커널 버전에 따라 범위와 default 값이 약간씩 다르다.
  • 커널 버전이 높아질 때마다 조금씩 증가되는 추세이다.
  • arm
    • 2 ~ 32의 범위에 default는 4이다.
  • arm64
  • x86_32
    • 2 ~ 8의 범위에 default는 8이다.
    • 추가로 X86_BIGSMP 설정을 사용하는 경우 2 ~ 32의 범위에 default는 32이다.
  • x86_64
    • 2 ~ 64의 범위에 default는 64이다.
    • 추가로 MAXSMP 설정을 사용하는 경우 2 ~ 8192 범위에 default는 8192이다.

 


smp_setup_processor_id() – ARM64

arch/arm64/kernel/setup.c

void __init smp_setup_processor_id(void)
{
        u64 mpidr = read_cpuid_mpidr() & MPIDR_HWID_BITMASK;
        set_cpu_logical_map(0, mpidr);

        /*
         * clear __my_cpu_offset on boot CPU to avoid hang caused by
         * using percpu variable early, for example, lockdep will
         * access percpu variable inside lock_release
         */
        set_my_cpu_offset(0);
        pr_info("Booting Linux on physical CPU 0x%010lx [0x%08x]\n",
                (unsigned long)mpidr, read_cpuid_id());
}

부트 cpu인 로지컬 cpu 0번에 대한 mpidr 값을 읽어 매핑한다. (처음 부팅 시 로지컬 cpu는 항상 0번이다.)

  • 코드 라인 3~4에서 MPIDR의 Aff3, Aff2, Aff1, Aff0 필드들만을 가져오기 위해 비트마스킹을 하여 읽어온다. 그런 후 이를 로지컬 cpu 0번에 해당하는 cpu_logical_map에 저장한다.
    • CPU Affinity는 여러 개의 CPU core 중에서 각각의 cpu가 가지는 고유 번호같은 것.
    • Affinity는 계층적으로 표현된다.
      • 아래 별도 섹션에서 다룬다.
  • 코드 라인 11에서 현재 cpu가 로지컬 cpu 0 이므로 per-cpu에서 사용할 현재 부트 cpu에 대한 offset을 0으로 설정한다.
  • 코드 라인 12~13에서 부팅 cpu에 대한 mpidr 값과 midr 정보를 출력한다.
    • 예) “Booting Linux on physical CPU 0x0000000000 [0x410fd083]

 

다음 그림은 arm64 시스템에서 8개의 cpu가 있는 경우를 가정하여 위의 함수가 처음 동작한 경우이다.

  • arm64에서는 로지컬 cpu 0번에 해당하는 매핑만 설정해둔다.

 

__cpu_logical_map[] – ARM64

arch/arm64/kernel/setup.c

u64 __cpu_logical_map[NR_CPUS] = { [0 ... NR_CPUS-1] = INVALID_HWID};

 

MPIDR 관련 매크로 – ARM64

#define INVALID_HWID            ULONG_MAX

#define MPIDR_UP_BITMASK        (0x1 << 30)
#define MPIDR_MT_BITMASK        (0x1 << 24)
#define MPIDR_HWID_BITMASK      UL(0xff00ffffff)

#define MPIDR_LEVEL_BITS_SHIFT  3
#define MPIDR_LEVEL_BITS        (1 << MPIDR_LEVEL_BITS_SHIFT)
#define MPIDR_LEVEL_MASK        ((1 << MPIDR_LEVEL_BITS) - 1)

#define MPIDR_LEVEL_SHIFT(level) \
        (((1 << level) >> 1) << MPIDR_LEVEL_BITS_SHIFT)

#define MPIDR_AFFINITY_LEVEL(mpidr, level) \
        ((mpidr >> MPIDR_LEVEL_SHIFT(level)) & MPIDR_LEVEL_MASK)
  • MPIDR_LEVEL_BITS
    • 하나의 affinity level 값이 몇 비트로 이루어져 있는지 나타내고 8로 설정되어 있다 (1바이트).
  • MPIDR_LEVEL_MASK
    • 0xff

 

다음은 2개의 cpu가 빅 클러스터를 이루고, 4개의 cpu가 리틀 클러스터를 가진 odroid-N2 시스템의 mpidrmidr 값들을 보여준다.

  • 클러스터 id가 0과 1로 구분되고, cpu id가 0~3으로 구분됨을 알 수 있다.
   Booting Linux on physical CPU 0x0000000000 [0x410fd034]
CPU1: Booted secondary processor 0x0000000001 [0x410fd034]
CPU2: Booted secondary processor 0x0000000100 [0x410fd092]
CPU3: Booted secondary processor 0x0000000101 [0x410fd092]
CPU4: Booted secondary processor 0x0000000102 [0x410fd092]
CPU5: Booted secondary processor 0x0000000103 [0x410fd092]

 


smp_setup_processor_id() – ARM32

arch/arm/kernel/setup.c

void __init smp_setup_processor_id(void)
{
        int i;
        u32 mpidr = is_smp() ? read_cpuid_mpidr() & MPIDR_HWID_BITMASK : 0;
        u32 cpu = MPIDR_AFFINITY_LEVEL(mpidr, 0);

        cpu_logical_map(0) = cpu;
        for (i = 1; i < nr_cpu_ids; ++i)
                cpu_logical_map(i) = i == cpu ? 0 : i;

        /*
         * clear __my_cpu_offset on boot CPU to avoid hang caused by
         * using percpu variable early, for example, lockdep will
         * access percpu variable inside lock_release
         */
        set_my_cpu_offset(0);

        pr_info("Booting Linux on physical CPU 0x%x\n", mpidr);
}

현재 로지컬 cpu 0번에 대한 물리 cpu 번호를 읽어 매핑한다. (처음 부팅 시 로지컬 cpu는 항상 0번이다.)

  • 코드 라인 4~7에서 MPIDR의 하위 3바이트(Aff0, Aff1, Aff2)를 가져오기 위해 비트마스킹을 한 후 가장 단계가 낮은 affinity level 0번 값을 읽어온다. 읽어온 값은 물리 cpu 번호이며 이를 로지컬 cpu 0번에 해당하는 cpu_logical_map에 저장한다.
    • CPU Affinity는 여러 개의 CPU core 중에서 각각의 cpu가 가지는 고유 번호같은 것.
    • Affinity는 계층적으로 표현된다.
    • x86의 하이퍼스레딩과 같이 arm에서도 가상 코어를 상용화하려 하였다가 포기하였다.
  • 코드 라인 8~9에서 임시로 로지컬 cpu 1번 부터 나머지 로지컬 cpu에 대해 물리 cpu 번호와 동일하게 구성한다.
  • 코드 라인 11에서 현재 cpu가 로지컬 cpu 0 이므로 per-cpu에서 사용할 현재 cpu에 대한 offset을 0으로 설정한다.
  • 코드 라인 13에서 어떤 물리 CPU로 리눅스 부팅이 되었는지 안내하는 정보 출력한다.

 

MPIDR 관련 매크로 – ARM32

arch/arm/include/asm/cputype.h

#define MPIDR_AFFINITY_LEVEL(mpidr, level) \
        ((mpidr >> (MPIDR_LEVEL_BITS * level)) & MPIDR_LEVEL_MASK)
#define MPIDR_LEVEL_BITS 8
#define MPIDR_LEVEL_MASK ((1 << MPIDR_LEVEL_BITS) - 1)
  • MPIDR_LEVEL_BITS
    • 하나의 affinity level 값이 몇 비트로 이루어져 있는지 나타내고 8로 설정되어 있다 (1바이트).
  • MPIDR_LEVEL_MASK
    • 하위 MPIDR_LEVEL_BITS개의 비트만 1이고 나머지는 0인 값이다.

 

다음 그림은 arm 시스템에서 8개의 cpu가 있는 경우를 가정하여 위의 함수가 처음 동작한 경우이다.

 


MPIDR(Multiprocessor Affinity Register)

멀티프로세서 시스템의 스케줄링을 위해 어떠한 코어들간에 친화력(affinity)이 있는지 레벨별로 제공한다.

 

Process Affnity(프로세스 친화력)

프로세스 스케쥴링시 한 번 배정되었던 프로세스를 어떤 CPU 코어를 사용하게 할 지 결정하기 위해 필요.

  • 리눅스 스케줄러는 가능하면 캐시 데이터 재활용을 위해 같은 코어에 배정
  • 로드 밸런스를 위해 같은 코어에 배정을 하지 않는 경우 affinity 0 레벨에서 검토하고 배정할 core가 busy한 경우 점차 상향하여 위로 올라간다.

 

1) ARM64(AArch64)

다음 그림은 ARM64의 MPIDR 레지스터를 보여준다.

  • 최대 4단계의 affinity 레벨을 제공한다.

 

U(Uniprocessor):

cpu가 UP 및 SMP 용도 두 가지로 표현되어 있다.

  • 0=Multiprocessor
  • 1=Uniprocessor

 

MT(Multi-Thread):

virtual core(hw 멀티스레드) 지원 여부 (멀티 스레딩 타입 접근으로 구현된 논리 프로세서의 밀결합 최소 레벨)

  • 0
    • 최소 affinity 레벨에서의 프로세스 성능이 최대 독립적
      • affinity 레벨 0은 독립적인 core id를 사용하므로 각각의 core 성능은 최대한 독립적으로 운영된다.
  • 1
    • 최소 affinity 레벨에서의 프로세스 성능이 매우 의존적
      • affinity 레벨 0은 virtual core id를 사용하므로 각각의 virtual core 성능은 같은 affinity 레벨의 virtual core의 성능에 영향을 끼친다.
      • x86의 하이퍼스레드와 동일한 개념이다.

 

ARM64 CPU Topology

리눅스 커널 구현에 따라 보통 2개 중 하나를 사용한다. ARM64의 경우도 virtual core를 개발하여 상용화하려다 계획을 포기하였다. 따라서 현재 모든 arm64 cpu들은 MT=0 모드만 사용한다. 향후 2021년 ARMv9 아키텍처 출시하고, 2022년 이후 어느 시점에서 추가 개발되어 출시할 예정이다.

  • 3/4단계 affinity 레벨 사용 (MT=1)
    • affinity 2 + (affnity 3 << 8):
      • package id
    • affinity 1:
      • core id
    • affinity 0:
      • thread id
  • 2단계 affinity 레벨 사용 (MT=0)
    • affinity 1 + (affnity 2 << 8) + (affnity 3 << 16) :
      • package id
    • affinity 0:
      • core id

 

다음 그림은 ARM64 CPU topology에서 MT(Multi Thread) 지원 여부에 따라 구성된 모습을 보여준다.

  • 리눅스는 MT=1일 때 8개의 cpu로 인식하고, MT=0일 때 4개의 cpu로 인식한다.

 

2) ARM(AArch32)

다음 그림은 32bit ARM의 MPIDR 레지스터를 보여준다.

  • cpu가 UP 및 SMP 용도 두 가지로 표현되어 있다.
  • 최대 3단계의 affinity 레벨을 제공한다.

mpidr

 

ARM CPU Topology

구현에 따라 보통 2개 중 하나를 사용한다. ARM의 경우 virtual core를 개발하여 상용화하려다 계획을 포기하였다. 따라서 현재 모든 arm cpu들은 MT=0 모드만 사용한다.

  • 3단계 affinity 레벨 사용 (MT=1)
    • affinity 2:
      • socket id
    • affinity 1:
      • core id
    • affinity 0:
      • thread id
  • 2단계 affinity 레벨 사용 (MT=0)
    • affinity 2:
      • (reserved)
    • affinity 1:
      • socket id
    • affinity 0:
      • core id

 

다음 그림은 ARM CPU topology에서 MT(MultiThread) 지원 여부에 따라 구성된 모습을 보여준다.

  • 리눅스는 MT=1일 때 8개의 cpu로 인식하고, MT=0일 때 4개의 cpu로 인식한다.

 


per-cpu 관련 offset 설정

set_my_cpu_offset() – ARM64

arch/arm64/include/asm/percpu.h

static inline void set_my_cpu_offset(unsigned long off)
{
        asm volatile(ALTERNATIVE("msr tpidr_el1, %0",
                                 "msr tpidr_el2, %0",
                                 ARM64_HAS_VIRT_HOST_EXTN)
                        :: "r" (off) : "memory");
}

현재 cpu의 per-cpu offset을 @offset 값으로 설정한다

  • TPIDR 레지스터를 사용하여 현재 cpu에 대한 per-cpu offset 값을 저장하여 per-cpu 변수에 대한 빠른 access를 가능하게 한다.
  • 부팅 cpu를 제외한 나머지 cpu들에 대해 nVHE 기능 지원 여부에 따라 VHE 지원하는 경우 tpidr_el1으로 동작하고, nVHE의 경우 tpidr_el2 레지스터를 사용해야 한다.

 

TPIDR_EL1 또는 TPIDR_EL2 – ARM64

  • TPID(스레드 ID) 정보가 기록된 레지스터
    • 현재 리눅스에서는 TPID를 저장하는 목적으로 사용하지 않고 각 cpu의 per-cpu offset를 저장하여 더 빠른 per-cpu data의 access를 위해 사용된다.

 

set_my_cpu_offset() – ARM32

arch/arm/include/asm/percpu.h

/*
 * Same as asm-generic/percpu.h, except that we store the per cpu offset 
 * in the TPIDRPRW. TPIDRPRW only exists on V6K and V7
 */
#if defined(CONFIG_SMP) && !defined(CONFIG_CPU_V6)
static inline void set_my_cpu_offset(unsigned long off)
{
        /* Set TPIDRPRW */

        asm volatile("mcr p15, 0, %0, c13, c0, 4" : : "r" (off) : "memory");
}

현재 cpu의 per-cpu offset을 @offset 값으로 설정한다

  • per-cpu 자료구조에서 사용되는 cpu마다 개별적으로 가지는 offset 값.
  • ARMv7에서는 속도 향상을 위해서 TPIDRPRW register를 사용함.
    • ARMv7 이전 아키텍처에서는 이 레지스터를 사용하지 않고 메모리를 사용하여 연산하느라 메모리에 대한 접근이 2번 필요하여 느렸었고 이를 극복하기 위해 본래의 목적으로 사용하지 않는 레지스터인 TPIDRPRW를 사용하여 메모리 접근을 한 번으로 줄이기 위해 사용한다.
    • 참고: ARM: implement optimized percpu variable | LWN.net

 

TPIDRPRW (Thread ID-R) – ARM32

  • Multiprocessor Extension에서 사용하며 PL1(Previlidge Level 1으로 커널 레벨) 이상에서만 사용가능하다.
  • Security Extension에서는 레지스터는 뱅크된다.
  • TPID(스레드 ID) 정보가 기록된 레지스터
    • 현재 리눅스에서는 TPID를 저장하는 목적으로 사용하지 않고 각 cpu의 per-cpu offset를 저장하여 더 빠른 per-cpu data의 access를 위해 사용된다.

 

참고

 

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다