Linux Kernel(4.19) Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

15192
888
89788


[라즈베리파이] 커널 타이머 - 동적 타이머 초기화 7장. 타이머관리

동적 타이머 초기화를 알아 보기 앞서 동적 타이머는 전체 동작 흐름에 대해 알아 봅시다.
동적 타이머 동작은 3단계로 나눌 수 있습니다. 
1. 동적 타이머 초기화
2. 동적 타이머 실행
3. 동적 타이머 만료
 3.1 동적 타이머 해제
 3.2 동적 타이머 핸들러 실행

첫 번째 초기화 단계입니다.
동적 타이머 초기화는 보통 드라이버 레벨에서 수행합니다. 동적 타이머는 struct timer_list 이란 자료구조로 표현할 수 있는데 이 멤버 중 flags만 업데이트 합니다.

두 번째 동적 타이머 실행 단계입니다.
동적 타이머 실행도 마찬가지로 드라이버 레벨에서 이루어집니다. 각자 드라이버 시나리오에 따라 동적 타이머 만료 시간을 HZ 단위로 지정한 다음에 add_timer() 함수를 호출합니다.

이제는 동적 타이머가 지정한 만료 시간이 되면 커널 시스템 타이머가 동적 타이머를 처리합니다.

3단계 중 첫 단계인 동적 타이머를 어떻게 초기화하는지 살펴봅시다. 이 과정에서 어떤 자료구조가 업데이트 되는지도 알아보겠습니다.

동적 타이머 초기화 과정을 살펴보기 전에 우선 동적 타이머를 표현하는 자료구조를 알아 봅시다.
동적 타이머를 표현하는 자료구조는 struct timer_list이며 코드는 다음과 같습니다.
[include/linux/timer.h]
1 struct timer_list {
2 struct hlist_node entry;
3 unsigned long expires;
4 void (*function)(unsigned long);
5 unsigned long data;
6 u32 flags;
#ifdef CONFIG_LOCKDEP
7 struct lockdep_map lockdep_map;
#endif
};

각 멤버들의 의미를 살펴보겠습니다.

entry
해시 링크드 리스트인데 이 멤버는 timer_bases 전역 변수에 동적 타이머를 등록할 때 쓰입니다.

expires
동적 타이머가 만료할 시각을 저장합니다. 이 시각에 커널 시스템 타이머가 동적 타이머의 핸들러 함수를 호출합니다. 이 값의 단위는 HZ입니다.

(*function)(unsigned long)
동적 타이머 핸들러 함수 주소를 저장하는 변수입니다. 커널 시스템 타이머는 call_timer_fn() 함수를 실행할 때 이 변수에 접근해서 동적 타이머 핸들러를 호출합니다.

data
동적 타이머 핸들러에 전달하는 매개변수입니다. 보통 디바이스 드라이버 정보를 모두 담긴 주소를 전달합니다.

flags
타이머 설정 필드입니다. 다음 매크로 중 하나입니다.
[include/linux/timer.h]
#define TIMER_CPUMASK 0x0003FFFF
#define TIMER_MIGRATING 0x00040000
#define TIMER_BASEMASK (TIMER_CPUMASK | TIMER_MIGRATING)
#define TIMER_DEFERRABLE 0x00080000
#define TIMER_PINNED 0x00100000
#define TIMER_IRQSAFE 0x00200000
#define TIMER_ARRAYSHIFT 22
#define TIMER_ARRAYMASK 0xFFC00000

라즈비안은 CONFIG_LOCKDEP 컨피그가 선언돼 있지 않기 때문에 lockdep_map 변수를 struct timer_list 구조체에 포함돼 있지 않습니다.

동적 타이머를 초기화하면 struct timer_list 중 어떤 멤버가 초기화될까요? flags만 업데이트됩니다. 리눅스 커널 버전에 따라 동적 타이머를 초기화하는 범위가 다른데 라즈비안에서 구동하는 커널 4.14.43 버전은 flags 멤버만 업데이트 하는 수준입니다.
 
