<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 값과 동일하다.
- nr_cpu_ids
- 커널 버전에 따라 범위와 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 레벨을 제공한다.
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 성능은 최대한 독립적으로 운영된다.
- 최소 affinity 레벨에서의 프로세스 성능이 최대 독립적
- 1
- 최소 affinity 레벨에서의 프로세스 성능이 매우 의존적
- affinity 레벨 0은 virtual core id를 사용하므로 각각의 virtual core 성능은 같은 affinity 레벨의 virtual core의 성능에 영향을 끼친다.
- x86의 하이퍼스레드와 동일한 개념이다.
- 최소 affinity 레벨에서의 프로세스 성능이 매우 의존적
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
- affinity 2:
- 2단계 affinity 레벨 사용 (MT=0)
- affinity 2:
- (reserved)
- affinity 1:
- socket id
- affinity 0:
- core id
- affinity 2:
다음 그림은 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
- affinity 2 + (affnity 3 << 8):
- 2단계 affinity 레벨 사용 (MT=0)
- affinity 1 + (affnity 2 << 8) + (affnity 3 << 16) :
- package id
- affinity 0:
- core id
- affinity 1 + (affnity 2 << 8) + (affnity 3 << 16) :
다음 그림은 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를 위해 사용된다.
참고
- Per-cpu | 문c
- arm_dt_init_cpu_maps() | 문c
- ALTERNATIVE() | 문c