PELT(Per-Entity Load Tracking) – v4.0

<kernel v4.0>

PELT: Per-Entity Load Tracking

커널 v5.4에 대한 최근 글은 다음을 참고한다.

 

엔티티 로드 평균을 산출하는 update_entity_load_avg() 함수는 다음과 같은 함수에서 호출된다.

  • enqueue_entity_load_avg()
    • cfs 런큐에 태스크가 엔큐될 때
  • dequeue_eneity_load_avg()
    • cfs 런큐에서 태스크가 디큐될 때
  • put_prev_entity()
    • 현재 동작 중인 태스크를 런큐의 대기로 돌려질 때
  • entity_tick()
    • 스케줄 틱마다 호출될 때

 

엔큐 시 엔티티 로드 평균 산출

enqueue_entity_load_avg()

kernel/sched/fair.c

/* Add the load generated by se into cfs_rq's child load-average */
static inline void enqueue_entity_load_avg(struct cfs_rq *cfs_rq,
                                                  struct sched_entity *se,
                                                  int wakeup)
{       
        /*
         * We track migrations using entity decay_count <= 0, on a wake-up
         * migration we use a negative decay count to track the remote decays
         * accumulated while sleeping.
         *
         * Newly forked tasks are enqueued with se->avg.decay_count == 0, they
         * are seen by enqueue_entity_load_avg() as a migration with an already
         * constructed load_avg_contrib.
         */
        if (unlikely(se->avg.decay_count <= 0)) {
                se->avg.last_runnable_update = rq_clock_task(rq_of(cfs_rq));
                if (se->avg.decay_count) {
                        /*
                         * In a wake-up migration we have to approximate the
                         * time sleeping.  This is because we can't synchronize
                         * clock_task between the two cpus, and it is not
                         * guaranteed to be read-safe.  Instead, we can
                         * approximate this using our carried decays, which are
                         * explicitly atomically readable.
                         */
                        se->avg.last_runnable_update -= (-se->avg.decay_count)
                                                        << 20;
                        update_entity_load_avg(se, 0);
                        /* Indicate that we're now synchronized and on-rq */
                        se->avg.decay_count = 0;
                }
                wakeup = 0;
        } else {
                __synchronize_entity_decay(se);
        }

        /* migrated tasks did not contribute to our blocked load */
        if (wakeup) {
                subtract_blocked_load_contrib(cfs_rq, se->avg.load_avg_contrib);
                update_entity_load_avg(se, 0);
        }

        cfs_rq->runnable_load_avg += se->avg.load_avg_contrib;
        /* we force update consideration on load-balancer moves */
        update_cfs_rq_blocked_load(cfs_rq, !wakeup);
}

엔티티 로드 평균을 갱신한다.

  • 코드 라인 15~32에서 낮은 확률로 decay_count가 0이하인 경우 avg.last_runnable_update에서 음수 decay_count 만큼의 ms 단위의 기간을 감소시키고 decay_count는 0으로 변경한다. 그리고 엔티티 로드 평균을 구하고 wakeup 요청을 0으로 한다.
    • 처음 태스크가 fork된 경우 decay_count 값은 0이하 이다.
  • 코드 라인 33~35에서 스케줄 엔티티의 decay_count가 0을 초과한 경우 엔티티 decay를 동기화시킨다.
    • 스케줄 엔티티의 로드 평균 기여를 ‘cfs 런큐의 decay_counter – 스케줄 엔티티의 avg.decay_count’ 만큼 decay 한다.
  • 코드 라인 38~41에서 decay_count가 0을 초과하였었고, 인수wakeup 요청이 있는 경우 migrate된 태스크들은 blocked 로드에서 기여 값을 감소시키고 엔티티 로드 평균을 구한다.
  • 코드 라인 43에서 스케줄 엔티티에 있는 로드 평균 기여를 cfs 런큐의 러너블 로드 평균에  추가한다
  • 코드 라인 45에서 블럭드 로드를 갱신하고 러너블 평균과 더해 cfs 런큐의 tg_load_contrib와 태스크 그룹의 load_avg 에 추가한다.

 

rq_clock_task()

kernel/sched/sched.h

static inline u64 rq_clock_task(struct rq *rq) 
{
        lockdep_assert_held(&rq->lock);
        return rq->clock_task;
}

런큐의 clock_task 를 반환한다.

 

__synchronize_entity_decay()

kernel/sched/fair.c

/* Synchronize an entity's decay with its parenting cfs_rq.*/
static inline u64 __synchronize_entity_decay(struct sched_entity *se)
{
        struct cfs_rq *cfs_rq = cfs_rq_of(se);
        u64 decays = atomic64_read(&cfs_rq->decay_counter);

        decays -= se->avg.decay_count;
        se->avg.decay_count = 0;
        if (!decays)
                return 0;

        se->avg.load_avg_contrib = decay_load(se->avg.load_avg_contrib, decays);

        return decays;
}

스케줄 엔티티의 로드 평균 기여를 cfs 런큐의 decay_counter – 스케줄 엔티티의 avg.decay_count 만큼 decay 한다.

  • 코드 라인 5~10에서 cfs 런큐의 decay_counter에서 스케줄 엔티티의 decay_count 값을 뺀다. 스케줄 엔티티의 decay_count 값은 0으로 리셋한다. 적용할 decays 값이 없으면 결과 값을 0으로 함수를 빠져나간다.
  • 코드 라인 12에서 스케줄 엔티티의 load_avg_contrib를 decays 만큼 decay한다.
  • 코드 라인 14에서 decays 값으로 함수를 빠져나간다.

 

다음 그림은 cfs 런큐에 소속된 하나의 스케줄 엔티티에 대해 decay할 기간을 적용시킨 모습을 보여준다.

 

디큐 시 엔티티 로드 평균 산출

dequeue_entity_load_avg()

kernel/sched/fair.c

/*
 * Remove se's load from this cfs_rq child load-average, if the entity is
 * transitioning to a blocked state we track its projected decay using
 * blocked_load_avg.
 */
static inline void dequeue_entity_load_avg(struct cfs_rq *cfs_rq,
                                                  struct sched_entity *se,
                                                  int sleep)
{
        update_entity_load_avg(se, 1);
        /* we force update consideration on load-balancer moves */
        update_cfs_rq_blocked_load(cfs_rq, !sleep);

        cfs_rq->runnable_load_avg -= se->avg.load_avg_contrib;
        if (sleep) {
                cfs_rq->blocked_load_avg += se->avg.load_avg_contrib;
                se->avg.decay_count = atomic64_read(&cfs_rq->decay_counter);
        } /* migrations, e.g. sleep=0 leave decay_count == 0 */
}

스케줄 엔티티가 디큐되면서 수행할 로드 평균을 갱신한다. cfs 런큐의 러너블 로드 평균에서 스케줄 엔팉티의 로드 평균 기여를 감소시키고 인수 sleep=1 요청된 경우 cfs 런큐의 블럭드 로드 평균에 추가한다.

  • 코드 라인 10~12에서 디큐 관련 산출을 하기 전에 먼저 스케줄 엔티티의 로드 평균을 산출하고 블럭드 로드 평균까지 갱신한다.
  • 코드 라인 14에서 cfs 런큐의 러너블 로드 평균을 스케줄 엔티티의 로드 평균 기여분만큼 감소시킨다.
  • 코드 라인 15~18에서 인수 sleep=1인 경우 감소 시킨 기여분을 cfs 런큐의 블럭드 로드 평균에 추가하고 cfs 런큐의 decay_counter를 스케줄 엔티티에 복사한다.

 

다음 그림은 dequeue_eneity_load_avg() 함수의 처리 과정을 보여준다.

 

Idle 관련 엔티티 로드 평균 산출

idle 상태로 진입하거나 빠져나올 때마다 러너블 평균을 산출한다.

idle_enter_fair()

kernel/sched/fair.c

/*
 * Update the rq's load with the elapsed running time before entering
 * idle. if the last scheduled task is not a CFS task, idle_enter will
 * be the only way to update the runnable statistic.
 */
void idle_enter_fair(struct rq *this_rq)
{
        update_rq_runnable_avg(this_rq, 1);
}

idle 상태로 진입하기 전에  러너블 평균을 갱신한다.

 

idle_exit_fair()

kernel/sched/fair.c

/*
 * Update the rq's load with the elapsed idle time before a task is
 * scheduled. if the newly scheduled task is not a CFS task, idle_exit will
 * be the only way to update the runnable statistic.
 */
void idle_exit_fair(struct rq *this_rq)
{
        update_rq_runnable_avg(this_rq, 0);
}

idle 상태에서 빠져나오면 러너블 평균을 갱신하고 그 값을 태스크 그룹에도 반영한다.

 

틱 마다 스케줄 엔티티에 대한 PELT 갱신과 preemption 요청 체크

스케줄 틱마다 호출되는 scheduler_tick() 함수와 hr 틱마다 호출되는 hrtick()  함수는 현재 curr에서 동작하는 태스크의 스케줄러의 (*task_tick) 후크 함수를 호출한다. 예를 들어 현재 런큐의 curr가 cfs 태스크인 경우 task_tick_fair() 함수가 호출되는데 태스크와 관련된 스케줄 엔티티부터 최상위 스케줄 엔티티까지 entity_tick() 함수를 호출하여 PELT와 관련된 함수들을 호출하고 스케줄 틱으로 호출된 경우 리스케줄 여부를 체크하고 hr 틱으로 호출되는 경우 무조건 리스케줄 요청한다.

  • 함수 호출 경로 예)
    • 스케줄 틱: scheduler_tick() -> task_tick_fair() -> entity_tick()
      • entity_tick() 함수를 호출할 때 인수 queued=0으로 호출된다.
    • hr 틱: hrtick() -> task_tick_fair() -> entity_tick()
      • entity_tick() 함수를 호출할 때 인수 queued=1로 호출된다.

 

다음 그림은 entity_tick()에 대한 함수 흐름을 보여준다.

entity_tick()

kernel/sched/fair.c

static void
entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
        /*
         * Update run-time statistics of the 'current'.
         */
        update_curr(cfs_rq);

        /*
         * Ensure that runnable average is periodically updated.
         */
        update_entity_load_avg(curr, 1);
        update_cfs_rq_blocked_load(cfs_rq, 1);
        update_cfs_shares(cfs_rq);

#ifdef CONFIG_SCHED_HRTICK
        /*
         * queued ticks are scheduled to match the slice, so don't bother
         * validating it and just reschedule.
         */
        if (queued) {
                resched_curr(rq_of(cfs_rq));
                return;
        }
        /*
         * don't let the period tick interfere with the hrtick preemption
         */
        if (!sched_feat(DOUBLE_TICK) &&
                        hrtimer_active(&rq_of(cfs_rq)->hrtick_timer))
                return;
#endif

        if (cfs_rq->nr_running > 1)
                check_preempt_tick(cfs_rq, curr);
}

이 함수는 스케줄 틱 또는 hr 틱을 통해 호출되며 인수로 요청한 엔티티에 대한 로드(PELT) 갱신과 preemption 필요를 체크한다.

  • 코드 라인 7에서 PELT 관련 산출을 하기 전에 사전 준비를 위해 인수로 요청한 스케줄 엔티티의 런타임 통계들을 갱신한다.
  • 코드 라인 12에서 엔티티의 로드 평균 기여값을 갱신한다.
  • 코드 라인 13에서 스케줄 엔티티가 포함된 cfs 런큐의 러너블 로드와 블럭드 로드를 합하여 기여분을 cfs 런큐와 tg에 갱신한다.
  • 코드 라인 14에서 shares * 비율(tg에서 cfs 런큐의 로드 기여값이 차지하는 일정 비율)을 사용하여 스케줄 엔티티의 weight을 갱신하고 cfs 런큐의 로드 weight도 갱신한다.
  • 코드 라인 16~24에서 hrtick을 사용하는 커널에서 hrtick() 함수를 통해 호출된 경우 인수 queued=1로 진입한다. 이 때에는 무조건 리스케줄 요청을 하고 함수를 빠져나간다.
  • 코드 라인 28~30에서 DOUBLE_TICK feature를 사용하지 않는 경우 hrtick을 프로그래밍한다.
    • rpi2: DOUBLE_TICK 기능을 사용하지 않는다.
  • 코드 라인 33~34에서 이 루틴은 hrtick을 제외한 스케줄 틱을 사용할 때에만 진입되는데 러너블 태스크가 2개 이상인 경우 preemption 체크를 수행한다.

 

스케줄 엔티티의 러너블 로드 평균  갱신

 

update_entity_load_avg()

kernel/sched/fair.c

/* Update a sched_entity's runnable average */
static inline void update_entity_load_avg(struct sched_entity *se,
                                          int update_cfs_rq)
{
        struct cfs_rq *cfs_rq = cfs_rq_of(se);
        long contrib_delta;
        u64 now;

        /*
         * For a group entity we need to use their owned cfs_rq_clock_task() in
         * case they are the parent of a throttled hierarchy.
         */
        if (entity_is_task(se))
                now = cfs_rq_clock_task(cfs_rq);
        else
                now = cfs_rq_clock_task(group_cfs_rq(se));

        if (!__update_entity_runnable_avg(now, &se->avg, se->on_rq))
                return;

        contrib_delta = __update_entity_load_avg_contrib(se);

        if (!update_cfs_rq)
                return;

        if (se->on_rq)
                cfs_rq->runnable_load_avg += contrib_delta;
        else
                subtract_blocked_load_contrib(cfs_rq, -contrib_delta);
}

스케줄 엔티티에 대한 러너블 로드 평균을 갱신한다. 인수로 cfs 런큐까지 갱신 요청하는 경우 기여도를 계산하여 cfs 런큐의 러너블 로드 평균도 갱신한다.

  • a) 스케줄 엔티티에 대한 러너블 로드 평균을 산출하기 위한 러너블 평균합(runnable_avg_sum) 및 러너블 평균 기간(runnable_avg_periods) 산출
    • 기존 러너블 평균합을 decay 시킨 값을 old라고 하고,
    • 산정할 기간을 decay한 값을 new라고 할 때
    • se->avg.runnable_avg_sum에는
      • 스케줄 엔티티에 대해 러너블 상태(러닝 포함)의 태스크에 대해서는  new + old 값을 반영하고, 러너블 상태가 아닌 태스크에 대해서는 old 값만 반영한다.
      • 추후 커널 소스에서는 러닝 상태를 별도로 관리한다.
    • se->avg.runnable_avg_periods에는
      • 스케줄 엔티티의 상태와 관계 없이 new + old 값을 반영한다.
  • b) cfs 런큐의 러너블 로드 평균 갱신
    • se->avg.load_avg_contrib

 

  • 코드 라인 13~16에서 스케줄 엔티티가 태스크인 경우 now에 스케줄 엔티티가 가리키는 cfs 런큐의 클럭 태스크 값을 대입하고 그렇지 않은 경우 태스크 그룹용 cfs 런큐의 클럭 태스크 값을 대입한다.
    • 외부에서 최상위 스케줄 엔티티까지 순회하며 이 함수를 호출하는 경우 가장 하위 cfs 런큐를 두 번 호출한다.
  • 코드 라인 18~19에서 엔티티 러너블 평균 갱신을 수행하고 실패한 경우 함수를 빠져나간다.
  • 코드 라인 21에서 엔티티 로드 평균 기여값을 갱신하고 그 delta 값을 controb_delta에 대입한다.
  • 코드 라인 23~24에서 인수 update_cfs_rq가 0인 경우 함수를 빠져나간다.
  • 코드 라인 26~29에서 스케줄 엔티티가 런큐에 있는 경우 러너블 로드 평균 값에 contrib_delta 값을 더하고 그렇지 않은 경우 blocked 로드 기여 값에 contrib_delta 값을 추가한다.

 

다음 그림은 __update_entity_load_avg() 함수가 스케줄 엔티티의 러너블 평균을 구하는 모습을 보여준다.

 

다음 그림은 스케줄 틱이 동작할 때 entity_tick() 함수에서 update_entity_load_avg() 함수를 호출할 때 동작하는 모습을 보여준다.

 

entity_is_task()

kernel/sched/fair.c

/* An entity is a task if it doesn't "own" a runqueue */
#define entity_is_task(se)      (!se->my_q)

스케줄 엔티티가 태스크인지 여부를 반환한다.

  • 태스크인 경우 se->cfs_rq를 사용하고 태스크 그룹을 사용하는 경우 se->my_q를 사용한다.

 

cfs_rq_clock_task()

kernel/sched/fair.c

/* rq->task_clock normalized against any time this cfs_rq has spent throttled */
static inline u64 cfs_rq_clock_task(struct cfs_rq *cfs_rq)
{
        if (unlikely(cfs_rq->throttle_count))
                return cfs_rq->throttled_clock_task;

        return rq_clock_task(rq_of(cfs_rq)) - cfs_rq->throttled_clock_task_time;
}

스로틀 카운트가 있는 경우 cfs->throttled_clock_task를 반환하고 그렇지 않은 경우 cfs_rq->clock_task에서 스로틀 클럭 태스크 시간을 뺀 값을 반환한다.

 

group_cfs_rq()

kernel/sched/fair.c

/* runqueue "owned" by this group */
static inline struct cfs_rq *group_cfs_rq(struct sched_entity *grp)
{
        return grp->my_q;
}

스케줄 엔티티의 런큐를 반환한다.

 

update_rq_runnable_avg()

kernel/sched/fair.c

static inline void update_rq_runnable_avg(struct rq *rq, int runnable)
{
        __update_entity_runnable_avg(rq_clock_task(rq), &rq->avg, runnable);
        __update_tg_runnable_avg(&rq->avg, &rq->cfs);
}

엔티티 러너블 평균을 산출하고 태스크 그룹에 반영한다.

 

스케줄 엔티티의 러너블 평균합 갱신

__update_entity_runnable_avg()

kernel/sched/fair.c

/*
 * We can represent the historical contribution to runnable average as the
 * coefficients of a geometric series.  To do this we sub-divide our runnable
 * history into segments of approximately 1ms (1024us); label the segment that
 * occurred N-ms ago p_N, with p_0 corresponding to the current period, e.g.
 *
 * [<- 1024us ->|<- 1024us ->|<- 1024us ->| ...
 *      p0            p1           p2
 *     (now)       (~1ms ago)  (~2ms ago)
 *
 * Let u_i denote the fraction of p_i that the entity was runnable.
 *
 * We then designate the fractions u_i as our co-efficients, yielding the
 * following representation of historical load:
 *   u_0 + u_1*y + u_2*y^2 + u_3*y^3 + ...
 *
 * We choose y based on the with of a reasonably scheduling period, fixing:
 *   y^32 = 0.5
 *
 * This means that the contribution to load ~32ms ago (u_32) will be weighted
 * approximately half as much as the contribution to load within the last ms
 * (u_0).
 *
 * When a period "rolls over" and we have new u_0`, multiplying the previous
 * sum again by y is sufficient to update:
 *   load_avg = u_0` + y*(u_0 + u_1*y + u_2*y^2 + ... )
 *            = u_0 + u_1*y + u_2*y^2 + ... [re-labeling u_i --> u_{i+1}]
 */
static __always_inline int __update_entity_runnable_avg(u64 now,
                                                        struct sched_avg *sa,
                                                        int runnable)
{
        u64 delta, periods;
        u32 runnable_contrib;
        int delta_w, decayed = 0;

        delta = now - sa->last_runnable_update;
        /*
         * This should only happen when time goes backwards, which it
         * unfortunately does during sched clock init when we swap over to TSC.
         */
        if ((s64)delta < 0) {
                sa->last_runnable_update = now;
                return 0;
        }

        /*
         * Use 1024ns as the unit of measurement since it's a reasonable
         * approximation of 1us and fast to compute.
         */
        delta >>= 10;
        if (!delta)
                return 0;
        sa->last_runnable_update = now;

엔티티 러너블 평균을 산출한다.

  • rq->runnable_avg_sum
    • 태스크가 런큐에 있거나 동작중 일때의 러너블 평균 합
    • runnable load.on_rq=1
  • rq->runnable_avg_period
    • 태스크의 동작 유무와 관련 없이 러너블 평균 기간

 

  • 코드 라인 37에서 현재 시각 now에서 마지막 러너블 업데이트 갱신 시각을 뺀 값을 delta에 대입한다.
  • 코드 라인 42~45에서 delta가 음수인 경우 시간이 거꾸로 되돌려 졌다고 판단하고 예외 케이스로 last_runnable_update를 갱신하고 함수를 빠져나온다.
    • 부트업 타임에 발생할 가능성이 있다.
  • 코드 라인 51~53에서 PELT(Per Entity Load Tracking)의 스케쥴링 시간 처리 최소 단위는 1024ns(1us)이다 따라서 delta를 최소 단위 us단위로 바꾸고 만일 1us도 안되는 경우 함수를 빠져나간다.
  • 코드 라인 54에서 last_runnable_update에 현재 시각(ns)을 기록한다.

 

        /* delta_w is the amount already accumulated against our next period */
        delta_w = sa->runnable_avg_period % 1024;
        if (delta + delta_w >= 1024) {
                /* period roll-over */
                decayed = 1;

                /*
                 * Now that we know we're crossing a period boundary, figure
                 * out how much from delta we need to complete the current
                 * period and accrue it.
                 */
                delta_w = 1024 - delta_w;
                if (runnable)
                        sa->runnable_avg_sum += delta_w;
                sa->runnable_avg_period += delta_w;

                delta -= delta_w;

                /* Figure out how many additional periods this update spans */
                periods = delta / 1024;
                delta %= 1024;

                sa->runnable_avg_sum = decay_load(sa->runnable_avg_sum,
                                                  periods + 1);
                sa->runnable_avg_period = decay_load(sa->runnable_avg_period,
                                                     periods + 1);

                /* Efficiently calculate \sum (1..n_period) 1024*y^i */
                runnable_contrib = __compute_runnable_contrib(periods);
                if (runnable)
                        sa->runnable_avg_sum += runnable_contrib;
                sa->runnable_avg_period += runnable_contrib;
        }

        /* Remainder of delta accrued against u_0` */
        if (runnable)
                sa->runnable_avg_sum += delta;
        sa->runnable_avg_period += delta;

        return decayed;
}

PELT(Per-Entity Load Tracking)에 사용되는 러너블 평균 합계(sa->runnable_avg_sum)와 러너블 평균 기간(sa->runnable_avg_period)을 갱신한다.

  • 코드 라인 2에서 기존 산출된 평균 기간 runnable_avg_period를 1024us(약 1ms) 단위로 잘라 남는 기간을 delta_w에 대입한다.
  • 코드 라인 3~5에서 delta와 delta_w 값이 1ms 이상인 경우 decay 처리 한다.
    • PELT에서 1 스케쥴링(1ms)이 지나면 과거 로드에 그 기간만큼 decay 요율을 곱해서 산출한다.
  • 코드 라인 12에서 1024(약 1ms) PELT 스케쥴링 단위에서 delta_w를 빼면 남은 기간(us)이 산출된다. 이를 다시 delta_w에 대입한다.
  • 코드 라인 13~15에서 sa->runnable_avg_sum 및 sa->runnable_avg_period에 delta_w 값을 추가하여 PELT 스케줄링 최소 단위로 정렬하게 한다. 만일 인수 runnable이 0인 경우에는 sa->runnable_avg_sum은 갱신하지 않는다.
  • 코드 라인 17에서 delta 값에서 방금 처리한 delta_w를 감소시킨다.
  • 코드 라인 20~21에서 delta에 몇 개의 1ms 단위가 있는지 그 횟수를 periods에 대입한다. 이 값은 decay 횟수로 사용된다. 그리고 delta 값도 나머지 값만 사용한다.
  • 코드 라인 23~24에서 sa->runnable_avg_sum에 periods+1 만큼의 decay 횟수를 적용하여 다시 갱신한다.
  • 코드 라인 25~26에서 sa->runnable_avg_period에 periods+1 만큼의 decay 횟수를 적용하여 다시 갱신한다.
  • 코드 라인 29~32에서 runnable_contrib를 산출하여 sa->runnable_avg_sum 및 runnable_avg_period에 더해 갱신한다. 만일 인수 runnable이 0인 경우에는 sa->runnable_avg_sum은 갱신하지 않는다.
  • 코드 라인 36~40에서 남은 delta를 sa->runnable_avg_sum 및 runnable_avg_period에 더해 갱신하고 decay 여부를 반환한다. 만일 인수 runnable이 0인 경우에는 sa->runnable_avg_sum은 갱신하지 않는다.

 

다음 그림은 갱신 기간이 1ms가 못되어 decay를 수행하지 않는 경우의 모습을 보여준다.

 

다음 그림은 갱신 기간이 1ms를 초과하여 decay를 수행하는 경우의 모습을 보여준다.

 

기존(old) 평균의 감소(decay)

기존 로드 값에 대해 ms 단위의 지나간 기간 만큼의 감쇠 비율(decay factor)로 곱한다. decay 요율은 ‘y^32 = 0.5’를 사용하는데 32ms 이전의 값은 0.5의 감쇠 비율(decay factor)을 사용한다. 이러한 경우 기존 로드 값이 512라고 할 때 4ms의 기간이 지나 감소된 로드 값은 y^4가 된다. 먼저 y에 대한 값을 계산해 보면 y=0.5^(1/32) = 0.978572… 가 산출된다. 이제 결정된 y 값을 사용하여 4ms 기간이 지난 감쇠 비율(decay factor)을 계산해 보면 ‘0.5^(1/32)^4 = 0.917004… ‘와 같이 산출된다.  결국 ‘512 * 0.91700404 = 469’라는 로드 값으로 줄어든다. 리눅스는 소수 표현을 사용하지 않으므로 빠른 연산을 하기 위해 ms 단위의 기간별로 미리 산출해둔 감쇠 비율을 32bit shift 값을 사용한 정수로 mult화 시켜 미리 테이블로 만들어 두고 활용한다. (0.91700404 << 32 = 0xeac0c6e6)

