티스토리 뷰

6.2.4 스케줄링 요청하기, 요청 체크해서 스케줄링 시도하기

6-4 kernel/sched/core./c resched_curr

  • 스케줄링이 필요한 경우 TIF_NEED_RESCHED 플래그를 설정
  • _TIF_POLLING_NRFLAG : CPU가 스케줄러 인터럽트를 기다리지 않고, polling 방식으로 상태를 확인. (IDLE task 에서 주로 사용?, ARM64 는 사용하지 않음) -> 따라서 이 플래그가 켜있었으면 다른 cpu 가 알아치리도록 별도로 알림 줄 필요 없음.

6-5 arch/arm64/kernel/entry.S work_pending

  • tbnz : Test bit and Branch if Nonzero.
  • cbnz : Compare and branch if nonzero
// arm64 의 WORK_MASK
#define _TIF_WORK_MASK        (_TIF_NEED_RESCHED | _TIF_SIGPENDING | \
                 _TIF_NOTIFY_RESUME | _TIF_FOREIGN_FPSTATE)

work_pending:
    tbnz    x1, #TIF_NEED_RESCHED, work_resched // NEED_RESCHED flag 켜져있는 경우 - 스케줄 호출

    // 그 외 WORK_MASK 켜져있는 경우
    // 별도 처리 과정..
work_resched:
    bl    schedule

ret_to_user:
        and    x2, x1, #_TIF_WORK_MASK
        cbnz    x2, work_pending // WORK_MASK 걸려있으면 다시 work_pending

6-6 include/linux/sched.h need_resched

  • NEED_RESCHED 비트 설정됐는지 확인 task_struct -> thread_info -> flags & TIF_NEED_RESCHED

6.2.5 스케줄링의 핵심

6-7 kernel/sched/core.c __schedule

// 부가 설명
4) Per-task and per-thread statistics
__u64    nvcsw;            /* Context voluntary switch counter */
__u64    nivcsw;            /* Context involuntary switch counter */

#define TASK_RUNNING        0
#define TASK_INTERRUPTIBLE    1
#define TASK_UNINTERRUPTIBLE    2
#define __TASK_STOPPED        4
#define __TASK_TRACED        8
// @param preempt 커널이 선점됐는지 여부
// preempt = true → 강제 선점(타이머 인터럽트, 인터럽트 핸들러, 높은 우선순위 태스크 깨어남 등).
// preempt = false → 자발적 스케줄링(태스크가 직접 schedule() 호출, 락 등으로 인해 선점 방지됨).
static void __sched notrace __schedule(bool preempt)
{
    if (unlikely(prev->state == TASK_DEAD)) // do_exit 에서 선점 카운트를 1증가시켜뒀어서, 예외처리로 1 감소가 필요
        preempt_enable_no_resched_notrace();

    switch_count = &prev->nivcsw; // 자발적이지 않은
    // !preempt 커널을 선점하고 있지 않았고
    // prev->state 커널이 RUNNING 상태가 아닐때 
    if (!preempt && prev->state) { 
        // pending 상태 : 스위칭 되기전 처리해야 할 시그널이 있는 경우
        // prev가 원래 잠자려고 했던 상태였는데, 잠들기 전에 시그널이 도착해서 다시 깨어나도록 처리.
        if (unlikely(signal_pending_state(prev->state, prev))) {
            prev->state = TASK_RUNNING; // deque 하지 않음
        } else {
            deactivate_task(rq, prev, DEQUEUE_SLEEP); // 처리할 시그널이 없는 경우 signal 로 만들고 deque
            prev->on_rq = 0;

            /*
             * If a worker went to sleep, notify and ask workqueue
             * whether it wants to wake up a task to maintain
             * concurrency.
             */
            if (prev->flags & PF_WQ_WORKER) { // 다른 task 를 같은 WORKER_THREAD 라서 깨워야 하는 경우
                struct task_struct *to_wakeup;

                to_wakeup = wq_worker_sleeping(prev);
                if (to_wakeup)
                    try_to_wake_up_local(to_wakeup);
            }
        }
        switch_count = &prev->nvcsw;
    }

    // task_on_rq_queued { return p->on_rq == TASK_ON_RQ_QUEUED; }
    if (task_on_rq_queued(prev))
        update_rq_clock(rq); // queue 에서 빠지지 않은 경우 clock update

    next = pick_next_task(rq, prev); // 다음 task 선택
    clear_tsk_need_resched(prev); // 스케줄링 됐으니 플래그 제거

    // 이후 next 와 prev 가 달랐다면 switching, 그렇지 않다면 끝
    if (likely(prev != next)) {
        rq = context_switch(rq, prev, next); /* unlocks the rq */
    } else {
        ...
    }
}

