가상화 지원 (하이퍼 모드)

<kernel v5.4>

가상화 지원 (하이퍼 모드)

하이퍼바이저라는 용어는 VM(Virtual Machine)을 생성하고, 관리하고 스케줄링 하기 위해 사용되는 소프트웨어이다.

 

ARM 시스템에서 가상화를 지원하기 위해서는 AVE(Architecture Virtualization Extension)가 필요하다.

  • ARM32에서는 SoC 마다 탑재 여부가 다르다.
  • ARM64에서는 대부분 내장되어 있다.

 

ARM32와 ARM64 시스템의에서 다음과 같은 하이퍼 바이저 운영 모드들이 지원된다. 대부분의 설명은 ARM64를 디폴트로 한다.

  • ARM32
    • 하이퍼 바이저는 HYP 모드에서 운영
    • Guest OS는 SVC 모드에서 운영
  • ARM64
    • 하이퍼 바이저는 EL2에서 운영
    • Guest OS는 EL1 모드에서 운영

 

하이퍼 바이저를 운영하는 방법은 다음과 같이 두 가지가 있다.

  • Type 1 (Standalone)
    • EL2에서 Host OS 없이 전용 하이퍼바이저를 운영하고, Host OS 없이 EL1에서 Guest OS를 운영한다.
      • 예) Xen Server, VMWare ESXi, VMWare vSphere, Oracle VM Server, Hyper-V
  • Type 2 (Hypervisors and Hosted)
    • EL2에서 하이퍼바이저와 Host OS를 같이 운영하고, EL1에서 Guest OS를 운영한다.
      • 예) VMware worksataion, Virtual Box, QEMU/KVM

 

VHE(Virutalization Host Extension)

ARM 리눅스를 Type 2 하이퍼 바이저로 운영될 때 ARMv8 VHE 지원 여부에 따라 EL2 모드와 EL1 모드를 사용한다.

  • VHE 미지원시
    • EL2 스위칭 + EL1 Host & Guest OS
    • Host OS를 EL2로 부팅한 후 EL2에 관련 스위칭(irq 라우팅 포함) stub 코드만 남겨 놓고 EL1으로 전환하여 운영한다.
      • 예) Host용 리눅스 커널이 EL2에서 부팅되고, EL1으로 전환한다.
  • VHE 지원시
    • EL2 Host OS + EL1 Guest OS
    • Host OS를 EL2로 부팅 및 운영하는 것으로 EL2 및 EL1 간 많은 스위칭을 제거해 성능을 향상시켰다.
      • 예)  지원을 통해 Host용 리눅스 커널이 EL2에서 부팅 및 운영을 한다.
    • VHE 기능을 사용하는 동안 EL1 레지스터를 사용하는 리눅스 커널의 코드 변경없이 EL2 모드에서 사용할 수 있게 하였다.즉 코드에서 접근하는 *_EL1 레지스터들은 실제 *_EL2 레지스터와 동일한 동작을 한다.
      • HCR_EL2.E2H 비트를 설정할 때에 사용할 수 있다.
      • EL2 모드에서 원래 EL0 및 EL1 레지스터를 사용하기 위해서는 *_EL02 및 *_EL12 레지스터를 사용한다.
    • 참고:

 

다음 그림은 리눅스 커널등의 OS가 다양한 하이퍼바이저 운영 상황에서 동작하는 모습을 보여준다.

  • 좌측 그림은 Xen, VMware/ESX 같은 전용 하이퍼바이저가 동작하고, guest 운영 체제들은 EL1에서 동작한다.
  • 중앙 및 우측 그림은 VHE 지원여부에 따라 각각 EL2 및 EL1에서 호스트 운영체제가 동작하고, guest 운영 체제들은 EL1에서 동작한다.
    • SCR_EL2.E2H==1 (VHE 운영) 에서 Host OS가 운영되는 cpu는 SCR_EL2.TGE 값이 1이고, Guest OS가 운영되는 cpu는 0이다.

 

하이퍼모드 지원 유무 확인

하이퍼모드로 부팅된 경우인지 아닌지 메시지(dmesg)를 출력하여 안내한다.

ARM64 예)

  • EL2 부팅
    • CPU: All CPU(s) started at EL2
  • EL1 부팅
    • “CPU: All CPU(s) started at EL1”

 

ARM32 예)

  • HYP 모드 부팅
    • “CPU: All CPU(s) started in HYP mode.”
    • “CPU: Virtualization extensions available.”
  • SVC 모드 부팅
    • “CPU: All CPU(s) started in SVC mode.”

 

Secure EL2 support

  • ARMv8.4 아키텍처부터 Secure EL2를 지원하기 시작하였다.

 

Full-virtualization vs Para-virtualization