기존 로드 평균 값에 대해 ms 단위의 지나간 기간 만큼 감소(decay)한다.

  • 예) val=0x64, 기간 n=0~63(ms)일 때 산출되는 값들의 변화
    • 산출 식: ((val >> (n / 32)) * 테이블[n % 32]) >> 32
    • 1 = (0x64 * 0xfa83b2da) >> 32 = 0x61
    • 2 = (0x64 * 0xf5257d14) >> 32 = 0x5f
    • 31 = (0x64 * 0x82cd8698) >> 32 = 0x33
    • 32 = (0x32 * 0xffffffff) >> 32 = 0x31
    • 33 = (0x32 * 0xfa83b2da) >> 32 = 0x30
    • 34 = (0x32 * 0xf5257d14) >> 32 = 0x2f
    • 63 = (0x32 * 0x82cd8698) >> 32 = 0x19

 

decay_load()

kernel/sched/fair.c

/*
 * Approximate:
 *   val * y^n,    where y^32 ~= 0.5 (~1 scheduling period)
 */
static __always_inline u64 decay_load(u64 val, u64 n)
{
        unsigned int local_n;

        if (!n)
                return val;
        else if (unlikely(n > LOAD_AVG_PERIOD * 63))
                return 0;

        /* after bounds checking we can collapse to 32-bit */
        local_n = n;

        /*
         * As y^PERIOD = 1/2, we can combine
         *    y^n = 1/2^(n/PERIOD) * y^(n%PERIOD)
         * With a look-up table which covers y^n (n<PERIOD)
         *
         * To achieve constant time decay_load.
         */
        if (unlikely(local_n >= LOAD_AVG_PERIOD)) {
                val >>= local_n / LOAD_AVG_PERIOD;
                local_n %= LOAD_AVG_PERIOD;
        }

        val *= runnable_avg_yN_inv[local_n];
        /* We don't use SRR here since we always want to round down. */
        return val >> 32;
}

로드 값 val을 n 기간(ms) 에 해당하는 감소 비율로 줄인다. n=0인 경우  1.0 요율이고, n=32인 경우 0.5 요율이다.

  • 코드 라인 9~10에서 n이 0인 경우 val 값을 그대로 사용하는 것으로 반환한다. (요율 1.0)
  • 코드 라인 11~12에서 n이 1356(32 * 63)을 초과하는 경우 너무 오래된 기간이므로 0을 반환한다.
  • 코드 라인 24~27에서 n이 32 이상인 경우 val 값을 n/32 만큼 시프트 하고 n 값은 32로 나눈 나머지를 사용한다.
  • 코드 라인 29~31에서 미리 계산해둔 테이블에서 n 인덱스에 해당하는 값을 val에 곱하고 32비트 우측 시프트하여 반환한다.

 

미리 만들어진 PELT용 decay factor

kernel/sched/fair.c

/*
 * We choose a half-life close to 1 scheduling period.
 * Note: The tables below are dependent on this value.
 */
#define LOAD_AVG_PERIOD 32
#define LOAD_AVG_MAX 47742 /* maximum possible load avg */
#define LOAD_AVG_MAX_N 345 /* number of full periods to produce LOAD_MAX_AVG */

/* Precomputed fixed inverse multiplies for multiplication by y^n */
static const u32 runnable_avg_yN_inv[] = {
        0xffffffff, 0xfa83b2da, 0xf5257d14, 0xefe4b99a, 0xeac0c6e6, 0xe5b906e6,
        0xe0ccdeeb, 0xdbfbb796, 0xd744fcc9, 0xd2a81d91, 0xce248c14, 0xc9b9bd85,
        0xc5672a10, 0xc12c4cc9, 0xbd08a39e, 0xb8fbaf46, 0xb504f333, 0xb123f581,
        0xad583ee9, 0xa9a15ab4, 0xa5fed6a9, 0xa2704302, 0x9ef5325f, 0x9b8d39b9,
        0x9837f050, 0x94f4efa8, 0x91c3d373, 0x8ea4398a, 0x8b95c1e3, 0x88980e80,
        0x85aac367, 0x82cd8698,
};

32ms까지 decay될 요율(factor)이 미리 계산되어 있는 runnable_avg_yN_inv[] 테이블은 나눗셈 연산을 하지 않게 하기 위해 decay 요율마다 mult 값이 다음 수식으로 만들어졌다.

  • n 번째 요율을 계산하는 수식(32bit shift 적용):
    • ‘y^n’은 실수 요율이고 커널에서 사용하기 위해 32비트 shift를 적용하여 정수로 바꾼다.
      • ‘y^k * 2^32’ 수식을 사용한다.  (단 y^32=0.5)
    • y값을 먼저 구해보면
      • y=0.5^(1/32)=0.97852062…
    • 인덱스 값 n에 0부터 32까지 사용한 결과 값은? (테이블 구성은 0~31 까지)
      • 인덱스 n에 0을 주면 y^0 * 2^32 = 0.5^(1/32)^0 * (2^32) = 1.0 << 32 = 0x1_0000_0000 (32bit로 구성된 테이블 값은 0xffff_ffff 사용)
      • 인덱스 n에 1 을 주면 y^1 * 2^32 = 0.5^(1/32)^1 * (2^32) = 0.97852062 << 32 = 0xfa83_b2db
      • 인덱스 n에 2을 주면 y^2 * 2^32 = 0.5^(1/32)^2 * (2^32) = 0.957603281 << 32 = 0xf525_7d15
      • 인덱스 n에 31을 주면 y^2 * 2^32 = 0.5^(1/32)^31 * (2^32) = 0.510948574 << 32 = 0x82cd_8698
      • 인덱스 n에 32을 주면 y^2 * 2^32 = 0.5^(1/32)^31 * (2^32) = 0.5 << 32 = 0x7fff_ffff

 

새(new) 기간에 대한 기여 감소 합계(decay sum)

기존 값이 ms 단위의 지나간 기간 만큼 감쇠 비율(decay factor)을 사용하여 산출한 것에 반해 새 로드 값은 1 ms 단위의 각각의 지나간 기간에 대해 적용한 감쇠 비율을 곱한 값을 모두 더해 산출한다. 예를 들어 3ms의 기간이 흐른 로드 값은 ‘1024*y^3 + 1024*y^2 + 1024*y’ = ‘959 + 980 + 1002 = 2941’이다. (y 값은 ‘0.5^(1/32) = 0.97857206…;) 즉 1ms에 해당하는 1024us의 값이 3ms가 지나면 959us로 작아졌음을 알 수 있다. 리눅스는 빠른 산출을 위해 각 기간 별로 산출된 감쇠 비율에 대한 총합(deacy sum)을 테이블로 만들어 사용한다.

__compute_runnable_contrib()

kernel/sched/fair.c

/*
 * For updates fully spanning n periods, the contribution to runnable
 * average will be: \Sum 1024*y^n
 *
 * We can compute this reasonably efficiently by combining:
 *   y^PERIOD = 1/2 with precomputed \Sum 1024*y^n {for  n <PERIOD}
 */
static u32 __compute_runnable_contrib(u64 n)
{
        u32 contrib = 0;

        if (likely(n <= LOAD_AVG_PERIOD))
                return runnable_avg_yN_sum[n];
        else if (unlikely(n >= LOAD_AVG_MAX_N))
                return LOAD_AVG_MAX;

        /* Compute \Sum k^n combining precomputed values for k^i, \Sum k^j */
        do {
                contrib /= 2; /* y^LOAD_AVG_PERIOD = 1/2 */
                contrib += runnable_avg_yN_sum[LOAD_AVG_PERIOD];

                n -= LOAD_AVG_PERIOD;
        } while (n > LOAD_AVG_PERIOD);

        contrib = decay_load(contrib, n);
        return contrib + runnable_avg_yN_sum[n];
}

인수로 받은 1ms 단위 기간의 수 n 만큼 러너블 평균에 기여할 누적값을 산출하여 반환한다.

  • y^32=0.5일 때, 1024 * sum(y^n)에 대한 결과 값을 산출하되 미리 계산된 테이블을 이용한다.
  • 스케쥴링 기간 n은 ms 단위이며 결과는 us단위로 반환된다.
  • 343ms 이상은 최대 47742us 값을 반환한다.

 

  • 코드 라인 12~13에서 n이 32 이하인 경우 미리 계산된 테이블을 사용하여 곧바로 결과 값을 반환한다.
    • 예) n=2 -> 1982
  • 코드 라인 14~15에서 n이 LOAD_AVG_MAX_N(345) 이상인 경우 최대치인 47742를 반환한다.
  • 코드 라인 18~20에서 루프를 돌 때마다 contrib를 절반으로 감소시키며 contrib에 최대치인 테이블 최대치인 23371을 더한다.
  • 코드 라인 22~23에서 LOAD_AVG_PERIOD(32) 만큼씩 n을 줄이며 32를 초과하는 경우 계속 루프를 돈다.
  • 코드 라인 25~26에서 마지막으로 contrib를 가지고 n 만큼 decay한 후 n 번째 테이블 값을 더해 반환한다.

 

다음과 같이 다양한 조건에서 결과값을 알아본다.

  • n=0
    • =runnable_avg_yN_sum[0] =0
  • n=1
    • =runnable_avg_yN_sum[1] =1002
  • n=2
    • =runnable_avg_yN_sum[2] =1982
  • n=10
    • =runnable_avg_yN_sum[10] =9103
  • n=32
    • =runnable_avg_yN_sum[32] =23371
  • n=33
    • =decay_load(23371, 1) + runnable_avg_yN_sum[1] = 22870 + 1002 = 23872
  • n=100
    • = decay_load(23371 + 23371/2 + 23371/4, 4) + runnable_avg_yN_sum[4] = decay_load(40898, 4) +  3880 = 37503 + 3880 = 41383
  • n=343
    • =decay_load(23371 + 23371/2, 23371/4, 23371/8 + 23371/16 + 23371/32, + 23371/64 + 23371/128 + 23371/256 + 23371/512, 23) + runnable_avg_yN_sum[23] = decay_load(23371 + 11685 + 5842 + 2921 + 1460 + 730 + 365 + 182 + 91 + 45, 4) + 3880 = decay_load(46692, 23) + 18340 = 28371 + 18340 = 46711
  • n >= 344
    • =47742 (max)

 

미리 만들어진 PELT용 decay factor sum

kernel/sched/fair.c

/*
 * Precomputed \Sum y^k { 1<=k<=n }.  These are floor(true_value) to prevent
 * over-estimates when re-combining.
 */
static const u32 runnable_avg_yN_sum[] = {
            0, 1002, 1982, 2941, 3880, 4798, 5697, 6576, 7437, 8279, 9103,
         9909,10698,11470,12226,12966,13690,14398,15091,15769,16433,17082,
        17718,18340,18949,19545,20128,20698,21256,21802,22336,22859,23371,
};

runnable_avg_yN_sum[] 테이블은 다음 수식으로 만들어졌다.

  • ‘sum(y^k) { 1<=k<=n } y^32=0.5’은 실수 요율이고 커널에서 사용하기 위해 정수로 바꾸기 위해 산출된 값에 12bit shift를 적용한다.
    • 1024 * Sum(y^k) { 1<=k<=n }  y^32=0.5
  • y값을 먼저 구해보면
    • y=0.5^(1/32)=0.97852062…
  • 인덱스 값 n에 1부터 31까지 사용한 결과 값은?
    • sum(1024 * y^1) { 1<=k<=1 } = 1024 * (y^1) = 1002
    • sum(1024 * y^2) { 1<=k<=2 } =1024 * (y^1 + y^2) = 1002 + 980 = 1,982
    • sum(1024 * y^3) { 1<=k<=3 } =1024 * (y^1 + y^2 + y^3) = 1,002 + 980 + 959=2,941
    • sum(1024 * y^32)=1024 * (y^1 + y^2 + y^3 + … + y^32) = 1,002 + 980 + 959 + … + 512=23,371

 

스케줄링 엔티티 로드 평균 기여 갱신

스케줄링 엔티티의 러너블 로드 평균 값과 러너블 로드 기간의 decay 산출이 완료된 후 아래 함수가 호출된다. 여기에서는 엔티티가 태스크용인지 태스크 그룹용인지에 따라 처리하는 방법이 나뉜다.

  • 태스크용 스케줄 엔티티의 경우 se->avg.load_avg_contrib 값을 산출할 때 로드 weight과 러너블 기간 비율을 곱하여 산출한다.
    • = weight * 러너블 기간 비율(러너블 평균 합 / 러너블 기간 합)
    • = se->load.weight * se->avg.runnable_avg_sum / (se->avg.runnable_avg_period+1)
  • 태스크 그룹용 스케줄 엔티티의 경우 태스크 그룹에 기여하는 정도를 알아보기 위해 nice-0의 weight를 사용하여 러너블 기간 비율을 곱하여 산출한다.
    • = nice-0용 weight * 러너블 기간 비율(러너블 평균 합 / 러너블 기간 합)
    • = 1024 * se->avg.runnable_avg_sum / (se->avg.runnable_avg_period+1)

__update_entity_load_avg_contrib()

kernel/sched/fair.c

/* Compute the current contribution to load_avg by se, return any delta */
static long __update_entity_load_avg_contrib(struct sched_entity *se)
{
        long old_contrib = se->avg.load_avg_contrib;

        if (entity_is_task(se)) {
                __update_task_entity_contrib(se);
        } else {
                __update_tg_runnable_avg(&se->avg, group_cfs_rq(se));
                __update_group_entity_contrib(se);
        }

        return se->avg.load_avg_contrib - old_contrib;
}

스케줄링 엔티티의 로드 평균 기여값을 갱신하고 그 변화값을 반환한다.

  • 코드 라인 4에서 스케줄링 엔티티의 로드 평균 기여 값을 old_contrib에 보관해둔다.
  • 코드 라인 6~7에서 태스크용 스케줄 엔티티인 경우 스케줄 엔티티의 로드 평균 기여값을 갱신한다.
  • 코드 라인 8~11에서 태스크 그룹용 스케줄 엔티티인 경우 스케줄 엔티티의 태스크 그룹용 로드 평균 기여값을 갱신한다.
  • 코드 라인 13에서 스케줄링 엔티티의 로드 평균 기여 값의 변화값을 반환한다.

 

스케줄링 엔티티 로드 평균 기여 갱신 – a) for 태스크용 se

__update_task_entity_contrib()

kernel/sched/fair.c

static inline void __update_task_entity_contrib(struct sched_entity *se)
{
        u32 contrib;

        /* avoid overflowing a 32-bit type w/ SCHED_LOAD_SCALE */
        contrib = se->avg.runnable_avg_sum * scale_load_down(se->load.weight);
        contrib /= (se->avg.runnable_avg_period + 1);
        se->avg.load_avg_contrib = scale_load(contrib);
}

태스크용 스케줄 엔티티의 로드 평균 기여값을 다음과 같이 산출한다.

  • avg.load_avg_contrib = load.weight * avg.runnable_avg_sum / (avg.runnable_avg_period + 1)

 

다음 그림은 위의 태스크용 스케줄 엔티티의 로드 평균 기여값 산출 과정을 보여준다.

 

스케줄링 엔티티 로드 평균 기여 갱신 – b) for 태스크 그룹용 se

__update_tg_runnable_avg()

kernel/sched/fair.c

/*
 * Aggregate cfs_rq runnable averages into an equivalent task_group
 * representation for computing load contributions.
 */
static inline void __update_tg_runnable_avg(struct sched_avg *sa,
                                                  struct cfs_rq *cfs_rq)
{
        struct task_group *tg = cfs_rq->tg;
        long contrib;

        /* The fraction of a cpu used by this cfs_rq */
        contrib = div_u64((u64)sa->runnable_avg_sum << NICE_0_SHIFT,
                          sa->runnable_avg_period + 1);
        contrib -= cfs_rq->tg_runnable_contrib;

        if (abs(contrib) > cfs_rq->tg_runnable_contrib / 64) {
                atomic_add(contrib, &tg->runnable_avg);
                cfs_rq->tg_runnable_contrib += contrib;
        }
}

cfs 런큐와 태스크 그룹용 스케줄 엔티티의 러너블 평균 기여값을 산출한다. 단 변화분이 절대값이 기존 값의 1/64을 초과하는 경우에만 갱신한다. 태스크 그룹은 cpu별로 동작하는 cfs 런큐의 tg_runnable_contrib를 모두 합한 값이다. 이렇게 산출된 러너블 평균은 이어진 다음 함수에서 사용된다.

  • cont = sa->runnable_avg_sum * 1024 / (sa->runnable_avg_period + 1) – cfs_rq->tg_runnable_contrib
  • tg->runnable_avg = cont
  • cfs_rq->tg_runnable_contrib += cont

 

 

__update_group_entity_contrib()

kernel/sched/fair.c

static inline void __update_group_entity_contrib(struct sched_entity *se)
{
        struct cfs_rq *cfs_rq = group_cfs_rq(se);
        struct task_group *tg = cfs_rq->tg;
        int runnable_avg;

        u64 contrib;

        contrib = cfs_rq->tg_load_contrib * tg->shares;
        se->avg.load_avg_contrib = div_u64(contrib,
                                     atomic_long_read(&tg->load_avg) + 1);

        /*
         * For group entities we need to compute a correction term in the case
         * that they are consuming <1 cpu so that we would contribute the same
         * load as a task of equal weight.
         *
         * Explicitly co-ordinating this measurement would be expensive, but
         * fortunately the sum of each cpus contribution forms a usable
         * lower-bound on the true value.
         *
         * Consider the aggregate of 2 contributions.  Either they are disjoint
         * (and the sum represents true value) or they are disjoint and we are
         * understating by the aggregate of their overlap.
         *
         * Extending this to N cpus, for a given overlap, the maximum amount we
         * understand is then n_i(n_i+1)/2 * w_i where n_i is the number of
         * cpus that overlap for this interval and w_i is the interval width.
         *
         * On a small machine; the first term is well-bounded which bounds the
         * total error since w_i is a subset of the period.  Whereas on a
         * larger machine, while this first term can be larger, if w_i is the
         * of consequential size guaranteed to see n_i*w_i quickly converge to
         * our upper bound of 1-cpu.
         */
        runnable_avg = atomic_read(&tg->runnable_avg);
        if (runnable_avg < NICE_0_LOAD) {
                se->avg.load_avg_contrib *= runnable_avg;
                se->avg.load_avg_contrib >>= NICE_0_SHIFT;
        }
}

태스크 그룹용 스케줄 엔티티의 로드 평균 기여값을 갱신한다.

  • 코드 라인 9~11에서 태스크 그룹용 로드 기여값과 shares를 곱하고 태스크 그룹용 로드 평균+1로 나눈 값을 se->avg.load_avg_contrib에 대입한다.
  • 코드 라인 36~40에서 태스크 그룹의 러너블 평균이 nice-0 weight 값인 1024보다 작은 경우에 한해 이 값을 nice-0 weight에 대한 비율로 바꾼 후 위에서 산출한 값을 곱하여 갱신한다.
    • se->avg.load_avg_contrib *= runnable_avg / 1024

 

다음 그림은 태스크 그룹용 스케줄 엔티티의 로드 평균 기여값을 산출하는 수식이다.

 

스케줄링 엔티티 블럭드 로드 갱신

update_cfs_rq_blocked_load()

kernel/sched/fair.c

/*
 * Decay the load contributed by all blocked children and account this so that
 * their contribution may appropriately discounted when they wake up.
 */
static void update_cfs_rq_blocked_load(struct cfs_rq *cfs_rq, int force_update)
{                                             
        u64 now = cfs_rq_clock_task(cfs_rq) >> 20;
        u64 decays;

        decays = now - cfs_rq->last_decay;
        if (!decays && !force_update)
                return;

        if (atomic_long_read(&cfs_rq->removed_load)) {
                unsigned long removed_load;
                removed_load = atomic_long_xchg(&cfs_rq->removed_load, 0);
                subtract_blocked_load_contrib(cfs_rq, removed_load);
        }

        if (decays) {
                cfs_rq->blocked_load_avg = decay_load(cfs_rq->blocked_load_avg,
                                                      decays);
                atomic64_add(decays, &cfs_rq->decay_counter);
                cfs_rq->last_decay = now;
        }

        __update_cfs_rq_tg_load_contrib(cfs_rq, force_update);
}

blocked 로드 평균을 업데이트한다. blocked_load_avg에서 removed_load 만큼을 감소시킨 값을 ms 단위로 decay할 기간만큼 decay 한다.

  • 코드 라인 7에서 현재 클럭 태스크의 시각을 ms 단위로 변환하여 now에 대입한다.
  • 코드 라인 10~12에서 now와 최종 decay 시각과의 차이를 decays에 대입한다. 만일 decays가 0이고 force_update 요청이 없는 경우 함수를 빠져나간다.
  • 코드 라인 14~18에서 removed_load가 있는 경우 0으로 대입하고 기존 값을 blocked 로드 평균에서 감소시킨다.
  • 코드 라인 20~25에서 decays 기간이 있는 경우 blocked_load_avg 값을 decays 만큼 decay 시킨 후 decay_counter에 decays를 추가하고 현재 갱신 시각 now를 last_decay에 저장한다.
  • 코드 라인 27에서 태스크 그룹용 로드 평균 기여를 갱신한다.

 

다음 그림은 blocked_load_avg – removed_load 값을 10ms decay 하는 모습을 보여준다.

 

subtract_blocked_load_contrib()

kernel/sched/fair.c

static inline void subtract_blocked_load_contrib(struct cfs_rq *cfs_rq,
                                                 long load_contrib)
{
        if (likely(load_contrib < cfs_rq->blocked_load_avg))
                cfs_rq->blocked_load_avg -= load_contrib;
        else
                cfs_rq->blocked_load_avg = 0;
}

blocked 로드 평균에서 요청한 로드 기여값을 감소시킨다. 만일 0보다 작아지는 경우 0을 대입한다.

 

__update_cfs_rq_tg_load_contrib()

kernel/sched/fair.c

static inline void __update_cfs_rq_tg_load_contrib(struct cfs_rq *cfs_rq,
                                                 int force_update)
{
        struct task_group *tg = cfs_rq->tg;
        long tg_contrib;

        tg_contrib = cfs_rq->runnable_load_avg + cfs_rq->blocked_load_avg;
        tg_contrib -= cfs_rq->tg_load_contrib;

        if (!tg_contrib)
                return;

        if (force_update || abs(tg_contrib) > cfs_rq->tg_load_contrib / 8) {
                atomic_long_add(tg_contrib, &tg->load_avg);
                cfs_rq->tg_load_contrib += tg_contrib;
        }
}

태스크 그룹용 로드 평균 기여를 갱신한다.

  • tmp = cfs_rq->runnable_load_avg + cfs_rq->blocked_load_avg – cfs_rq->tg_load_contrib
    • cfs_rq->tg_load_contrib = tmp
    • tg->load_avg = tmp

 

다음 그림은 태스크 그룹용 스케줄 엔티티의 로드 평균과 로드 기여값을 갱신하는 모습을 보여준다.

 

CFS Shares 갱신으로 인한 로드 weight 재반영

kernel/sched/fair.c

update_cfs_shares()

static void update_cfs_shares(struct cfs_rq *cfs_rq)
{
        struct task_group *tg;
        struct sched_entity *se;
        long shares;

        tg = cfs_rq->tg;
        se = tg->se[cpu_of(rq_of(cfs_rq))];
        if (!se || throttled_hierarchy(cfs_rq))
                return;
#ifndef CONFIG_SMP
        if (likely(se->load.weight == tg->shares))
                return;
#endif
        shares = calc_cfs_shares(cfs_rq, tg);

        reweight_entity(cfs_rq_of(se), se, shares);
}

태스크 그룹의 shares 값에 대한 cfs 런큐의 로드 비율을 적용하여 스케줄 엔티티, cfs 런큐 및 런큐등의 로드 weight을 갱신한다.

  • 코드 라인 7~8에서 cfs 런큐로 태스크 그룹 및 스케줄 엔티티를 알아온다.
  • 코드 라인 9~10에서 스케줄 엔티티가 없거나 cfs 런큐가 스로틀된 경우 함수를 빠져나간다.
  • 코드 라인 11~14에서 UP 시스템에서 태스크 그룹의 shares 값과 스케줄 엔티티의 로드 값이 동일하면 최대치를 사용하는 중이므로 함수를 빠져나간다.
  • 코드 라인 15에서 태스크 그룹의 cfs shares 값에 cfs 런큐 로드 비율이 반영된 로드 weight 값을 산출한다
  • 코드 라인 17에서 산출된 shares 값으로 스케줄 엔티티의 로드 weight 값을 재계산한다.

 

calc_cfs_shares()

kernel/sched/fair.c

static long calc_cfs_shares(struct cfs_rq *cfs_rq, struct task_group *tg)
{
        long tg_weight, load, shares;

        tg_weight = calc_tg_weight(tg, cfs_rq);
        load = cfs_rq->load.weight;

        shares = (tg->shares * load);
        if (tg_weight)
                shares /= tg_weight; 

        if (shares < MIN_SHARES)
                shares = MIN_SHARES;
        if (shares > tg->shares) 
                shares = tg->shares;

        return shares;
}

태스크 그룹의 cfs shares 값에 대한 cfs 런큐  로드 비율이 반영된 로드 weight 값을 산출한다. (태스크 그룹의 shares * cfs 런큐 로드 weight / 태스크 그룹 weight)

  • 코드 라인 5에서 태스크 그룹의 weight 값을 tg_weight에 대입한다.
  • 코드 라인 6에서 cfs 런큐의 로드 weight 값을 load에 대입한다.
  • 코드 라인 8~10에서 태스크 그룹의 shares 값과 load를 곱한 후 태스크 그룹의 weight 값으로 나눈다.
    • tg->shares * cfs_rq->load.weight / tg_weight
  • 코드 라인 12~17에서 산출된 share 값이 최소 shares(2)보다 작지 않도록 제한하고 태스크 그룹의 shares 값보다 크지 않도록 제한하고 반환한다.
    • 2 <= 계산된 shaers <= tg->shares

 

다음 그림은 태스크 그룹의 shares 값에 대하여 태스크 그룹 대비 cfs 런큐의 로드기여 비율을 반영하여 산출한다.

  • UP 시스템에서는 태스크 그룹 대비 cfs 런큐의 로드 weight 비율을 반영하지 않는다. 따라서 태스크 그룹의 shares 값을 100% 반영한다.

 

