티스토리 뷰

9 시간과 타이머 관리

9.3 타이머 관리

9.3.1 타이머 개요

  • 과거 리눅스의 클래식 타이머는 HZ 단위 (1초에 n 번) 로 동작
  • 밀리초 단위로 동작하여 고해상도 처리를 하지 못해, 다른 기법들이 요구됨

타이머의 용도

  • 타임 아웃: 지정된 시간안에 조건이나 이벤트가 발생하기를 기다림. (네트워크의 패킷을 대기)
  • 타이머: 지정된 시간 후에 일을 처리함

타이머의 요구사항

  • 전력 사용 측면: 제한된 전격 용량을 사용하는 경우 에너지를 줄임.
  • 고해상도의 필요: 고해상도 타이머는 세밀한 단위로 시간(clock) 을 제공하는 디바이스가 시스템에 존재해야함. 나노초 단위로 관리

타이머의 구성요소

  • clock chip 을 CCF 등 추상화된 프레임 워크로 사용
  • 클록 이벤트 서브 시스템을 사용하여 주기적 or oneShot 을 처리

9.3.2 저해상도 타이머

타이머 활성화 및 프로세스 사용량

HZ 단위로 사용. 상호작용이 중요하지 않은 서버 및 배치 시스템은 오히려 HZ 값이 낮을 수록 좋은 평가를 받기도 함

  • do_timer: 하나의 cpu 에서 jiffies 값을 갱신. 다른 cpu 는 해당 작업을 수행하지 않음
  • update_process_time: smp 시스템의 모든 cpu 에서 수행. 모든 저해상도 타이머를 활성화, 만료 하고 스케줄링 처리

타이머 인터럽트가 발생하면, IRQ

[EL0 - 유저 앱 실행 중]
     ↓ 타이머 IRQ 발생
[GIC 인터럽트 컨트롤러]
     ↓ IRQ → EL1로 라우팅됨 (기본 설정)
[CPU 진입: EL1]
     ↓
[EL1 인터럽트 핸들러 실행] 인터럽트 번호를 가져와서 호출
     ↓
[OS 타이머 핸들러 호출]

entry.S
ENTRY(vectors)
    ventry    el1_sync_invalid        // Synchronous EL1t
    ventry    el1_irq_invalid            // IRQ EL1t
    ventry    el1_fiq_invalid            // FIQ EL1t
    ventry    el1_error_invalid        // Error EL1t

jiffies

시스템 부팅후 발생한 틱 수를 나타내는 전역 변수

// jiffies.h
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;

// kernel/time/timer.c
__visible u64 jiffies_64 __cacheline_aligned_in_smp = INITIAL_JIFFIES;
EXPORT_SYMBOL(jiffies_64);

타이머 관리 데이터 구조

타이머 별로 하나의 틱이 얼마의 시간인지 다르기 때문에, jiffies 에 원하는 값을 더해서 사용한다

약자 명칭 설명
TVR Timer Vector Root 가장 낮은 레벨 (즉시 만료에 가까운 타이머)
TVN Timer Vector Next 더 멀리 있는 만료 시각용 타이머들을 저장하는 상위 레벨
// kernel/time/timer.c

#define TVN_BITS (CONFIG_BASE_SMALL ? 4 : 6)
#define TVR_BITS (CONFIG_BASE_SMALL ? 6 : 8)
#define TVN_SIZE (1 << TVN_BITS)
#define TVR_SIZE (1 << TVR_BITS)
#define TVN_MASK (TVN_SIZE - 1)
#define TVR_MASK (TVR_SIZE - 1)
#define MAX_TVAL ((unsigned long)((1ULL << (TVR_BITS + 4*TVN_BITS)) - 1))

struct tvec {
    struct hlist_head vec[TVN_SIZE];
};

struct tvec_root {
    struct hlist_head vec[TVR_SIZE];
};

struct tvec_base {
    spinlock_t lock;
    struct timer_list *running_timer; // 특정 프로세서에서 대해 현재 실행중인 타이머
    unsigned long timer_jiffies; // 아직 체크해야할 dynamic 타이머들의 만료시간 중 가장 빨리 만료되는 값
    unsigned long next_timer; // 다음 타이머 인터럽트에 대한 보류시간
    unsigned long active_timers; // 활성화된 타이머. (연기할 수 없는 타이머가 존재할때)
    unsigned long all_timers; // active_timers + deferrable timers
    int cpu; // 타이머를 소유한 프로세서
    bool migration_enabled; // 타이머의 다른 프로세서로 이동 여부
    bool nohz_active;
    struct tvec_root tv1; // 가까운 타이머용
    struct tvec tv2; // 시간이 지나면서 점점 cascade. 한단계씩 내려옴 (아래 __run_timers)
    struct tvec tv3;
    struct tvec tv4;
    struct tvec tv5;
} ____cacheline_aligned;

