TIL/2025

Virtualizing the generic timers, Arm architecture aarch64

고무 오리 2025. 4. 29. 22:13
728x90
📢 Timer(Physical, Virtual)에 대해 알아봐요

 

 

하루 8시간 책상에 앉아 공부를 했을 때 정말 내 순수 공부 시간이 8시간이 맞을까요?

딴생각 안 하고 집중해서 공부를 한 순수 공부시간이 의미 있는 게 아닐까요?

 

Physical Timer와 Virtual Timer도 위와 유사한 개념이에요

 

 

무심히 흐르는 벽시계, System Counter

모든 코어가 공유하는 System Counter(CNTPCT_EL0)는 멈추지 않고 계속 증가해요

이 카운트를 각 코어의 Physical Comparator가 감시하다 비교 값에 도달하면 “띠링!” 하고 Physical IRQ를 발생시켜요

(Virtual IRQ도 마찬가지죠)

                        +------------------------+
                        |    System Counter      |  (CNTPCT_EL0)
                        +-----------+------------+
                                    |
    CNTPCT_EL0 >= CNTP_CVAL_EL0     |    (CNTPCT_EL0 – CNTVOFF_EL2) >= CNTV_CVAL_EL0           
                    +---------------+---------------+
                    |                               |
        +-----------v-----------+       +-----------v-----------+
        |  Physical Comparator  |       |  Virtual Comparator   |
        +-----------+-----------+       +-----------+-----------+
                    |                               |
                    v                               v
                 pIRQ line                      vIRQ line

 

Physical, Virtual Timer 각각 Comparator와 CVAL 레지스터가 존재해요

 

 

가상 머신 등장, Hypervisor와 두 vCPU

Hypervisor가 vCPU0와 vCPU1을 번갈아 스케줄링해요

물리 시간이 4ms 흘렀을 때, 실제 각 vCPU는 2ms씩 수행됐죠

Wall-clock: 0 ----- 1 ----- 2 ----- 3 ----- 4 (ms)
Schedule:    [vCPU0] [vCPU1] [vCPU0] [vCPU1]

 

여기서 질문!

① vCPU0가 T=0에 “3ms 뒤에 IRQ”를 예약했으면, 4ms 시점에 IRQ가 올까?

  • 답 : physical timer를 쓰면 가능 vtimer 쓴다면 안 돼요;;

② “내가 실제로 2ms 뛰었을 때” IRQ가 오길 바란다면?

  • 답 : vtimer를 쓰면 되죠!

이를 위해 두 종류의 타이머를 제공합니다. (아래 내용을 먼저 숙지하고 드래그하여 답을 확인해 봐요)

 

 

EL1에서 쓰는 두 가지 타이머

타이머 종류 레지스터 시간 기준
Physical Timer CNTP_TVAL_EL0, CNTP_CTL_EL0 벽시계 시간
Virtual Timer CNTV_TVAL_EL0, CNTV_CTL_EL0 “vCPU 실행 시간”

 

Physical Timer은 그대로 System Counter를 보고

Virtual Timer은 System Counter에서 Offset만큼 빼서 봐요

  • Virtual Count(CNTVCT_EL0) = CNTPCT_EL0 − CNTVOFF_EL2
      +------------------------------+      +------------------------------+
      |     Multi-core Processor     |      |     Multi-core Processor     |
      |                              |      |                              |
      | +-----------+  +-----------+ |      | +-----------+  +-----------+ | 
      | |  Core 0   |  |  Core 1   | |      | |  Core 0   |  |  Core 1   | |
      | +-----┳-----+  +-----┳-----+ |      | +-----┳-----+  +-----┳-----+ |
      | +-----┴-----+  +-----┴-----+ |      | +-----┴-----+  +-----┴-----+ |
      | | phy | vir |  | phy | vir | |      | | phy | vir |  | phy | vir | |
      | +-----------+  +-----------+ |      | +-----------+  +-----------+ |
      | |  Cmptr 0  |  |  Cmptr1   | |      | |  Cmptr 0  |  |  Cmptr1   | | 
      | +-----┳-----+  +-----┳-----+ |      | +-----┳-----+  +-----┳-----+ |
      +-------|--------------|-------+      +-------|--------------|-------+
              └──────────────┤                      ├──────────────┘
                             │                      │
                       +-----┴----------------------┴-----+
                       |         Counter Module           |
                       +----------------------------------+

 

 

“내가 뛴 시간”만 세는 비밀, CNTVOFF_EL2

vCPU context switch out 순간: “여태까지 내가 본 가상 시간” 저장

이 vCPU가 잠시 멈추기 직전, 아래 코드를 통해 지금까지 누적된 가상 시간(saved_virt)을 읽어서 보관해요

saved_virt = read(CNTVCT_EL0);

 

vCPU context switch in 순간: 새 오프셋(offset) 계산

vCPU를 다시 실행시킬 때, 아래와 같이 OFFSET 계산해요

phys_now   = read(CNTPCT_EL0);           // 지금 벽시계(Physical) 시간
new_offset = phys_now − saved_virt;      // “쉬고 있던 시간”을 반영한 오프셋
write(CNTVOFF_EL2, new_offset);

 

그 뒤 가상 타이머가 어떻게 흘러갈까?

가상 타이머는 System timer에서 Offset을 뺀 값이 돼요

CNTVCT_EL0 = CNTPCT_EL0 − CNTVOFF_EL2

 

즉, 마지막 saved_virt 시점부터 타이머가 이어진 것처럼 동작하죠

 

 

예제로 살펴보면

+------------+------+------+------+------+------+------+ 
| Time (ms)  |  0   |  1   |  2   |  3   |  4   |  5   |
+------------+------+------+------+------+------+------+ 
| Scheduled  | vCPU0| vCPU1| vCPU0| vCPU1| vCPU1| vCPU0|
+------------+------+------+------+------+------+------+ 
| PhysCount  |   0  |   1  |   2  |   3  |   4  |   5  | <-- CNTPCT_EL0 
+------------+------+------+------+------+------+------+ 
| Offset     |   0  |   1  |   1  |   2  |   2  |   3  | <-- CNTVOFF_EL2
+------------+------+------+------+------+------+------+ 
| VirtCount  |   0  |   0  |   1  |   1  |   2  |   2  | <-- CNTVCT_EL0 
+------------+------+------+------+------+------+------+

 

 

결론, 두 가지 선택지

EL1의 Guest OS에서는 두 가지 타이머 중 하나를 선택

벽시계 기준(Physical Timer)

→ CNTP_TVAL_EL0에 3ms 설정 → 3ms 벽시계 후 IRQ

vCPU 기준(Virtual Timer)

→ CNTV_TVAL_EL0에 2ms 설정 → vCPU가 2ms 뛰었을 때 vIRQ

 

가장 중요한 Offset 로직만 구현해 두면 모든 vCPU가 “내가 뛴 시간만 세는 타이머”를 갖게 돼요

 

 

Reference

https://developer.arm.com/documentation/102142/0100/Virtualizing-the-generic-timers

 

Documentation – Arm Developer

 

developer.arm.com

 

728x90