calc_tg_weight()

kernel/sched/fair.c

static inline long calc_tg_weight(struct task_group *tg, struct cfs_rq *cfs_rq)
{
        long tg_weight;

        /*
         * Use this CPU's actual weight instead of the last load_contribution
         * to gain a more accurate current total weight. See
         * update_cfs_rq_load_contribution().
         */
        tg_weight = atomic_long_read(&tg->load_avg);
        tg_weight -= cfs_rq->tg_load_contrib;
        tg_weight += cfs_rq->load.weight;

        return tg_weight;
}

태스크 그룹의 로드 평균 값을 읽어와서 cfs 런큐의 태스크 그룹 로드 기여 값을 감소시키고 다시 cfs 런큐의 로드 weight 값을 더한 후 반환한다.

  • tg->load_avg – cfs_rq->tg_load_contrib + cfs_rq->load.weight

 

로드 weight 재설정

reweight_entity()

kernel/sched/fair.c

static void reweight_entity(struct cfs_rq *cfs_rq, struct sched_entity *se,
                            unsigned long weight)
{
        if (se->on_rq) {
                /* commit outstanding execution time */
                if (cfs_rq->curr == se)
                        update_curr(cfs_rq);
                account_entity_dequeue(cfs_rq, se);
        }

        update_load_set(&se->load, weight);

        if (se->on_rq)
                account_entity_enqueue(cfs_rq, se);
}

스케줄 엔티티의 로드 weight 값을 변경 시킨다. 스케줄 엔티티가 cfs 런큐에서 이미 동작중인 경우 cfs 런큐와 런큐의 로드 값을 다음 예와 같이 조정한다.

  • 예) se->load.weight 값이 5000 -> 6024로 변경되는 경우
    • cfs_rq->load -= 5000   -> cfs_rq->load += 6024
    • rq->load -= 5000 -> rq->load += 6024 (최상위 스케줄 엔티티인 경우)

 

  • 코드 라인 4~7에서 요청한 스케줄 엔티티가 런큐에 있고 cfs 런큐에서 지금 돌고 있는 중이면 현재 태스크의 런타임 통계를 갱신한다.
  • 코드 라인 8에서 스케줄 엔티티의 로드 weight 값을 cfs 런큐에서 감소시킨다. 또한 최상위 스케줄 엔티티인 경우 런큐의 로드 weight 값을 동일하게 감소시킨다.
  • 코드 라인 11에서 스케줄 엔티티의 로드 weight 값을 갱신한다.
  • 코드 라인 13~14에서 요청한 스케줄 엔티티가 런큐에 있는 경우 스케줄 엔티티의 로드 weight 값을 cfs 런큐에 추가한다. 또한 최상위 스케줄 엔티티인 경우 런큐의 로드 weight 값에 동일하게 추가한다.

 

로드 weight이 재산출되야 하는 상황들을 알아본다.

  • 태스크 그룹의 shares 값을 변경 시 sched_group_set_shares() -> 변경할 그룹부터 최상위 그룹까지 반복: update_cfs_shares() -> reweight_entity()에서 사용된다.
  • 요청 스케줄 엔티티가 엔큐될 때 enqueue_entity() -> update_cfs_shares() -> reweight_entity()
  • 요청 스케줄 엔티티가 디큐될 때 enqueue_entity() -> update_cfs_shares() -> reweight_entity()

 

다음 그림은 스케줄 엔티티의 로드 weight 값이 변경될 때 관련된 cfs 런큐 및 런큐값도 재산출되는 과정을 보여준다.

 

update_load_set()

kernel/sched/fair.c

static inline void update_load_set(struct load_weight *lw, unsigned long w)
{
        lw->weight = w;
        lw->inv_weight = 0;
}

로드 weight 걊을 설정하고 inv_weight 값은 0으로 리셋한다.

 

Account Entity Enqueue & Dequeue

account_entity_enqueue()

kernel/sched/fair.c

static void
account_entity_enqueue(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
        update_load_add(&cfs_rq->load, se->load.weight);
        if (!parent_entity(se))
                update_load_add(&rq_of(cfs_rq)->load, se->load.weight);
#ifdef CONFIG_SMP
        if (entity_is_task(se)) {
                struct rq *rq = rq_of(cfs_rq);

                account_numa_enqueue(rq, task_of(se));
                list_add(&se->group_node, &rq->cfs_tasks);
        }
#endif
        cfs_rq->nr_running++;
}

cfs 런큐에 스케줄 엔티티의 로드를 추가한다. 부모 스케줄 엔티티가 없으면 cfs 런큐가 속한 런큐의 로드에도 추가한다.

  • 코드 라인 4에서 cfs 런큐에 스케줄 엔티티의 로드를 추가한다.
  • 코드 라인 5~6에서 부모 스케줄 엔티티가 없으면 cfs 런큐가 속한 런큐의 로드에도 추가한다.
  • 코드 라인 8~13에서 smp 시스템에서 태스크용 스케줄 엔티티인 경우 NUMA 시스템을 위해 런큐의 nr_numa_running 및 nr_preferred_running을 갱신한다. 그런 후 런큐의 cfs_tasks 리스트에 se->group_node를 추가한다.
  • 코드 라인 15에서 런큐의 nr_running 카운터를 1 증가시킨다.

스케줄 엔티티가 cfs 런큐에 엔큐되는 경우 관련 값들을 추가한다.

  • 코드 라인 4에서 스케줄 엔티티의 로드 weight 값을 cfs 런큐에 추가한다.
  • 코드 라인 5~6에서 최상위 스케줄 엔티티인 경우 런큐에도 추가한다.
  • 코드 라인 7~14에서 태스크 타입 스케줄 엔티티인 경우 누마 관련 값도 증가시키고 스케줄 엔티티를 런큐의 cfs_tasks 리스트에 추가한다.
  • 코드 라인 15에서 cfs 런큐의 nr_running을 1 증가시킨다.

 

account_numa_enqueue()

kernel/sched/fair.c

static void account_numa_enqueue(struct rq *rq, struct task_struct *p)
{
        rq->nr_numa_running += (p->numa_preferred_nid != -1);
        rq->nr_preferred_running += (p->numa_preferred_nid == task_node(p));
}

태스크가 엔큐되고 선호하는 누마 노드로 추가된 경우 다음의 값을 1씩 증가시킨다.

  • rq->nr_numa_running++ (태스크의 권장 누마 노드가 지정된 경우에만)
  • rq->nr_preferred_running++ (태스크의 노드가 권장 누마 노드와 동일한 경우에만)

 

다음 그림은 스케줄 엔티티의 로드 weight 값을 런큐 및 cfs 런큐에 증감시키는 상황을 보여준다.

  • 태스크형 스케줄 엔티티의 경우 cfs_tasks 리스트에 추가/삭제되는 것도 알 수 있다.
  • 런큐의 load.weight 증감은 최상위 엔티티의 load.weight에 대해서만 해당한다.

 

account_entity_dequeue()

kernel/sched/fair.c

static void
account_entity_dequeue(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
        update_load_sub(&cfs_rq->load, se->load.weight);
        if (!parent_entity(se))
                update_load_sub(&rq_of(cfs_rq)->load, se->load.weight);
        if (entity_is_task(se)) {
                account_numa_dequeue(rq_of(cfs_rq), task_of(se));
                list_del_init(&se->group_node);
        }
        cfs_rq->nr_running--;
}

스케줄 엔티티가 cfs 런큐에서 디큐되는 경우 관련 값들을 감소시킨다.

  • 코드 라인 4에서 스케줄 엔티티의 로드 weight 값을 cfs 런큐에서 감소시킨다.
  • 코드 라인 5~6에서 최상위 스케줄 엔티티인 경우 런큐에서도 감소시킨다.
  • 코드 라인 7~10에서 태스크 타입 스케줄 엔티티인 경우 누마 관련 값을 감소시키고, 런큐의 cfs_tasks 리스트에서 스케줄 엔티티를 제거하고 초기화한다.
  • 코드 라인 11에서 cfs 런큐의 nr_running을 1 감소시킨다.

 

account_numa_dequeue()

kernel/sched/fair.c

static void account_numa_dequeue(struct rq *rq, struct task_struct *p)
{
        rq->nr_numa_running -= (p->numa_preferred_nid != -1);
        rq->nr_preferred_running -= (p->numa_preferred_nid == task_node(p));
}

태스크가 디큐되고 선호하는 누마 노드로 추가된 경우 다음의 값을 1씩 감소시킨다.

  • rq->nr_numa_running– (태스크의 권장 누마 노드가 지정된 경우에만)
  • rq->nr_preferred_running– (태스크의 노드가 권장 누마 노드와 동일한 경우에만)

 

엔큐 & 디큐 시 엔티티 로드 평균 산출

enqueue_entity_load_avg()

kernel/sched/fair.c

/* Add the load generated by se into cfs_rq's child load-average */
static inline void enqueue_entity_load_avg(struct cfs_rq *cfs_rq,
                                                  struct sched_entity *se,
                                                  int wakeup)
{
        /*
         * We track migrations using entity decay_count <= 0, on a wake-up
         * migration we use a negative decay count to track the remote decays
         * accumulated while sleeping.
         *
         * Newly forked tasks are enqueued with se->avg.decay_count == 0, they
         * are seen by enqueue_entity_load_avg() as a migration with an already
         * constructed load_avg_contrib.
         */
        if (unlikely(se->avg.decay_count <= 0)) {
                se->avg.last_runnable_update = rq_clock_task(rq_of(cfs_rq));
                if (se->avg.decay_count) {
                        /*
                         * In a wake-up migration we have to approximate the
                         * time sleeping.  This is because we can't synchronize
                         * clock_task between the two cpus, and it is not
                         * guaranteed to be read-safe.  Instead, we can
                         * approximate this using our carried decays, which are
                         * explicitly atomically readable.
                         */
                        se->avg.last_runnable_update -= (-se->avg.decay_count)
                                                        << 20;
                        update_entity_load_avg(se, 0);
                        /* Indicate that we're now synchronized and on-rq */
                        se->avg.decay_count = 0;
                }
                wakeup = 0;
        } else {
                __synchronize_entity_decay(se);
        }

        /* migrated tasks did not contribute to our blocked load */
        if (wakeup) {
                subtract_blocked_load_contrib(cfs_rq, se->avg.load_avg_contrib);
                update_entity_load_avg(se, 0);
        }

        cfs_rq->runnable_load_avg += se->avg.load_avg_contrib;
        /* we force update consideration on load-balancer moves */
        update_cfs_rq_blocked_load(cfs_rq, !wakeup);
}

스케줄 엔티티가 엔큐될 때 로드 평균을 갱신한다.

  • 코드 라인 15~16에서 낮은 확률로 스케줄 엔티티의 decay_count가 0이하인 경우 스케줄 엔티티의 avg.last_runnable_update를 현재 시각(clock_task)으로 갱신한다.
  • 코드 라인 17~31에서 decay_count가 음수인 경우 스케줄 엔티티의 avg.last_runnable_update(ns)를 decay_count(ms) 만큼 이전으로 감소시킨다. 그리고 update_entity_load_avg()를 호출하여 로드 평균을 갱신하고 decay_count는 0으로 초기화한다.
  • 코드 라인 33~35에서 스케줄 엔티티의 decay_count가 0을 초과한 경우 스케줄 엔티티의 로드 평균 기여를 ‘cfs 런큐의 decay_counter – 스케줄 엔티티의 avg.decay_count’ 만큼 decay 한다.
  • 코드 라인 38~41에서 decay_count가 0을 초과하였었고 인수 wakeup=1이 주어진 경우 블럭드로드 기여를 감소시키고 다시 update_entity_load_avg()를 호출하여 로드 평균을 갱신한다.
  • 코드 라인 43에서 스케줄 엔티티에 있는 로드 평균 기여를 cfs 런큐의 러너블 로드 평균에  추가한다.
  • 코드 라인 45에서 블럭드 로드를 갱신하고 러너블 평균과 더해 cfs 런큐의 tg_load_contrib와 태스크 그룹의 load_avg 에 추가한다.

 


구조체

sched_avg 구조체

include/linux/sched.h

struct sched_avg {
        /*
         * These sums represent an infinite geometric series and so are bound
         * above by 1024/(1-y).  Thus we only need a u32 to store them for all
         * choices of y < 1-2^(-32)*1024.
         */
        u32 runnable_avg_sum, runnable_avg_period;
        u64 last_runnable_update;
        s64 decay_count;
        unsigned long load_avg_contrib;
};
  • runnable_avg_sum
    • 러너블 로드라 불리며 러너블(curr + rb 트리에서 대기) 타임 평균을 합하였다.
  • runnable_avg_period
    • Idle 타임을 포함한 전체 시간 평균을 합하였다.
  • last_runnable_update
    • 러너블 로드를 갱신한 마지막 시각
  • decay_count
    • 트래킹 migration에 사용하는 엔티티 decay 카운터로 슬립되어 엔티티가 cfs 런큐를 벗어난 시간을 decay하기 위해 사용된다.
  • load_avg_contrib
    • 평균 로드 기여(weight * runnable_avg_sum / (runnable_avg_period+1))
    • 태스크용 스케줄 엔티티에서는 엔티티의 load.weight이 러너블 평균 비율로 곱하여 산출된다.
    • 태스크 그룹용 스케줄 엔티티에서는 shares 값이 tg에 대한 cfs 비율 및 로드 비율 등이 적용되어 산출된다.

 

task_group 구조체

kernel/sched/sched.h

/* task group related information */
struct task_group {
        struct cgroup_subsys_state css;

#ifdef CONFIG_FAIR_GROUP_SCHED
        /* schedulable entities of this group on each cpu */
        struct sched_entity **se;
        /* runqueue "owned" by this group on each cpu */
        struct cfs_rq **cfs_rq;
        unsigned long shares;

#ifdef  CONFIG_SMP
        atomic_long_t load_avg;
        atomic_t runnable_avg;
#endif
#endif

#ifdef CONFIG_RT_GROUP_SCHED
        struct sched_rt_entity **rt_se;
        struct rt_rq **rt_rq;

        struct rt_bandwidth rt_bandwidth;
#endif

        struct rcu_head rcu;
        struct list_head list;

        struct task_group *parent;
        struct list_head siblings;
        struct list_head children;

#ifdef CONFIG_SCHED_AUTOGROUP
        struct autogroup *autogroup;
#endif

        struct cfs_bandwidth cfs_bandwidth;
};
  • css
    • cgroup 서브시스템 상태
    • rcu
    • list
    • *parent
      • 부모 태스크 그룹
    • siblings
      • 현재 태스크들
    • children
      • 자식 태스크 그룹
    • cfs_bandwidth
  • cfs 그룹 스케줄링 관련
    • **se
      • 각 cpu에서 이 그룹에 연결된 스케줄 엔티티(cpu 수 만큼)
    • **cfs_rq
      • 각 cpu에서 이 그룹에 연결된 cfs 런큐
    • shares
      • 그룹에 적용할 로드 weight 비율
    • load_avg
      • cfs 런큐의 로드 평균을 모두 합한 값으로 결국 스케줄 엔티티의 로드 평균 기여값을 모두 합한 값이다.
    • runnable_avg
      • cfs 런큐의 러너블 평균을 모두 합한 값으로 결국 스케줄 엔티티의 러너블 평균을 모두 합한 값이다.
  • rt 그룹 스케줄링 관련
    • **rt_se
      • 각 cpu에서 이 그룹에 연결된 rt 스케줄 엔티티(cpu 수 만큼)
    • **rt_rq
      • 각 cpu에서 이 그룹에 연결된 rt 런큐
    • rt_bandwidth
  • autogroup 관련
    • *autogroup

 

참고

런큐 로드 평균(cpu_load[]) – v4.0

<kernel v4.0>

런큐 로드 평균 – rq->cpu_load[] 산출 – (1, 2, 4, 8, 16 ticks)

cpu 로드 밸런스에 사용하였던 이 방법은  커널 v5.3-rc1에서 제거되었다.

 

런큐 로드 평균 (1 tick ~ 16 ticks)

  • 매 tick 마다 평균 cpu 로드를 산출하여 rq->cpu_load[5]에 저장하고 로드밸런스를 위해 사용된다.
  • rq->cpu_load[]에 5개의 cpu 로드가 저장되는데 각각의 기간은 2배씩 커진다.
    • cpu_load[0] <- 1 tick 기간
    • cpu_load[1] <- 2 ticks 기간
    • cpu_load[1] <- 4 ticks 기간
    • cpu_load[3] <- 8 ticks 기간
    • cpu_load[4] <- 16 ticks 기간
  • “/proc/sched_debug” 파일을 확인하여
# cat /proc/sched_debug 
Sched Debug Version: v0.11, 4.4.11-v7+ #888
ktime                                   : 4393169013.219659
sched_clk                               : 4393169057.443466
cpu_clk                                 : 4393169057.443935
jiffies                                 : 439286901

sysctl_sched
  .sysctl_sched_latency                    : 18.000000
  .sysctl_sched_min_granularity            : 2.250000
  .sysctl_sched_wakeup_granularity         : 3.000000
  .sysctl_sched_child_runs_first           : 0
  .sysctl_sched_features                   : 44859
  .sysctl_sched_tunable_scaling            : 1 (logaritmic)

cpu#0
  .nr_running                    : 0
  .load                          : 0
  .nr_switches                   : 145928196
  .nr_load_updates               : 57706792
  .nr_uninterruptible            : -324009
  .next_balance                  : 439.286901
  .curr->pid                     : 0
  .clock                         : 4393169054.864143
  .clock_task                    : 4393169054.864143
  .cpu_load[0]                   : 81
  .cpu_load[1]                   : 41
  .cpu_load[2]                   : 21
  .cpu_load[3]                   : 11
  .cpu_load[4]                   : 6

 


update_cpu_load_active()

kernel/sched/proc.c

/*
 * Called from scheduler_tick()
 */
void update_cpu_load_active(struct rq *this_rq)
{
        unsigned long load = get_rq_runnable_load(this_rq);
        /*
         * See the mess around update_idle_cpu_load() / update_cpu_load_nohz().
         */
        this_rq->last_load_update_tick = jiffies;
        __update_cpu_load(this_rq, load, 1);

        calc_load_account_active(this_rq);
}

요청한 런큐의 cpu 로드 active를 갱신한다.

  • 코드 라인 6에서 런큐의 load 평균 값을 알아온다.
  • 코드 라인 10에서 현재 시각(jiffies)으로 cpu 로드가 산출되었음을 기록한다.
  • 코드 라인 11에서 cpu 로드를 산출한다.
  • 코드 라인 13에서 5초 간격으로 전역 calc_load_tasks 값을 갱신한다.
    • 매 스케줄 tick 마다 nr_active를 산출하지 않기 위해 5초 간격으로 nr_active를 산출하여 그 차이 값 만큼만 calc_load_tasks에 반영한다.

 

다음 그림은 런큐의 cpu 로드가 산출되기 위해 처리되는 흐름을 보여준다.

 

get_rq_runnable_load()

kernel/sched/proc.c

#ifdef CONFIG_SMP
static inline unsigned long get_rq_runnable_load(struct rq *rq)
{
        return rq->cfs.runnable_load_avg;
}
#else
static inline unsigned long get_rq_runnable_load(struct rq *rq)
{
        return rq->load.weight;
}
#endif

런큐의 로드 평균 값을 반환한다.

  • enqueue_entity_load_avg(), dequeue_entity_load_avg(), update_entity_load_avg() 함수 등에서 cfs 태스크들의 load 평균이 산출된다.

 

__update_cpu_load()

kernel/sched/proc.c

/*
 * Update rq->cpu_load[] statistics. This function is usually called every
 * scheduler tick (TICK_NSEC). With tickless idle this will not be called
 * every tick. We fix it up based on jiffies.
 */
static void __update_cpu_load(struct rq *this_rq, unsigned long this_load,
                              unsigned long pending_updates)
{
        int i, scale;

        this_rq->nr_load_updates++;

        /* Update our load: */
        this_rq->cpu_load[0] = this_load; /* Fasttrack for idx 0 */
        for (i = 1, scale = 2; i < CPU_LOAD_IDX_MAX; i++, scale += scale) {
                unsigned long old_load, new_load;

                /* scale is effectively 1 << i now, and >> i divides by scale */

                old_load = this_rq->cpu_load[i];
                old_load = decay_load_missed(old_load, pending_updates - 1, i);
                new_load = this_load;
                /*
                 * Round up the averaging division if load is increasing. This
                 * prevents us from getting stuck on 9 if the load is 10, for
                 * example.
                 */
                if (new_load > old_load)
                        new_load += scale - 1;

                this_rq->cpu_load[i] = (old_load * (scale - 1) + new_load) >> i;
        }

        sched_avg_update(this_rq);
}

jiffies 기반 tick 마다 런큐의 cpu 로드를 갱신한다.

  • 코드 라인 11에서 nr_load_updates 카운터를 1 증가시킨다. (단순 카운터)
  • 코드 라인 14에서 cpu_load[0]에는 load 값을 100% 반영한다.
  • 코드 라인 15에서 cpu_load[1] ~ cpu_load[4]까지 갱신하기 위해 루프를 돈다. 루프를 도는 동안 scale 값은 2, 4, 8, 16과 같이 2배씩 커진다.
  • 코드 라인 20~21에서 4개 배열에 있는 기존 cpu 로드값들은 지연 틱 값에 의해 미리 준비된 테이블의 값을 참고하여 decay load 값으로 산출된다.
    • decay_load = old_load * miss 틱을 사용한 decay factor
  • 코드 라인28~29에서 새 로드가 기존 로드보다 큰 경우에 한하여 즉, 로드가 상승되는 경우 올림을 할 수 있도록 새 로드에 scale-1을 추가한다.
  • 코드 라인 31에서 기존 로드에 (2^i-1)/2^i 의 비율을 곱하고 새 로드에 1/(2^i) 비율을 곱한 후 더한다.
    •  i=1:
      • cpu_load[1] = decay_load * 1/2 + new_load * 1/2
    • i=2:
      • cpu_load[2] = decay_load * 3/4 + new_load * 1/4
    • i=3:
      • cpu_load[3] = decay_load * 7/8 + new_load * 1/8
    • i=4
      • cpu_load[4] = decay_load * 15/16 + new_load * 1/16
  • 코드 라인 34에서 런큐의 age_stamp와 rt_avg 값을 갱신한다.

 

 

다음 그림은 cpu_load[]를 산출하는 식이다.

 

다음 그림은 런큐의 cpu_load[] 값이 매 tick 마다 변화되는 모습을 보여준다.

 

decay_load_missed()

kernel/sched/proc.c

/*
 * Update cpu_load for any missed ticks, due to tickless idle. The backlog
 * would be when CPU is idle and so we just decay the old load without
 * adding any new load.
 */
static unsigned long
decay_load_missed(unsigned long load, unsigned long missed_updates, int idx)
{
        int j = 0;

        if (!missed_updates)
                return load;

        if (missed_updates >= degrade_zero_ticks[idx])
                return 0;

        if (idx == 1)
                return load >> missed_updates;

        while (missed_updates) {
                if (missed_updates % 2)
                        load = (load * degrade_factor[idx][j]) >> DEGRADE_SHIFT;

                missed_updates >>= 1;
                j++;
        }
        return load;
}

nohz idle 기능이 동작하여 tick이 발생하지 않은 횟수를 사용하여 미리 계산된 테이블에서 load 값을 산출한다.

  • 코드 라인 11~12에서 miss 틱이 없는 경우 그냥 load 값을 그대로 반환한다.
  • 코드 라인 14~15에서 miss 틱이 idx 순서대로 8, 32, 64, 128 값 이상인 경우 너무 작은 load 값을 반영해봤자 미미하므로 load 값으로 0을 반환한다.
  • 코드 라인 17~18에서 인덱스 값이 1인 경우 load 값을 miss 틱 만큼 우측으로 쉬프트한다.
  • 코드 라인 20~26에서 missed_updates 값의 하위 비트부터 설정된 경우 이 위치에 해당하는 테이블에서 값을 곱하고 128로 나눈다.

 

다음 그림은 idx=2, miss 틱=13이 주어졌고 이에 해당하는 decay 로드 값을 구하는 모습을 보여준다.

 

/*
 * The exact cpuload at various idx values, calculated at every tick would be
 * load = (2^idx - 1) / 2^idx * load + 1 / 2^idx * cur_load
 *
 * If a cpu misses updates for n-1 ticks (as it was idle) and update gets called
 * on nth tick when cpu may be busy, then we have:
 * load = ((2^idx - 1) / 2^idx)^(n-1) * load
 * load = (2^idx - 1) / 2^idx) * load + 1 / 2^idx * cur_load
 *
 * decay_load_missed() below does efficient calculation of
 * load = ((2^idx - 1) / 2^idx)^(n-1) * load
 * avoiding 0..n-1 loop doing load = ((2^idx - 1) / 2^idx) * load
 *
 * The calculation is approximated on a 128 point scale.
 * degrade_zero_ticks is the number of ticks after which load at any
 * particular idx is approximated to be zero.
 * degrade_factor is a precomputed table, a row for each load idx.
 * Each column corresponds to degradation factor for a power of two ticks,
 * based on 128 point scale.
 * Example:
 * row 2, col 3 (=12) says that the degradation at load idx 2 after
 * 8 ticks is 12/128 (which is an approximation of exact factor 3^8/4^8).
 *
 * With this power of 2 load factors, we can degrade the load n times
 * by looking at 1 bits in n and doing as many mult/shift instead of
 * n mult/shifts needed by the exact degradation.
 */
#define DEGRADE_SHIFT           7
static const unsigned char
                degrade_zero_ticks[CPU_LOAD_IDX_MAX] = {0, 8, 32, 64, 128};

idx별로 miss 틱에 대한 반영 비율을 적용할 지 여부를 확인하기 위한 테이블이다.

  • miss 틱 수가 idx에 따른 테이블 값 이상으로 오래된 경우 기존 load 값을 사용할 필요 없다.
  • 예) idx=2, miss 틱=33인 경우 기존 cpu 로드 값을 0으로 간주하게 한다.

 