9-5 kernel/time/timer.c init_timers

void __init init_timers(void)
{
    init_timer_cpus(); // 각 cpu 의 tvec_base 를 현재 jiffies 값으로 초기화
    init_timer_stats();
    timer_register_cpu_notifier();
    open_softirq(TIMER_SOFTIRQ, run_timer_softirq); // irq 의 타이머 액션을 등록
}

static void run_timer_softirq(struct softirq_action *h)
{
    struct tvec_base *base = this_cpu_ptr(&tvec_bases);

    if (time_after_eq(jiffies, base->timer_jiffies)) // timer_jiffies 를 넘어가면 돌려주자
        __run_timers(base);
}

static inline void __run_timers(struct tvec_base *base)
{
        if (!index &&
            (!cascade(base, &base->tv2, INDEX(0))) &&
                (!cascade(base, &base->tv3, INDEX(1))) &&
                    !cascade(base, &base->tv4, INDEX(2)))
            cascade(base, &base->tv5, INDEX(3));
}

9.3.3 고해상도 타이머

  • 고해상도 타이머는 레드 블랙 트레이서 시간 순서대로 정렬된다.
  • 고해상도 타이머는 주기적인 틱에 독립적이며 jiffies 를 사용하지 않고 나노초 타임 스탬프를 사용한다.

데이터 구조

  • monotonic : 부팅 후부터 시작한 나노 타임
  • realtime : 실제 세계 시간
  • ...

고해상도 타이머의 특징

  • 고해상도 타이머는 저해상도 모드에서도 구동이 가능하다.
  • 부팅중 or 커널에서 고해상도 모드 지원하지 않는 경우.

해상도 별 인터럽트 처리

타이머 모드 흐름 설명
고해상도 모드 hrtimer_interrupt()__hrtimer_run_queues() 디바이스 (clock source)에서 인터럽트 호출.
저해상도 모드 run_local_timers()__hrtimer_run_queues() 주기적인 cpu tick 인터럽트

9-6 kernel/time/hrtimer.c hrtimer_run_queues

상태 hrtimer_is_hres_enabled() allow_nohz 값 의미
고해상도 타이머 활성화 true false nohz 불필요 (이미 hrtimer 정확)
고해상도 타이머 비활성 false true nohz 필요 (틱 줄여야 함)
void hrtimer_run_queues(void)
{
    if (__hrtimer_hres_active(cpu_base)) // 고해상도 타이머가 활성되어있으면 처리할 필요가 없음. interrupt 에서 처리할테니
        return;

    if (tick_check_oneshot_change(!hrtimer_is_hres_enabled())) { // 시스템이 nohz (tickless) 로 변경 될 수 있는지 확인
        hrtimer_switch_to_hres();
        return;
    }

    now = hrtimer_update_base(cpu_base); // monotonic 클록 기준으로 offset 갱신
    __hrtimer_run_queues(cpu_base, now); // 타이머 구동
}

HRTIMER_NORESTART -> 이걸로 oneshot, periodic 구분하는듯

// allow_nohz가 true면:
// → 정말로 nohz로 바꿀지 판단하고, 가능하면 전환까지 함
// allow_nohz가 false면:
// → 전환은 안 하지만, 시스템이 nohz 가능한지 확인만 함
int tick_check_oneshot_change(int allow_nohz)
{
    struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);

    if (!test_and_clear_bit(0, &ts->check_clocks)) // 상태 체크가 필요한지 확인. 한번만 하기 위함인 것으로 보임
        return 0;

    if (ts->nohz_mode != NOHZ_MODE_INACTIVE) // 이미 nohz 모드인 경우 무시
        return 0;

    if (!timekeeping_valid_for_hres() || !tick_is_oneshot_available()) // 고해상도 모드 조건 체크
        return 0;

    if (!allow_nohz)
        return 1;

    tick_nohz_switch_to_nohz(); // nohz 모드로 전환
    return 0;
}
  • tick_nohz_switch_to_nohz : 현재 cpu 가 주기적인 틱이 아니라 oneshot 모드로 다음 필요한 타이머만 예약함
  • hrtimer_switch_to_hres : jiffies 가 아니라 고해상도 타이머로 전환