한글로는 전가상화 또는 반가상화라고 불리운다. 오늘 날 가상화를 위한 하드웨어 지원이 있는 cpu에서 Guest OS들은 Guest OS 코드 변경 없이 전가상화를 지원하도록 빠르게 동작하고 있다.

  • 현재 전가상화와 반가상화를 구분하여 사용하지 않는다.
  • Guest OS를 보다 효과적인 성능으로 사용하기 위해 대부분의 운영체제에 반가상화용 드라이버(virtio)들이 포함되어 있다.

 

Stage 2 변환

하이퍼 모드를 사용하는 경우 운영체제 별로 매핑된 Stage 2 테이블을 사용는 변환을 통해 물리주소(PA)를 얻어낸다.

  • 하이퍼 모드 사용하지 않을 때
    • VA —-> (Stage 1 Transalation MMU) —-> PA
  • 하이퍼 모드 사용할 때
    • VA —-> (Stage 1 Transalation MMU) —-> IPA —-> (Stage 2 Transalation MMU) —-> PA

 

ASID(Address Space IDentifier)

Stage 1 변환에서 VA에 대해 application을 식별하기 위해 ASID를 사용한다.

 

VMID(Virtual Machine IDentifier)

Stage 2 변환에서 IPA에 대해 운영체제를 식별하기 위해 VMID를 사용한다.

 

속성 병합(Combining)

Stage 1과 Stage 2의 매핑은 각각 메모리 타입과 속성을 가지고 있고, 이들은 각각의 제한을 가지고 있는데 병합(Combining)하여 더 많은 제한을 가진다.

  • 예) 디바이스 + 메모리 = 디바이스

 

속성 오버라이딩(Overinding)

Stage 1과 Stage 2의 매핑은 각각 메모리 타입과 속성에 대해 동작 방식을 결정할 수 있는 레지스터가 있다.

  • HCR_EL2.CD
    • Stage 1 속성을 모두 Non-cacheable로 운영한다.
  • HCR_EL2.DC
    • Stage 1 속성을 모두 Normal, Write-Back 캐시 속성으로 운영한다.
  • HCR_EL2.FWB
    • 속성 병합대신 Stage 1 속성으로 오버라이딩한다. (forARMv8.4)

 

Virtual CPU

vCPU

하이퍼 바이저를 운영하면 cpu core를 각각의 운영체제가 공유하여 사용할 수 있도록 가상 cpu 개념이 사용된다. 즉 1개의 cpu를 여러 개의 vcpu로 나누어 운용할 수 있다.

예) 4 개의 core에 리눅스 Guest OS를 vcpu0~3까지 4개의 코어에서 운용하고, 또 다른 리눅스 Guest OS를 vcpu0~1까지 2개의 코어에서 운용할 수 있다.

 

Virtual Exception

vIRQ, vFIQ 및 vSErrors

하이퍼 바이저를 운영하면 EL2에서 수신된 물리 인터럽트를 vCPU로 forwarding할 때 가상 Exception인 vIRQ, vFIQ 및 vSErrors가 사용된다.

 


하이퍼 모드 체크

hyp_mode_check() – ARM64

arch/arm64/kernel/setup.c

static void __init hyp_mode_check(void)
{
        if (is_hyp_mode_available())
                pr_info("CPU: All CPU(s) started at EL2\n");
        else if (is_hyp_mode_mismatched())
                WARN_TAINT(1, TAINT_CPU_OUT_OF_SPEC,
                           "CPU: CPUs started in inconsistent modes");
        else
                pr_info("CPU: All CPU(s) started at EL1\n");
}

커널이 EL2로 부팅되었는지 EL1으로 부팅되었는지 여부를 메시지로 출력한다.

 

hyp_mode_check() – ARM32

arch/arm/kernel/setup.c

#ifndef ZIMAGE
void __init hyp_mode_check(void)
{
#ifdef CONFIG_ARM_VIRT_EXT
        sync_boot_mode();

        if (is_hyp_mode_available()) {
                pr_info("CPU: All CPU(s) started in HYP mode.\n");
                pr_info("CPU: Virtualization extensions available.\n");
        } else if (is_hyp_mode_mismatched()) {
                pr_warn("CPU: WARNING: CPU(s) started in wrong/inconsistent modes (primary CPU mode 0x%x)\n",
                        __boot_cpu_mode & MODE_MASK);
                pr_warn("CPU: This may indicate a broken bootloader or firmware.\n");
        } else       
                pr_info("CPU: All CPU(s) started in SVC mode.\n");
#endif               
}
#endif

커널이 HYP 모드로 부팅되었는지 SVC 모드로 부팅되었는지 여부를 메시지로 출력한다.

 

하이퍼 모드 운영 여부

is_hyp_mode_available() – ARM64

arch/arm64/include/asm/virt.h