static const unsigned char
                degrade_factor[CPU_LOAD_IDX_MAX][DEGRADE_SHIFT + 1] = {
                                        {0, 0, 0, 0, 0, 0, 0, 0},
                                        {64, 32, 8, 0, 0, 0, 0, 0},
                                        {96, 72, 40, 12, 1, 0, 0},
                                        {112, 98, 75, 43, 15, 1, 0},
                                        {120, 112, 98, 76, 45, 16, 2} };

idx 별로 miss 틱에 대한 decay_load를 구하기 위한 테이블이다. (각 값은 x/128에 해당하는 분자 값이다.)

  • 예) idx=2, miss 틱=6, load=1024 일 때
    • 분모가 128이고, miss 틱에 대한 비트에 해당하는 72와 40에 해당하는 분자 값을 사용한다.
    • decay_load = 1024 * 72/128 * 40/128 = 1024 * 2880 / 16384 = 180

 

평균 스케쥴 갱신

sched_avg_update()

kernel/sched/core.c

void sched_avg_update(struct rq *rq)
{
        s64 period = sched_avg_period();

        while ((s64)(rq_clock(rq) - rq->age_stamp) > period) {
                /*
                 * Inline assembly required to prevent the compiler
                 * optimising this loop into a divmod call.
                 * See __iter_div_u64_rem() for another example of this.
                 */
                asm("" : "+rm" (rq->age_stamp));
                rq->age_stamp += period;
                rq->rt_avg /= 2;
        }
}

런큐의 age_stamp와 rt_avg 값을 갱신한다.

  • 코드 라인 3에서 평균 스케줄 타임(ns)의 절반을 가져와서 period에 대입한다.
  • 코드 라인 5~14에서 런큐 클럭에서 age_stamp를 뺀 간격이 period보다 큰 경우에 한해 계속 루프를 돌며 age_stamp를 period만큼 더하고 rt_avg 값은 절반으로 나눈다.

 

sched_avg_period()

kernel/sched/sched.h

static inline u64 sched_avg_period(void)
{
        return (u64)sysctl_sched_time_avg * NSEC_PER_MSEC / 2;
}

평균 스케줄 타임(ns)의 절반을 가져온다.

 

5초 간격으로 active 태스크 수 산출

calc_load_account_active()

kernel/sched/proc.c

/*
 * Called from update_cpu_load() to periodically update this CPU's
 * active count.
 */
static void calc_load_account_active(struct rq *this_rq)
{
        long delta;

        if (time_before(jiffies, this_rq->calc_load_update))
                return;

        delta  = calc_load_fold_active(this_rq);
        if (delta)
                atomic_long_add(delta, &calc_load_tasks);

        this_rq->calc_load_update += LOAD_FREQ;
}

로드 산출 주기(5초) 간격으로 active 태스크 수를 전역 calc_load_tasks에 갱신한다.

  • 코드 라인 9~10에서 지정된 5초 만료시간이 안된 경우 함수를 빠져나간다.
  • 코드 라인 12~14에서 기존 active 태스크 수와 현재 산출한 active 태스크 수의 차이를 알아와서 calc_load_tasks에 추가하여 갱신한다.
  • 코드 라인 16에서 다음 산출할 시간을 위해 5초를 더한다.

 

다음 그림은 런큐의 active 태스크 수를 런큐와 전역 변수에 갱신하는 것을 보여준다.

 

calc_load_fold_active()

kernel/sched/proc.c

long calc_load_fold_active(struct rq *this_rq)
{
        long nr_active, delta = 0;

        nr_active = this_rq->nr_running;
        nr_active += (long) this_rq->nr_uninterruptible;

        if (nr_active != this_rq->calc_load_active) {
                delta = nr_active - this_rq->calc_load_active;
                this_rq->calc_load_active = nr_active;
        }

        return delta;
}

요청한 런큐의 기존 active 태스크 수와 현재 산출한 active 태스크 수의 차이를 산출한다.

  • 코드 라인 5~6에서 요청한 런큐의 active 태스크 수를 알아온다.
  • 코드 라인 8~13에서 런큐의 calc_load_active와 다른 경우 이 값을 갱신하고 그 차이를 delta 값으로 하여 반환한다.

 

참고

Persistent Memory & DAX

 

Persistent Memory

Persistent 메모리는 다음과 같은 속성이 있다.

  • Solid State 고성능 바이트 단위 주소 접근이 가능한 디바이스로 메모리 버스위에서 동작한다.
    • DRAM과 같이 직접 addressing이 가능한 물리 주소를 지원하여 시스템 메모리로도 사용할 수 있다.
  • 전원이 차단된 상태에서도 데이터가 보존되는 비휘발성 메모리이다.
    • Flash SSD 같은 종류들보다 훨씬 access 타임이 빠르다.
    • 주의: 기존 NVRAM의 주요 특성을 가지고 있으나 혼동하면 안된다.
  • NVDIMM 버스 사용
    • DRAM이 장착되는 DIMM 슬롯을 이용하여 DRAM과 같은 대역폭을 사용한다.
    • SSD 등이 사용하는 pci/pcie 및 usb 버스 등의 IO 채널을 사용하는 것보다 훨씬 빠르다.
  • DRAM 보다 더 저렴하여 같은 비용으로 DRAM 보다 더 큰 용량을 사용할 수 있다.
  • DRAM 처럼 캐시 사용이 가능한 상태로 동작할 수 있다. (보통 그렇게 사용한다)
  • Pmem으로 줄여 표현하기도 한다.

 

NVDIMM Pmem

현재 가장 많이 사용하고 있는 DIMM 버스를 이용하는 제품 두 종류를 소개한다.

  • NVDIMM-N 타입 persistent 메모리
    • 8/16/32G 제품이 먼저 출시되었고, DRAM + NAND 플래시 구성으로 만들어졌다.
    • 장점: 읽고 쓰기 성능이 3D XPoint보다 빠르다.
    • 단점: 정전시 DRAM 데이터를 NAND에 백업하기 위해 super capacitor가 필수로 사용된다.
  • 3D XPoint persistent 메모리
    • 장점: 자체적으로 비휘발성이 유지되므로 정전을 대비하기 위해 super capacitor를 필요로 하지 않는다.
    • 단점: 읽고 쓰기 성능이 DRAM+NAND 구성보다 느리다.
    • 인텔과 마이크론이 개발하였으며, 인텔은 Optane, 마이크론은 QuantX라는 브랜드명을 사한다. 현재 8~512G 제품이 출시되어 있다.

 

참고: HBM 버스

  • DIMM 보다 더 빠른 메모리 버스로 테라 단위의 대역폭을 요구하는 고속 GPU등에 사용되었다. 다만 고속을 위해 칩 외부의 슬롯 형태가 아닌 칩에 통합되어 제작해야 하는 단점이 있다.

 

다음 그림은 NVDIMM-N 타입 persistent 메모리이다. 배터리(super capacitor)가 외부에서 공급되는 것을 확인할 수 있다.

 

다음 그림은 NVDIMM-N 타입 persistent 메모리의 블럭 다이어그램이다.

 

다음 그림은 인텔 XEON CPU와 같이 사용해야 하는 Optane DC Persistent 메모리를 보여준다.

 

NVDIMM 표준화 및 규격

  • NVDIMM 규격에 사용되는 NV(Non-Volatile) 미디어는 NAND 플래시, 3D XPoint, 자기 메모리, 상변화 메모리, 저항 변화 메모리 등이 포함된다.
  • 다음 3개의 규격이 사용된다.
    • NVDIMM-N
      • DRAM(DDR3/DDR4) + NV 미디어(최소 DRAM과 1:1 용량)로 구성하고 메모리 성격을 가진다.
      • DRAM과 바이트 단위 주소 액세스를 할 수 있으나 NV 미디어와는 직접 액세스하지 않는다.
      • NV 미디어는 DRAM의 백업 역할을 한다. 전원 fail 시에는 DRAM의 백업을 위해 추가 배터리 연결이 필요하다.
      • 세 타입 중 가장 빠른 수십 나노초(ns)의 레이튼시를 갖는다.
    • NVDIMM-F
      • NV 미디어로만 구성하고, 스토리지 성격을 가진다.
      • 윈도 매커니즘을 통해 매핑이 가능하고, 블럭 단위의 액세스를 지향한다. (마운트하여 사용하는 형태 등)
      • 세 타입 중 가장 느린 수십 마이크로초(us)의 레이튼시를 갖는다.
    • NVDIMM-P
      • NVDIMM-N과 F를 섞은 형태로 DRAM+NV 미디어(DRAM보다 훨씬 큰 용량)로 구성된 하이브리드 성격을 가진다.
      • DDR(4 or 5) 프로토콜을 사용하는 DRAM 인터페이스를 사용하지만 non-volatile 성격을 갖는 DRAM을 사용하여 배터리 백업을 필요치 않는다.
      • 세 타입 중 중간인 수백 나노초(ns)의 레이튼시를 갖는다.
      • 2018년에 규격이 확정되었고, 아직 제품화되지는 않았다.

 

NVDIMM pmem 드라이버

커널에서의 persistent 메모리 지원은 커널 4.2 이후부터 nvdimm 드라이버가 소개되었고, 커널 v4.4에서 안정화되었다.

  • 디바이스 트리에서 nvdimm pmem 드라이버 지원
    • compatible = “pmem-region
    • volatile 속성 여부에 따라 두 가지 타입의 메모리를 지원한다.
      • volatile region
        • 속성이 있는 경우 시스템 메모리와 같이 물리 주소가 부여되어 시스템 메모리로 관리된다. (ZONE_DEVICE)
        • 두 개 이상의 pmem을 사용하는 경우 인터리브 구성을 한다.
          • 시스템 메모리에 매핑 시 pmem을 1:1로 리니어하게 매핑하지 않고, pmem 드라이버에서 인터리브하게 매핑한다.
      • pmem region
        • volatile 속성이 지정되지 않은 이 region은 DAX를 지원하는 파일 시스템에 마운트하여 사용한다.

 

pmem region으로 등록되어 사용하는 예)

        /*
         * This node specifies one 4KB region spanning from
         * 0x5000 to 0x5fff that is backed by non-volatile memory.
         */
        pmem@5000 {
                compatible = "pmem-region";
                reg = <0x00005000 0x00001000>;
        };

 

volatile region으로 등록되어 사용하는 예)

        /*
         * This node specifies two 4KB regions that are backed by
         * volatile (normal) memory.
         */
        pmem@6000 {
                compatible = "pmem-region";
                reg = < 0x00006000 0x00001000
                        0x00008000 0x00001000 >;
                volatile;
        };

 

다음 그림은 인텔에서 보여준 nvdimm pmem을 이용하는 여러 가지 방법을 보여준다.

 

NVDIMM 블럭 매핑

다음 그림은 커널 블럭디바이스에서의 로지컬 블럭과 실제 NVDIMM의 물리 블럭이 매핑되어 사용되는 모습을 보여준다.

  • 매번 같은 블럭에 기록하는 것처럼 행동하여도, NVDIMM 내부에서는 새로운 빈 블럭을 찾아 기록한다. (SSD와 동일)

 


DAX(Direct Access)

DAX를 사용하면 Persistent 메모리 또는 블럭 장치에 저장된 파일에 직접 액세스할 수 있다. 직접 액세스하므로 커널은 페이지 캐시를 사용하지 않는다. 그러나 파일 시스템에서 DAX 지원이 없으면 Standard File API를 통해 파일 읽기 및 쓰기를 버퍼링하는데 페이지 캐시를 사용하고 추가적인 복사 작업이 소요된다.

 

다음 그림에서 파란색은 파일 API를 사용한 접근 방법과, DAX를 지원하는 파일 시스템을 통해 직접 접근하는 방법을 보여준다.

 

위의 개념을 인텔 Optane DC Persistent Memory를 사용하는 경우 앞으로 다룰 namespace와 region 개념을 포함하여 보여주고 있다.

 

Persistent memory-aware File System

운영체제에 따라 다음 파일 시스템이 DAX를 지원한다.

  •  리눅스
    • ext2, ext4, xfs 파일시스템
  • 윈도우 서버
    • ntfs 파일 시스템

 

파일마운트 시 DAX enable

파일 시스템을 마운트 시 파일 시스템 종류별로 DAX를 enable 하는 방법이 약간 차이가 있다.

  • ext2
    • -o dax 옵션을 사용하면 마운트된 모든 파일에 적용된다. (/etc/fstab 에서는 dax 옵션을 사용한다.)
  • ext4 & xfs
    • 다음 옵션에 따라 개별 디렉토리 및 파일을 DAX 지원 상태로 변경할 수 있다.
      • -o dax=inode
        • persistent 속성(FS_XFLAG_DAX)을 사용하여 정규 파일 및 디렉토리에 적용/제거 할 수 있고, 이 옵션은 디폴트로 사용된다.
        • 예) xfs_io -c ‘chattr +x’ <dirname>
          • <dirname>부터 이후에 만들어지는 파일이나 하위 디렉토리는 dax가 enable 상태로 적용된다.
      • -o dax=never
        • 파일 및 디렉토리를 생성시 dax가 적용되지 않는다.
      • -o dax=always
        • 파일 및 디렉토리를 생성시 항상 dax가 적용된다.
      • -o dax
        • -o dax=always와 동일한 옵션으로 -o dax 만을 사용하는 경우는 제거될 예정이다.

 

DAX 파일 시스템의 블럭 사이즈

DAX를 지원하기 위한 블럭 디바이스는 파일 시스템의 블럭 사이즈를 커널의 PAGE_SIZE와 동일하게 사용해서 생성해야 한다.

# fdisk -l /dev/ndblk0.1s
Disk /dev/ndblk0.1s: 32 GiB, 34325135360 bytes, 8380160 sectors
Units: sectors of 1 * 4096 = 4096 bytes
Sector size (logical/physical): 4096 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes

 

DAX(Direct Access) 커널 옵션

다음과 같은 커널 옵션들을 살펴본다.

  • DAX: direct access to differentiated memory”
    • CONFIG_DEV_DAX
    • mmap() 사용을 통해 저수준의 접근이 가능한 캐릭터 디바이스이다.
    • /dev/daxX.Y
  • PMEM DAX: direct access to persistent memory”
    • CONFIG_DEV_DAX_PMEM
    • 유저 스페이스 헬퍼 라이브러리인 libnvdimm 서브시스템을 통해 저수준의 접근이 가능하다.
      • ndctl 유틸리티
  • KMEM DAX: volatile-use of persistent memory”
    •  CONFIG_DEV_DAX_KMEM
    • DRAM과 같은 형태로 이용하므로 어떠한 applicaton의 수정 없이 사용할 수 있다.
    • ZONE_DEVICE 및 MEMORY_HOTPLUG를 사용한다.
    • 현재 pfn 메타데이터를 DRAM에 저장하므로 Persistent 메모리가 DRAM 용량보다 훨씬 큰 시스템 환경의 경우 현재 커널 버전에는 사용하지 않아야 한다.
      • PMEM DAX 처럼 pfn 메타데이터를 Persistent 메모리에 저장할 수 있는 옵션도 개발 완료된 상태였으나, 커널에 업스트림되지 않은 상태이다.
    • 참고: device-dax: “Hotplug” persistent memory for use like normal RAM (2019, v5.1-rc1)
  • HMEM DAX: direct access to ‘specific purpose’ memory”

 

 

KMEM DAX

시스템 물리 메모리로 구성하는 KMEM DAX는 DRAM이 사용하는 ZONE_NORMAL로 통합하지 않고, 약간 느린 메모리로 별도 분류하기 위해 ZONE_DEVICE를 사용한다. 한편 물리 메모리를 관리하기 위해 각 물리 메모리의 모든 페이지에 대응하는 pfn 메타데이터를 별도로 할당해서 가장 빠른 DRAM에 상주시켜 사용해야 한다. 따라서 persistent 메모리 용량이 매우 큰 경우에는 pfn 메타데이터 용량도 전체 메모리의 약 1.5% 만큼 소요되어 매우 커진다. 이렇게 만들어진 pfn 메타데이터는 성능을 위해 커널 메모리로 사용되는 pre-mapping된 ZONE_NORMAL(빠른 DRAM)에 할당해야 하므로 DRAM의 낭비마저 발생하는 단점이 있다. 그러므로 DRAM보다 약 8배 이상 큰 용량을 가진 persistent 메모리를 가진 시스템 구성의 경우에는 이 persistent 메모리를 물리 주소 번지를 갖는 ZONE_DEVICE로의 사용을 권장하지 않는다.

 

PMEM DAX

persistent 메모리의 할당 관리를 위한 pfn 메타데이터를 persistent storage에 만들어 관리를 하는 방법이 있다. 시스템 메모리에 비해 8배 이상 큰 용량의 persistent 메모리를 DAX를 지원하는 파일 시스템에 사용하여 마운트하여 사용하여 운용할 수 있다.

 

다음은 블럭 디바이스로 인식한 persistent 메모리의 파티션을 보여준다.

$ fdisk -l /dev/pmem0
Disk /dev/pmem0: 4 GiB, 4223664128 bytes, 8249344 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
Disklabel type: gpt
Disk identifier: 10B97DA8-F537-6748-9E6F-ED66BBF7A047

Device       Start     End Sectors Size Type
/dev/pmem0p1  4096 8249310 8245215   4G Linux filesystem

 

다음은 블럭 디바이스로 인식한 persistent 메모리를 DAX를 지원하는 ext4 파일시스템으로 사용한 후 마운트한 예를 보여준다.

$ mkfs -t xfs /dev/pmem0
$ mount -o dax /dev/pmem0 /mnt/ext4-pmem0/

 

다음은 persistent 메모리들로 DAX를 지원하는 여러 파일시스템을 사용하여 마운트한 예를 보여준다.

$ lsblk
NAME                   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
pmem0                  259:0    0    16G  0 disk
├─pmem0p1              259:6    0     4G  0 part /mnt/ext4-pmem0
└─pmem0p2              259:7    0  11.9G  0 part /mnt/btrfs-pmem0
pmem1                  259:1    0    16G  0 disk /mnt/xfs-pmem1

 

DAX Driver

 

Deprecated /sys/class/dax 지원

 

필요한 커널 설정

  • CONFIG_ZONE_DEVICE=y
  • CONFIG_TRANSPARENT_HUGEPAGE=y
  • CONFIG_ACPI_NFIT=m
  • CONFIG_LIBNVDIMM=m
  • CONFIG_BLK_DEV_PMEM=m
  • CONFIG_ND_BLK=m
  • CONFIG_BTT=y
  • CONFIG_NVDIMM_PFN=y
  • CONFIG_NVDIMM_DAX=y
  • CONFIG_DEV_DAX_KMEM=m
  • CONFIG_FS_DAX=y

 


NVDIMM Pmem 관리(libndctl)

 

Region

NVDIMM들을 각각의 region 또는 N-way 인터리브 세트로 묶어 하나의 region을 만들어낸다. 각 Region은 운영 가능한 타입이 있다.

 

다음 그림은 NVDIMM을 각각의 region으로 사용하거나 N-way 인터리브 세트로 구성하여 하나로 사용하는 방법을 보여준다.

  • 좌측: 각각의 NVDIMM을 각각의 region으로 구성
  • 우측: 두 개의 NVDIMM을 2-way 인터리브 세트로 구성하여 하나의 region으로 구성

(벤더의 하드웨어 설정 및 소프트웨어 툴 사용: 예) Xeon 서버의 UEFI 펌웨어 설정 + ipmctl 툴 사용)

 

Namespace

