Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

38107
469
422670


[Kernel][Timer] jiffies & jiffies_to_msecs 8. 커널 타이머 관리

이번 시간에는 jiffies 값에 대해 다음과 같이 알아볼게요. 
1. jiffies 변수의 의미
2. 각 아키텍쳐별로 jiffies에 접근하는 방법
3. jiffies을 밀리 초로 변환하는 방법

jiffies 변수의 의미

jiffies 를 알려면 HZ에 대해 배워야 합니다. 그럼 HZ는 뭘 의미하죠?
HZ는 1초당 타이머 인터럽트를 처리하는 횟수를 의미합니다. 만약 HZ가 500이면 1초당 jiffies가 500번 업데이트됩니다. 이번엔 만약 HZ가 300이라고 가정하고 현재 jiffies가 1000이면, jiffies 값이 1300이 되면 1초가 지났음을 알 수 있습니다.

현재 jiffies가 1000이면 초당 다음 값으로 업데이트 됩니다.
1초 후: 1300
2초 후: 1600
3초 후: 1900 

그럼 이번에는 jiffies란 값을 어떻게 가져오는지 알아볼게요. 

<<ARM64 아키텍처>>

jiffies란

우선 샘플 코드를 같이 볼게요.
아래 wq_watchdog_touch 함수는 워크큐에서 wq_watchdog_touched_cpu 란 per-cpu 타입의 변수에 jiffies 값을 저장합니다. 참고로 wq_watchdog_touch  함수는 리눅스 커널 4.9 버젼에서 확인했다는 점 기억해주세요.
void wq_watchdog_touch(int cpu)
{
if (cpu >= 0)
per_cpu(wq_watchdog_touched_cpu, cpu) = jiffies;
else
wq_watchdog_touched = jiffies;
}