/* Reports the availability of HYP mode */
static inline bool is_hyp_mode_available(void)
{
        return (__boot_cpu_mode[0] == BOOT_CPU_MODE_EL2 &&
                __boot_cpu_mode[1] == BOOT_CPU_MODE_EL2);
}

boot cpu가 EL2로 부팅되었는지 여부를 알아온다.

  • __boot_cpu_mode[] 저장 루틴은 다음을 참고한다.

 

is_hyp_mode_available() – ARM32

arch/arm/include/asm/virt.h

/* Reports the availability of HYP mode */
static inline bool is_hyp_mode_available(void)
{
        return ((__boot_cpu_mode & MODE_MASK) == HYP_MODE &&
                !(__boot_cpu_mode & BOOT_CPU_MODE_MISMATCH));
}

boot cpu가 EL2로 부팅되었는지 여부를 알아온다.

 


기타

sync_boot_mode()

arch/arm/include/asm/virt.h

/*
 * __boot_cpu_mode records what mode the primary CPU was booted in.
 * A correctly-implemented bootloader must start all CPUs in the same mode:
 * if it fails to do this, the flag BOOT_CPU_MODE_MISMATCH is set to indicate
 * that some CPU(s) were booted in a different mode.
 *
 * This allows the kernel to flag an error when the secondaries have come up.
 */
extern int __boot_cpu_mode;

static inline void sync_boot_mode(void)
{
        /*  
         * As secondaries write to __boot_cpu_mode with caches disabled, we
         * must flush the corresponding cache entries to ensure the visibility
         * of their writes.
         */
        sync_cache_r(&__boot_cpu_mode);
}
  • 전역 __boot_cpu_mode 변수 영역에 대해 inner & outer 캐시 flush를 수행한다.

 

sync_cache_r()

arch/arm/include/asm/cacheflush.h

#define sync_cache_r(ptr) __sync_cache_range_r(ptr, sizeof *(ptr))
  • long으로 선언된 전역 __boot_cpu_mode 변수 위치에 대해 inner & outer 캐시 flush를 수행한다.
    • long 값이므로 32bit 시스템에서는 4 byte , 64bit 시스템에서는 8 byte 영역만큼에 대해 flush를 수행하게 된다.

 

__sync_cache_range_r()

arch/arm/include/asm/cacheflush.h

/*
 * Ensure preceding writes to *p by other CPUs are visible to
 * subsequent reads by this CPU.  We must be careful not to
 * discard data simultaneously written by another CPU, hence the
 * usage of flush rather than invalidate operations.
 */
static inline void __sync_cache_range_r(volatile void *p, size_t size)
{
        char *_p = (char *)p;

#ifdef CONFIG_OUTER_CACHE
        if (outer_cache.flush_range) {
                /*
                 * Ensure dirty data migrated from other CPUs into our cache
                 * are cleaned out safely before the outer cache is cleaned:
                 */
                __cpuc_clean_dcache_area(_p, size);

                /* Clean and invalidate stale data for *p from outer ... */
                outer_flush_range(__pa(_p), __pa(_p + size));
        }
#endif

        /* ... and inner cache: */
        __cpuc_flush_dcache_area(_p, size);
}