6-8 kernel/sched/core.c context_switch

static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
           struct task_struct *next)
{
    struct mm_struct *mm, *oldmm;

    prepare_task_switch(rq, prev, next); // arm64 next 의 on_cpu 를 1로 변경

    mm = next->mm;
    oldmm = prev->active_mm;
    /*
     * For paravirt, this is coupled with an exit in switch_to to
     * combine the page table reload and the switch backend into
     * one hypercall.
     */
    arch_start_context_switch(prev);

    if (!mm) { // mm 이 없는 경우 커널 쓰레드. 유저 공간이 없기때문에 빌려서 사용
        next->active_mm = oldmm;
        atomic_inc(&oldmm->mm_count);
        enter_lazy_tlb(oldmm, next);
    } else // mm 이 있기때문에 내껄로 변경
        switch_mm(oldmm, mm, next); // 유저 공간인 mm 을 ttbr0 에 등록

    if (!prev->mm) { // 이전 task 가 커널 테스크였던 경우 정리
        prev->active_mm = NULL;
        rq->prev_mm = oldmm; // 이후 이 필드를 사용하여 정리됨
    }
    /*
     * Since the runqueue lock will be released by the next
     * task (which is an invalid locking op but in the case
     * of the scheduler it's an obvious special-case), so we
     * do an early lockdep release here:
     */
    lockdep_unpin_lock(&rq->lock);
    spin_release(&rq->lock.dep_map, 1, _THIS_IP_);

    /* Here we just switch the register state and the stack. */
    // cpu 레지스터와 커널 스택을 스위칭 6-10
    // 세번째 인자인 last 는 __switch_to 가 리턴한 값을 저장
    // 다시 받는 이유는 여기에서 커널 스택이 변경되기 때문에 prev 의 주소를 찾기 위함
    switch_to(prev, next, prev); 
    barrier();

    return finish_task_switch(prev); // 태스크 스위칭이 끝난 후 마무리 작업 6-12
}

6-8 arch/arm64/include/asm/mmu_context.h switch_mm

  • TTBR0 레지스터에 next 태스크의 유저 주소 공간 페이지 테이블 주소를 설정
  • asid : Address Space ID

asid_generation이 증가하는 경우?

  • ASID 개수 초과(예: 8비트면 256개 이상)
  • TLB가 가득 차서 ASID 재사용이 필요할 때
  • 이 경우 이전 ASID 를 사용중이었으면 잘못된 곳이므로 플러시 필요

void check_and_switch_context(struct mm_struct *mm, unsigned int cpu)
{
    unsigned long flags;
    u64 asid;

    asid = atomic64_read(&mm->context.id);

    /*
     * The memory ordering here is subtle. We rely on the control
     * dependency between the generation read and the update of
     * active_asids to ensure that we are synchronised with a
     * parallel rollover (i.e. this pairs with the smp_wmb() in
     * flush_context).
     */
    if (!((asid ^ atomic64_read(&asid_generation)) >> asid_bits) // asid 가 유효하고, xor 하는 이유는 하나라도 달라졌는지 확인
        && atomic64_xchg_relaxed(&per_cpu(active_asids, cpu), asid)) // 현재 cpu 의 asid 를 교체 성공한 경우
        goto switch_mm_fastpath;

    raw_spin_lock_irqsave(&cpu_asid_lock, flags);
    /* Check that our ASID belongs to the current generation. */
    asid = atomic64_read(&mm->context.id);
    if ((asid ^ atomic64_read(&asid_generation)) >> asid_bits) { // 유효하지 않은 경우 (하나라도 다른 경우)
        asid = new_context(mm, cpu); // 새로 지정이 필요함
        atomic64_set(&mm->context.id, asid);
    }

    if (cpumask_test_and_clear_cpu(cpu, &tlb_flush_pending)) // tlb_flush_pending 플래그가 켜져있다면 flush
        local_flush_tlb_all();

    atomic64_set(&per_cpu(active_asids, cpu), asid);
    raw_spin_unlock_irqrestore(&cpu_asid_lock, flags);

switch_mm_fastpath:
    cpu_switch_mm(mm->pgd, mm); // cpu 의 pgd 를 변경
}

switch_to, __switch_to : 태스크 스위칭 하기