이제 어떤 함수을 써야 동적 타이머를 초기화할 수 있는지 알아 봅시다.
드라이버에서 로컬 타이머를 초기화할 때 보통 init_timer() 함수 혹은 setup_timer() 함수를 씁니다. 

이해를 돕기 위해 먼저 두 함수들을 써서 동적 타이머를 초기화하는 코드를 보겠습니다. 다음 두 코드들은 라즈비안와 다른 리눅스 시스템에서 로컬 커널 타이머를 초기화 합니다.
먼저 라즈비안에서 구현된 드라이버 코드를 봅시다.
[drivers/mmc/host/bcm2835-sdhost.c]
1 int bcm2835_sdhost_add_host(struct bcm2835_host *host)
2 {
3 struct mmc_host *mmc;
...
4 setup_timer(&host->timer, bcm2835_sdhost_timeout,
5     (unsigned long)host);

첫 번째 bcm2835_sdhost_add_host() 함수 4번째 줄 코드를 보면 setup_timer() 함수를 써서 로컬 타이머를 초기화합니다. setup_timer() 함수로 3개 인자를 전달합니다. host->timer 는 struct timer_list 구조체 주소, bcm2835_sdhost_timeout() 함수는 동적 타이머 핸들러 그리고 host는 bcm2835_sdhost_timeout() 핸들러 함수로 전달하는 매개변수입니다.

이번에는 다른 리눅스 시스템에서 코드를 보겠습니다.
[https://elixir.bootlin.com/linux/v4.14.43/source/drivers/usb/phy/phy-isp1301-omap.c#L1491] 
static int
6 isp1301_probe(struct i2c_client *i2c, const struct i2c_device_id *id)
7 {
...
8 init_timer(&isp->timer);
9 isp->timer.function = isp1301_timer;
10 isp->timer.data = (unsigned long) isp;

isp1301_probe() 함수에서는 8번 코드와 같이 init_timer() 함수로 struct timer_list 구조체인 &isp->timer 로 동적 타이머를 초기화합니다. 다음 9번 줄 코드에서 isp1301_timer () 함수를 핸들러 함수로 지정하고 10번 줄 코드에서 isp는 핸들러 함수 매개 변수로 지정합니다. 

서로 다른 함수를 써서 동적 타이머를 초기화합니다. 그럼 두 함수의 차이점은 뭘까요? 각각 함수를 분석하면서 두 함수의 차이점을 확인하겠습니다. 

먼저 init_timer() 함수 코드를 분석 합시다.
#define init_timer(timer) \
__init_timer((timer), 0)

#define __init_timer(_timer, _flags) \
do { \
static struct lock_class_key __key; \
init_timer_key((_timer), (_flags), #_timer, &__key); \
} while (0)

void init_timer_key(struct timer_list *timer, unsigned int flags,
    const char *name, struct lock_class_key *key)
{
debug_init(timer);
do_init_timer(timer, flags, name, key);
}

init_timer() 함수 구현부를 보니 여러 매크로 함수로 치환되고 있습니다. init_timer()  함수는 __init_timer() 함수로 치환되며 __init_timer 함수에서는 init_timer_key() 함수를 호출합니다. 

이번에는 setup_timer()  함수를 보겠습니다. setup_timer() 함수도 init_timer() 매크로 함수와 마찬가지로 init_timer_key 함수를 호출합니다.
1 #define setup_timer(timer, fn, data) \
2 __setup_timer((timer), (fn), (data), 0)
3
4#define __setup_timer(_timer, _fn, _data, _flags) \
5 do { \
6 __init_timer((_timer), (_flags)); \
7 (_timer)->function = (_fn); \
8 (_timer)->data = (_data); \
9 } while (0)
10
11#define __init_timer(_timer, _flags) \
12 do { \
13 static struct lock_class_key __key; \
14 init_timer_key((_timer), (_flags), #_timer, &__key); \
15 } while (0)

setup_timer() 함수를 호출하면 위 7~8번 줄 코드와 같이 struct timer_list란 구조체 멤버 중 function과 data를 setup_timer() 함수에서 전달된 인자로 초기화합니다. 여기 function은 동적 타이머 핸들러 함수의 위치이고 data는 동적 타이머 핸들러 함수에 전달하는 매개 인자를 의미합니다. 이점이 init_timer() 함수를 호출해서 로컬 타이머를 초기화할 때와 다른 점입니다. 정리하면 init_timer()과 setup_timer() 두 함수를 호출하면 init_timer_key() 이라는 함수가 실행합니다. 2번, 6번, 11번, 14번 줄 코드를 눈여겨 봅시다.

어느 함수를 써도 로컬 타이머를 초기화하는 동작은 같습니다. 이를 확인하기 위해 두 함수의 구현부 코드를 봅시다.
1 #define init_timer(timer) \
2 __init_timer((timer), 0)
3
4#define setup_timer(timer, fn, data) \
5 __setup_timer((timer), (fn), (data), 0)

1번 줄을 보면 init_timer이란 매크로에 struct timer_list 타입인 timer이란 인자를 전달합니다. 여기서 timer 인자는 struct timer_list 구조체 주소를 의미합니다.

이번에는 4번 줄을 보겠습니다.
setup_timer() 함수에는 3개 인자를 전달합니다. 입력인자 timer, fn, data에 struct timer_list 구조체의 포인터와 타이머 핸들러 함수 그리고 타이머 핸들러 핸들을 지정해줘야 합니다.

이제 두 매크로 함수에서 공통으로 호출하는 init_timer_key() 함수를 봅시다.
void init_timer_key(struct timer_list *timer, unsigned int flags,
    const char *name, struct lock_class_key *key)
{
debug_init(timer);
do_init_timer(timer, flags, name, key);
}

init_timer_key() 함수는 debug_init()와 do_init_timer() 함수를 호출하니 우선 debug_init() 함수부터 살펴봐야겠습니다. 
1 static inline void debug_init(struct timer_list *timer)
2 {
3 debug_timer_init(timer);
4 trace_timer_init(timer);
5}

debug_init() 함수를 보니 debug_timer_init() 함수와 trace_timer_init() 함수를 호출합니다.

3번째 줄 debug_timer_init() 함수는 CONFIG_DEBUG_OBJECTS_TIMERS란 디버그용 컨피그를 설정하면 실행하는 함수입니다. 동적 타이머를 중복해서 초기화를 할 경우 WARN 매크로를 실행해서 콜스택을 출력하는 동작을 수행합니다. 라즈베리안에서는 기본설정으로 이 컨피그는 꺼져 있습니다.

4번 줄 trace_timer_init 함수는 ftrace timer_init 이벤트 로그를 출력합니다. 다음 명령어로 ftrace의 timer_init 이벤트를 설정하면 trace_timer_init () 함수가 실행하면서 ftrace 로그를 출력합니다. 
“echo 1 > /sys/kernel/debug/tracing/event/timer/timer_init/enable"

위와 같이 timer_init 이벤트를 킨 상태에서 trace_timer_init() 함수가 실행하면 다음과 같은 ftrace 로그를 출력합니다.
rcu_sched-8     [002] ....  5181.011526: timer_init: timer=b9e7bed0

위 로그는 5181.011526 초에 동적 타이머를 초기화했다는 메시지입니다. 동적 타이머를 표현하는 struct timer_list 구조체가 위치한 주소는 b9e7bed0입니다.

정리하면 debug_init() 함수는 로컬 커널 타이머 디버깅 정보를 출력합니다.

이제, do_init_timer 함수를 호출하면 어떤 과정으로 해당 타이머를 초기화하는지 알아봅시다. 
1 static void do_init_timer(struct timer_list *timer, unsigned int flags,
2   const char *name, struct lock_class_key *key)
3 {
4 timer->entry.pprev = NULL;
5 timer->flags = flags | raw_smp_processor_id();
6 lockdep_init_map(&timer->lockdep_map, name, key, 0);
7}

5줄 코드부터 분석하겠습니다.
입력인자 flags와 현재 실행 중인 CPU번호를 OR 연산한 후 timer->flags 멤버에 저장합니다. raw_smp_processor_id() 함수는 현재 실행 중인 코드가 몇 번 째 CPU에서 수행 중인지 알려줍니다. 

만약 do_init_timer() 함수로 전달되는 flags 인자가 0x2000000이고 CPU3번에서 do_init_timer() 함수가 실행했다면 다음 공식으로 timer->flags는 0x2000003이 됩니다.
timer->flags= 0x2000003 = 0x2000000 + 0x3

이렇게 동적 타이머를 초기화하는 흐름을 코드 분석으로 알아봤습니다. 분석한 내용이 잘 이해가시나요? 소스 코드만 분석하면 배운 내용이 머릿속에 잘 남지 않을 수도 있습니다.

리눅스 커널 코드는 함수 흐름으로 개념을 이해할 수도 있지만 정밀히 데이터를 연산하거나 변환하는 코드는 자료구조를 직접 눈으로 확인하는 것이 이해가 빠릅니다. 이해를 돕기 위해 이번에는 ftrace와 Trace32 프로그램으로 코드로 분석한 동적 타이머를 초기화할 때 자료구조를 확인하겠습니다.

ftrace로 timer_init 이벤트를 키면 다음과 같은 ftrace 로그를 볼 수 있다고 했습니다.
 rcu_sched-8     [002] ....  5181.011526: timer_init: timer=b9e7bed0

위 코드에서 볼드체로 002라고 돼 있는 숫자는 이 코드를 실행하고 있는 CPU번호를 의미합니다. rcu_sched는 프로세스 이름이고 8을 pid를 의미합니다. CPU2에서 “rcu_sched” 이란 프로세스가 구동 중입니다. 만약 위 ftrace 로그가 실행됐을 때는 timer->flags 멤버는 2로 업데이트 됩니다. 이 함수는 CPU2에서 구동하고 있기 때문입니다. 

이번에 ftrace 로그가 출력할 때 struct timer_list 구조체를 Trace32로 확인해 봅시다.
view.var %type %symbol struct timer_list *)0xb9e7bed0
 (struct timer_list *) (struct timer_list *)0xb9e7bed0 = 0xB9E7BED0 -> (
    (struct hlist_node) entry = ((struct hlist_node *) next = 0x0, 
    (long unsigned int) expires = 0,
    (void (*)()) function = 0x0,
    (long unsigned int) data = 0,
    (u32) flags = 2)

각각 멤버들은 0x0인데 flags 멤버만 2입니다. CPU2에서 init_timer_key() 함수를 실행해기 때문입니다.

이번에 분석한 내용을 정리하면, init_timer() 함수를 호출하면 간단히 로컬 타이머를 초기화합니다. 현재 구동 중인 CPU 번호를 계산해서 timer->flags 변수에 저장할 뿐입니다.

그러면 현재 실행 중인 CPU번호로 timer->flags 변수를 저장하는 이유는 뭘까요? 동적 타이머를 실행할 때 이 값을 바탕으로 struct timer_base 구조체 per-cpu 타입 timer_bases 주소에 접근합니다. 조금 쉽게 설명을 드리면 로컬 타이머를 설정한 CPU번호가 1번이면 per-cpu1 timer_bases를 로딩해서 로컬 타이머를 저장합니다. 이 내용은 다음에 상세히 다룰 예정입니다.



.

핑백

덧글

댓글 입력 영역