Preemptive scheduling 이란

  • 실행 중인 프로세스를 Arm 코어에서 빼내 우선순위가 높은 프로세스를
    Arm 코어에서 실행
  • Race가 발생하는 주요 요인 중 하나
  • 시스템 응답성을 키울 수 있는 기능
  • CONFIG_PREEMPTION

Non-preemptive scheduling

  • 프로세스가 schedule() 함수를 호출해 자발적으로 스케줄링 요청

Non-preemptive scheduling의 예시

  • msleep()
  • 입출력(I/O) 동작을 시작할 때
  • 뮤텍스를 획득하지 못하고 휴면 상태에 진입할 때

Preemption

  • 레이스 컨디션이 발생하는 원인 중 하나는 Preemption
  • 안정적인 디바이스 드라이버를 디자인할 수 있는 기반 지식

Preemptive scheduling를 체크하는 3가지 포인트

  • 인터럽트 핸들러를 처리하고 난 후 인터럽트가 발생하기 전 코드
    로 되돌아가기 직전 (유저 공간, 커널 코드)
  • 시스템 콜 핸들러 함수의 실행을 마무리한 후 유저 공간으로 복귀
    하기 직전

Preemptive scheduling 1st 스택

  • 인터럽트 서비스 루틴 실행을 마무리하고 인터럽트가 유발된 커널 코드(EL1)로 복귀하
    기 직전 시점
el1h_64_irq
el1h_64_irq_handler
el1_interrupt
do_interrupt_handler // IRQ 서비스 루틴 실행
arm64_preempt_schedule_irq // 프리엠션을 할 조건이면 이 함수가 호출
preempt_schedule_irq /
__schedule

Preemptive scheduling 2nd 스택

  • 인터럽트 서비스 루틴 실행을 마무리하고 인터럽트가 유발된 유저 공간(EL0)의 코드로
    복귀하기 직전 시점
el0t_64_irq
el0t_64_irq_handler
__el0_irq_handler_common
el0_interrupt
do_interrupt_handler // IRQ 서비스 루틴 실행
exit_to_user_mode // 프리엠션 할 조건이면 아래로
prepare_exit_to_user_mode
do_notify_resu

Preemptive scheduling 3rd 스택

  • 시스템 콜 핸들러 서브 루틴 실행을 마무리하고 유저 공간(EL0)의 코드로 복귀하기 직전
    시점
el0t_64_sync
el0t_64_sync_handler
el0_svc
do_el0_svc
el0_svc_common
invoke_syscall // 시스템 콜 서브 루틴 실행
exit_to_user_mode // 프리엠션 조건이면 스케쥴
prepare_exit_to_user_mode
do_notify_resume
schedule

Preemption: 커널 코드 실행 도중 인터럽트 유발

https://elixir.bootlin.com/linux/v5.15.30/source/arch/arm64/kernel/entry-common.c
static void noinstr el1_interrupt(struct pt_regs *regs,
void (*handler)(struct pt_regs *))
{
write_sysreg(DAIF_PROCCTX_NOIRQ, daif);
enter_el1_irq_or_nmi(regs);
do_interrupt_handler(regs, handler);
if (IS_ENABLED(CONFIG_PREEMPTION) &&
READ_ONCE(current_thread_info()->preempt_count) == 0) // 프리엠트 카운트가 0인지 체크, 0이면 함수를 호출
arm64_preempt_schedule_irq(); 
exit_el1_irq_or_nmi(regs);
}

preempt_schedule_irq() 함수

https://elixir.bootlin.com/linux/v5.15.30/source/kernel/sched/core.c
asmlinkage __visible void __sched preempt_schedule_irq(void)
{
enum ctx_state prev_state;
/* Catch callers which need to be fixed */
BUG_ON(preempt_count() || !irqs_disabled());
prev_state = exception_enter();
do {
preempt_disable(); // 프로세스의 프리엠트 카운트를 1만큼 증가시킴, 프리엠션 비활성화
local_irq_enable(); // 로컬 인터럽트를 다시 활성화 시키는 함수
__schedule(SM_PREEMPT); // 
local_irq_disable();
sched_preempt_enable_no_resched();
} while (need_resched());
exception_exit(prev_state);
}

__schedule() 함수