위에서 구성한 Region을 1개 이상의 Namespace로 나누어 사용할 수 있다.

  • 1개 이상의 pmem 장치가 “/dev/*”에 연결되어 사용되는 단위이다.
  • 1개 또는 여러 개의 pmem을 인터리브로 연결하여 하나의 /dev/*에 사용할 수 있는 입출력 단위이다.
  • 각 Namespace는 아래 타입과 모드를 지정하여 구성한다.
  • Namespace를 구성하는 툴은 ndctl이다.

 

다음 그림에서 두 개의 Region위에 각각 두 개의 Namespace를 만들어 총 4개의 Namespace를 구성한 모습을 볼 수 있다.

 

Type

Namespace의 액세스 타입이다. pmem은 물리적으로 아래 두 타입 중 하나만을 지원하기도 하고, 둘 다 지원하는 경우도 있다. 두 가지 타입을 모두 지원하는 경우엔 두 개 이상의 Namespace에 각각의 타입을 섞어 구성할 수도 있다.

  • PMEM
    • 휘발성 DRAM과 같은 형태이다.
    • 유저 모드 및 커널 모드에서 가상 주소를 통해 직접 액세스가 가능하다.
  • BLK
    • 슬라이딩 윈도우를 통해 수 페이지를 액세스할 수 있고, 블럭 윈도우를 이동(슬라이딩)하는 방법으로 컨트롤한다.
    • 유저 모드에서는 가상 주소를 통해 직접 액세스가 가능하지만 커널 모드에서는 직접 대응하지 않는다.

 

다음 그림은 SPA(System Physical Address)로 DPA(DIMM Physicall Address)로 향하는 모습을 보여준다.

  • PMEM 타입은 직접 액세스가 가능하지만 BLK 타입은 직접 액세스가 불가능하며 블럭 윈도우를 이동하는 방법으로 접근할 수 있다.

 

Mode

각 Namespace를 위해 다음 모드들 중 하나를 선택한다.

  • fsdax (aka. memory)
  • devdax (aka. dax)
  • raw
  • sector

 

다음 그림은 Namespace를 생성할 때 사이즈, 타입 및 모드를 지정하는 모습을 보여준다.

 

Map

PFN 메타 데이터(memmap, struct page 배열)의 저장 장소를 선택한다. (fsdax & devdax만 유효)

  • mem
    • DRAM에 저장한다.
  • dev
    • persistent storage에 저장한다.

 


각 모드별 특징과 구성 방법

fsdax (aka. memory)

  • 이 모드는 DAX를 지원하는 ext2, ext4 및 xfs 파일시스템을 사용한다.
  • ndctl create-namespace 명령을 사용할 때 옵션을 지정하지 않으면 지정되는 디폴트 모드이다.
  • mmap을 사용하여 가상 주소에 매핑할 수 있다.
  • 페이지 캐시를 제거한다.
  • /dev/pmemN[.M]
  • 블럭 디바이스 – 파일시스템
  • DAX
  • PFN 메타데이터 필요
  • Label 메타데이터

 

다음 그림은 두 개의 namespace를 fsdax 모드로 구성하는 모습을 보여준다. DAX 지원하는 파일 시스템에서 유저 application이 이 디바이스를 표준  파일 시스템을 통한 접근과 mmap()을 통한 직접 액세스  모두 사용할 수 있다.

 

devdax (aka. dax)

  • fsdax와 유사하게 이 디바이스를 mmap을 사용하여 가상 주소에 매핑할 수 있다.
  • 페이지 캐시를 제거한다.
  • /dev/daxN.M
  • 캐릭터 디바이스
  • DAX
  • PFN 메타데이터 필요
  • Label 메타데이터

 

다음 그림은 두 개의 namespace를 devdax 모드로 구성하는 모습을 보여준다. 파일 시스템이 없는 캐릭터 디바이스 형태로 유저 application이 이 디바이스를 mmap() 하여 직접 액세스할 수 있다.

 

sector

  • DAX를 지원하지 않는 모든 파일시스템에도 마운트하여 사용할 수 있다.
  • 섹터/블럭 단위를 atomic하게 수정한다.
  • /dev/pmemNs
  • 블럭 디바이스 – 파일시스템
  • no-DAX
  • no-PFN 메타데이터
  • Label 메타데이터

 

다음 그림은 두 개의 namespace를 sector 모드로 구성하는 모습을 보여준다. DAX를 지원하는 파일 시스템이라도 유저 application이 이 디바이스를 표준  파일 시스템을 통한 접근만을 허용한다.

 

다음 그림은 3개의 NVDIMM을 3-way 인터리브 세트로 구성하여 하나의 region으로 만들고, 그 위에 1 개의 namespace를 sector 모드로 구성하는 모습을 보여준다. 유저 application이 이 namespace에는 DAX를 지원하지 않는 파일 시스템을 통해 접근을 허용한다.

 

raw

  • 메모리 디스크처럼 동작한다.
  • /dev/pmemN
  • 블럭 디바이스 – 파일시스템
  • no-DAX
  • no-PFN 메타데이터
  • no Label 메타데이터

 

다음 그림은 복합 구성한 예를 보여준다.

  • 구성 정보는 Labels에 저장된다.
    • Legacy NVDIMM은 Label 저장을 지원하지 않는다.
  • 3개의 NVDIMM 유닛을 3-way 인터리브 세트로 묶어 전체 영역을 1개의 Region으로 사용한다.
  • 그리고 3 개의 Region 각각을 BLK 타입 – 섹터 모드로 구성하고, 1 개의 PMEM 타입 – fsdax 모드로 구성한다.

 

다음은 위의 그림과 동일한 설정으로 NVDIMM 컨트롤 유틸리티인 ndctl 툴로 구성을 하는 모습을 보여준다.

  • 섹터 사이즈는 운영할 리눅스 커널의 PAGE_SIZE와 동일해야 한다.
# ndctl disable-namespace all
disabled 7 namespaces

# ndctl destroy-namespace all
destroyed 7 namespaces

# ndctl create-namespace --type=blk --size=32g
{
  "dev":"namespace2.1",
  "mode":"sector",
  "uuid":"37c254cd-b123-4b13-b5b0-cd06c30e4efb",
  "sector_size":4096,
  "blockdev":"ndblk2.1s"
}

# ndctl create-namespace --type=blk --size=32g
{
  "dev":"namespace1.1",
  "mode":"sector",
  "uuid":"e1f5fa9f-4820-42f4-b8a3-be90fa00fe79",
  "sector_size":4096,
  "blockdev":"ndblk1.1s"
}

# ndctl create-namespace --type=blk --size=32g
{
  "dev":"namespace0.1",
  "mode":"sector",
  "uuid":"1f84a98c-8dac-4a29-966a-42a5ac78d78f",
  "sector_size":4096,
  "blockdev":"ndblk0.1s"
}

# ndctl create-namespace --type=pmem --mode=memory
{
  "dev":"namespace3.0",
  "mode":"memory",
  "size":99881058304,
  "uuid":"33311d73-487d-4d27-8f2a-9d682570e312",
  "blockdev":"pmem3"
}

 

다음은 fdisk를 사용하여 각 NVDIMM 디바이스들의 블럭 장치 구성 정보를 보여준다.

# fdisk -l /dev/pmem3 /dev/ndblk*
Disk /dev/pmem3: 93 GiB, 99881058304 bytes, 195080192 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes

Disk /dev/ndblk0.1s: 32 GiB, 34325135360 bytes, 8380160 sectors
Units: sectors of 1 * 4096 = 4096 bytes
Sector size (logical/physical): 4096 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes

Disk /dev/ndblk1.1s: 32 GiB, 34325135360 bytes, 8380160 sectors
Units: sectors of 1 * 4096 = 4096 bytes
Sector size (logical/physical): 4096 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes

Disk /dev/ndblk2.1s: 32 GiB, 34325135360 bytes, 8380160 sectors
Units: sectors of 1 * 4096 = 4096 bytes
Sector size (logical/physical): 4096 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes

 

참고

 

Exception -9- (ARM64 Fault Handler)

<kernel v5.4>

Exception -9- (ARM64 Fault Handler)

 

메모리 fault 처리는 아키텍처 관련 코드를 수행한 후 아키텍쳐 공통 메모리 fault 코드를 수행한다.

  • 먼저 do_mem_abort() 함수를 호출하며, 페이지 fault 처리에 대해  do_page_fault() -> __do_page_fault() 함수 순서대로 수행한다.
  • 그 후 아키텍처 공통(generic) 메모리 fault 처리 함수인 handle_mm_fault() 함수로 진입한다.

 

다음 그림은 유저 및 커널 모드 수행 중 sync exception 발생 시 처리되는 함수 흐름이다.

 

do_mem_abort()

arch/arm64/mm/fault.c

asmlinkage void __exception do_mem_abort(unsigned long addr, unsigned int esr,
                                         struct pt_regs *regs)
{
        const struct fault_info *inf = esr_to_fault_info(esr);

        if (!inf->fn(addr, esr, regs))
                return;

        if (!user_mode(regs)) {
                pr_alert("Unhandled fault at 0x%016lx\n", addr);
                mem_abort_decode(esr);
                show_pte(addr);
        }

        arm64_notify_die(inf->name, regs,
                         inf->sig, inf->code, (void __user *)addr, esr);
}

가상 주소 @addr에 해당하는 공간에 접근하다 fault가 발생하였고, @esr(Exception Syndrom Register) 값과 스택에 저장한 context(pt_regs)를 인자로 가지고 진입하였다. 해당 가상 메모리 영역에 대한 fault 처리를 수행한다.

  • 코드 라인 4에서 @esr 값으로 static하게 정의된 fault_info[] 배열에서 구조체 fault_info를 알아온다.
  • 코드 라인 6~7에서 fault_info[] 배열에 미리 static으로 지정해둔 해당 함수를 실행한다.
    • 예) “level 1 permission fault”가 발생한 경우 do_page_fault() 함수를 호출한다.
  • 코드 라인 9~13에서 유저 영역이 아닌 가상 주소에서 fault가 발생한 경우 다음과 같은 디버그용 정보를 출력한다.
    • “Unhandled fault at 0x################” 에러 메시지
    • “Mem abort info: ESR=0x########” 등의 메모리 abort 정보
    • 데이터 abort 인 경우 “Data abort info:” 정보도 추가
    • swapper 또는 user 페이지 테이블 정보
  • 코드 라인 15~16에서 유저 영역에서의 fault는 해당 프로세스를 kill하고, 커널 영역에서의 fault인 경우 die 처리한다.

 


페이지 fault 처리

do_page_fault()

arch/arm64/mm/fault.c -1/3-

static int __kprobes do_page_fault(unsigned long addr, unsigned int esr,
                                   struct pt_regs *regs)
{
        const struct fault_info *inf;
        struct mm_struct *mm = current->mm;
        vm_fault_t fault, major = 0;
        unsigned long vm_flags = VM_READ | VM_WRITE;
        unsigned int mm_flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;

        if (kprobe_page_fault(regs, esr))
                return 0;

        /*
         * If we're in an interrupt or have no user context, we must not take
         * the fault.
         */
        if (faulthandler_disabled() || !mm)
                goto no_context;

        if (user_mode(regs))
                mm_flags |= FAULT_FLAG_USER;

        if (is_el0_instruction_abort(esr)) {
                vm_flags = VM_EXEC;
                mm_flags |= FAULT_FLAG_INSTRUCTION;
        } else if (is_write_abort(esr)) {
                vm_flags = VM_WRITE;
                mm_flags |= FAULT_FLAG_WRITE;
        }

        if (is_ttbr0_addr(addr) && is_el1_permission_fault(addr, esr, regs)) {
                /* regs->orig_addr_limit may be 0 if we entered from EL0 */
                if (regs->orig_addr_limit == KERNEL_DS)
                        die_kernel_fault("access to user memory with fs=KERNEL_DS",
                                         addr, esr, regs);

                if (is_el1_instruction_abort(esr))
                        die_kernel_fault("execution of user memory",
                                         addr, esr, regs);

                if (!search_exception_tables(regs->pc))
                        die_kernel_fault("access to user memory outside uaccess routines",
                                         addr, esr, regs);
        }

        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr);
  • 코드 라인 5에서 현재 태크스가 사용중인 메모리 관리에 해당하는 mm을 알아온다.
  • 코드 라인 7에서 vm 플래그 초기 값으로 VM_READ와 VM_WRITE플래그를 지정한다.
  • 코드 라인 8에서 mm 플래그 초기 값으로 FAULT_FLAG_ALLOW_RETRY와 FAULT_FLAG_KILLABLE 플래그를 지정한다.
  • 코드 라인 10~11에서 mm fault 발생 하였으므로 kprobe 디버그를 사용중이면 kprobe 상태에 따라 해당 kprobe fault 함수를 호출한다.
  • 코드 라인 17~18에서 irq context에서 fault가 발생하였거나, 해당 태스크의 fault hander를 disable한 경우 유저 fault 처리를 생략하고 곧장 커널에 대한 fault 처리 루틴을 수행하러 no_context: 레이블로 이동한다.
  • 코드 라인 20~21에서 유저 모드에서 fault가 발생한 경우 mm 플래그에 FAULT_FLAG_USER 플래그를 추가한다.
  • 코드 라인 23~25에서 유저 모드에서 명령어를 수행하다 fault가 발생한 경우 vm 플래그에 VM_EXEC를 대입하고, mm 플래그에 FAULT_FLAG_INSTRUCTION을 추가한다.
  • 코드 라인 26~29에서 쓰기 시도 중에 fault가 발생한 경우 vm 플래그에 VM_WRITE를 대입하고, mm 플래그에 FAULT_FLAG_WRITE를 추가한다.
  • 코드 라인 31~44에서 커널 모드에서 유저 영역에 접근 시 permission 문제로 fault가 발생한 경우 다음 3 가지 경우에 한하여 die 처리한다.
    • 메모리 제한이 없었던 경우
    • 커널 모드의 명령어를 처리중 fault가 발생한 경우
    • exception 테이블에 등록되지 않은 예외인 경우
  • 코드 라인 46에서 perf 디버그를 목적으로 page fault에 대한 카운팅을 알 수 있게 하기 위해 PERF_COUNT_SW_PAGE_FAULTS 카운터를 1 증가시킨다.

 

arch/arm64/mm/fault.c -2/3-

        /*
         * As per x86, we may deadlock here. However, since the kernel only
         * validly references user space from well defined areas of the code,
         * we can bug out early if this is from code which shouldn't.
         */
        if (!down_read_trylock(&mm->mmap_sem)) {
                if (!user_mode(regs) && !search_exception_tables(regs->pc))
                        goto no_context;
retry:
                down_read(&mm->mmap_sem);
        } else {
                /*
                 * The above down_read_trylock() might have succeeded in which
                 * case, we'll have missed the might_sleep() from down_read().
                 */
                might_sleep();
#ifdef CONFIG_DEBUG_VM
                if (!user_mode(regs) && !search_exception_tables(regs->pc)) {
                        up_read(&mm->mmap_sem);
                        goto no_context;
                }
#endif
        }

       fault = __do_page_fault(mm, addr, mm_flags, vm_flags);
        major |= fault & VM_FAULT_MAJOR;

        if (fault & VM_FAULT_RETRY) {
                /*
                 * If we need to retry but a fatal signal is pending,
                 * handle the signal first. We do not need to release
                 * the mmap_sem because it would already be released
                 * in __lock_page_or_retry in mm/filemap.c.
                 */
                if (fatal_signal_pending(current)) {
                        if (!user_mode(regs))
                                goto no_context;
                        return 0;
                }

                /*
                 * Clear FAULT_FLAG_ALLOW_RETRY to avoid any risk of
                 * starvation.
                 */
                if (mm_flags & FAULT_FLAG_ALLOW_RETRY) {
                        mm_flags &= ~FAULT_FLAG_ALLOW_RETRY;
                        mm_flags |= FAULT_FLAG_TRIED;
                        goto retry;
                }
        }
        up_read(&mm->mmap_sem);

mm->mmap_sem 읽기 락을 획득한 채로 __do_page_fault() 함수를 수행한다.

  • 코드 라인 6에서 현재 태스크의 mm에서 read 락을 획득 시도한다.
  • 코드 라인 7~8에서 만일 락을 획득하지 못했고, fault 발생한 곳이 유저 모드가 아니면서 exception 테이블에 등록된 예외도 없으면 곧장 커널에 대한 fault 처리 루틴을 수행하러 no_context: 레이블로 이동한다.
  • 코드 라인 9~10에서 retry: 레이블이다. 여기서 read 락을 획득한다.
  • 코드 라인 11~23에서 만일 읽기 락을 획득 시도하다 정상적으로 획득한 경우, 즉 race 컨디션 없이 락을 획득한 경우에 먼저 premption point를 수행해준다.
    • preemption 옵션 중 하나인 voluntry 커널 설정인 경우 더 높은 우선 순위의 다른 태스크를 먼저 처리하기 위해 sleep 할 수 있다.
  • 코드 라인 25에서 __do_page_fault() 함수를 호출하여 페이지 fault에 대한 처리를 수행한 후 fault 결과를 알아온다.
  • 코드 라인 26에서 fault 결과 중 VM_FAULT_MAJOR 플래그가 있는 경우 major에도 추가한다.
  • 코드 라인 28~50에서 fault 결과에 retry 요청이 있는 경우이다.
    • fatal 시그널이 존재하는 경우 유저 모드에서 fault가 발생한 경우이면 0을 반환하고, 커널 모드에서 fault가 발생한 경우 no_context: 레이블로 이동한다.
    • mm 플래그에 retry를 허용하는 경우에 한하여 mm 플래그에서 FAULT_FLAG_ALLOW_RETRY 플래그를 제거하고, retry를 시도했다는 뜻의 FAULT_FLAG_TRIED를 추가한 후 retry 레이블로 이동한다.
  • 코드 라인 51에서 fault 처리를 위해 획득했었던 read 락을 풀어준다.

 

arch/arm64/mm/fault.c -3/3-

        /*
         * Handle the "normal" (no error) case first.
         */
        if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP |
                              VM_FAULT_BADACCESS)))) {
                /*
                 * Major/minor page fault accounting is only done
                 * once. If we go through a retry, it is extremely
                 * likely that the page will be found in page cache at
                 * that point.
                 */
                if (major) {
                        current->maj_flt++;
                        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1, regs,
                                      addr);
                } else {
                        current->min_flt++;
                        perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1, regs,
                                      addr);
                }

                return 0;
        }

        /*
         * If we are in kernel mode at this point, we have no context to
         * handle this fault with.
         */
        if (!user_mode(regs))
                goto no_context;

        if (fault & VM_FAULT_OOM) {
                /*
                 * We ran out of memory, call the OOM killer, and return to
                 * userspace (which will retry the fault, or kill us if we got
                 * oom-killed).
                 */
                pagefault_out_of_memory();
                return 0;
        }

        inf = esr_to_fault_info(esr);
        set_thread_esr(addr, esr);
        if (fault & VM_FAULT_SIGBUS) {
                /*
                 * We had some memory, but were unable to successfully fix up
                 * this page fault.
                 */
                arm64_force_sig_fault(SIGBUS, BUS_ADRERR, (void __user *)addr,
                                      inf->name);
        } else if (fault & (VM_FAULT_HWPOISON_LARGE | VM_FAULT_HWPOISON)) {
                unsigned int lsb;

                lsb = PAGE_SHIFT;
                if (fault & VM_FAULT_HWPOISON_LARGE)
                        lsb = hstate_index_to_shift(VM_FAULT_GET_HINDEX(fault));

                arm64_force_sig_mceerr(BUS_MCEERR_AR, (void __user *)addr, lsb,
                                       inf->name);
        } else {
                /*
                 * Something tried to access memory that isn't in our memory
                 * map.
                 */
                arm64_force_sig_fault(SIGSEGV,
                                      fault == VM_FAULT_BADACCESS ? SEGV_ACCERR : SEGV_MAPERR,
                                      (void __user *)addr,
                                      inf->name);
        }

        return 0;

no_context:
        __do_kernel_fault(addr, esr, regs);
        return 0;
}
  • 코드 라인 4~23에서 fault 처리 결과 에러도 없고, 매핑도 정상이고 접근에 문제가 없었던 경우 major 변수 값에 따른 perf 카운터를 증가시키고, 정상 결과(0)를 반환한다.
  • 코드 라인 29~30에서 fault가 커널에서 발생한 경우는 커널을 계속 진행할 수 없어 no_context로 이동한다.
  • 코드 라인 32~40에서 OOM(메모리 부족) 상황인 경우 OOM 킬을 수행하고 정상 결과(0)를 반환한다.
    • OOM score가 높은 유저 태스크를 죽여 메모리를 확보한다.
  • 코드 라인 42에서 esr 값으로 fault_info를 알아온다.
  • 코드 라인 43에서 현재 태스크에 esr 값(내부 함수에서 조건에 따라 esr 값이 변경된다)을 기록한다.
  • 코드 라인 44~69에서 fault 주소에 접근하려 했던 프로세스에 다음과 같은 시그널을 전송한다. 참고로 해당 프로세스는 시그널을 받아 처리할 수 있는 핸들러를 추가할 수 있다. SIGBUS 및 SIGSEGV 시그널 등에 대한 처리 핸들러가 설치되지 않은 프로세스는 디폴트로 kill 된다.
    • SIGBUS fault인 경우 해당 프로세스에 SIGBUS 시그널을 전송한다.
    • HWPOISON fault 검출인 경우 메모리 에러로 프로세스에 SIGBUS 시그널을 전송한다.
    • 그 외 fault인 경우 해당 프로세스에 SIGSEGV 시그널을 전송한다.
  • 코드 라인 71에서 정상 결과(0)를 반환한다.
  • 코드 라인 73~75에서 no_context: 레이블이다. 커널이 더 이상 처리를 수행할 수 없어 fault에 대한 메시지를 출력하고 커널을 die 처리한다단 exception이 설치된 명령은 die 처리하지 않는다.

 

유저 페이지 fault 처리

__do_page_fault()

arch/arm64/mm/fault.c

static vm_fault_t __do_page_fault(struct mm_struct *mm, unsigned long addr,
                           unsigned int mm_flags, unsigned long vm_flags)
{
        struct vm_area_struct *vma = find_vma(mm, addr);

        if (unlikely(!vma))
                return VM_FAULT_BADMAP;

        /*
         * Ok, we have a good vm_area for this memory access, so we can handle
         * it.
         */
        if (unlikely(vma->vm_start > addr)) {
                if (!(vma->vm_flags & VM_GROWSDOWN))
                        return VM_FAULT_BADMAP;
                if (expand_stack(vma, addr))
                        return VM_FAULT_BADMAP;
        }

        /*
         * Check that the permissions on the VMA allow for the fault which
         * occurred.
         */
        if (!(vma->vm_flags & vm_flags))
                return VM_FAULT_BADACCESS;
        return handle_mm_fault(vma, addr & PAGE_MASK, mm_flags);
}

유저 주소에 대한 페이지 fault 처리이다.

  • 코드 라인 4~7에서 현재 태스크의 mm내에서 fault 주소로 vma 영역을 찾는다.
  • 코드 라인 13~18에서 vma 영역이 스택인 경우 스택을 확장 시도한다.
  • 코드 라인 24~25에서 vma 영역이 요청한 속성을 허용하지 않는 경우 VM_FAULT_BADACCESS 결과를 반환한다.
    • vma 공간이 vm 플래그(VM_READ, VM_WRITE, VM_EXEC 등)를 지원하지 않은 경우이다.
  • 코드 라인 26에서 아키텍처 mm fault 처리는 완료되었다. 이 다음부터는 아키텍처와 무관한 코드로 작성된 generic mm fault 루틴을 수행한다.

 

커널 페이지 fault 처리

__do_kernel_fault()

arch/arm64/mm/fault.c

static void __do_kernel_fault(unsigned long addr, unsigned int esr,
                              struct pt_regs *regs)
{
        const char *msg;

        /*
         * Are we prepared to handle this kernel fault?
         * We are almost certainly not prepared to handle instruction faults.
         */
        if (!is_el1_instruction_abort(esr) && fixup_exception(regs))
                return;

        if (WARN_RATELIMIT(is_spurious_el1_translation_fault(addr, esr, regs),
            "Ignoring spurious kernel translation fault at virtual address %016lx\n", addr))
                return;

        if (is_el1_permission_fault(addr, esr, regs)) {
                if (esr & ESR_ELx_WNR)
                        msg = "write to read-only memory";
                else
                        msg = "read from unreadable memory";
        } else if (addr < PAGE_SIZE) {
                msg = "NULL pointer dereference";
        } else {
                msg = "paging request";
        }

        die_kernel_fault(msg, addr, esr, regs);
}

커널 페이지에 대한 fault 처리를 수행한다.

  • 코드 라인 10~11에서 커널 명령(instruction)에서 fault가 발생하지 않았거나, 커널 명령에서 fault가 발생했어도 이에 대응하는 exception 테이블이 등록되어 있는 경우 커널 페이지에 대한 fault가 아니므로 그냥 함수를 빠져나간다.
  • 코드 라인 13~15에서 거짓(spurious) 커널 페이지 변환 fault인 경우 경고 메시지를 출력한다.
    • ratelimit 방식을 사용하여 디폴트로 최대 5초 이내에 10번만 메시지를 출력하도록 제한한다.
  • 코드 라인 17~21에서 커널 permission fault인 경우의 메시지를 선택한다.
  • 코드 라인 22~23에서 첫 페이지에 대한 접근의 경우의 메시지를 선택한다.
  • 코드 라인 24~26에서 그 외의 경우 메시지를 선택한다.
  • 코드 라인 28에서 커널 context를 덤프하고 die 처리를 한다.

 

die_kernel_fault()

arch/arm64/mm/fault.c

static void die_kernel_fault(const char *msg, unsigned long addr,
                             unsigned int esr, struct pt_regs *regs)
{
        bust_spinlocks(1);

        pr_alert("Unable to handle kernel %s at virtual address %016lx\n", msg,
                 addr);

        mem_abort_decode(esr);

        show_pte(addr);
        die("Oops", regs, esr);
        bust_spinlocks(0);
        do_exit(SIGKILL);
}

커널 context를 덤프하고 die 처리를 한다. 다음과 같은 내용들을 출력한다.

  • 메모리 abort 정보
  • 데이터 abort 정보
  • 페이지 테이블 정보
  • 레지스터 덤프
  • 커널 스택 덤프

 

다음은 0x00003fc8에 접근하다 발생한 커널 fault 예제이다.