include/asm-generic/switch_to.h

#define switch_to(prev, next, last)                    \
    do {                                \
        ((last) = __switch_to((prev), (next)));            \
    } while (0)

next task 의 x0 에 이전 커널 스택에서의 task_struct 를 보관해서, next task 로 간 이후에도 접근이 가능하고 이후 정리할때 사용한다.

6-10 __switch_to

struct task_struct *__switch_to(struct task_struct *prev,
                struct task_struct *next)
{
    // 유저 테스크가 변경되더라도, 내 task 가 어디를 실행되어야 했는지 등은 커널 스택에서 관리하기 때문에, 커널스택의 레지스터들이 변경된다.
    last = cpu_switch_to(prev, next);
    return last;
}

6-11 arch/arm64/kernel/entry.S cpu_switch_to

DEFINE(THREAD_CPU_CONTEXT,    offsetof(struct task_struct, thread.cpu_context));

ENTRY(cpu_switch_to)
    mov    x10, #THREAD_CPU_CONTEXT // x10 = task_struct 의 cpu_context 주소만큼
    add    x8, x0, x10                                 // x8 = prev task.cpu_context

    mov    x9, sp                               // x9 = sp
    stp    x19, x20, [x8], #16        // store callee-saved registers (이전 task 의 cpu_context 에 레지스터 저장
    stp    x21, x22, [x8], #16
    stp    x23, x24, [x8], #16
    stp    x25, x26, [x8], #16
    stp    x27, x28, [x8], #16
    stp    x29, x9, [x8], #16
    str    lr, [x8]

    add    x8, x1, x10                        // x8 = current task.cpu_context
    ldp    x19, x20, [x8], #16        // restore callee-saved registers
    ldp    x21, x22, [x8], #16
    ldp    x23, x24, [x8], #16
    ldp    x25, x26, [x8], #16
    ldp    x27, x28, [x8], #16
    ldp    x29, x9, [x8], #16
    ldr    lr, [x8]

    // sp는 현재 실행 중인 레벨의 스택 포인터
    // sp_el0는 유저 모드에서의 스택 포인터

    mov    sp, x9                               // 스택 포인터 지정

    and    x9, x9, #~(THREAD_SIZE - 1) // 페이지 단위로 정렬
    // sp_el0 는 커널 모드에서는 실제로 유저 스택을 가리키고있지 않고, 유저모드로 돌아갈때 스택포인터가됨
    // ARM 64 에서는 유저모드의 스택포인터로 설계됐으나
    // 리눅스에서는 sp_el0 를 current 관리용으로 사용함
    // Ref: https://www.inflearn.com/community/questions/1476439/current-%EB%A7%A4%ED%81%AC%EB%A1%9C%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%A0%EB%95%8C-sp-el0-%EB%A5%BC-%ED%99%9C%EC%9A%A9%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0?srsltid=AfmBOoqE9l36JvnlPRnpV9iCsD_ONNMTnaEdX9gwpB22kRSRrhHZxB1B
    msr    sp_el0, x9
    ret // 리턴값은 x0 에 저장된 prev task_struct. 이전 pc(lr) 인 x30 으로 돌아감

ENDPROC(cpu_switch_to) 

6-12 kernel/sched/core.c finish_task_switch