https://elixir.bootlin.com/linux/v5.15.30/source/kernel/sched/core.c
static void __sched notrace __schedule(unsigned int sched_mode)
{
struct task_struct *prev, *next;
...
if (likely(prev != next)) {
...
trace_sched_switch(sched_mode & SM_MASK_PREEMPT, prev, next);
/* Also unlocks the rq: */
rq = context_switch(rq, prev, next, &rf); // 우선순위가 높은 프로세스가 프리엠션으로 cpu코어에서 실행, 컨텍스트 스위칭 발생
...
}
}

el0_interrupt() 함수

https://elixir.bootlin.com/linux/v5.15.30/source/arch/arm64/kernel/entry-common.c
static void noinstr el0_interrupt(struct pt_regs *regs,
void (*handler)(struct pt_regs *))
{
enter_from_user_mode(regs);
write_sysreg(DAIF_PROCCTX_NOIRQ, daif);
if (regs->pc & BIT(55))
arm64_apply_bp_hardening();
do_interrupt_handler(regs, handler); // IRQ 서비스 루틴 실행
exit_to_user_mode(regs);
}

exit_to_user_mode() 함수

https://elixir.bootlin.com/linux/v5.15.30/source/arch/arm64/kernel/entry-common.c
static __always_inline void exit_to_user_mode(struct pt_regs *regs)
{
prepare_exit_to_user_mode(regs); // 
mte_check_tfsr_exit();
__exit_to_user_mode();
}

prepare_exit_to_user_mode() 함수

https://elixir.bootlin.com/linux/v5.15.30/source/arch/arm64/kernel/entry-common.c
static __always_inline void prepare_exit_to_user_mode(struct pt_regs *regs)
{
unsigned long flags;
local_daif_mask();
flags = READ_ONCE(current_thread_info()->flags); // 스레드 인포의 플래그 정보, 
if (unlikely(flags & _TIF_WORK_MASK)) // tif 워크 마스크와 & 연산 , 플래그가 TIF need resched를 포함하고 있거나 다른 매크로를 포함하고 있으면 아래 함수 호출
do_notify_resume(regs, flags);
}
  • _TIF_WORK_MASK와 _TIF_NEED_RESCHED
(where)
https://elixir.bootlin.com/linux/v5.15.30/source/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_MTE_ASYNC_FAULT | \
_TIF_NOTIFY_SIGNAL)

do_notify_resume() 함수

https://elixir.bootlin.com/linux/v5.15.30/source/arch/arm64/kernel/signal.c
void do_notify_resume(struct pt_regs *regs, unsigned long thread_flags)
{
do {
if (thread_flags & _TIF_NEED_RESCHED) {
/* Unmask Debug and SError for the next task */
local_daif_restore(DAIF_PROCCTX_NOIRQ);
schedule();
} else {
...
if (thread_flags & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))
do_signal(regs);

el0_svc() 함수

  • do_el0_svc() 함수에서 시스템 콜 핸들러를 호출
  • 시스템 콜 핸들러 실행이 마무리된 후 exit_to_user_mode() 함수로 복귀
https://elixir.bootlin.com/linux/v5.15.30/source/arch/arm64/kernel/entry-common.c
static void noinstr el0_svc(struct pt_regs *regs)
{
enter_from_user_mode(regs);
cortex_a76_erratum_1463225_svc_handler();
do_el0_svc(regs);
exit_to_user_mode(regs)

exit_to_user_mode() 함수

https://elixir.bootlin.com/linux/v5.15.30/source/arch/arm64/kernel/entry-common.c
static __always_inline void exit_to_user_mode(struct pt_regs *regs)
{
prepare_exit_to_user_mode(regs);
mte_check_tfsr_exit();
__exit_to_user_mode();
}

prepare_exit_to_user_mode() 함수

https://elixir.bootlin.com/linux/v5.15.30/source/arch/arm64/kernel/entry-common.c
static __always_inline void prepare_exit_to_user_mode(struct pt_regs *regs)
{
unsigned long flags;
local_daif_mask();
flags = READ_ONCE(current_thread_info()->flags);
if (unlikely(flags & _TIF_WORK_MASK))
do_notify_resume(regs, flags);
}
Preemption될 조건
(where)
https://elixir.bootlin.com/linux/v5.15.30/source/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_MTE_ASYNC_FAULT | \
_TIF_NOTIFY_SIGNAL)

0개의 댓글