• 2023. 9. 10.

    by. 도 현

    반응형

    1. 동적 타이머의 전체 흐름

    동적 타이머의 동작은 3단계로 나눌 수 있다. 각 단계별 세부 동작을 살펴보면 다음과 같다.

    - 1단계 : 동적 타이머 초기화

     동적 타이머 초기화는 보통 드라이버 레벨에서 수행한다. 동적 타이머를 나타내는 timer_list 구조체의 필드 중에서 flags와 function만 바뀐다

    - 2단계 : 동적 타이머 등록

     동적 타이머도 마찬가지로 드라이버 레벨에서 등록된다. 각 드라이버의 시나리오에 따라 동적 타이머의 만료 시간을 1/HZ 단위로 지정한 다음 add_timer() 함수를 호출한다.

    - 3단계 : 동적 타이머 실행

     동적 타이머가 지정한 만료 시간이 되면 Soft IRQ 타이머 서비스가 동적 타이머를 실행한다.

    3단계 중 첫 단계인 동적 타이머를 초기화하는 과정을 분석해 보자. 또한 어떤 자료구조의 데이터가 바뀌는지 알아보자.

     

    2. 동적 타이머 자료구조

    동적 타이머의 초기화 과정을 살펴보기에 앞서 동적 타이머를 나타내는 자료구조인 timer_list 구조체를 구성하는 각 필드의 의미를 살펴보자.

    - struct hlist_node entry

    해시 연결 리스트이다. timer_bases 전역변수에 동적 타이머를 등록할 때 쓰인다.

    - unsigned long expires

    동적 타이머 만료 시각을 나타낸다. 이 시각에 커널 타이머가 동적 타이머의 핸들러 함수를 호출한다. 이 값의 단위는 1/HZ이다.

    - void (*function)(struct timer_list *)

    동적 타이머 핸들러 함수의 주소를 저장하는 필드이다. call_timer_fn() 함수에서 이 필드에 접근해 동적 타이머 핸들러를 호출한다.

    - u32 flags

    동적 타이머의 설정 필드이며 다음 값 중 하나로 설정된다.

    라즈비안은 CONFIG_LOCKDEP 컨피그가 기본으로 선언돼 있지 않으므로 lockdep_map 필드는 컴파일되지 않는다. 즉, lockdep_map 필드는 timer_list 구조체에 포함되지 않는다. 그렇다면 동적 타이머를 초기화하면 timer_list 중 flags만 업데이트된다. 리눅스 커널 버전에 따라 동적 타이머를 초기화하는 범위는 다르다. 라즈비안에서 구동하는 커널 4.19.30 버전에서는 flags 필드만 바뀐다.

    3. 동적 타이머 초기화 함수

    동적 타이머를 쓰려면 먼저 동적 타이머를 초기화해야한다.

    timer_setup() 함수 분석

    동적 타이머를 초기화하려면 timer_setup() 함수를 호출해야 한다. 먼저 timer_setup() 함수의 선언부를 보자. 함수에 전달되는 인자의 속성은 다음과 같다.

    - struct timer_list *timer : 동적 타이머를 나타내는 정보

    - void *func : 동적 타이머 핸들러 함수

    - unsigned int flags : 동적 타이머 플래그

    timer_setup() 함수의 구현부 코드를 분석해 보자.

    timer_setup() 함수는 timer, callback, flags 인자를 채워 그대로 __init_timer() 함수로 치환된다. __init_timer() 함수 선언부를 보면 init_timer_key() 함수로 치환된다. init_timer_key() 함수는 debug_init()과 do_init_timer() 함수를 호출한다. debug_init() 함수는 debug_timer_init() 함수와 trace_timer_init() 함수를 호출한다. 각 함수의 역할을 알아보자. debug_timer_init() 함수는 CONFIG_DEBUG_OBJECTS_TIMERS라는 디버그용 컨피그를 설정하면 실행되는 함수이다. 동적 타이머를 중복해서 초기화할 경우 WARN() 함수를 실행해서 커널 로그로 에러 메시지를 출력한다. 라즈비안에서는 기본 설정으로 이 컨피그가 비활성화돼 있다. trace_timer_init() 함수를 실행하면 ftrace의 timer_init 이벤트 로그를 출력한다. 다음 명령어로 ftrace의 timer_init  이벤트를 설정하면 trace_timer_init() 함수가 실행되며 ftrace 로그를 출력한다. timer_init 이벤트를 활성화한 상태에서 trace_timer_init() 함수를 호출하면 ftrace 로그를 출력한다. 동적 타이머를 표현하는 timer_list 구조체가 위치한 주소는 b9e7bed0이다. 정리하면 debug_init() 함수는 동적 타이머가 초기화됐다는 정보를 출력하는 역할을 한다. 이번에는 init_timer_key() 함수에서 호출하는 do_init_timer() 함수를 보자. 동적 타이머 핸들러 함수의 주소를 timer->function에 저장한다. 인자인 flags와 현재 실행 중인 CPU번호를 OR 연산한 결과를 timer->flags 필드에 저장한다. raw_smp_processor_id() 함수는 현재 실행 중인 코드가 몇 번째 CPU에서 실행 중인지 알려준다. 만약 CPU3에서 do_init_timer() 함수가 실행됐다면 timer->flags는 무슨 값일까? CPU3에서 실행 중이니 0x3이 된다. 동적 타이머를 초기화하는 흐름을 코드 분석을 통해 알아보았다. 소스코드만 분석하면 배운 내용이 머릿속에 잘 남지 않을 수도 있다. 리눅스 커널 코드는 함수의 흐름을 토대로 개념을 이해할 수도 있지만 정밀히 데이터를 연산하거나 반환하는 코드는 자료구조를 직접 눈으로 확인하면 더 빨리 이해할 수 있다. 이해를 돕기 위해 이번에는 ftrace를 이용해 앞에서 코드를 통해 분석한 동적 타이머를 초기화할 때의 자료구조를 확인해 보자. ftrace로 timer_init 관련 이벤트를 활성화하면 ftrace로그를 볼 수 있다. 

    반응형