C 코드로 구현된 wq_watchdog_touch 함수는 다음과 같이 어셈블리 코드로 구현돼 있습니다.
아래 코드에서 jiffies값을 어떻게 접근하는지 알아볼게요.
1 NSX:FFFFFF97D6ACC170|wq_watchdog_touch: stp x29,x30,[SP,#-0x20]!   ; x29,x30,[SP,#-32]!
2 NSX:FFFFFF97D6ACC174|    mov     x29,SP
3 NSX:FFFFFF97D6ACC178|    str     x19,[SP,#0x10]   ; x19,[SP,#16]
4 NSX:FFFFFF97D6ACC17C|    mov     w19,w0
5 NSX:FFFFFF97D6ACC180|    mov     x0,x30
6 NSX:FFFFFF97D6ACC184|    nop
7 NSX:FFFFFF97D6ACC188| tbnz x19,#0x1F,0xFFFFFF97D6ACC1B8  ; x19,#31,0xFFFFFF97D6ACC1B8
8 NSX:FFFFFF97D6ACC18C|    adrp    x1,0xFFFFFF97D91F1000
9 NSX:FFFFFF97D6ACC190|    adrp    x0,0xFFFFFF97D91E7000
10 NSX:FFFFFF97D6ACC194|   add     x1,x1,#0xA0      ; x1,x1,#160
11 NSX:FFFFFF97D6ACC198|   ldr     x2,[x0,#0xA80]   ; x2,[x0,#2688]
12 NSX:FFFFFF97D6ACC19C|                      adrp    x0,0xFFFFFF97D8BC5000
13 NSX:FFFFFF97D6ACC1A0|                      ldr     x1,[x1,w19,sxtw #0x3]   ; x1,[x1,w19,sxtw #3]
14 NSX:FFFFFF97D6ACC1A4|                      add     x0,x0,#0xB10     ; x0,x0,#2832
15 NSX:FFFFFF97D6ACC1A8|                      str     x2,[x0,x1,lsl #0x0]   ; x2,[x0,x1,lsl #0]

우선 다음 코드를 볼게요. 아래 코드가 실행되면 x0은 0xFFFFFF97D91E7000로 업데이트됩니다.
adrp 명령어는 주소를 복사하는 역할을 수행합니다.
9 NSX:FFFFFF97D6ACC190|   adrp    x0,0xFFFFFF97D91E7000

이제 다음 코드에서 jiffies값을 가져 옵니다.  
0xFFFFFF97D91E7000 주소에서 0xA80만큼 떨어진 메모리에 담긴 값을 읽어 옵니다. 즉, 0xFFFFFF97D91E7A80에 있는 메모리 값을 읽어오는 것이죠.
11 NSX:FFFFFF97D6ACC198|                      ldr     x2,[x0,#0xA80]   ; x2,[x0,#2688]

0xFFFFFF97D91E7A80 주소에 가보면 0x1006FF436 값이 있고 이 값을 10진수로 변환하면 4302304310이 되는거죠.
_________address|_data____________________|value_____________|symbol
NSD:FFFFFF97D91E7A80| 36 F4 6F 00 01 00 00 00  0x1006FF436

여기서 jiffies 변수는 0xFFFFFF97D91E7A80 주소에 있습니다.
그런데 실제 0xFFFFFF97D91E7A80 메모리에는 jiffies_64란 변수가 있습니다.
v.v %all %l &jiffies_64
  (long unsigned int *) [-] &jiffies_64 = 0xFFFFFF97D91E7A80 = jiffies_64 -> 4302304310 = 0x00000001006FF436

여기까지 분석한 내용을 유추하면 ARM64 비트 아키텍처에선 jiffies을 jiffies_64 변수로 변환한다는 점을 알 수 있습니다.

jiffies에서 밀리 초 변환
jiffies 변수를 밀리초 단위로 변환해서 보고 싶을 때 jiffies_to_msecs 함수를 호출하면 됩니다.
해당 함수 코드 구현부는 아래와 같은데요. 그런데 참 코드 보기가 어렵죠. 
여러 매크로에 따라 코드가 다르게 구현됩니다.
unsigned int jiffies_to_msecs(const unsigned long j)
{
#if HZ <= MSEC_PER_SEC && !(MSEC_PER_SEC % HZ)
return (MSEC_PER_SEC / HZ) * j;
#elif HZ > MSEC_PER_SEC && !(HZ % MSEC_PER_SEC)
return (j + (HZ / MSEC_PER_SEC) - 1)/(HZ / MSEC_PER_SEC);
#else
# if BITS_PER_LONG == 32
return (HZ_TO_MSEC_MUL32 * j) >> HZ_TO_MSEC_SHR32;
# else
return (j * HZ_TO_MSEC_NUM) / HZ_TO_MSEC_DEN;
# endif
#endif
}
EXPORT_SYMBOL(jiffies_to_msecs);

이럴 땐 차라리 jiffies_to_msecs 함수를 어셈블리 코드로 보는게 더 편합니다.

코드 좀 분석해볼까요? 우선 이 함수에 전달되는 파라미터는 jiffies 값이라고 알고 있죠?
ARM64 함수 호출 규약에 따라 파라미터는 x0 레지스터로 전달되니 아래 코드 0xFFFFFF97D6B3FA24 주소에선 
x0가 jiffies 값을 담고 있다고 머리 속으로 그려주세요. 
그리고 이번엔 jiffies를 0x1006FF436(4302304310: 10진수)라고 가정할께요.
1 NSX:FFFFFF97D6B3FA24|jiffies_to_msecs:    stp     x29,x30,[SP,#-0x20]!   ; x29,x30,[SP,#-32]!
2 NSX:FFFFFF97D6B3FA28|                     mov     x29,SP
3 NSX:FFFFFF97D6B3FA2C|                     str     x19,[SP,#0x10]   ; x19,[SP,#16]
4 NSX:FFFFFF97D6B3FA30|                     mov     x19,x0
5 NSX:FFFFFF97D6B3FA34|                     mov     x0,x30
6 NSX:FFFFFF97D6B3FA38|                     nop
7 NSX:FFFFFF97D6B3FA3C|                     lsl     w0,w19,#0x3      ; w0,w19,#3
8 NSX:FFFFFF97D6B3FA40|                     add     w0,w0,w19,lsl #0x1   ; w0,w0,w19,lsl #1
9 NSX:FFFFFF97D6B3FA44|                     ldr     x19,[SP,#0x10]   ; x19,[SP,#16]
10 NSX:FFFFFF97D6B3FA48|                     ldp     x29,x30,[SP],#0x20   ; x29,x30,[SP],#32
11 NSX:FFFFFF97D6B3FA4C|                     ret

4번 째 줄 코드 부터 볼게요. 파라미터인 jiffies 값은 x0으로 전달된다고 했죠.
jiffies을 x19 레지스터에 저장합니다. x19은 0x1006FF436으로 업데이트됩니다.
4 NSX:FFFFFF97D6B3FA30|     mov     x19,x0

이번엔 7번째 줄 코드입니다.
7 NSX:FFFFFF97D6B3FA3C|   lsl     w0,w19,#0x3   ; w0,w19,#3

lsl 명령어는 왼쪽으로 비트 시프트 연산을 수행하는 동작입니다.
w19 주소인 0x1006FF436을 (0x1006FF436 << 3)로 연산하는 거죠.

0x1006FF436 값을 2진수로 변환하면 다음과 같은데요. 
0x1006FF436 << 3 연산 과정으로 아래 결과를 나옵니다.
100000000011011111111010000110110       // 0x1006FF436
100000000011011111111010000110110000 // 위 비트를 왼쪽으로 3비트 쉬프트한 비트    

11011111111010000110110000 이진수를 16진수로 변환하면 0x37FA1B0 값으로 되겠죠?
결국 x0에 0x37FA1B0 값이 업데이트됩니다.


이번에는 마지막 코드입니다.
8 NSX:FFFFFF97D6B3FA40|   add     w0,w0,w19,lsl #0x1   ; w0,w0,w19,lsl #1
 
이 코드는 w19 레지스터를 왼쪽으로 1비트 쉬프트한 값을 w0에 더해서 w0에 저장하는 동작입니다.
w0 = w0 + (w19,lsl #0x1)  

그럼 우선 (w19,lsl #0x1) 코드가 어떻게 실행되는지 알아볼게요.
x19에는 이 함수에 전달된 jiffies 값 0x1006FF436이 있었죠.

0x1006FF436를 왼쪽으로 1비트 쉬프트시키면 결괏값은 0xDFE86C 입니다.
0xDFE86C = 0x1006FF436 << 0x1

(2진수 쉬프트 값)
100000000011011111111010000110110
1000000000110111111110100001101100

이제까지 계산했던 정보를 모으면 다름 명령어의 결괏값을 알 수 있습니다.
8 NSX:FFFFFF97D6B3FA40|   add     w0,w0,w19,lsl #0x1   ; w0,w0,w19,lsl #1

아래 계산식으로 0x45F8A1C(73370140: 10진수)이 됩니다.
0x37FA1B0 + 0xDFE86C = w0 + (0x1006FF436 << 0x1) = w0 + (w19 << 0x1) = w0 + (w19,lsl #0x1)


<< ARM32 아키텍처 >>
jiffies란
ARM32 아키텍처에선 jiffies 변수를 확인하면 바로 jiffies 값을 확인할 수 있습니다.
그런데 long long 타입으로 jiffies_64이란 변수도 같이 선언돼 있습니다. 

jiffies는 6905112으로 처리합니다.
(long unsigned int *) &jiffies = 0xC1A02100 = jiffies_64 -> 6905112 = 0x00695D18 = '.i].'
(u64 *) &jiffies_64 = 0xC1A02100 = jiffies_64 -> 4301872408 = 0x0000000100695D18

jiffies에 접근하는 jiffy_sched_clock_read 함수를 어셈블리 코드로 볼까요? 실제 코드에서jiffies을 어떻게 접근하는지 알아보기 위해서죠.
static u64 notrace jiffy_sched_clock_read(void)
{
return (u64)(jiffies - INITIAL_JIFFIES);
}

0xC019A75C 메모리 공간엔 jiffies 변수의 위치인 0xC1A02100 메모리 값을 담고 있습니다.
NSR:C019A744|E59F3010  jiffy_sched_clock_read:    ldr     r3,  0xC019A75C
NSR:C019A748|E3A01000      mov     r1,#0x0          ; r1,#0
(long unsigned int *) &jiffies = 0xC1A02100 = jiffies_64 -> 6905112 = 0x00695D18  
 (u64 *) &jiffies_64 = 0xC1A02100 = jiffies_64 -> 4301872408
_____address|________0________4________8________C 
NSD:C1A02100|>00695D18 00000001 00000000 00000000

jiffies_to_msecs
jiffies 값에서 0xA를 곱하면 밀리 초로 변환됩니다. 이 함수에 전달된 파라미터는 r0에 담겨 있다는 점 기억하세요.
NSR:C0188C78|E3A0300A  jiffies_to_msecs:  mov     r3,#0x0A         ; r3,#10
NSR:C0188C7C|E0000093                     mul     r0,r3,r0         ; j,r3,j
NSR:C0188C80|E12FFF1E                     bx      r14

jiffies값이 0x695D18(6905112)이면 0x41DA2F0(69051120) 밀리초로 변환되는군요.
0x41DA2F0(69051120) = 0x695D18 * 0xA

jiffies 값을 보면 밀리 초 단위로 바로 변환해서 확인하거나, jiffies 값 차이를 보면 얼만큼 몇 밀리 초 만큼 차이가 있는지 바로 확인해야 합니다.

"이 포스팅이 유익하다고 생각되시면 댓글로 응원해주시면 감사하겠습니다.  
그리고 혹시 궁금점이 있으면 댓글로 질문 남겨주세요. 상세한 답글 올려드리겠습니다!"

# Reference (커널 타이머관리)

덧글

  • ym0914 2020/12/10 11:09 # 삭제 답글

    "만약 HZ가 500이면 1초당 HZ가 500번 업데이트됩니다." --> "만약 HZ 가 500이면 1초당 jiffies 가 500번 업데이트 됩니다."
    요렇게 의미하신걸까요?
  • AustinKim 2020/12/10 11:13 #

    이해하신 게 맞습니다.

    ==> '만약 HZ 가 500이면 1초당 jiffies 가 500번 업데이트됩니다.'

    ARM 기반의 실전 프로젝트에서는 HZ가 100 혹은 300입니다. 이 내용도 참고하세요.

  • ym0914 2020/12/10 13:58 # 삭제 답글

    예 감사합니다.
댓글 입력 영역