Skip to content

타이머

  • 커널은 <asm/param.h> 헤더파일에 시스템 타이머의 진동수를 HZ라는 값에 저장한다.

  • 일반적으로 HZ 값은 100 또는 1000으로 설정돼있고, 커널 2.5 버전부터 ​기본값이 1000으로 상향됐다.

    • 장점: 타이머 인터럽트의 해상도와 정확도가 향상돼 더 정확한 프로세스 선점이 가능해졌다.
    • 단점: 타이머 인터럽트 처리에 더 많은 시간을 소모하고, 전력 소모가 늘어난다.
    • 실험결과 시스템 타이머를 1,000Hz로 변경해도 성능을 크게 해치지 않는다는 결론이 났다.
  • <linux/jiffies.h>에 jiffies 라는 전역변수에는 시스템 시작 이후 발생한 틱 횟수가 저장된다.

  • 타이머 인터럽트가 초당 HZ회 발생하므로 jiffies는 1초에 HZ만큼 증가한다.

  • 따라서 시스템 가동 시간은 jiffies / HZ 로 계산할 수 있다.

  • 32-bit 시스템에선 unsigned long 형인 jiffies는 HZ 100에선 497일, 1,000에선 50일이면 오버플로우가 발생한다.

  • 반면에, 64-bit 시스템에선 평생 발생하지 않는다.

    • 이 문제를 해결하기 위해 32-bit 시스템에서는 extern u64 jiffies_64 라는 변수를 만들고
    • 주 커널 이미지 링커 스크립트에 jiffies = jiffies_64라고 써서 두 변수를 겹쳐버린다.
    • 이러면 jiffies 변수는 32-bit 시스템에서도 오버플로우가 발생하지 않는다.
  • jiffies는 오버플로우일 때 다시 0으로 돌아간다.

  • 서로 다른 두 jiffies값을 올바르게 비교할 수 있도록 매크로 함수를 제공하고 있다.

    • #define time_after(a, b) ((long)(b) - (long)(a) < 0)
    • #define time_before(a, b) ((long)(a) - (long)(b) < 0)
    • 보통 a는 현재 jiffies값이, b는 비교하려는 값이 들어간다.

​1. 타이머 인터럽트

  • 타이머 인터럽트는 다음 과정을 처리한다.
  1. xtime_lock 락을 얻어 xtime, jiffies 변수에 안전하게 접근한다.
  2. 아키텍처 종속적인 tick_periodic() 함수를 호출한다.
  3. jiffies 값을 1 증가, xtime에 현재 시간을 갱신한다.
  4. 설정 시간이 만료된 동적 타이머의 핸들러를 실행한다.
// https://github.com/torvalds/linux/blob/f2e8a57ee9036c7d5443382b6c3c09b51a92ec7e/kernel/time/tick-common.c#L82C1-L102C2
/*
* Periodic tick
*/
static void tick_periodic(int cpu)
{
if (tick_do_timer_cpu == cpu) {
raw_spin_lock(&jiffies_lock);
write_seqcount_begin(&jiffies_seq);
/* Keep track of the next tick event */
tick_next_period = ktime_add_ns(tick_next_period, TICK_NSEC);
do_timer(1);
write_seqcount_end(&jiffies_seq);
raw_spin_unlock(&jiffies_lock);
update_wall_time();
}
update_process_times(user_mode(get_irq_regs()));
profile_tick(CPU_PROFILING);
}
// https://github.com/torvalds/linux/blob/f2e8a57ee9036c7d5443382b6c3c09b51a92ec7e/kernel/time/timekeeping.c#L2289
void do_timer(unsigned long ticks)
{
jiffies_64 += ticks;
calc_global_load();
}
// https://github.com/torvalds/linux/blob/f2e8a57ee9036c7d5443382b6c3c09b51a92ec7e/kernel/time/timer.c#L2064
/*
* Called from the timer interrupt handler to charge one tick to the current
* process. user_tick is 1 if the tick is user time, 0 for system.
*/
void update_process_times(int user_tick)
{
struct task_struct *p = current;
/* Note: this timer irq context must be accounted for as well. */
account_process_tick(p, user_tick);
run_local_timers();
rcu_sched_clock_irq(user_tick);
#ifdef CONFIG_IRQ_WORK
if (in_irq())
irq_work_tick();
#endif
scheduler_tick();
if (IS_ENABLED(CONFIG_POSIX_TIMERS))
run_posix_cpu_timers();
}
  • 1/HZ 초마다 한 번씩 타이머 인터럽트가 발생해 tick_periodic() 핸들러가 호출된다.
  • do_timer() 에서는 jiffies를 증가하고 시스템 내 여러 통계 변수를 갱신한다.
  • update_process_time() 에서는
    • account_process_tick() 함수에서 프로세서의 시간을 갱신한다.
    • run_local_timers() 함수에서 제한시간이 만료된 타이머들의 핸들러를 실행한다.
    • schedule_tick() 함수는 현재 프로세스의 타임슬라이스 값을 줄이고, 필요한 경우 need_sched 플래그를 설정해 스케줄링 여부를 결정한다.

​2. 타이머

// <linux/timer.h>
struct timer_list {
struct list_head entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
struct tvec_base *base;
};
struct timer_list my_timer;
// 1. 타이머를 생성하고 초기화한다.
init_timer(&my_timer);
my_timer.expires = jiffies + delay;
my_timer.data = 0;
my_timer.function = my_function;
// 2. 타이머를 활성화한다.
add_timer(&my_timer);
// 3. 타이머의 만료 시간을 갱신한다.
mod_timer(&my_timer, jiffies + new_delay);
// 4. 타이머를 제거한다.
del_timer(&my_timer);
  • 커널 타이머는 초기화 작업 → 핸들러 설정 → 타이머 활성화로 사용하고 만료된 이후 자동으로 소멸된다.
  • 타이머는 비동기적으로 실행되므로 race condition이 발생할 잠재적인 위험이 있다. 따라서 del_timer() 함수 보다는 조금 더 안전한 버전인 del_timer_sync() 함수를 사용하자.

3. 작은 지연

  • 간혹 커널 코드에서는 아주 짧지만 정확한 지연 시간이 필요한 경우가 있다.
  • 커널의 <linux/delay.h>에는 jiffies 값을 사용하지 않고도 지연처리를 하는 mdelay(), udelay(), ndelay() 3가지 함수를 제공한다.
    • 물론, 지연 하는 동안 시스템이 동작을 정지하므로 꼭 필요한 경우가 아니면 절대 쓰면 안 된다.
  • 더 적당한 해결책은 schedule_timeout() 함수를 사용하는 것이다.
    • 최소한 인자로 넘긴 지정한 시간만큼 해당 작업이 휴면 상태로 전환됨을 보장한다.
    • 당연하지만, 이 함수는 프로세스 컨텍스트에서만 사용할 수 있다.

참고