static struct rq *finish_task_switch(struct task_struct *prev) // 이전 task 가 들어옴
    __releases(rq->lock)
{
    struct rq *rq = this_rq();
    struct mm_struct *mm = rq->prev_mm;
    long prev_state;

    /*
     * The previous task will have left us with a preempt_count of 2
     * because it left us after:
     *
     *    schedule()
     *      preempt_disable();            // 1
     *      __schedule()
     *        raw_spin_lock_irq(&rq->lock)    // 2
     *
     * Also, see FORK_PREEMPT_COUNT.
     */
    if (WARN_ONCE(preempt_count() != 2*PREEMPT_DISABLE_OFFSET,
              "corrupted preempt_count: %s/%d/0x%x\n",
              current->comm, current->pid, preempt_count()))
        preempt_count_set(FORK_PREEMPT_COUNT);

    rq->prev_mm = NULL;

    /*
     * A task struct has one reference for the use as "current".
     * If a task dies, then it sets TASK_DEAD in tsk->state and calls
     * schedule one last time. The schedule call will never return, and
     * the scheduled task must drop that reference.
     *
     * We must observe prev->state before clearing prev->on_cpu (in
     * finish_lock_switch), otherwise a concurrent wakeup can get prev
     * running on another CPU and we could rave with its RUNNING -> DEAD
     * transition, resulting in a double drop.
     */
    prev_state = prev->state;
    vtime_task_switch(prev); // 내 task 의 vtime 을 타이머의 이전값에서 가져와서 넣고, 내 새 task 의 vtime 은 타이머에 등록
    perf_event_task_sched_in(prev, current);
    finish_lock_switch(rq, prev); // prev 가 on_cpu 가 아닐때까지 기다리고, current task 가 lock 의 오너가 되도록 설정
    finish_arch_post_lock_switch();

    fire_sched_in_preempt_notifiers(current);
    if (mm)
        mmdrop(mm); // mm ref 카운트 감소, 0이라면 자료구조 해제
    if (unlikely(prev_state == TASK_DEAD)) {
        if (prev->sched_class->task_dead)
            prev->sched_class->task_dead(prev); // 스케줄러에 콜백이 붙어있었다면 처리

        /*
         * Remove function-return probe instances associated with this
         * task and put them back on the free list.
         */
        kprobe_flush_task(prev);
        put_task_struct(prev); // task 의 usage 가 없다면, task 제거 처리
    }

    tick_nohz_task_switch(); // Re-evaluate the need for the tick as we switch the current task. tick 이 멈춰있던 경우 다시 실행할지
    return rq;
}

6.2.6 태스크 깨우기: try_to_wake_up (ttwu)

잠들어있던 task 가 깨우기에 적절한 실행 상태인지 체크하고 실행할 cpu 를 결정

6-13 kernel/sched/core.c try_to_wake_up

  • 태스크가 이전 실행되던 cpu 에서 wakeup
  • 새로운 cpu 에서 wakeup

static int
try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
{
    if (!(p->state & state)) // 깨우는 조건을 확인
        goto out;

    success = 1; /* we're going to change ->state */
    cpu = task_cpu(p);

    if (p->on_rq && ttwu_remote(p, wake_flags)) // 이미 rq 에 들어있거나 마이그레이션 진행중이라면
        goto stat; // light wakeup 을 수행

#ifdef CONFIG_SMP
    /*
     * Ensure we load p->on_cpu _after_ p->on_rq, otherwise it would be
     * possible to, falsely, observe p->on_cpu == 0.
     *
     * One must be running (->on_cpu == 1) in order to remove oneself
     * from the runqueue.
     *
     *  [S] ->on_cpu = 1;    [L] ->on_rq
     *      UNLOCK rq->lock
     *            RMB
     *      LOCK   rq->lock
     *  [S] ->on_rq = 0;    [L] ->on_cpu
     *
     * Pairs with the full barrier implied in the UNLOCK+LOCK on rq->lock
     * from the consecutive calls to schedule(); the first switching to our
     * task, the second putting it to sleep.
     */
    smp_rmb();

    /*
     * If the owning (remote) cpu is still in the middle of schedule() with
     * this task as prev, wait until its done referencing the task.
     *
     * Pairs with the smp_store_release() in finish_lock_switch().
     *
     * This ensures that tasks getting woken will be fully ordered against
     * their previous state and preserve Program Order.
     */
    smp_cond_acquire(!p->on_cpu);

    // 태스크가 runque 의 로드에 기여하는지
    //  = 스케줄러가 시스템 로드를 계산할 때 해당 태스크를 포함할지 여부를 나타내는 플래그
    p->sched_contributes_to_load = !!task_contributes_to_load(p); 
    p->state = TASK_WAKING;

    if (p->sched_class->task_waking)
        p->sched_class->task_waking(p); // fair 스케줄링이라면, CFS 런큐의 min_vruntime 을 빼준다

    cpu = select_task_rq(p, p->wake_cpu, SD_BALANCE_WAKE, wake_flags); // 태스크가 실행될 cpu 를 선택
    if (task_cpu(p) != cpu) { // 새로운 cpu 에서 실행되어야 한다면, MIGRATION 되어야 하기때문에 플래그 설정
        wake_flags |= WF_MIGRATED;
        set_task_cpu(p, cpu);
    }
#endif /* CONFIG_SMP */

    ttwu_queue(p, cpu); // 6-15 에서 자세히, remote wakeup (?) 을 실행
stat:
    if (schedstat_enabled())
        ttwu_stat(p, cpu, wake_flags);
out:
    raw_spin_unlock_irqrestore(&p->pi_lock, flags);

    return success;
}

댓글