Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

230224
1178
109352


[리눅스커널] 시간관리: msecs_to_jiffies 함수란 7. Kernel Timer Management

msecs_to_jiffies() 함수는 밀리초를 입력으로 받아 jiffies 단위 시각 정보를 반환합니다. 리눅스 커널에서 실행 시간 기준으로 흐름을 제어할 때 많이 씁니다.

msecs_to_jiffies() 함수 세부 코드 분석에 앞서 이 함수를 왜 쓰는지 알아봅시다.

당연한 이야기지만 개발자들은 실행 시간을 '초'나 '밀리 초' 단위로 생각하는 습관이 있습니다. 
100밀리 초 후에 이미 실행한 함수가 1을 반환하면 어떻게 예외 처리를 할까?” 
200밀리 초 정도 딜레이를 줘야 할까?”

그런데 커널은 HZ 단위로 실행 시간을 관리합니다. 따라서 커널이 시간을 처리하는 단위로 시간 정보를 변환시켜야 합니다. 이를 위해 msecs_to_jiffies() 함수를 호출해야 합니다. 이 함수는 가독성도 높습니다. 함수 이름처럼 ‘밀리초를 지피스로 바꾸는’ 기능이라고 유추할 수 있습니다. 

msecs_to_jiffies() 함수 선언부 분석하기
먼저 msecs_to_jiffies() 함수 선언부를 소개합니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/jiffies.h]
static __always_inline unsigned long msecs_to_jiffies(const unsigned int m);

함수에 전달하는 const unsigned int m 인자는 밀리 초 단위 정수입니다. 이 함수는 unsigned long 타입의 jiffies 를 반환합니다.

msecs_to_jiffies() 함수 예제 코드 알아보기
이번에는 실제 리눅스 커널에서 msecs_to_jiffies() 함수로 실행 흐름을 제어하는 예제 코드를 보겠습니다. 다음은 pstore 파일시스템 일부 코드입니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/fs/pstore/platform.c]
01 int pstore_register(struct pstore_info *psi)
02 {
03 struct module *owner = psi->owner;
...
04 if (pstore_update_ms >= 0) {
05 pstore_timer.expires = jiffies +
06 msecs_to_jiffies(pstore_update_ms);
07 add_timer(&pstore_timer);
08 }

06~08 번째 줄 코드를 먼저 보겠습니다. pstore_update_ms이란 밀리초 단위 상수를 msecs_to_jiffies() 함수를 써서 HZ 단위로 변환합니다. msect_to_jiffies() 함수 반환 값과 현재 시간 정보인 jiffies를 더해 pstore_timer.expires에 저장합니다. 

pstore_update_ms 는 밀리초 단위 동적 타이머 만료 시간을 저장하는 전역 변수입니다. 이렇게 동적 타이머 만료 시각을 설정한 후 07번째 줄 코드와 같이add_timer() 함수를 호출해 동적 타이머를 등록합니다. 

이번에는 다른 예제 코드를 소개합니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/arch/ia64/kernel/salinfo.c]
01 #define SALINFO_TIMER_DELAY (60*HZ)
...
02 static int __init
03 salinfo_init(void)
04 {
05 struct proc_dir_entry *salinfo_dir; /* /proc/sal dir entry */
...
06 timer_setup(&salinfo_timer, salinfo_timeout, 0);
07 salinfo_timer.expires = jiffies + SALINFO_TIMER_DELAY;
08 add_timer(&salinfo_timer);

07번째 줄 코드를 보겠습니다. 
07 salinfo_timer.expires = jiffies + SALINFO_TIMER_DELAY;

현재 시각인 jiffies에 SALINFO_TIMER_DELAY 매크로를 더해서 salinfo_timer.expires에 더합니다. salinfo_timer.expires는 동적 타이머의 만료 시간을 저장하는 필드입니다.  

여기서 SALINFO_TIMER_DELAY 플래그는 무엇일까요? 01 번째 줄 코드를 보면 알 수 있습니다. 

    SALINFO_TIMER_DELAY는 (60 * HZ)입니다. 

HZ는 1초를 의미입니다. 따라서 이 값에 60을 곱하니 SALINFO_TIMER_DELAY는 60초를 의미합니다. 

이어서 08번째 줄 코드를 해석해볼까요?
add_timer() 함수를 호출해서 60초 이후를 만료 시각으로 동적 타이머를 등록합니다.

이렇게 msecs_to_jiffies() 함수는 커널 코드에서 많이 쓰니 이 함수의 의미를 잘 알아 둡시다.

msecs_to_jiffies() 함수 구현부 분석하기
msecs_to_jiffies() 함수 선언부와 사용 예를 소개했으니 이어서 함수 구현부를 분석하겠습니다.
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/jiffies.h]
1 static __always_inline unsigned long msecs_to_jiffies(const unsigned int m)
2 {
3 if (__builtin_constant_p(m)) {
4 if ((int)m < 0)
5 return MAX_JIFFY_OFFSET;
6 return _msecs_to_jiffies(m);
7 } else {
8 return __msecs_to_jiffies(m);
9 }
10}

msecs_to_jiffies() 함수 코드는 크게 2가지 동작을 수행합니다.
입력 인자 예외 처리
실제 jiffies 단위 시간 정보 계산