지정된 range에 대해 inner & outer 캐시 flush를 수행한다.

  • CONFIG_OUTER_CACHE
    • outer 캐시가 사용되는 시스템에서 사용하는 커널 옵션
    • rpi2: outer 캐시를 사용하지 않는다.
  • if (outer_cache.flush_range) {
    • outer 캐시를 사용하는 시스템에서 flush_range에 연결된 함수가 존재하는 경우
  • __cpuc_clean_dcache_area(_p, size);
    • 지정 range의 outer 캐시를 flush하기 전에 먼저 cpu 캐시 즉 inner 캐시를 먼저 clean 작업을 해야한다.
    • ARMv7:
      • inner d-cache 영역에 대해 clean 오퍼레이션 대신 flush를 구현하였다.
  • outer_flush_range(__pa(_p), __pa(_p + size));
    • 지정 range의 outer cache에 대한 flush(clean & invalidate)를 수행한다.
  • __cpuc_flush_dcache_area(_p, size);
    • 지정 range의 inner d-cache에 대한 flush(clean & invalidate)를 수행한다.

 

arch/arm/include/asm/cacheflush.h

/*
 * There is no __cpuc_clean_dcache_area but we use it anyway for
 * code intent clarity, and alias it to __cpuc_flush_dcache_area.
 */
#define __cpuc_clean_dcache_area __cpuc_flush_dcache_area
  • ARMv7에서는 clean 구현을 따로 준비하지 않았고 따라서 flush 구현을 사용한다.

 

arch/arm/include/asm/cacheflush.h

#define __cpuc_flush_dcache_area        cpu_cache.flush_kern_dcache_area
  • ARMv7: v7_flush_kern_dcache_area()

 

outer_flush_range()

arch/arm/include/asm/outercache.h

/**     
 * outer_flush_range - clean and invalidate outer cache lines
 * @start: starting physical address, inclusive
 * @end: end physical address, exclusive
 */
static inline void outer_flush_range(phys_addr_t start, phys_addr_t end)
{
        if (outer_cache.flush_range)
                outer_cache.flush_range(start, end);
}
  • outer_cache가 구현된 머신에서 지정 range의 outer cache에 대한 flush(clean & invalidate)를 수행한다.

 

is_hyp_mode_mismatched()

arch/arm/include/asm/virt.h

/* Check if the bootloader has booted CPUs in different modes */
static inline bool is_hyp_mode_mismatched(void)
{
        return !!(__boot_cpu_mode & BOOT_CPU_MODE_MISMATCH);
}

 

arch/arm/include/asm/virt.h

/*
 * Flag indicating that the kernel was not entered in the same mode on every
 * CPU.  The zImage loader stashes this value in an SPSR, so we need an
 * architecturally defined flag bit here.
 */
#define BOOT_CPU_MODE_MISMATCH  PSR_N_BIT

 

arch/arm/include/uapi/asm/ptrace.h

/*
 * PSR bits
 * Note on V7M there is no mode contained in the PSR
 */
#define USR26_MODE      0x00000000
#define FIQ26_MODE      0x00000001
#define IRQ26_MODE      0x00000002
#define SVC26_MODE      0x00000003
#if defined(__KERNEL__) && defined(CONFIG_CPU_V7M)
/*
 * Use 0 here to get code right that creates a userspace
 * or kernel space thread.
 */
#define USR_MODE        0x00000000
#define SVC_MODE        0x00000000
#else
#define USR_MODE        0x00000010
#define SVC_MODE        0x00000013
#endif
#define FIQ_MODE        0x00000011
#define IRQ_MODE        0x00000012
#define ABT_MODE        0x00000017
#define HYP_MODE        0x0000001a
#define UND_MODE        0x0000001b
#define SYSTEM_MODE     0x0000001f
#define MODE32_BIT      0x00000010
#define MODE_MASK       0x0000001f

 

arch/arm/include/uapi/asm/ptrace.h

/*
 * Use 0 here to get code right that creates a userspace
 * or kernel space thread.
 */
#define USR_MODE        0x00000000
#define SVC_MODE        0x00000000
#else
#define USR_MODE        0x00000010
#define SVC_MODE        0x00000013
#endif
#define FIQ_MODE        0x00000011
#define IRQ_MODE        0x00000012
#define ABT_MODE        0x00000017 
#define HYP_MODE        0x0000001a
#define UND_MODE        0x0000001b
#define SYSTEM_MODE     0x0000001f
#define MODE32_BIT      0x00000010
#define MODE_MASK       0x0000001f

 


전역 변수

__boot_cpu_mode – ARM64

arch/arm64/include/asm/virt.h

/*
 * __boot_cpu_mode records what mode CPUs were booted in.
 * A correctly-implemented bootloader must start all CPUs in the same mode:
 * In this case, both 32bit halves of __boot_cpu_mode will contain the
 * same value (either 0 if booted in EL1, BOOT_CPU_MODE_EL2 if booted in EL2).
 *
 * Should the bootloader fail to do this, the two values will be different.
 * This allows the kernel to flag an error when the secondaries have come up.
 */
extern u32 __boot_cpu_mode[2];

 

arch/arm64/kernel/hyp-stub.S

/*
 * We need to find out the CPU boot mode long after boot, so we need to
 * store it in a writable variable.
 *
 * This is not in .bss, because we set it sufficiently early that the boot-time
 * zeroing of .bss would clobber it.
 */
ENTRY(__boot_cpu_mode)
        .long   BOOT_CPU_MODE_EL2
        .long   BOOT_CPU_MODE_EL1

 

__boot_cpu_mode – ARM32

arch/arm/include/asm/virt.h

/*
 * __boot_cpu_mode records what mode the primary CPU was booted in.
 * A correctly-implemented bootloader must start all CPUs in the same mode:
 * if it fails to do this, the flag BOOT_CPU_MODE_MISMATCH is set to indicate
 * that some CPU(s) were booted in a different mode.
 *
 * This allows the kernel to flag an error when the secondaries have come up.
 */
extern int __boot_cpu_mode;

arch/arm/kernel/hyp-stub.S

/*
 * For the kernel proper, we need to find out the CPU boot mode long after
 * boot, so we need to store it in a writable variable.
 *
 * This is not in .bss, because we set it sufficiently early that the boot-time
 * zeroing of .bss would clobber it.
 */
.data
ENTRY(__boot_cpu_mode)
        .long   0

 

참고

 

댓글 남기기