9-7 kernel/time/hrtimer.c hrtimer_switch_to_hres

static void hrtimer_switch_to_hres(void)
{
    struct hrtimer_cpu_base *base = this_cpu_ptr(&hrtimer_bases);

    // tick_nohz_switch_to_nohz 에서도 사용
    // 고해상도 사용하는 원샷모드로 변경. (tick_switch_to_oneshot) 
    if (tick_init_highres()) { 
        printk(KERN_WARNING "Could not switch to high resolution "
                    "mode on CPU %d\n", base->cpu);
        return;
    }
    base->hres_active = 1;
    hrtimer_resolution = HIGH_RES_NSEC;

    tick_setup_sched_timer();
    /* "Retrigger" the interrupt to get things going */
    retrigger_next_event(NULL);
}

9-8 kernel/time/hrtimer.c hrtimers_init

고해상도 타이머는 ktime 이라는 데이터 유형으로 나노초로 작동.

static int hrtimer_cpu_notify(struct notifier_block *self,
                    unsigned long action, void *hcpu)
{
    int scpu = (long)hcpu;

    switch (action) {

    case CPU_UP_PREPARE:
    case CPU_UP_PREPARE_FROZEN:
        init_hrtimers_cpu(scpu);
        break;

#ifdef CONFIG_HOTPLUG_CPU
    case CPU_DEAD:
    case CPU_DEAD_FROZEN:
        migrate_hrtimers(scpu);
        break;
#endif

    default:
        break;
    }

    return NOTIFY_OK;
}
static struct notifier_block hrtimers_nb = {
    .notifier_call = hrtimer_cpu_notify,
};

void __init hrtimers_init(void)
{
    hrtimer_cpu_notify(&hrtimers_nb, (unsigned long)CPU_UP_PREPARE, // init_hrtimers_cpu 콜백 호출
              (void *)(long)smp_processor_id());
    register_cpu_notifier(&hrtimers_nb); // cpu notify 체인에 등록, up/down 이벤트 발생 시 초기화 수행
}

스케줄 틱

  • 커널이 고해상도 타이머를 사용하면. 클록 이벤트 디바이스가 원샷 모드로 실행됨.
  • 스케줄 틱을 고해상도 모드에서 사용하면 주기적으로 확인하지 않고 인터럽트로 처리함. tick_cpu_scched ->sched_timer -> tick_sched_timer

주요 업무

  • cpu 가 글로벌 틱을 처리할 수 없다면 대행
  • 핸들러가 끝나면, 다음 틱이 발생 할 수 있도록 디바이스를 다시 프로그래밍.

저해상도 모드에서는 주기적인 틱을 사용중일때 jiffies 가 증가함에 따라 do_timer 를 호출했으나, 고해상도에서는 모든 cpu 가 idle 상태이고 전역 틱이 없는 상황이 발생 할 수 있음. 고해상도 모드에서는 tick_sched_timer 에서 주기적인 틱도 담당해줘야함

struct tick_sched {
    struct hrtimer            sched_timer; // 틱을 구현하는데 사용하는 타이머
    unsigned long            check_clocks; // 클록 소스에 변화가 있거나 틱 디바이스 모드가 변경될 때 설정
    enum tick_nohz_mode        nohz_mode; // 작동 모드.
    ktime_t                last_tick; // 마지막으로 프로그래밍 된 틱 시간 (ns)
    int                inidle; // nohz idle 진입 여부 확인
    int                tick_stopped; // nohz 로 틱이 멈춘 상태 확인
    unsigned long            idle_jiffies;
    unsigned long            idle_calls;
    unsigned long            idle_sleeps;
    int                idle_active;
    ktime_t                idle_entrytime;
    ktime_t                idle_waketime;
    ktime_t                idle_exittime;
    ktime_t                idle_sleeptime;
    ktime_t                iowait_sleeptime; // 주기 틱이 마지막으로 비활성화된 시간
    ktime_t                sleep_length; // 주기 틱이 비활성화된 상태로 유지되는 시간
    unsigned long            last_jiffies; // 다음 타이머가 만료될 jiffy 값
    u64                next_timer;
    ktime_t                idle_expires; // nohzidle 진입 시 nohz 만료 예정 시각 (ns)
    int                do_timer_last;
    atomic_t            tick_dep_mask;
};
댓글