먼저 3번째 줄 코드를 봅시다.
3 if (__builtin_constant_p(m)) {

msecs_to_jiffies() 함수 입력 인자인 m 값이 정수인지 체크하는 조건문입니다. msecs_to_jiffies() 함수는 인라인 형태 함수입니다. 심볼 없이 함수에 복사되는 코드 블록입니다. 컴파일러는 m이 정수가 아닌 경우 0을 정수인 경우 1을 반환합니다.

다음 4~5 번째 줄 코드를 보겠습니다.
m 인자가 0보다 작으면 MAX_JIFFY_OFFSET 플래그를 반환합니다. msecs_to_jiffies() 함수 선언부를 보면 unsigned long 타입 정수값을 반환하니 MAX_JIFFY_OFFSET 필드도 unsigned long 타입입니다.

그러면 MAX_JIFFY_OFFSET 필드는 어떤 값일까요? 이를 알아보기 위해 MAX_JIFFY_OFFSET 매크로 선언부를 보겠습니다.
#define MAX_JIFFY_OFFSET ((LONG_MAX >> 1)-1)
#define LONG_MAX ((long)(~0UL>>1))

MAX_JIFFY_OFFSET 매크로는 LONG_MAX 매크로를 치환해서 비트 연산자를 구성합니다.
결국 LONG_MAX 매크로에 선언된 연산자를 MAX_JIFFY_OFFSET에 대입하면 다음과 같습니다.
#define MAX_JIFFY_OFFSET ((((long)(~0UL>>1)) >> 1)-1)

MAX_JIFFY_OFFSET 매크로에 선언된 연산자를 단계별로 풀어 볼까요? 
((((long)(~0UL>>1)) >> 1)-1)
((((long)(0xFFFFFFFF >> 1)) >> 1)-1)
((((long)(0x3FFFFFFF)) >> 1)-1)
((0x1FFFFFFF)-1)
0x1FFFFFFE (536870910)
 
단계별로 계산한 연산 결과는 0x1FFFFFFE (536870910)입니다. 라즈베리파이에서는 HZ가 100이니 연산 결과는 5368709초이고 89,478분입니다. 밀리초를 음수로 전달하면 엄청나게 큰 HZ 단위 jiffies 값을 반환합니다.  

이번에 6번째 줄 코드를 보겠습니다. 
6 return _msecs_to_jiffies(m);

msecs_to_jiffies() 함수에 전달된 m인자로 _msecs_to_jiffies() 함수를 호출합니다.

이제 _msecs_to_jiffies() 함수를 봅시다. 
[https://github.com/raspberrypi/linux/blob/rpi-4.19.y/include/linux/jiffies.h]
01 static inline unsigned long _msecs_to_jiffies(const unsigned int m)
02 {
03 return (m + (MSEC_PER_SEC / HZ) - 1) / (MSEC_PER_SEC / HZ);
04 }
05
06 static inline  unsigned long _msecs_to_jiffies(const unsigned int m)
07 {
08  return (m + (1000L / 100) - 1) / (1000L / 100);
09 }


01~04 번째 줄 코드는 C 포멧 함수 코드입니다. 06~09번째 줄 코드는 커널 전처리 파일에서 확인한 내용입니다. _msecs_to_jiffies() 함수 구현부는 매크로 연산이니 전처리 코드로 함수를 보면 빨리 코드를 읽을 수 있습니다.


MSEC_PER_SEC 매크로는 1000L, HZ는 100입니다. 각 CPU 별로 다른 HZ 설정값에 따라 다른 연산을 수행합니다. 

_msecs_to_jiffies() 함수는 다음 수식 연산 결과를 반환합니다. 
(m + (1000L / 100) - 1) / (1000L / 100) 

이번에도 복잡해 보이는 연산자 수식을 단계별로 풀어 봅시다.
m + (1000L / 100) – 1)/(1000L / 100);
m + (10) – 1) / 10;
(m + 9) / 10;

위 연산자를 차례로 풀면 밀리 초는 (m + 9)/10 공식으로 HZ 단위로 변환됩니다. 

만약 10ms이면 jiffies 값은 어떻게 변환할까요? 다음 공식으로 1이 됩니다.
(10 + 9) / 10 = 19 / 10 = 1

그러면 11ms부터 20ms를 jiffie로 변환한 결과를 테이블로 정리해볼까요? 
밀리초  jiffies
11 2
12 2
13 2
14 2
15 2
16 2
17 2
18 2
19 2
20 2
21 3

입력이 11~20 범위면 2를 반환합니다. 따라서 HZ가 100이면 -+5ms만큼 총 10ms 오차가 있음을 알 수 있습니다.

msecs_to_jiffies() 함수 분석으로 다음 내용을 알게 됐습니다.
인자 값 음수면 엄청나게 큰 0x1FFFFFFE 값을 반환 
HZ가 100이면 10ms 시간만큼 오차가 발생

 다음 절에서는 HZ단위인 jiffies로 커널 실행 흐름을 제어하는 방법을 알아보겠습니다. 

#커널 시간관리 목차
커널 타이머 관리 주요 개념 소개
jiffies란
커널 타이머 제어
동적 타이머 초기화
동적 타이머 등록하기
동적 타이머는 누가 언제 실행하나?
라즈베리파이 커널 타이머 실습 및 로그 분석

"혹시 궁금점이 있으면 댓글로 질문 남겨주세요. 아는 한 성실히 답글 올려드리겠습니다!"
 




핑백

덧글

댓글 입력 영역