[  110.965771] Unable to handle kernel paging request at virtual address 00003fc8
[  110.968799] Mem abort info:
[  110.969351]   Exception class = DABT (current EL), IL = 32 bits
[  110.969828]   SET = 0, FnV = 0
[  110.969988]   EA = 0, S1PTW = 0
[  110.970142] Data abort info:
[  110.970560]   ISV = 0, ISS = 0x00000006
[  110.970758]   CM = 0, WnR = 0
[  110.971357] user pgtable: 4k pages, 39-bit VAs, pgd = ffffffc06527e000
[  110.972077] [0000000000003fc8] *pgd=00000000a6761003, *pud=00000000a6761003, *pmd=0000000000000000
[  110.972846] Internal error: Oops: 96000006 [#1] PREEMPT SMP
[  110.973711] Modules linked in:
[  110.974527] CPU: 3 PID: 1034 Comm: sleep Not tainted 4.14.67-v8-qemu+ #74
[  110.974840] Hardware name: linux,dummy-virt (DT)
[  110.975749] task: ffffffc0650bd700 task.stack: ffffff800b0d8000
[  110.976622] PC is at cpuacct_charge+0x34/0xa8
[  110.977911] LR is at update_curr+0x98/0x228
[  110.978260] pc : [<ffffff80080ec54c>] lr : [<ffffff80080d5030>] pstate: a00001c5
[  110.978803] sp : ffffff800b0dba60
[  110.978985] x29: ffffff800b0dba60 x28: ffffffc06ffb0480 
[  110.979768] x27: ffffff80092aa7c0 x26: ffffffc06d57d780 
[  110.980104] x25: ffffff80092aa000 x24: 0000000000000008 
[  110.980295] x23: ffffffc06ffb0480 x22: 0000000001c8e1f0 
[  110.980460] x21: 0000000000000001 x20: 0000000001c8e1f0 
[  110.980858] x19: ffffffc0650b9d00 x18: 0000000000000000 
[  110.981157] x17: 0000000000000000 x16: ffffff8008111500 
[  110.981402] x15: 0000000000000000 x14: 00000000004bb6d4 
[  110.981611] x13: 00000000ff9cea5c x12: 0000000000000000 
[  110.981902] x11: 00000000ff9cea94 x10: 00000000004ce000 
[  110.982104] x9 : 000000000000075a x8 : 0000000000000400 
[  110.982268] x7 : 000000000000075a x6 : 0000000002739d7c 
[  110.982424] x5 : 00ffffffffffffff x4 : 0000000000000015 
[  110.983490] x3 : 0000000000000000 x2 : ffffffffff76abc0 
[  110.983734] x1 : 0000000000003ec0 x0 : 0000000000003ec0 
[  110.983940] Process sleep (pid: 1034, stack limit = 0xffffff800b0d8000)
[  110.984213] Call trace:
[  110.984388] Exception stack(0xffffff800b0db920 to 0xffffff800b0dba60)
[  110.984723] b920: 0000000000003ec0 0000000000003ec0 ffffffffff76abc0 0000000000000000
[  110.984970] b940: 0000000000000015 00ffffffffffffff 0000000002739d7c 000000000000075a
[  110.985185] b960: 0000000000000400 000000000000075a 00000000004ce000 00000000ff9cea94
[  110.985396] b980: 0000000000000000 00000000ff9cea5c 00000000004bb6d4 0000000000000000
[  110.985624] b9a0: ffffff8008111500 0000000000000000 0000000000000000 ffffffc0650b9d00
[  110.985878] b9c0: 0000000001c8e1f0 0000000000000001 0000000001c8e1f0 ffffffc06ffb0480
[  110.986092] b9e0: 0000000000000008 ffffff80092aa000 ffffffc06d57d780 ffffff80092aa7c0
[  110.986308] ba00: ffffffc06ffb0480 ffffff800b0dba60 ffffff80080d5030 ffffff800b0dba60
[  110.986528] ba20: ffffff80080ec54c 00000000a00001c5 0000000000000003 ffffffc06d553e00
[  110.986825] ba40: 0000007fffffffff ffffff80092a9b20 ffffff800b0dba60 ffffff80080ec54c
[  110.987497] [<ffffff80080ec54c>] cpuacct_charge+0x34/0xa8
[  110.987842] [<ffffff80080d5030>] update_curr+0x98/0x228
[  110.988085] [<ffffff80080d6108>] dequeue_task_fair+0x68/0x528
[  110.988251] [<ffffff80080ce440>] deactivate_task+0xa8/0xf0
[  110.988406] [<ffffff80080da9f4>] load_balance+0x454/0x960
[  110.988693] [<ffffff80080db2a4>] pick_next_task_fair+0x3a4/0x6c8
[  110.988987] [<ffffff8008b00bac>] __schedule+0x104/0x898
[  110.989226] [<ffffff8008b01374>] schedule+0x34/0x98
[  110.989459] [<ffffff8008b05084>] do_nanosleep+0x7c/0x168
[  110.989739] [<ffffff80081113f4>] hrtimer_nanosleep+0xa4/0x128
[  110.990058] [<ffffff8008111570>] compat_SyS_nanosleep+0x70/0x90
[  110.990300] Exception stack(0xffffff800b0dbec0 to 0xffffff800b0dc000)
[  110.990825] bec0: 00000000ff9cea6c 0000000000000000 00000000f7b82590 00000000ff9cea6c
[  110.991237] bee0: 0000000005f5e100 0000000000000000 00000000004b8e80 00000000000000a2
[  110.991768] bf00: 0000000000000000 0000000000000000 00000000004ce000 00000000ff9cea94
[  110.992084] bf20: 0000000000000000 00000000ff9cea5c 00000000004bb6d4 0000000000000000
[  110.992521] bf40: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
[  110.993118] bf60: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
[  110.993655] bf80: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
[  110.994227] bfa0: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
[  110.995517] bfc0: 00000000f7ab4a10 0000000060040010 00000000ff9cea6c 00000000000000a2
[  110.995998] bfe0: 0000000000000000 0000000000000000 0000000000000000 0000000000000000
[  110.996235] [<ffffff8008083b18>] __sys_trace_return+0x0/0x4
[  110.996618] Code: 52800035 f9401260 8b010000 b4000080 (f9408400) 
[  110.997850] ---[ end trace cdf6df9c4e5b5c95 ]---
[  110.998316] note: sleep[1034] exited with preempt_count 2

 


테이블 변환 fault 처리

do_translation_fault()

arch/arm64/mm/fault.c

static int __kprobes do_translation_fault(unsigned long addr,
                                          unsigned int esr,
                                          struct pt_regs *regs)
{
        if (is_ttbr0_addr(addr))
                return do_page_fault(addr, esr, regs);

        do_bad_area(addr, esr, regs);
        return 0;
}

가상 주소를 물리 주소로 변환하는 페이지 테이블을 사용한 변환에 fault가 발생하였을 수행을 한다.

  • 코드 라인 5~6에서 fault 발생 주소가 유저 영역에 접근하는 경우 페이지 fault 처리로 이동한다.
  • 코드 라인 8에서 영역 침범과 관련한 fault 처리를 수행하도록 이동한다.

 

영역 침범 fault 처리

do_bad_area()

arch/arm64/mm/fault.c

static void do_bad_area(unsigned long addr, unsigned int esr, struct pt_regs *regs)
{
        /*
         * If we are in kernel mode at this point, we have no context to
         * handle this fault with.
         */
        if (user_mode(regs)) {
                const struct fault_info *inf = esr_to_fault_info(esr);

                set_thread_esr(addr, esr);
                arm64_force_sig_fault(inf->sig, inf->code, (void __user *)addr,
                                      inf->name);
        } else {
                __do_kernel_fault(addr, esr, regs);
        }
}

영역 침범에 대한 fault 처리를 수행한다.

  • 코드 라인 7~12에서 유저 모드에서 동작 중에 fault 발생한 경우 해당 프로세스에 시그널을 전송한다.
    • 해당 프로세스가 die 한다. 만일 프로세스에 시그널 핸들러가 설치된 경우 해당 핸들러 코드가 동작하다.
  • 코드 라인 13~15에서 커널 fault 처리를 수행하고 die 한다.

 


정렬 fault 처리

do_alignment_fault()

arch/arm64/mm/fault.c

static int do_alignment_fault(unsigned long addr, unsigned int esr,
                              struct pt_regs *regs)
{
        do_bad_area(addr, esr, regs);
        return 0;
}

정렬 fault가 발생한 겨우 영역 침범과 동일한 코드를 호출하여 처리한다.

 

참고

 

Exception -8- (ARM64 Handler)

<kernel v5.4>

 

Context 백업

pt_regs 구조체

arch/arm64/include/asm/ptrace.h

/*
 * This struct defines the way the registers are stored on the stack during an
 * exception. Note that sizeof(struct pt_regs) has to be a multiple of 16 (for
 * stack alignment). struct user_pt_regs must form a prefix of struct pt_regs.
 */
struct pt_regs {
        union {
                struct user_pt_regs user_regs;
                struct {
                        u64 regs[31];
                        u64 sp;
                        u64 pc;
                        u64 pstate;
                };
        };
        u64 orig_x0;
#ifdef __AARCH64EB__
        u32 unused2;
        s32 syscallno;
#else
        s32 syscallno;
        u32 unused2;
#endif

        u64 orig_addr_limit;
        /* Only valid when ARM64_HAS_IRQ_PRIO_MASKING is enabled. */
        u64 pmr_save;
        u64 stackframe[2];
};

exception 발생 시 스택에 저장될 레지스터들이다.

  •  regs[31]
    • x0~x30(lr) 범용 레지스터가 저장된다.
  • sp
    • 중단될 때의 스택 레지스터가 저장된다.
  • pc
    • 중단될 때 복귀할 주소가 저장된다.
  • pstate
    • 중단될 때의 PSTATE 값이 저장된다.
  • orig_x0
  • syscallno
    • syscall 호출 번호
  • orig_addr_limit
    • 유저 태스크의 주소 제한 값이 저장된다.
  • pmr_save
    • irq priority mask 값이 저장된다.
  • stackframe[]

 

user_pt_regs 구조체

arch/arm64/include/uapi/asm/ptrace.h

struct user_pt_regs {
        __u64           regs[31];
        __u64           sp;
        __u64           pc;
        __u64           pstate;
};

pr_regs의 앞부분이 동일하다.

 

kernel_entry 매크로

arch/arm64/kernel/entry.S -1/2-

.       .macro  kernel_entry, el, regsize = 64
        .if     \regsize == 32
        mov     w0, w0                          // zero upper 32 bits of x0
        .endif
        stp     x0, x1, [sp, #16 * 0]
        stp     x2, x3, [sp, #16 * 1]
        stp     x4, x5, [sp, #16 * 2]
        stp     x6, x7, [sp, #16 * 3]
        stp     x8, x9, [sp, #16 * 4]
        stp     x10, x11, [sp, #16 * 5]
        stp     x12, x13, [sp, #16 * 6]
        stp     x14, x15, [sp, #16 * 7]
        stp     x16, x17, [sp, #16 * 8]
        stp     x18, x19, [sp, #16 * 9]
        stp     x20, x21, [sp, #16 * 10]
        stp     x22, x23, [sp, #16 * 11]
        stp     x24, x25, [sp, #16 * 12]
        stp     x26, x27, [sp, #16 * 13]
        stp     x28, x29, [sp, #16 * 14]

        .if     \el == 0
        clear_gp_regs
        mrs     x21, sp_el0
        ldr_this_cpu    tsk, __entry_task, x20  // Ensure MDSCR_EL1.SS is clear,
        ldr     x19, [tsk, #TSK_TI_FLAGS]       // since we can unmask debug
        disable_step_tsk x19, x20               // exceptions when scheduling.

        apply_ssbd 1, x22, x23

        .else
        add     x21, sp, #S_FRAME_SIZE
        get_current_task tsk
        /* Save the task's original addr_limit and set USER_DS */
        ldr     x20, [tsk, #TSK_TI_ADDR_LIMIT]
        str     x20, [sp, #S_ORIG_ADDR_LIMIT]
        mov     x20, #USER_DS
        str     x20, [tsk, #TSK_TI_ADDR_LIMIT]
        /* No need to reset PSTATE.UAO, hardware's already set it to 0 for us */
        .endif /* \el == 0 */
        mrs     x22, elr_el1
        mrs     x23, spsr_el1
        stp     lr, x21, [sp, #S_LR]

        /*
         * In order to be able to dump the contents of struct pt_regs at the
         * time the exception was taken (in case we attempt to walk the call
         * stack later), chain it together with the stack frames.
         */
        .if \el == 0
        stp     xzr, xzr, [sp, #S_STACKFRAME]
        .else
        stp     x29, x22, [sp, #S_STACKFRAME]
        .endif
        add     x29, sp, #S_STACKFRAME

context 전환을 목적으로 레지스터들을 백업한다. el0 또는 AArch32 여부에 따라 약간 차이가 있다.

  • 코드 라인 2~4에서 EL0 AArch32에서 exception 진입한 경우 32비트 레지스터를 사용하기 위해 x0 레지스터의 상위 32비트를 0으로 클리어한다.
  • 코드 라인 5~19에서 context 저장을 목적으로 x0~x29 레지스터를 스택에 백업한다.
  • 코드 라인 21~22에서 EL0에서 exception 진입한 경우 x0~x29 레지스터를 0으로 클리어한다.
  • 코드 라인 23에서 중단된 기존 스택 주소(sp_el0)를 잠시 x21에 백업해둔다.
  • 코드 라인 24~26에서 __entry_task 태스크의 thread_info.flag 값을 x19 레지스터로 알아와서 싱글 스텝 디버깅 비트가 켜져있으면 싱글 스텝 디버거를 끈다.
    • __entry_task는 per-cpu 변수로 선언되었다.
  • 코드 라인 28에서 SSBD(Speculative Store Bypass Disable) 기능을 사용하는 경우 Speculative Store 공격으로 부터 커널을 보호하기 위해 유저와 커널의 전환 시 EL1이 아닌 상위(EL2 or EL3)에서 수행하는 workaround를 적용한다.  사용하지 않는 경우 그냥 skip 한다.
  • 코드 라인 30~31에서 EL0가 아닌 곳에서 exception 진입한 경우 pt_regs 만큼 스택을 키우기 전의 스택 주소를 잠시 x21에 저장한다.
  • 코드 라인 32~37에서 현재 thread_info.addr_limit 값을 스택의 orig_addr_limit 위치에 백업한 후 최대 유저 주소 한계 값을 지정한다.
    • pt_regs.orig_addr_limit <- thread_info.addr_limit
    • thread_info.addr_limit <- USER_DS
      • 예) CONFIG_ARM64_VA_BITS_48 커널 옵션이 사용되는 경우 USER_DS=0x0000_ffff_ffff_ffff
      • 예) CONFIG_ARM64_VA_BITS_52 커널 옵션이 사용되는 경우 USER_DS=0x000f_ffff_ffff_ffff
  • 코드 라인 40~41에서 x22 <- elr_el1 및 x23 <- spsr_el1을 수행한다.
  • 코드 라인 42에서 lr과 pt_regs 만큼 스택 키우기 전의 스택 주소가 담긴 x21 레지스터 값을 pt_regs.x30(lr)과 pt_regs.sp 위치에 저장한다.
  • 코드 라인 49~50에서 EL0에서 진입한 경우 스택의 x0, x1 위치에 0을 저장한다.
  • 코드 라인 51~52에서 EL1에서 진입한 경우 x29와 중단되어 복귀할 주소(pc -> elr_el1)가 담긴 x22 레지스터 값을 pt_regs.stackframe[] 위치에 저장한다.
  • 코드 라인 54에서 pt_regs.stackframe[] 주소를 x29 레지스터에 대입한다.

 

arch/arm64/kernel/entry.S -2/2-

#ifdef CONFIG_ARM64_SW_TTBR0_PAN
        /*
         * Set the TTBR0 PAN bit in SPSR. When the exception is taken from
         * EL0, there is no need to check the state of TTBR0_EL1 since
         * accesses are always enabled.
         * Note that the meaning of this bit differs from the ARMv8.1 PAN
         * feature as all TTBR0_EL1 accesses are disabled, not just those to
         * user mappings.
         */
alternative_if ARM64_HAS_PAN
        b       1f                              // skip TTBR0 PAN
alternative_else_nop_endif

        .if     \el != 0
        mrs     x21, ttbr0_el1
        tst     x21, #TTBR_ASID_MASK            // Check for the reserved ASID
        orr     x23, x23, #PSR_PAN_BIT          // Set the emulated PAN in the saved SPSR
        b.eq    1f                              // TTBR0 access already disabled
        and     x23, x23, #~PSR_PAN_BIT         // Clear the emulated PAN in the saved SPSR
        .endif

        __uaccess_ttbr0_disable x21
1:
#endif

        stp     x22, x23, [sp, #S_PC]

        /* Not in a syscall by default (el0_svc overwrites for real syscall) */
        .if     \el == 0
        mov     w21, #NO_SYSCALL
        str     w21, [sp, #S_SYSCALLNO]
        .endif

        /*
         * Set sp_el0 to current thread_info.
         */
        .if     \el == 0
        msr     sp_el0, tsk
        .endif

        /* Save pmr */
alternative_if ARM64_HAS_IRQ_PRIO_MASKING
        mrs_s   x20, SYS_ICC_PMR_EL1
        str     x20, [sp, #S_PMR_SAVE]
alternative_else_nop_endif

        /*
         * Registers that may be useful after this macro is invoked:
         *
         * x20 - ICC_PMR_EL1
         * x21 - aborted SP
         * x22 - aborted PC
         * x23 - aborted PSTATE
        */
        .endm
  • 코드 라인 1에서 CONFIG_ARM64_SW_TTBR0_PAN 커널 옵션은 보안을 향상 시키기 위해 커널이 유저 영역의 접근을 방지하기 위해 사용된다.
  • 코드 라인 10~12에서 ARM64_HAS_PAN 기능을 갖춘 시스템에서는 skip 한다.
  • 코드 라인 14~20에서 EL0에서 진입한 경우가 아니면 ttbr0_el1 레지스터의 ASID 필드 값이 0인 경우 이미 ttbr0 액세스가 disable 되어 있으므로 x23 레지스터에 PSR_PAN_BIT를 설정하고 skip 한다. ASID 필드 값이 존재하는 경우 x23 레지스터에 PSR_PAN_BIT를 클리어하고 계속 진행한다.
  • 코드 라인 22에서 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 금지하도록 ttbr을 설정한다.
    • x21 레지스터는 매크로에서 임시로 사용되며 크래시된다.
    • 참고: copy_from_user() | 문c
  • 코드 라인 26에서 중단되어 복귀할 주소(pc -> elr_el1)가 담긴 x22 레지스터와 중단될 때의 상태(spsr) 값이 담긴 x23 레지스터를 pt_regs.pc와 pt_regs->pstate 위치에 저장한다.
  • 코드 라인 29~32에서 EL0에서 진입한 경우이면 초기 값으로 NO_SYSCALL(-1) 값을 pt_regs.syscallno 위치에 백업한다.
    • -1은 단지 초기 값일뿐 syscall 번호가 아니다.
  • 코드 라인 37~39에서 EL0에서 진입한 경우이면 현재 스레드 주소를 스택(sp_el0)으로 지정한다.
  • 코드 라인 42~45에서 irq priority masking이 가능한 시스템이면 SYS_ICC_PMR_EL1 레지스터를 읽어 pt_regs.pmr_save위치에 백업해둔다. irq priority masking이 가능한 시스템이 아닌 경우 nop으로 채운다.

 

clear_gp_regs 매크로

arch/arm64/kernel/entry.S

.       .macro  clear_gp_regs
        .irp    n,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29
        mov     x\n, xzr
        .endr
        .endm

컴파일러로 하여금 x0~x29 레지스터를 0으로 채우게 한다.

  • mov x0, xzr
  • mov z1, xzr
  • mov z29, xzr

 

ldr_this_cpu 매크로

arch/arm64/include/asm/assembler.h

        /*
         * @dst: Result of READ_ONCE(per_cpu(sym, smp_processor_id()))
         * @sym: The name of the per-cpu variable
         * @tmp: scratch register
         */
        .macro ldr_this_cpu dst, sym, tmp
        adr_l   \dst, \sym
alternative_if_not ARM64_HAS_VIRT_HOST_EXTN
        mrs     \tmp, tpidr_el1
alternative_else
        mrs     \tmp, tpidr_el2
alternative_endif
        ldr     \dst, [\dst, \tmp]
        .endm

로컬 cpu에서 per-cpu 변수 @sym의 주소를 @dst에 알아온다. 임시 @tmp 레지스터는 스크래치 된다.

  • tpidr_el1 (또는 tpidr_el2)는 per-cpu offset을 담고 있다.

 

disable_step_tsk 매크로

arch/arm64/include/asm/assembler.h

        .macro  disable_step_tsk, flgs, tmp
        tbz     \flgs, #TIF_SINGLESTEP, 9990f
        mrs     \tmp, mdscr_el1
        bic     \tmp, \tmp, #DBG_MDSCR_SS
        msr     mdscr_el1, \tmp
        isb     // Synchronise with enable_dbg
9990:
        .endm

싱글 스텝 디버깅을 사용하는 중이면 디버거를 끈다.

  • 어떤 태스크의 SINGLESTEP 플래그(@flgs) 비트가 사용중인 경우 MDSCR_EL1(Monitor Debug System Control Register EL1) 레지스터의 SS(Software Step control) 비트를 disable 한다.

 

get_current_task 매크로

arch/arm64/include/asm/assembler.h

/*
 * Return the current task_struct.
 */
.       .macro  get_current_task, rd
        mrs     \rd, sp_el0
        .endm

현재 태스크를 @rd 레지스터로 가져온다.

  • 스택의 가장 아래에 struct task_struct가 존재한다.

 

 


Context 복구

kernel_exit 매크로

arch/arm64/kernel/entry.S -1/2-

        .macro  kernel_exit, el
        .if     \el != 0
        disable_daif

        /* Restore the task's original addr_limit. */
        ldr     x20, [sp, #S_ORIG_ADDR_LIMIT]
        str     x20, [tsk, #TSK_TI_ADDR_LIMIT]

        /* No need to restore UAO, it will be restored from SPSR_EL1 */
        .endif

        /* Restore pmr */
alternative_if ARM64_HAS_IRQ_PRIO_MASKING
        ldr     x20, [sp, #S_PMR_SAVE]
        msr_s   SYS_ICC_PMR_EL1, x20
        /* Ensure priority change is seen by redistributor */
        dsb     sy
alternative_else_nop_endif

        ldp     x21, x22, [sp, #S_PC]           // load ELR, SPSR
        .if     \el == 0
        ct_user_enter
        .endif

#ifdef CONFIG_ARM64_SW_TTBR0_PAN
        /*
         * Restore access to TTBR0_EL1. If returning to EL0, no need for SPSR
         * PAN bit checking.
         */
alternative_if ARM64_HAS_PAN
        b       2f                              // skip TTBR0 PAN
alternative_else_nop_endif

        .if     \el != 0
        tbnz    x22, #22, 1f                    // Skip re-enabling TTBR0 access if the PSR_PAN_BIT is set
        .endif

        __uaccess_ttbr0_enable x0, x1

        .if     \el == 0
        /*
         * Enable errata workarounds only if returning to user. The only
         * workaround currently required for TTBR0_EL1 changes are for the
         * Cavium erratum 27456 (broadcast TLBI instructions may cause I-cache
         * corruption).
         */
        bl      post_ttbr_update_workaround
        .endif
1:
        .if     \el != 0
        and     x22, x22, #~PSR_PAN_BIT         // ARMv8.0 CPUs do not understand this bit
        .endif
2:
#endif
  • 코드 라인 2~7에서 el0 exception에서 진입하였던 경우가 아니면 PSTATE의 DAIF 플래그들을 마스크하여 인터럽트 및 Abort Exception들을 허용하지 않게 한다. 그런 후 스택에 백업해둔 tsk->addr_limit을 복구한다.
  • 코드 라인 13~18에서 Pesudo-NMI 기능을 사용할 수 있는 시스템의 경우 스택에 백업해둔 pt_regs->pmr_save를 SYS_ICC_PMR_EL1 레지스터로 복구한다. 그런 후 redistributor가 변경된 값을 액세스 할 수 있도록 dsb 명령을 통해 operation이 완료될 때까지 대기한다.
  • 코드 라인 20에서 중단 점으로 복귀하기 위해 pt_regs->pc와 spsr 값을 x21, x22 레지스터로 읽어온다.
  • 코드 라인 21~23에서 el0 exception에서 진입하였던 경우 context 트래킹 디버그를 지원한다.
SW TTBR0 PAN 동작 – 커널의 유저 영역 분리
  • 코드 라인 30~32에서 ARMv8.1 아키텍처 이상에서 커널에서 유저 공간의 접근을 막는 PAN(Privilege Access Never) 기능을 지원하는 경우 SW  TTBR0 PAN을 skip 한다.
  • 코드 라인 34~36에서 el0 exception에서 진입하였던 경우가 아니면 중단 점에서의 PSTATE 레지스터의 PAN 비트가 설정되어 동작 중인 경우 재설정을 피하기 위해 1f 레이블로 이동한다.
  • 코드 라인 38에서 SW_TTBR0_PAN 기능을 통해 커널에서 유저 공간의 접근을 허용하게 한다.
  • 코드 라인 40~48에서 유저 복귀 전에 Cavium 칩의 erratum을 지원한다.
    • 내부적으로 TTBRx_el1을 갱신하는 경우 ic iallu; dsb nsh; isb 등의 베리어등을 수행해야 한다.
  • 코드 라인 49~52에서 1: 레이블에서는 el0 exception에서 진입하였던 경우가 아니면 ARMv8.0 아키텍처는 PSTATE의 PAN 비트를 모르므로 클리어한다.

 

arch/arm64/kernel/entry.S -2/2-

        .if     \el == 0
        ldr     x23, [sp, #S_SP]                // load return stack pointer
        msr     sp_el0, x23
        tst     x22, #PSR_MODE32_BIT            // native task?
        b.eq    3f

#ifdef CONFIG_ARM64_ERRATUM_845719
alternative_if ARM64_WORKAROUND_845719
#ifdef CONFIG_PID_IN_CONTEXTIDR
        mrs     x29, contextidr_el1
        msr     contextidr_el1, x29
#else
        msr contextidr_el1, xzr
#endif
alternative_else_nop_endif
#endif
3:
#ifdef CONFIG_ARM64_ERRATUM_1418040
alternative_if_not ARM64_WORKAROUND_1418040
        b       4f
alternative_else_nop_endif
        /*
         * if (x22.mode32 == cntkctl_el1.el0vcten)
         *     cntkctl_el1.el0vcten = ~cntkctl_el1.el0vcten
         */
        mrs     x1, cntkctl_el1
        eon     x0, x1, x22, lsr #3
        tbz     x0, #1, 4f
        eor     x1, x1, #2      // ARCH_TIMER_USR_VCT_ACCESS_EN
        msr     cntkctl_el1, x1
4:
#endif
        apply_ssbd 0, x0, x1
        .endif

        msr     elr_el1, x21                    // set up the return data
        msr     spsr_el1, x22
        ldp     x0, x1, [sp, #16 * 0]
        ldp     x2, x3, [sp, #16 * 1]
        ldp     x4, x5, [sp, #16 * 2]
        ldp     x6, x7, [sp, #16 * 3]
        ldp     x8, x9, [sp, #16 * 4]
        ldp     x10, x11, [sp, #16 * 5]
        ldp     x12, x13, [sp, #16 * 6]
        ldp     x14, x15, [sp, #16 * 7]
        ldp     x16, x17, [sp, #16 * 8]
        ldp     x18, x19, [sp, #16 * 9]
        ldp     x20, x21, [sp, #16 * 10]
        ldp     x22, x23, [sp, #16 * 11]
        ldp     x24, x25, [sp, #16 * 12]
        ldp     x26, x27, [sp, #16 * 13]
        ldp     x28, x29, [sp, #16 * 14]
        ldr     lr, [sp, #S_LR]
        add     sp, sp, #S_FRAME_SIZE           // restore sp

        .if     \el == 0
alternative_insn eret, nop, ARM64_UNMAP_KERNEL_AT_EL0
#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
        bne     5f
        msr     far_el1, x30
        tramp_alias     x30, tramp_exit_native
        br      x30
5:
        tramp_alias     x30, tramp_exit_compat
        br      x30
#endif
        .else
        eret
        .endif
        sb
        .endm
  • 코드 라인 1~3에서 el0에서 exception 진입한 경우 sp_el0를 복원한다.
  • 코드 라인 4~5에서 중단점에서의 PSTATE 값에 MODE32 비트가 설정된 경우 3f 레이블로 이동한다.
  • 코드 라인 7~16에서 Cortext-A53 아키텍처에서 잘못된 load가 발생할 수 있는 에러를 방지하기 위해 erratum 84715 코드를 지원하며, 설명은 생략한다.
  • 코드 라인 18~32에서 Cortex-A76 아키텍처의 AArch32에서 Generic 타이머의 잘못된 읽기가 발생할 수 있는 에러를 방지하기 위해 erratum 1165522 코드를 지원하며, 설명은 생략한다.
  • 코드 라인 33에서 SSBD(Speculative Store Bypass Disable) 기능을 사용하는 경우 Speculative Store 공격으로 부터 커널을 보호하기 위해 유저와 커널의 전환 시 EL1이 아닌 상위(EL2 or EL3)에서 수행하는 workaround를 적용한다.  사용하지 않는 경우 그냥 skip 한다.
레지스터들 복원
  • 코드 라인 36~37에서 중단 점 pc를 elr_el1 레지스터에 기록하고, 중단 점에서의 PSTATE 값을 spsr_el1에 다시 기록하여 복원한다.
  • 코드 라인 38~52에서 스택에 백업해둔 x0~x29까지의 레지스터 정보를 다시 복원한다.
  • 코드 라인 53~54에서 스택에 백업해둔 lr(x30) 레지스터를 복원하고, 스택을 pt_regs 사이즈만큼 pop 한다.
trampoline 사용 – 유저에서 커널 영역 분리
  • 코드 라인 56~66에서 el0에서 exception 진입한 경우이다. 유저 영역에서 커널 영역을 완전히 분리하여 액세스를 방지하기 위해 trampoline 벡터를 이용하여 tramp_vectors를 vbar_el1에 지정하고, 커널 영역은 분리한 후 exception 발생하였던 중단점으로 복귀한다.
  • 코드 라인 70에서 Speculation barrier 명령을 수행한다. (dsb, isb)

 

ct_user_enter 매크로

arch/arm64/kernel/entry.S

        .macro ct_user_enter
#ifdef CONFIG_CONTEXT_TRACKING
        bl      context_tracking_user_enter
#endif
        .endm

디버그를 위해 context 트래킹 커널 옵션을 사용하는 경우 유저 복귀 전 trace 출력을 지원한다.

 

sb 매크로

arch/arm64/include/asm/assembler.h

/*
 * Speculation barrier
 */
        .macro  sb
alternative_if_not ARM64_HAS_SB
        dsb     nsh
        isb
alternative_else
        SB_BARRIER_INSN
        nop
alternative_endif
        .endm

Speculation barrier로 시스템이 지원하지 않으면 dsb, isb를 사용하고, 지원하는 경우 sb 전용 명령을 사용한다.

 


SSBD(Speculative Store Bypass Disable)

Speculative Store 공격으로 부터 커널을 보호하기 위해 유저와 커널의 전환 시 EL1이 아닌 상위(EL2 or EL3)에서 수행하는 workaround를 적용한다.

 

apply_ssbd 매크로

arch/arm64/kernel/entry.S

.       // This macro corrupts x0-x3. It is the caller's duty
        // to save/restore them if required.
        .macro  apply_ssbd, state, tmp1, tmp2
#ifdef CONFIG_ARM64_SSBD
alternative_cb  arm64_enable_wa2_handling
        b       .L__asm_ssbd_skip\@
alternative_cb_end
        ldr_this_cpu    \tmp2, arm64_ssbd_callback_required, \tmp1
        cbz     \tmp2,  .L__asm_ssbd_skip\@
        ldr     \tmp2, [tsk, #TSK_TI_FLAGS]
        tbnz    \tmp2, #TIF_SSBD, .L__asm_ssbd_skip\@
        mov     w0, #ARM_SMCCC_ARCH_WORKAROUND_2
        mov     w1, #\state
alternative_cb  arm64_update_smccc_conduit
        nop                                     // Patched to SMC/HVC #0
alternative_cb_end
.L__asm_ssbd_skip\@:
#endif
        .endm

SSBD 기능을 적용할 수 있는 시스템에서 유저와 커널의 전환 시 상위(EL2 or EL3)에서 수행하게 한다. 적용되지 않는 시스템은 그냥 skip 한다.

  • 코드 라인 2~5에서 시스템이 SSBD 기능을 지원하는 경우 nop을 수행하고, 지원하지 않는 경우 branch 명령대로 .L__asm_ssbd_skip 레이블로 이동한다.
    • alternative_cb을 통해 부트업 타임에 arm64_enable_wa2_handling() 함수를 수행하여 SSBD 기능이 동작하는 경우 다음 branch 명령을 nop으로 대체한다.
  • 코드 라인 6~7에서 per-cpu 변수인 arm64_ssbd_callback_required 값이 0인 경우 .L__asm_ssbd_skip 레이블로 이동한다.
  • 코드 라인 8~9에서 현재 태스크에 TIF_SSBD 플래그가 설정된 경우가 아니면 .L__asm_ssbd_skip 레이블로 이동한다.
  • 코드 라인 10~14에서 w0 레지스터에 ARM_SMCCC_ARCH_WORKAROUND_2를 준비하고, w1 레지스터에 @state를 지정한 뒤 EL2의 hvc 또는 EL3의 smc를 호출한다.

 

arm64_update_smccc_conduit()

arch/arm64/kernel/cpu_errata.c

void __init arm64_update_smccc_conduit(struct alt_instr *alt,
                                       __le32 *origptr, __le32 *updptr,
                                       int nr_inst)
{
        u32 insn;

        BUG_ON(nr_inst != 1);

        switch (psci_ops.conduit) {
        case PSCI_CONDUIT_HVC:
                insn = aarch64_insn_get_hvc_value();
                break;
        case PSCI_CONDUIT_SMC:
                insn = aarch64_insn_get_smc_value();
                break;
        default:
                return;
        }

        *updptr = cpu_to_le32(insn);
}

SSBD 기능을 적용할 수 있는 시스템에서 hvc 또는 smc 명령으로 대체한다.

 

arm64_enable_wa2_handling()

arch/arm64/kernel/cpu_errata.c

void __init arm64_enable_wa2_handling(struct alt_instr *alt,
                                      __le32 *origptr, __le32 *updptr,
                                      int nr_inst)
{
        BUG_ON(nr_inst != 1);
        /*
         * Only allow mitigation on EL1 entry/exit and guest
         * ARCH_WORKAROUND_2 handling if the SSBD state allows it to
         * be flipped.
         */
        if (arm64_get_ssbd_state() == ARM64_SSBD_KERNEL)
                *updptr = cpu_to_le32(aarch64_insn_gen_nop());
}

SSBD 기능을 적용할 수 있는 시스템에서 nop으로 대체한다.

 


EL1 Exception

커널 동작 중 sync exception

el1_sync

arch/arm64/kernel/entry.S

        .align  6
el1_sync:
        kernel_entry 1
        mrs     x1, esr_el1                     // read the syndrome register
        lsr     x24, x1, #ESR_ELx_EC_SHIFT      // exception class
        cmp     x24, #ESR_ELx_EC_DABT_CUR       // data abort in EL1
        b.eq    el1_da
        cmp     x24, #ESR_ELx_EC_IABT_CUR       // instruction abort in EL1
        b.eq    el1_ia
        cmp     x24, #ESR_ELx_EC_SYS64          // configurable trap
        b.eq    el1_undef
        cmp     x24, #ESR_ELx_EC_PC_ALIGN       // pc alignment exception
        b.eq    el1_pc
        cmp     x24, #ESR_ELx_EC_UNKNOWN        // unknown exception in EL1
        b.eq    el1_undef
        cmp     x24, #ESR_ELx_EC_BREAKPT_CUR    // debug exception in EL1
        b.ge    el1_dbg
        b       el1_inv

el1_ia:
        /*
         * Fall through to the Data abort case
         */
el1_da:
        /*
         * Data abort handling
         */
        mrs     x3, far_el1
        inherit_daif    pstate=x23, tmp=x2
        untagged_addr   x0, x3
        mov     x2, sp                          // struct pt_regs
        bl      do_mem_abort

        kernel_exit 1
el1_pc:
        /*
         * PC alignment exception handling. We don't handle SP alignment faults,
         * since we will have hit a recursive exception when trying to push the
         * initial pt_regs.
         */
        mrs     x0, far_el1
        inherit_daif    pstate=x23, tmp=x2
        mov     x2, sp
        bl      do_sp_pc_abort
        ASM_BUG()
el1_undef:
        /*
         * Undefined instruction
         */
        inherit_daif    pstate=x23, tmp=x2
        mov     x0, sp
        bl      do_undefinstr
        kernel_exit 1
el1_dbg:
        /*
         * Debug exception handling
         */
        cmp     x24, #ESR_ELx_EC_BRK64          // if BRK64
        cinc    x24, x24, eq                    // set bit '0'
        tbz     x24, #0, el1_inv                // EL1 only
        gic_prio_kentry_setup tmp=x3
        mrs     x0, far_el1
        mov     x2, sp                          // struct pt_regs
        bl      do_debug_exception
        kernel_exit 1
el1_inv:
        // TODO: add support for undefined instructions in kernel mode
        inherit_daif    pstate=x23, tmp=x2
        mov     x0, sp
        mov     x2, x1
        mov     x1, #BAD_SYNC
        bl      bad_mode
        ASM_BUG()
ENDPROC(el1_sync)

el1에서 동기 exception이 발생하여 진입한 경우이다.

  • 코드 라인 3에서 context 전환을 위해 레지스터들을 스택에 pt_regs 구조체 만큼 백업한다.
  • 코드 라인 4~18에서 esr_el1(Exception Syndrom Register EL1) 레지스터를 읽어와서(x1) EC(Exception Class) 필드 값에 따라 다음과 같이 처리한다.
    • ESR_ELx_EC_DABT_CUR (0x25)
      • Data Abort인 경우 el1_da 레이블로 이동한다.
    • ESR_ELx_EC_IABT_CUR (0x21)
      • Instruction Abort인 경우 el1_ia 레이블로 이동한다.
    • ESR_ELx_EC_SYS64 (0x18)
      • Configurable Trap인 경우 el1_undef 레이블로 이동한다.
    • ESR_ELx_EC_PC_ALIGN (0x22)
      • PC Alignment Exception인 경우 el1_pc 레이블로 이동한다.
    • ESR_ELx_EC_UNKNOWN (0x00)
      • Unknown Exception인 경우 el1_undef 레이블로 이동한다.
    • ESR_ELx_EC_BREAKPT_CUR (0x31)
    • ESR_ELx_EC_SOFTSTP_CUR (0x33)
    • ESR_ELx_EC_WATCHPT_CUR (0x35)
      • 싱글 스텝 디버거를 사용하는 몇 종류의 Exception(0x31~)들인 경우 el1_dbg 레이블로 이동한다.
    • 그 외는 모두 처리 불가능한 abort exception이며, el1_inv 레이블로 이동한다.
el1_ia & el1_da 레이블 – 명령 및 데이터 abort exception
  • 코드 라인 28에서 far_el1(Fault Address Register EL1) 레지스터에서 fault 발생한 가상 주소를 읽어온다.
  • 코드 라인 29에서 중단된 곳의 PSTATE 값(x23) 중 DAIF 비트를 현재 PSTATE에 반영(복구)한다.
  • 코드 라인 30~32에서 do_mem_abort() 함수를 호출하고, 세 개의 인자는 다음과 같다.
    • 첫 번째 인자(x0) 읽어온 fault 가상 주소에 address 태그가 있으면 제거한 가상 주소
    • 두 번째 인자(x1) ESR 값
    • 세 번째 인자(x2) 스택의 pt_regs
  • 코드 라인 34에서 exception을 빠져나가서 중단점으로 복귀하기 위해 context를 복구한다.
    • 스택에 저장해둔 레지스터들을 다시 읽어들인다.
el1_pc 레이블 – PC alignment exception
  • 코드 라인 41~44에서 dp_sp_pc_abort() 함수를 호출하여 “SP/PC alignment exception” 출력하고 스스로 다운(die)된다.
    • 참고로 초기 pt_reg를 푸시하려고 할 때 재귀 예외가 발생하는 문제로 스택 alignment exceptin은 처리하지 않고, pc alignment excption만 처리한다.
  • 코드 라인 45에서 스스로 break 디버그 exception을 발생시킨다. exception이 발생하면 ESR_ELx 레지스터에는 다음과 같은 정보가 담긴다.
    • ESR_ELx.EC = 0x3c
    • ESR_ELx.ISS = BUG_BRK_IMM(0x800)
el1_undef 레이블 – 아키텍처 미정의 명령 exception
  • 코드 라인 50~52에서 do_undefinstr() 함수를 호출하여 아키텍처가 디코드하여 수행할 수 없는 명령(instruction)에 대해 다음과 같이 구분하여 처리한다.
    • 해당 명령에 대해 등록된 후크가 있는 경우 해당 후크 함수를 호출한다.
      • 후크 함수의 등록은 register_undef_hook() 함수를 사용한다.
      • 예) ssbd 기능 호출 시 아키텍처에 hvc 또는 smc 명령이 없으면 이를 emulation 한다.
    • 유저 application에서 아키텍처에 없는 명령을 사용한 경우 SIGILL 시그널을 발생시켜 application을 kill 시킨다.
    • 커널 코드에서 아키텍처에 없는 명령을 사용한 경우 시스템을 die() 처리한다. (커널 옵션에 따라 리셋)
  • 코드 라인 53에서 exception을 빠져나가서 중단점으로 복귀하기 위해 context를 복구한다.
el1_dbg 레이블 – 디버그 exception
  • 코드 라인 58~60에서 Exception 신드롬 레지스터의 exception class 코드 값이 짝수인 경우 싱글 스텝 디버그 처리를 하지 않고, el1_inv 레이블로 이동한다. 그런데 ESR_ELx_EC_BRK64(0x3c)의 경우는 짝수지만 강제로 홀수로 만들어 el1_inv 레이블로 보내지 않는다.
  • 코드 라인 61에서 Pesudo-NMI를 지원하는 시스템인 경우 irq 우선순위를 통과시키도록 허용한다.
  • 코드 라인 62~64에서 다음 exception class 들에 대해 싱글 스텝 디버깅 처리를 위한 do_debug_exception() 함수를 호출한다.
    • ESR_ELx_EC_BREAKPT_CUR(0x31)
    • ESR_ELx_EC_SOFTSTP_CUR(0x33)
    • ESR_ELx_EC_WATCHPT_CUR(0x35)
    • ESR_ELx_EC_BRK64(0x3c)
  • 코드 라인 65에서 exception을 빠져나가서 중단점으로 복귀하기 위해 context를 복구한다.
el1_inv 레이블 –  그 외 처리 불가능 abort exception
  • 코드 라인 68~72에서 bad_mode() 함수를 호출한 후 panic() 처리한다. (커널 옵션에 따라 리셋)

 

inherit_daif 매크로

arch/arm64/include/asm/assembler.h

.       /* Only on aarch64 pstate, PSR_D_BIT is different for aarch32 */
        .macro  inherit_daif, pstate:req, tmp:req
        and     \tmp, \pstate, #(PSR_D_BIT | PSR_A_BIT | PSR_I_BIT | PSR_F_BIT)
        msr     daif, \tmp
        .endm

PSTATE 값이 담긴 @pstate 레지스터의 DAIF 플래그만을 PSTATE에 반영한다. 임시로 사용된 @tmp 레지스터는 크래시된다.

 

untagged_addr 매크로

arch/arm64/include/asm/asm-uaccess.h

/*
 * Remove the address tag from a virtual address, if present.
 */
        .macro  untagged_addr, dst, addr
        sbfx    \dst, \addr, #0, #56
        and     \dst, \dst, \addr
        .endm

FAR(Fault Address Register)에서 읽어온 가상 주소의 상위 8비트 address 태그가 존재하면 제거한다.

  • 코드 라인 2에서 @addr 값의 하위 56비트를 @dst로 복사한다.
    • @addr 값은 FAR(Fault Address Register)에서 읽은 가상 주소가 담겨 있고, 상위 8비트는 unknown 상태일 수도 있다.
  • 코드 라인 3에서 위의 값을 다시 @addr 값과 and 하여 unknown 비트들을 제거한다.

 

gic_prio_kentry_setup 매크로

arch/arm64/kernel/entry.S

        .macro  gic_prio_kentry_setup, tmp:req
#ifdef CONFIG_ARM64_PSEUDO_NMI
        alternative_if ARM64_HAS_IRQ_PRIO_MASKING
        mov     \tmp, #(GIC_PRIO_PSR_I_SET | GIC_PRIO_IRQON)
        msr_s   SYS_ICC_PMR_EL1, \tmp
        alternative_else_nop_endif
#endif
        .endm

Pesudo-NMI를 지원하는 시스템인 경우 nmi 뿐만 아니라 일반 irq도 허용하도록 pmr에 기록한다. @tmp 레지스터에는 플래그 값을 반환한다.

 


커널 동작 중 irq exception

el1_irq

arch/arm64/kernel/entry.S

        .align  6
el1_irq:
        kernel_entry 1
        gic_prio_irq_setup pmr=x20, tmp=x1
        enable_da_f

#ifdef CONFIG_ARM64_PSEUDO_NMI
        test_irqs_unmasked      res=x0, pmr=x20
        cbz     x0, 1f
        bl      asm_nmi_enter
1:
#endif

#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_off
#endif

        irq_handler

#ifdef CONFIG_PREEMPT
        ldr     x24, [tsk, #TSK_TI_PREEMPT]     // get preempt count
alternative_if ARM64_HAS_IRQ_PRIO_MASKING
        /*
         * DA_F were cleared at start of handling. If anything is set in DAIF,
         * we come back from an NMI, so skip preemption
         */
        mrs     x0, daif
        orr     x24, x24, x0
alternative_else_nop_endif
        cbnz    x24, 1f                         // preempt count != 0 || NMI return path
        bl      arm64_preempt_schedule_irq      // irq en/disable is done inside
1:
#endif

#ifdef CONFIG_ARM64_PSEUDO_NMI
        /*
         * When using IRQ priority masking, we can get spurious interrupts while
         * PMR is set to GIC_PRIO_IRQOFF. An NMI might also have occurred in a
         * section with interrupts disabled. Skip tracing in those cases.
         */
        test_irqs_unmasked      res=x0, pmr=x20
        cbz     x0, 1f
        bl      asm_nmi_exit
1:
#endif

#ifdef CONFIG_TRACE_IRQFLAGS
#ifdef CONFIG_ARM64_PSEUDO_NMI
        test_irqs_unmasked      res=x0, pmr=x20
        cbnz    x0, 1f
#endif
        bl      trace_hardirqs_on
1:
#endif

        kernel_exit 1
ENDPROC(el1_irq)

커널 동작 중 발생한 인터럽트(irq, nmi)를 처리한다.

  • 코드 라인 3에서 커널에서 exception이 발생하여 진입하였다. context를 스택에 백업한다.
  • 코드 라인 4에서 Pesudo-NMI를 지원하는 시스템에서 priority mask 레지스터에 @pmr 값을 기록한다.
  • 코드 라인 5에서 DAIF 중 I(irq)를 제외하고 enable(unmask) 한다.
  • 코드 라인 7~12에서 Pesudo-NMI 기능을 사용하는 시스템에서 nmi 진입 코드를 수행한다. Pesudo-NMI 기능을 사용하지 않는 시스템이거나 irq들이 unmask 상태인 경우 nmi 진입과 관련된 코드를 수행하지 않고 skip 한다.
  • 코드 라인 14~16에서 트레이스 출력이 허용된 경우에만 수행한다.
  • 코드 라인 18에서 irq 전용 스택으로 전환하고 인터럽트 컨트롤러의 irq 핸들러를 호출한다. 완료 후 태스크 스택으로 복원한다.
  • 코드 라인 20~33에서 커널 preemption이 가능한 커널 옵션을 사용하는 시스템에서 thread_info->preempt_count 값을 읽어 0인 경우 preemption이 요청되었다. 이러한 경우 우선 순위가 더 높은 태스크를 처리하기 위해 현재 태스크를 슬립하고 리스케줄한다. 만일 nmi 처리를 끝내고 돌아온 경우라면 DAIF 플래그 중 하나라도 설정되어 있다. 이 경우도 preemption 처리를 skip 한다.
  • 코드 라인 35~45에서 Pesudo-NMI 기능을 사용하는 시스템에서 nmi 처리가 끝난 경우 nmi 완료 코드를 수행한다. Pesudo-NMI 기능을 사용하지 않는 시스템이거나 irq들이 unmask 상태인 경우 nmi 처리 완료와 관련된 코드를 수행하지 않고 skip 한다.
  • 코드 라인 47~54에서 hard irq on에 대한 트레이스 출력을 수행한다.
  • 코드 라인 56에서 스택으로부터 context를 복원하고 exception이 발생하였던 중단점으로 돌아간다.

 

gic_prio_irq_setup 매크로

arch/arm64/kernel/entry.S

        .macro  gic_prio_irq_setup, pmr:req, tmp:req
#ifdef CONFIG_ARM64_PSEUDO_NMI
        alternative_if ARM64_HAS_IRQ_PRIO_MASKING
        orr     \tmp, \pmr, #GIC_PRIO_PSR_I_SET
        msr_s   SYS_ICC_PMR_EL1, \tmp
        alternative_else_nop_endif
#endif
        .endm

Pesudo-NMI를 지원하는 시스템에서 priority mask 레지스터에 @pmr 값을 기록한다.

  • 실제 기록 시 @pmr 값에 GIC_PRIO_PSR_I_SET(0x10) 비트를 더해 SYS_ICC_PMR_EL1에 기록한다.
  • GIC_PRIO_PSR_I_SET 값은 PSTATE의 I 비트 복원이 잘안되는 현상이 있어 정확한 복원을 위해 비트 확인이 가능하도록 추가하였다.

 

enable_da_f  매크로

arch/arm64/include/asm/assembler.h

        /* IRQ is the lowest priority flag, unconditionally unmask the rest. */
.       .macro enable_da_f
        msr     daifclr, #(8 | 4 | 1)
        .endm

PSTATE의 DA_F 플래그들을 클리어한다.

  • I(irq) 플래그는 처리하지 않는다.
  • 참고로 IRQ exception된 경우 아키텍처는 자동으로 I를 마스크하며 exception 진입된다.

 

test_irqs_unmasked 매크로

arch/arm64/kernel/entry.S

.       /*
         * Set res to 0 if irqs were unmasked in interrupted context.
         * Otherwise set res to non-0 value.
         */
        .macro  test_irqs_unmasked res:req, pmr:req
alternative_if ARM64_HAS_IRQ_PRIO_MASKING
        sub     \res, \pmr, #GIC_PRIO_IRQON
alternative_else
        mov     \res, xzr
alternative_endif
        .endm
#endif

Priority masking이 가능한 시스템인 경우 @pmr 값을 확인하여 irq가 unmask 상태인지 확인한다.

  • Priority masking이 가능한 시스템이 아닌 경우 항상 0을 반환한다.

 

irq_handler 매크로

arch/arm64/kernel/entry.S

/*
 * Interrupt handling.
 */
.       .macro  irq_handler
        ldr_l   x1, handle_arch_irq
        mov     x0, sp
        irq_stack_entry
        blr     x1
        irq_stack_exit
        .endm

인터럽트를 처리하기 위해 인터럽트 컨트롤러의 핸들러 함수를 호출한다.

  • 인터럽트 컨트롤러가 초기화될 때 전역 (*handle_arch_irq) 후크 함수에 해당 인터럽트 컨트롤러의 핸들러 함수가 대입된다.
    • 예) gic-v3 -> gic_of_init()

 


IRQ 전용 스택 사용

irq_stack_ptr

arch/arm64/kernel/irq.c

DEFINE_PER_CPU(unsigned long *, irq_stack_ptr);

컴파일 타임에 static하게 irq 스택을 가리키는 포인터가 cpu 마다 사용된다.

  • 초기화 함수는 init_IRQ() -> init_irq_stacks() 함수이다.
  • CONFIG_VMAP_STACK을 사용하는 경우 vmalloc 영역에 irq 스택을 할당하여 사용한다.

 

irq_stack

arch/arm64/kernel/irq.c

/* irq stack only needs to be 16 byte aligned - not IRQ_STACK_SIZE aligned. */
DEFINE_PER_CPU_ALIGNED(unsigned long [IRQ_STACK_SIZE/sizeof(long)], irq_stack);
  • CONFIG_VMAP_STACK을 사용하지 않는 경우 cpu 마다 컴파일 타임에 준비한 irq_stack을 사용한다.

 

irq_stack_entry 매크로

arch/arm64/kernel/entry.S

        .macro  irq_stack_entry
        mov     x19, sp                 // preserve the original sp

        /*
         * Compare sp with the base of the task stack.
         * If the top ~(THREAD_SIZE - 1) bits match, we are on a task stack,
         * and should switch to the irq stack.
         */
        ldr     x25, [tsk, TSK_STACK]
        eor     x25, x25, x19
        and     x25, x25, #~(THREAD_SIZE - 1)
        cbnz    x25, 9998f

        ldr_this_cpu x25, irq_stack_ptr, x26
        mov     x26, #IRQ_STACK_SIZE
        add     x26, x25, x26

        /* switch to the irq stack */
        mov     sp, x26
9998:
        .endm

인터럽트 컨트롤러의 핸들러 함수를 호출하기 전에 irq 스택으로 전환한다.

  • 코드 라인 2에서 스택을 x19에 백업해둔다.
  • 코드 라인 9~12에서 현재 스택이 thread_info->stack에서 지정한 태스크용 스택 범위 밖에 있는 경우 이미 irq 스택을 사용 중이므로 9998: 레이블로 이동한다.
  • 코드 라인 14~19에서 irq 스택으로 전환한다.

 

irq_stack_exit 매크로

arch/arm64/kernel/entry.S

        /*
         * x19 should be preserved between irq_stack_entry and
         * irq_stack_exit.
         */
.       .macro  irq_stack_exit
        mov     sp, x19
        .endm

인터럽트 컨트롤러의 핸들러 함수의 호출 이후 태스크 스택으로 복원한다.

  • 이 매크로와 한쌍인 irq_stack_entry 매크로에서 x19 레지스터를 사용하여 기존 스택을 백업해두었었다.

 


EL0 Exception

유저 동작 중 sync exception

el0_sync

arch/arm64/kernel/entry.S -1/3-

/*
 * EL0 mode handlers.
 */
        .align  6
el0_sync:
        kernel_entry 0
        mrs     x25, esr_el1                    // read the syndrome register
        lsr     x24, x25, #ESR_ELx_EC_SHIFT     // exception class
        cmp     x24, #ESR_ELx_EC_SVC64          // SVC in 64-bit state
        b.eq    el0_svc
        cmp     x24, #ESR_ELx_EC_DABT_LOW       // data abort in EL0
        b.eq    el0_da
        cmp     x24, #ESR_ELx_EC_IABT_LOW       // instruction abort in EL0
        b.eq    el0_ia
        cmp     x24, #ESR_ELx_EC_FP_ASIMD       // FP/ASIMD access
        b.eq    el0_fpsimd_acc
        cmp     x24, #ESR_ELx_EC_SVE            // SVE access
        b.eq    el0_sve_acc
        cmp     x24, #ESR_ELx_EC_FP_EXC64       // FP/ASIMD exception
        b.eq    el0_fpsimd_exc
        cmp     x24, #ESR_ELx_EC_SYS64          // configurable trap
        ccmp    x24, #ESR_ELx_EC_WFx, #4, ne
        b.eq    el0_sys
        cmp     x24, #ESR_ELx_EC_SP_ALIGN       // stack alignment exception
        b.eq    el0_sp
        cmp     x24, #ESR_ELx_EC_PC_ALIGN       // pc alignment exception
        b.eq    el0_pc
        cmp     x24, #ESR_ELx_EC_UNKNOWN        // unknown exception in EL0
        b.eq    el0_undef
        cmp     x24, #ESR_ELx_EC_BREAKPT_LOW    // debug exception in EL0
        b.ge    el0_dbg
        b       el0_inv

#ifdef CONFIG_COMPAT
        .align  6
el0_sync_compat:
        kernel_entry 0, 32
        mrs     x25, esr_el1                    // read the syndrome register
        lsr     x24, x25, #ESR_ELx_EC_SHIFT     // exception class
        cmp     x24, #ESR_ELx_EC_SVC32          // SVC in 32-bit state
        b.eq    el0_svc_compat
        cmp     x24, #ESR_ELx_EC_DABT_LOW       // data abort in EL0
        b.eq    el0_da
        cmp     x24, #ESR_ELx_EC_IABT_LOW       // instruction abort in EL0
        b.eq    el0_ia
        cmp     x24, #ESR_ELx_EC_FP_ASIMD       // FP/ASIMD access
        b.eq    el0_fpsimd_acc
        cmp     x24, #ESR_ELx_EC_FP_EXC32       // FP/ASIMD exception
        b.eq    el0_fpsimd_exc
        cmp     x24, #ESR_ELx_EC_PC_ALIGN       // pc alignment exception
        b.eq    el0_pc
        cmp     x24, #ESR_ELx_EC_UNKNOWN        // unknown exception in EL0
        b.eq    el0_undef
        cmp     x24, #ESR_ELx_EC_CP15_32        // CP15 MRC/MCR trap
        b.eq    el0_cp15
        cmp     x24, #ESR_ELx_EC_CP15_64        // CP15 MRRC/MCRR trap
        b.eq    el0_cp15
        cmp     x24, #ESR_ELx_EC_CP14_MR        // CP14 MRC/MCR trap
        b.eq    el0_undef
        cmp     x24, #ESR_ELx_EC_CP14_LS        // CP14 LDC/STC trap
        b.eq    el0_undef
        cmp     x24, #ESR_ELx_EC_CP14_64        // CP14 MRRC/MCRR trap
        b.eq    el0_undef
        cmp     x24, #ESR_ELx_EC_BREAKPT_LOW    // debug exception in EL0
        b.ge    el0_dbg
        b       el0_inv

el0에서 동기 exception이 발생하여 진입한 경우이다.

  • 코드 라인 3에서 context 전환을 위해 레지스터들을 스택에 pt_regs 구조체 만큼 백업한다.
  • 코드 라인 4~29에서 esr_el1(Exception Syndrom Register EL1) 레지스터를 읽어와서 EC(Exception Class) 필드 값에 따라 다음과 같이 처리한다.
    • ESR_ELx_EC_SVC64 (0x15)
      • EL0 AArch64 application에서 syscall 호출한 경우 el0_svc 레이블로 이동한다.
    • ESR_ELx_EC_DABT_LOW (0x24)
      • EL0에서 Data Abort인 경우 el0_da 레이블로 이동한다.
    • ESR_ELx_EC_IABT_LOW (0x20)
      • EL0에서 Instruction Abort인 경우 el0_ia 레이블로 이동한다.
    • ESR_ELx_EC_FP_ASIMD (0x07)
      • Advanced SIMD 또는 부동 소숫점 명령 사용 시 트랩된 경우 el0_fpsimd_acc 레이블로 이동한다.
    • ESR_ELx_EC_SVE (0x19)
      • SVE 기능으로 트랩된 경우 el0_sve_acc 레이블로 이동한다.
    • ESR_ELx_EC_FP_EXC64 (0x2c)
      • 부동 소숫점 명령으로 exception 발생 시 el0_fpsimd_exc 레이블로 이동한다.
    • ESR_ELx_EC_SYS64 (0x18)
      • MSR 및 MRS 명령에 의해 트랩되었고
    • ESR_ELx_EC_WFx (0x1)
      • WFI 또는 WFE 명령으로 트랩된 경우가 아니면 el0_sys 레이블로 이동한다.
    • ESR_ELx_EC_SP_ALIGN (0x26)
      • SP Alignment Exception인 경우 el0_sp 레이블로 이동한다.
    • ESR_ELx_EC_PC_ALIGN (0x22)
      • PC Alignment Exception인 경우 el0_pc 레이블로 이동한다.
    • ESR_ELx_EC_UNKNOWN (0x00)
      • Unknown Exception인 경우 el0_undef 레이블로 이동한다.
    • ESR_ELx_EC_BREAKPT_LOW (0x30)
    • ESR_ELx_EC_SOFTSTP_LOW (0x32)
    • ESR_ELx_EC_WATCHPT_LOW (0x34)
    • ESR_ELx_EC_BRK64 (0x3c)
      • EL0에서 싱글 스텝 디버거를 사용한 트랩인 경우 el0_dbg 레이블로 이동한다.
    • 그 외는 모두 처리 불가능한 abort exception이며, el0_inv 레이블로 이동한다.
  • 코드 라인 34에서 context 전환을 위해 레지스터들을 스택에 pt_regs 구조체 만큼 백업한다.
  • 코드 라인 35~63에서 esr_el1(Exception Syndrom Register EL1) 레지스터를 읽어와서 EC(Exception Class) 필드 값에 따라 다음과 같이 처리한다.
    • ESR_ELx_EC_SVC32 (0x11)
      • AArch32 application에서 syscall 호출한 경우 el0_svc_compat 레이블로 이동한다.
    • ESR_ELx_EC_DABT_LOW (0x24)
      • EL0에서 Data Abort인 경우 el0_da 레이블로 이동한다.
    • ESR_ELx_EC_IABT_LOW (0x20)
      • EL0에서 Instruction Abort인 경우 el0_ia 레이블로 이동한다.
    • ESR_ELx_EC_FP_ASIMD (0x07)
      • Advanced SIMD 또는 부동 소숫점 명령 사용 시 트랩된 경우 el0_fpsimd_acc 레이블로 이동한다.
    • ESR_ELx_EC_FP_EXC32 (0x28)
      • 부동 소숫점 명령으로 exception 발생 시 el0_fpsimd_exc 레이블로 이동한다.
    • ESR_ELx_EC_PC_ALIGN (0x22)
      • PC Alignment Exception인 경우 el0_pc 레이블로 이동한다.
    • ESR_ELx_EC_UNKNOWN (0x00)
      • Unknown Exception인 경우 el0_undef 레이블로 이동한다.
    • ESR_ELx_EC_CP15_32 (0x03)
    • ESR_ELx_EC_CP15_64 (0x04)
      • CP15 명령으로 트랩된 경우 el0_cp15 레이블로 이동한다.
    • ESR_ELx_EC_CP14_MR (0x05)
    • ESR_ELx_EC_CP14_LS (0x06)
    • ESR_ELx_EC_CP14_64 (0x0c)
      • CP14 명령으로 트랩된 경우 el0_undef 레이블로 이동한다.
    • ESR_ELx_EC_BREAKPT_LOW (0x30)
    • ESR_ELx_EC_SOFTSTP_LOW (0x32)
    • ESR_ELx_EC_WATCHPT_LOW (0x34)
    • ESR_ELx_EC_BRK64 (0x3c)
      • EL0에서 싱글 스텝 디버거를 사용한 트랩인 경우 el0_dbg 레이블로 이동한다.
    • 그 외는 모두 처리 불가능한 abort exception이며, el0_inv 레이블로 이동한다.

 

arch/arm64/kernel/entry.S -2/3-

el0_svc_compat:
        gic_prio_kentry_setup tmp=x1
        mov     x0, sp
        bl      el0_svc_compat_handler
        b       ret_to_user

        .align  6
el0_cp15:
        /*
         * Trapped CP15 (MRC, MCR, MRRC, MCRR) instructions
         */
        ct_user_exit_irqoff
        enable_daif
        mov     x0, x25
        mov     x1, sp
        bl      do_cp15instr
        b       ret_to_user
#endif

el0_da:
        /*
         * Data abort handling
         */
        mrs     x26, far_el1
        ct_user_exit_irqoff
        enable_daif
        untagged_addr   x0, x26
        mov     x1, x25
        mov     x2, sp
        bl      do_mem_abort
        b       ret_to_user
el0_ia:
        /*
         * Instruction abort handling
         */
        mrs     x26, far_el1
        gic_prio_kentry_setup tmp=x0
        ct_user_exit_irqoff
        enable_da_f
#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_off
#endif
        mov     x0, x26
        mov     x1, x25
        mov     x2, sp
        bl      do_el0_ia_bp_hardening
        b       ret_to_user
el0_fpsimd_acc:
        /*
         * Floating Point or Advanced SIMD access
         */
        ct_user_exit_irqoff
        enable_daif
        mov     x0, x25
        mov     x1, sp
        bl      do_fpsimd_acc
        b       ret_to_user
el0_sve_acc:
        /*
         * Scalable Vector Extension access
         */
        ct_user_exit_irqoff
        enable_daif
        mov     x0, x25
        mov     x1, sp
        bl      do_sve_acc
        b       ret_to_user
el0_fpsimd_exc:
        /*
         * Floating Point, Advanced SIMD or SVE exception
         */
        ct_user_exit_irqoff
        enable_daif
        mov     x0, x25
        mov     x1, sp
        bl      do_fpsimd_exc
        b       ret_to_user
el0_svc_compat 레이블 – AArch32에서 SVC 호출
  • 코드 라인 2에서 Pesudo-NMI를 지원하는 시스템인 경우 irq 우선순위를 통과시키도록 허용한다.
  • 코드 라인 3~4에서 el0_svc_compat_handler() 함수를 호출하여 syscall 서비스를 수행한다.
  • 코드 라인 5에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_cp15 레이블 – CP15 (MRC, MCR, MRRC, MCRR) 트랩
  • 코드 라인 12에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 13에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 14~16에서 do_cp15instr() 함수를 호출한다.
    • 유저 application에서 타이머 카운터(CNTVCT) 레지스터 값을 읽기 위해 트랩을 사용하였다.
  • 코드 라인 17에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_da 레이블 – Data Abort exception
  • 코드 라인 24에서 far_el1(Fault Address Register EL1) 레지스터에서 fault 발생한 가상 주소를 읽어온다.
  • 코드 라인 25에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 26에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 27~30에서 do_mem_abort() 함수를 호출하고, 세 개의 인자는 다음과 같다.
    • 첫 번째 인자(x0) 읽어온 fault 가상 주소에 address 태그가 있으면 제거한 가상 주소
    • 두 번째 인자(x1) ESR(Exception Syndrom Register) 값
    • 세 번째 인자(x2) 스택의 pt_regs
  • 코드 라인 31에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_ia 레이블 – Instruction Abort exception
  • 코드 라인 36에서 far_el1(Fault Address Register EL1) 레지스터에서 fault 발생한 가상 주소를 읽어온다.
  • 코드 라인 37에서 Pesudo-NMI를 지원하는 시스템인 경우 irq 우선순위를 통과시키도록 허용한다.
  • 코드 라인 38에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 39에서 PSTATE의 DA_F를 클리어하여 인터럽트만 제외하고 exception을 허용하게 한다.
  • 코드 라인 40~42에서 트레이스 출력이 허용된 경우에만 수행한다.
  • 코드 라인 43~46에서 do_el0_ia_bp_hardening() 함수를 호출하고, 세 개의 인자는 다음과 같다.
    • 첫 번째 인자(x0) 읽어온 fault 가상 주소에 address 태그가 있으면 제거한 가상 주소
    • 두 번째 인자(x1) ESR(Exception Syndrom Register) 값
    • 세 번째 인자(x2) 스택의 pt_regs
  • 코드 라인 47에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_fpsimd_acc 레이블 – 부동 소숫점 또는 Advanced SIMD 트랩
  • 코드 라인 52에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 54에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 54~56에서 do_fpsimd_acc() 함수를 호출한다. 함수 내부는 경고만 출력한다.
  • 코드 라인 57에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_sve_acc 레이블 – SVE 호출
  • 코드 라인 62에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 63에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 64~66에서 do_sve_acc() 함수를 호출하여 태스크의 첫 호출인 경우 SVE를 지원하기 위해 준비 작업을 수행한다.
    • SVE(Scalable Vector Extension)는 ARMv8.2 아키텍처 이상에서 Advanced SIMD의 벡터를 더 wide한 벡터를 지원한다.
  • 코드 라인 67에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_fpsimd_exc 레이블 – 부동 소숫점 또는 Advanced SIMD exception
  • 코드 라인 72에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 73에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 74~76에서 do_fpsimd_exc() 함수를 호출하여 현재 태스크에 SIGFPE 시그널을 발생시킨다.
  • 코드 라인 77에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.

 

arch/arm64/kernel/entry.S -3/3-

el0_sp:
        ldr     x26, [sp, #S_SP]
        b       el0_sp_pc
el0_pc:
        mrs     x26, far_el1
el0_sp_pc:
        /*
         * Stack or PC alignment exception handling
         */
        gic_prio_kentry_setup tmp=x0
        ct_user_exit_irqoff
        enable_da_f
#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_off
#endif
        mov     x0, x26
        mov     x1, x25
        mov     x2, sp
        bl      do_sp_pc_abort
        b       ret_to_user
el0_undef:
        /*
         * Undefined instruction
         */
        ct_user_exit_irqoff
        enable_daif
        mov     x0, sp
        bl      do_undefinstr
        b       ret_to_user
el0_sys:
        /*
         * System instructions, for trapped cache maintenance instructions
         */
        ct_user_exit_irqoff
        enable_daif
        mov     x0, x25
        mov     x1, sp
        bl      do_sysinstr
        b       ret_to_user
el0_dbg:
        /*
         * Debug exception handling
         */
        tbnz    x24, #0, el0_inv                // EL0 only
        mrs     x24, far_el1
        gic_prio_kentry_setup tmp=x3
        ct_user_exit_irqoff
        mov     x0, x24
        mov     x1, x25
        mov     x2, sp
        bl      do_debug_exception
        enable_da_f
        b       ret_to_user
el0_inv:
        ct_user_exit_irqoff
        enable_daif
        mov     x0, sp
        mov     x1, #BAD_SYNC
        mov     x2, x25
        bl      bad_el0_sync
        b       ret_to_user
ENDPROC(el0_sync)
el0_sp 레이블 – SP alignment exection
  • 코드 라인 2~3에서 스택에 저장한 중단점에서의 스택 값을 읽은 후 아래 el0_sp_pc: 레이블로 이동한다.
el0_pc 레이블 – PC alignment exection
  • 코드 라인 5에서 far_el1(Fault Address Register EL1) 레지스터에서 fault 발생한 가상 주소를 읽어온다.
  • 코드 라인 6에서 el0_sp와 el0_pc가 공동으로 사용하는 el0_sp_pc 레이블이다.
  • 코드 라인 10에서 Pesudo-NMI를 지원하는 시스템인 경우 irq 우선순위를 통과시키도록 허용한다.
  • 코드 라인 11에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 12에서 PSTATE의 DA_F를 클리어하여 인터럽트만 제외하고 exception을 허용하게 한다.
  • 코드 라인 13~15에서 트레이스 출력이 허용된 경우에만 수행한다.
  • 코드 라인 16~19에서 do_sp_pc_abort() 함수를 호출하여 현재 태스크에 SIGKILL을 발생시킨다. 그리고, 세 개의 인자는 다음과 같다.
    • 첫 번째 인자(x0) 읽어온 fault 가상 주소에 address 태그가 있으면 제거한 가상 주소
    • 두 번째 인자(x1) ESR(Exception Syndrom Register) 값
    • 세 번째 인자(x2) 스택의 pt_regs
  • 코드 라인 20에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_undef 레이블 – Undefined instruction exception
  • 코드 라인 25에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 26에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 27~28에서 do_undefinstr() 함수를 호출하여 특정 명령에 대해 등록된 후크 함수를 호출한다. 만일 그러한 후크 함수가 없으면 현재 태스크에 SIGILL을 발생시킨다.
  • 코드 라인 29에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_sys 레이블 – 시스템 명령 트랩
  • 코드 라인 34에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 35에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 36~38에서 do_sysinstr() 함수를 호출하여 특정 시스템 명령에 대해 등록된 후크 함수를 호출한다. 만일 그러한 후크 함수가 없으면 현재 태스크에 SIGILL을 발생시킨다.
  • 코드 라인 39에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_dbg 레이블 – 디버그 트랩
  • 코드 라인 44에서 el0인 경우 el0_inv 레이블로 이동한다.
  • 코드 라인 45에서 far_el1(Fault Address Register EL1) 레지스터에서 fault 발생한 가상 주소를 읽어온다.
  • 코드 라인 46에서 Pesudo-NMI를 지원하는 시스템인 경우 irq 우선순위를 통과시키도록 허용한다.
  • 코드 라인 47에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 48~51에서 do_debug_exception() 함수를 호출한다.
  • 코드 라인 52에서 PSTATE의 DA_F를 클리어하여 인터럽트만 제외하고 exception을 허용하게 한다.
  • 코드 라인 53에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.
el0_inv 레이블 – 그 외 처리 불가능 abort exception
  • 코드 라인 55에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 56에서 PSTATE의 DAIF를 클리어하여 인터럽트 등을 허용하게 한다.
  • 코드 라인 57~60에서 bad_el0_sync 함수를 호출하여 현재 태스크에 SIGILL을 발생시킨다.
  • 코드 라인 61에서 exception을 빠져나가서 유저 중단점으로 복귀하기 위해 pending 작업을 처리한 후 context를 복구한다.

 


유저 동작 중 irq exception

el0_irq

arch/arm64/kernel/entry.S

        .align  6
el0_irq:
        kernel_entry 0
el0_irq_naked:
        gic_prio_irq_setup pmr=x20, tmp=x0
        ct_user_exit_irqoff
        enable_da_f

#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_off
#endif

#ifdef CONFIG_HARDEN_BRANCH_PREDICTOR
        tbz     x22, #55, 1f
        bl      do_el0_irq_bp_hardening
1:
#endif
        irq_handler

#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_on
#endif
        b       ret_to_user
ENDPROC(el0_irq)

64비트 유저 동작 중 발생한 인터럽트(irq, nmi)를 처리한다.

  • 코드 라인 3에서 64비트 유저에서 exception이 발생하여 진입하였다. context를 스택에 백업한다.
  • 코드 라인 4에서 32비트 유저 irq exception도 이곳에서 공통으로 처리하기 위해 진입을 위한 el0_irq_naked 레이블이다.
  • 코드 라인 5에서 Pesudo-NMI를 지원하는 시스템에서 priority mask 레지스터에 @pmr 값을 기록한다.
  • 코드 라인 6에서 유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.
  • 코드 라인 7에서 PSTATE의 DAIF 중 I(irq)를 제외하고 enable(unmask) 한다.
  • 코드 라인 9~11에서 hardirq off에 대해 트레이스 출력이 허용된 경우에만 수행한다.
  • 코드 라인 13~17에서 BP hardning을 위해 do_el0_irq_bp_hardening() 함수를 호출한다.
  • 코드 라인 18에서 irq 전용 스택으로 전환하고 인터럽트 컨트롤러의 irq 핸들러를 호출한다. 완료 후 태스크 스택으로 복원한다.
  • 코드 라인 20~22에서 hardirq on에 대해 트레이스 출력이 허용된 경우에만 수행한다.
  • 코드 라인 23에서 스택으로부터 context를 복원하고 exception이 발생하였던 중단점으로 돌아간다.

 

AArch32 EL0 irq exception

el0_irq_compat

arch/arm64/kernel/entry.S

        .align  6
el0_irq_compat:
        kernel_entry 0, 32
        b       el0_irq_naked

32bit 유저 동작 중 발생한 인터럽트(irq, nmi)를 처리한다.

  • 코드 라인 3에서 32비트 유저에서 exception이 발생하여 진입하였다. context를 스택에 백업한다.
  • 코드 라인 4에서 el0_irq_naked 레이블로 이동한다.

 


BP(Branch Predictor) Hardening

일부 고성능 ARM64 시스템의 경우 BP(Branch Predictor)를 사용한 Speculation 부채널 공격(side channel Attck)을 통해 시스템의 보안이 뚫리는 경우가 발생하여 이를 보완하기 위해 BP를 숨기는 기능을 수행한다.

 

do_el0_ia_bp_hardening()

arch/arm64/mm/fault.c

asmlinkage void __exception do_el0_ia_bp_hardening(unsigned long addr,
                                                   unsigned int esr,
                                                   struct pt_regs *regs)
{
        /*
         * We've taken an instruction abort from userspace and not yet
         * re-enabled IRQs. If the address is a kernel address, apply
         * BP hardening prior to enabling IRQs and pre-emption.
         */
        if (!is_ttbr0_addr(addr))
                arm64_apply_bp_hardening();

        local_daif_restore(DAIF_PROCCTX);
        do_mem_abort(addr, esr, regs);
}

유저 모드에서 instruction abort exception이 발생한 경우 bp hardning 기능을 수행한 후 메모리 fault 처리를 수행한다.

  • 코드 라인 10~11에서 fault가 발생한 주소가 유저 주소인 경우 bp hardning을 수행한다.
  • 코드 라인 13에서 PSTATE의 DAIF를 모두 클리어해 인터럽트 등을 허용한다.
  • 코드 라인 14에서 fault 처리를 수행한다.

 

is_ttbr0_addr()

arch/arm64/mm/fault.c

static inline bool is_ttbr0_addr(unsigned long addr)
{
        /* entry assembly clears tags for TTBR0 addrs */
        return addr < TASK_SIZE;
}

유저 가상 주소인지 여부를 반환한다.

 

arm64_apply_bp_hardening()

arch/arm64/include/asm/mmu.h

static inline void arm64_apply_bp_hardening(void)
{
        struct bp_hardening_data *d;

        if (!cpus_have_const_cap(ARM64_HARDEN_BRANCH_PREDICTOR))
                return;

        d = arm64_get_bp_hardening_data();
        if (d->fn)
                d->fn();
}

ARM64_HARDEN_BRANCH_PREDICTOR 기능(capability)을 가진 시스템에서 workround가 필요한 경우 호출된다.

  • Qualcom FALKOR 아키텍처의 경우다음 함수가 호출된다.
    • qcom_link_stack_sanitization()
  • 그 외 smccc 호출을 통해 해당 아키텍처가 workaround가 필요한 경우 다음 함수중 하나가 호출된다.
    • call_hvc_arch_workaround_1()
    • call_smc_arch_workaround_1()

 

arm64_get_bp_hardening_data()

arch/arm64/include/asm/mmu.h

static inline struct bp_hardening_data *arm64_get_bp_hardening_data(void)
{
        return this_cpu_ptr(&bp_hardening_data);
}

 

bp_hardening_data

arch/arm64/kernel/cpu_errata.c

DEFINE_PER_CPU_READ_MOSTLY(struct bp_hardening_data, bp_hardening_data);

 

bp_hardening_data 구조체

arch/arm64/include/asm/mmu.h

struct bp_hardening_data {
        int                     hyp_vectors_slot;
        bp_hardening_cb_t       fn;
};
  • hyp_vectors_slot
    • KVM을 사용하는 경우 사용될 4개의 하이퍼 바이저 벡터용 슬롯
  •  fn
    • Branch Predict 숨김 기능을 위해 workaround 기능을 수행할 콜백 함수가 담긴다.

 


유저 복귀

ct_user_exit_irqoff 매크로

arch/arm64/kernel/entry.S

/*
 * Context tracking subsystem.  Used to instrument transitions
 * between user and kernel mode.
 */
        .macro ct_user_exit_irqoff
#ifdef CONFIG_CONTEXT_TRACKING
        bl      enter_from_user_mode
#endif
        .endm

유저 모드에서 커널 모드 진입에 따른 디버그용 Context 트래킹을 호출한다.

  • 유저 사용 시간 산출 및 트레이스 출력을 수행한다.

 

ret_to_user & finish_ret_to_user 레이블

arch/arm64/kernel/entry.S

/*
 * "slow" syscall return path.
 */
ret_to_user:
        disable_daif
        gic_prio_kentry_setup tmp=x3
        ldr     x1, [tsk, #TSK_TI_FLAGS]
        and     x2, x1, #_TIF_WORK_MASK
        cbnz    x2, work_pending
finish_ret_to_user:
        enable_step_tsk x1, x2
#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
        bl      stackleak_erase
#endif
        kernel_exit 0
ENDPROC(ret_to_user)

exception을 빠져나가서 유저 중단점으로 복귀하기 전에 pending 작업을 처리한 후 context를 복구한다. pending 작업들에는 리스케줄, 시그널 처리 등이 있다.

  • 코드 라인 2에서 PSTATE의 DAIF를 마스크하여 인터럽트 등을 허용하지 않는다.
  • 코드 라인 3에서 Pesudo-NMI를 지원하는 시스템인 경우 irq 우선순위를 통과시키도록 허용한다.
  • 코드 라인 4~6에서 thread_info->flag에 _TIF_WORK_MASK에 해당하는 플래그들이 있는 경우 이에 대한 pending 작업을 수행한다.
  • 코드 라인 8에서 현재 태스크에 싱글 스텝 기능이 꺼져 있으면 활성화시킨다.
  • 코드 라인 9~11에서 보안을 위해 위해 syscall 호출후 복귀전에 커널 스택의 빈 공간을 STACKLEAK_POISON(-0xBEEF) 값으로 클리어한다.
  • 코드 라인 12에서 스택으로부터 context를 복원하고 exception이 발생하였던 중단점으로 돌아간다.

 

_TIF_WORK_MASK

arch/arm64/include/asm/thread_info.h

#define _TIF_WORK_MASK          (_TIF_NEED_RESCHED | _TIF_SIGPENDING | \
                                 _TIF_NOTIFY_RESUME | _TIF_FOREIGN_FPSTATE | \
                                 _TIF_UPROBE | _TIF_FSCHECK)

커널에서 유저로 Context 복귀하기 전에 수행할 작업에 대한 플래그들이다.

 

work_pending 레이블

arch/arm64/kernel/entry.S

/*
 * Ok, we need to do extra processing, enter the slow path.
 */
work_pending:
        mov     x0, sp                          // 'regs'
        bl      do_notify_resume
#ifdef CONFIG_TRACE_IRQFLAGS
        bl      trace_hardirqs_on               // enabled while in userspace
#endif
        ldr     x1, [tsk, #TSK_TI_FLAGS]        // re-check for single-step
        b       finish_ret_to_user

pending된 작업을 처리한다. (Slowpath 작업이므로 EL0 복귀 시에만 수행한다.)

  • 코드 라인 2~3에서 pending된 작업을 처리한다. 인자로 pt_regs와 thread_info->flag를 사용한다.
  • 코드 라인 4~6에서 hard irq on에 대한 트레이스 출력을 수행한다.
  • 코드 라인 7~8에서 thread_info->flags 값을 x1 레지스터로 다시 읽어들인 후 finish_ret_to_user 레이블로 이동한다.

 

enable_step_tsk 매크로

arch/arm64/include/asm/assembler.h

        /* call with daif masked */
        .macro  enable_step_tsk, flgs, tmp
        tbz     \flgs, #TIF_SINGLESTEP, 9990f
        mrs     \tmp, mdscr_el1
        orr     \tmp, \tmp, #DBG_MDSCR_SS
        msr     mdscr_el1, \tmp
9990:
        .endm

현재 태스크에 싱글 스텝 기능이 꺼져 있으면 활성화시킨다.

  • 코드 라인 3에서 @flgs(thread_info->flag) 값에 TIF_SINGLESTEP 값이 없으면 9990 레이블로 이동한다.
  • 코드 라인 4~6에서 MDSCR_EL1(Monitor Debug System Control Register)의 SS 비트를 설정하여 software step 기능을 활성화한다. @tmp 레지스터는 스크래치 레지스터로 이용된다.

 

참고