smp_setup_processor_id()

<kernel v5.0>

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

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

 

smp_setup_processor_id() – ARM

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에서도 가상 코어를 상용화하려 하였다가 포기하였다.
    • 만일 상용화가 되었으면 affinity 0번 레벨 값은 가상 코어 id별로 cpu 번호가 부여된다. 현재의 ARM은 물리 core id 별로 cpu 번호를 부여한다.
  • 코드 라인 8~9에서 임시로 로지컬 cpu 1번 부터 나머지 로지컬 cpu에 대해 물리 cpu 번호와 동일하게 구성한다.
  • 코드 라인 11에서 현재 cpu가 로지컬 cpu 0 이므로 per-cpu에서 사용할 현재 cpu에 대한 offset을 0으로 설정한다.
  • 코드 라인 13에서 어떤 물리 CPU로 리눅스 부팅이 되었는지 안내하는 정보 출력한다.

 

MPIDR_AFFINITY_LEVEL() 매크로

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가 있는 경우를 가정하여 위의 함수가 처음 동작한 경우이다.

 

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;
        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 0번에 대한 물리 cpu 번호를 읽어 매핑한다. (처음 부팅 시 로지컬 cpu는 항상 0번이다.)

  • 코드 라인 3~4에서 MPIDR로부터 최대 4개 레벨에 해당하는 비트 값만을 마스킹하여 읽어와서 cpu_logical_map에 저장한다.
  • 코드 라인 11에서 현재 cpu가 로지컬 cpu 0 이므로 per-cpu offset을 0으로 설정한다.
  • 코드 라인 12~13에서 어떤 물리 CPU로 리눅스 부팅이 되었는지 안내하는 정보 출력한다.
    • 예) “Booting Linux on physical CPU 0x0”

 

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

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

 

__cpu_logical_map[] – ARM64

arch/arm64/kernel/setup.c

u32 __cpu_logical_map[NR_CPUS] = { [0 ... NR_CPUS-1] = MPIDR_HWID };
  • Designated Initializers 라고 불리는 배열 초기화 방법.
  • MPIDR_HWID:
    • ulong_max 값
    • arm
      • 0xffff_ffff
    • arm64
      • 0xffff_ffff_ffff_ffff

 

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

 

set_my_cpu_offset() – ARM

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

 

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들에 대해 ARM64_HAS_VIRT_HOST_EXTN라고 불리는 cpu capability가 있는 경우 두 번째 인자에 있는 명령이 수행된다.

 

MPIDR(Multiprocessor Affinity Register)

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

 

Process Affnity(프로세스 친화력)

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

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

 

1) ARM(AArch32)

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

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

mpidr

 

2) ARM64(AArch64)

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

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

 

U(Uniprocessor):
  • 0=Multiprocessor
  • 1=Uniprocessor

 

MT(Multi-Thread):

멀티스레딩 타입 접근으로 구현된 논리 프로세서의 밀결합 최소 레벨

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

 

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로 인식한다.

ARM64 CPU Topology

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

  • 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로 인식한다.

 

TPIDRPRW (Thread ID-R)

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

 

참고

 

댓글 남기기

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