主要介紹內核搶占的相關概念和具體實現,以及搶占對內核調度和內核競態和同步的一些影響。
(所用內核版本3.19.3)
搶占計數8位, PREEMPT_MASK => 0x000000ff
軟中斷計數8位, SOFTIRQ_MASK => 0x0000ff00
硬中斷計數4位, HARDIRQ_MASK => 0x000f0000
不可屏蔽中斷1位, NMI_MASK => 0x00100000
PREEMPTIVE_ACTIVE(標識內核搶占觸發的schedule) => 0x00200000
調度標識1位, PREEMPT_NEED_RESCHED => 0x80000000
__preempt_count的作用
搶占計數 判斷當前所在上下文 重新調度標識thread_info的flags
thread_info的flags中有一個是TIF_NEED_RESCHED,在系統調用返回,中斷返回,以及preempt_disable的時候會檢查是否設置,如果設置並且搶占計數為0(可搶占),則會觸發重新調度schedule()或者preempt_schedule()或者preempt_schedule_irq()。通常在scheduler_tick中會檢查是否設置此標識(每個HZ觸發一次),然後在下一次中斷返回時檢查,如果設置將觸發重新調度,而在schedule()中會清除此標識。
// kernel/sched/core.c
// 設置thread_info flags和__preempt_count的need_resched標識
void resched_curr(struct rq *rq)
{
/*省略*/
if (cpu == smp_processor_id()) {
// 設置thread_info的need_resched標識
set_tsk_need_resched(curr);
// 設置搶占計數__preempt_count裡的need_resched標識
set_preempt_need_resched();
return;
}
/*省略*/
}
//在schedule()中清除thread_info和__preempt_count中的need_resched標識
static void __sched __schedule(void)
{
/*省略*/
need_resched:
// 關搶占讀取percpu變量中當前cpu id,運行隊列
preempt_disable();
cpu = smp_processor_id();
rq = cpu_rq(cpu);
rcu_note_context_switch();
prev = rq->curr;
/*省略*/
//關閉本地中斷,關閉搶占,獲取rq自旋鎖
raw_spin_lock_irq(&rq->lock);
switch_count = &prev->nivcsw;
// PREEMPT_ACTIVE 0x00200000
// preempt_count = __preempt_count & (~(0x80000000))
// 如果進程沒有處於running的狀態或者設置了PREEMPT_ACTIVE標識
//(即本次schedule是由於內核搶占導致),則不會將當前進程移出隊列
// 此處PREEMPT_ACTIVE的標識是由中斷返回內核空間時調用
// preempt_schdule_irq或者內核空間調用preempt_schedule
// 而設置的,表明是由於內核搶占導致的schedule,此時不會將當前
// 進程從運行隊列取出,因為有可能其再也無法重新運行。
if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
// 如果有信號不移出run_queue
if (unlikely(signal_pending_state(prev->state, prev))) {
prev->state = TASK_RUNNING;
} else { // 否則移除隊列讓其睡眠
deactivate_task(rq, prev, DEQUEUE_SLEEP);
prev->on_rq = 0;
// 是否喚醒一個工作隊列內核線程
if (prev->flags & PF_WQ_WORKER) {
struct task_struct *to_wakeup;
to_wakeup = wq_worker_sleeping(prev, cpu);
if (to_wakeup)
try_to_wake_up_local(to_wakeup);
}
}
switch_count = &prev->nvcsw;
}
/*省略*/
next = pick_next_task(rq, prev);
// 清除之前task的need_resched標識
clear_tsk_need_resched(prev);
// 清除搶占計數的need_resched標識
clear_preempt_need_resched();
rq->skip_clock_update = 0;
// 不是當前進程,切換上下文
if (likely(prev != next)) {
rq->nr_switches++;
rq->curr = next;
++*switch_count;
rq = context_switch(rq, prev, next);
cpu = cpu_of(rq);
} else
raw_spin_unlock_irq(&rq->lock);
post_schedule(rq);
// 重新開搶占
sched_preempt_enable_no_resched();
// 再次檢查need_resched
if (need_resched())
goto need_resched;
}
__preempt_count的相關操作
/////// need_resched標識相關 ///////
// PREEMPT_NEED_RESCHED位如果是0表示需要調度
#define PREEMPT_NEED_RESCHED 0x80000000
static __always_inline void set_preempt_need_resched(void)
{
// __preempt_count最高位清零表示need_resched
raw_cpu_and_4(__preempt_count, ~PREEMPT_NEED_RESCHED);
}
static __always_inline void clear_preempt_need_resched(void)
{
// __preempt_count最高位置位
raw_cpu_or_4(__preempt_count, PREEMPT_NEED_RESCHED);
}
static __always_inline bool test_preempt_need_resched(void)
{
return !(raw_cpu_read_4(__preempt_count) & PREEMPT_NEED_RESCHED);
}
// 是否需要重新調度,兩個條件:1. 搶占計數為0;2. 最高位清零
static __always_inline bool should_resched(void)
{
return unlikely(!raw_cpu_read_4(__preempt_count));
}
////////// 搶占計數相關 ////////
#define PREEMPT_ENABLED (0 + PREEMPT_NEED_RESCHED)
#define PREEMPT_DISABLE (1 + PREEMPT_ENABLED)
// 讀取__preempt_count,忽略need_resched標識位
static __always_inline int preempt_count(void)
{
return raw_cpu_read_4(__preempt_count) & ~PREEMPT_NEED_RESCHED;
}
static __always_inline void __preempt_count_add(int val)
{
raw_cpu_add_4(__preempt_count, val);
}
static __always_inline void __preempt_count_sub(int val)
{
raw_cpu_add_4(__preempt_count, -val);
}
// 搶占計數加1關閉搶占
#define preempt_disable() \
do { \
preempt_count_inc(); \
barrier(); \
} while (0)
// 重新開啟搶占,並測試是否需要重新調度
#define preempt_enable() \
do { \
barrier(); \
if (unlikely(preempt_count_dec_and_test())) \
__preempt_schedule(); \
} while (0)
// 搶占並重新調度
// 這裡設置PREEMPT_ACTIVE會對schdule()中的行為有影響
asmlinkage __visible void __sched notrace preempt_schedule(void)
{
// 如果搶占計數不為0或者沒有開中斷,則不調度
if (likely(!preemptible()))
return;
do {
__preempt_count_add(PREEMPT_ACTIVE);
__schedule();
__preempt_count_sub(PREEMPT_ACTIVE);
barrier();
} while (need_resched());
}
// 檢查thread_info flags
static __always_inline bool need_resched(void)
{
return unlikely(tif_need_resched());
}
////// 中斷相關 ////////
// 硬件中斷計數
#define hardirq_count() (preempt_count() & HARDIRQ_MASK)
// 軟中斷計數
#define softirq_count() (preempt_count() & SOFTIRQ_MASK)
// 中斷計數
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \
| NMI_MASK))
// 是否處於外部中斷上下文
#define in_irq() (hardirq_count())
// 是否處於軟中斷上下文
#define in_softirq() (softirq_count())
// 是否處於中斷上下文
#define in_interrupt() (irq_count())
#define in_serving_softirq() (softirq_count() & SOFTIRQ_OFFSET)
// 是否處於不可屏蔽中斷環境
#define in_nmi() (preempt_count() & NMI_MASK)
// 是否可搶占 : 搶占計數為0並且沒有處在關閉搶占的環境中
# define preemptible() (preempt_count() == 0 && !irqs_disabled())
(arch/x86/kernel/entry_64.S)
系統調用入口基本流程
保存當前rsp, 並指向內核棧,保存寄存器狀態 用中斷號調用系統調用函數表中對應的處理函數 返回時檢查thread_info的flags,處理信號以及need_resched中斷入口基本流程
保存寄存器狀態 call do_IRQ 中斷返回,恢復棧,檢查是中斷了內核上下文還是用戶上下文
// 系統調用的處理邏輯
ENTRY(system_call)
/* ... 省略 ... */
// 保存當前棧頂指針到percpu變量
movq %rsp,PER_CPU_VAR(old_rsp)
// 將內核棧底指針賦於rsp,即移到內核棧
movq PER_CPU_VAR(kernel_stack),%rsp
/* ... 省略 ... */
system_call_fastpath:
#if __SYSCALL_MASK == ~0
cmpq $__NR_syscall_max,%rax
#else
andl $__SYSCALL_MASK,%eax
cmpl $__NR_syscall_max,%eax
#endif
ja ret_from_sys_call /* and return regs->ax */
movq %r10,%rcx
// 系統調用
call *sys_call_table(,%rax,8) # XXX: rip relative
movq %rax,RAX-ARGOFFSET(%rsp)
ret_from_sys_call:
movl $_TIF_ALLWORK_MASK,%edi
/* edi: flagmask */
// 返回時需要檢查thread_info的flags
sysret_check:
LOCKDEP_SYS_EXIT
DISABLE_INTERRUPTS(CLBR_NONE)
TRACE_IRQS_OFF
movl TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET),%edx
andl %edi,%edx
jnz sysret_careful // 如果有thread_info flags需要處理,比如need_resched
//// 直接返回
CFI_REMEMBER_STATE
/*
* sysretq will re-enable interrupts:
*/
TRACE_IRQS_ON
movq RIP-ARGOFFSET(%rsp),%rcx
CFI_REGISTER rip,rcx
RESTORE_ARGS 1,-ARG_SKIP,0
/*CFI_REGISTER rflags,r11*/
// 恢復之前保存percpu變量中的棧頂地址(rsp)
movq PER_CPU_VAR(old_rsp), %rsp
// 返回用戶空間
USERGS_SYSRET64
CFI_RESTORE_STATE
//// 如果thread_info的標識被設置了,則需要處理後返回
/* Handle reschedules */
sysret_careful:
bt $TIF_NEED_RESCHED,%edx // 檢查是否需要重新調度
jnc sysret_signal // 有信號
// 沒有信號則處理need_resched
TRACE_IRQS_ON
ENABLE_INTERRUPTS(CLBR_NONE)
pushq_cfi %rdi
SCHEDULE_USER // 調用schedule(),返回用戶態不需要檢查__preempt_count
popq_cfi %rdi
jmp sysret_check // 再一次檢查
// 如果有信號發生,則需要處理信號
sysret_signal:
TRACE_IRQS_ON
ENABLE_INTERRUPTS(CLBR_NONE)
FIXUP_TOP_OF_STACK %r11, -ARGOFFSET
// 如果有信號,無條件跳轉
jmp int_check_syscall_exit_work
/* ... 省略 ... */
GLOBAL(int_ret_from_sys_call)
DISABLE_INTERRUPTS(CLBR_NONE)
TRACE_IRQS_OFF
movl $_TIF_ALLWORK_MASK,%edi
/* edi: mask to check */
GLOBAL(int_with_check)
LOCKDEP_SYS_EXIT_IRQ
GET_THREAD_INFO(%rcx)
movl TI_flags(%rcx),%edx
andl %edi,%edx
jnz int_careful
andl $~TS_COMPAT,TI_status(%rcx)
jmp retint_swapgs
/* Either reschedule or signal or syscall exit tracking needed. */
/* First do a reschedule test. */
/* edx: work, edi: workmask */
int_careful:
bt $TIF_NEED_RESCHED,%edx
jnc int_very_careful // 如果不只need_resched,跳轉
TRACE_IRQS_ON
ENABLE_INTERRUPTS(CLBR_NONE)
pushq_cfi %rdi
SCHEDULE_USER // 調度schedule
popq_cfi %rdi
DISABLE_INTERRUPTS(CLBR_NONE)
TRACE_IRQS_OFF
jmp int_with_check // 再次去檢查
/* handle signals and tracing -- both require a full stack frame */
int_very_careful:
TRACE_IRQS_ON
ENABLE_INTERRUPTS(CLBR_NONE)
int_check_syscall_exit_work:
SAVE_REST
/* Check for syscall exit trace */
testl $_TIF_WORK_SYSCALL_EXIT,%edx
jz int_signal
pushq_cfi %rdi
leaq 8(%rsp),%rdi # &ptregs -> arg1
call syscall_trace_leave
popq_cfi %rdi
andl $~(_TIF_WORK_SYSCALL_EXIT|_TIF_SYSCALL_EMU),%edi
jmp int_restore_rest
int_signal:
testl $_TIF_DO_NOTIFY_MASK,%edx
jz 1f
movq %rsp,%rdi # &ptregs -> arg1
xorl %esi,%esi # oldset -> arg2
call do_notify_resume
1: movl $_TIF_WORK_MASK,%edi
int_restore_rest:
RESTORE_REST
DISABLE_INTERRUPTS(CLBR_NONE)
TRACE_IRQS_OFF
jmp int_with_check // 再次檢查thread_info flags
CFI_ENDPROC
END(system_call)
// 中斷入口基本流程
// 調用do_IRQ的函數wrapper
.macro interrupt func
subq $ORIG_RAX-RBP, %rsp
CFI_ADJUST_CFA_OFFSET ORIG_RAX-RBP
SAVE_ARGS_IRQ // 進入中斷處理上下文時保存寄存器
call \func
/*... 省略 ...*/
common_interrupt:
/*... 省略 ...*/
interrupt do_IRQ // 調用c函數do_IRQ實際處理中斷
ret_from_intr: // 中斷返回
DISABLE_INTERRUPTS(CLBR_NONE)
TRACE_IRQS_OFF
decl PER_CPU_VAR(irq_count) // 減少irq計數
/* Restore saved previous stack */
// 恢復之前的棧
popq %rsi
CFI_DEF_CFA rsi,SS+8-RBP /* reg/off reset after def_cfa_expr */
leaq ARGOFFSET-RBP(%rsi), %rsp
CFI_DEF_CFA_REGISTER rsp
CFI_ADJUST_CFA_OFFSET RBP-ARGOFFSET
exit_intr:
GET_THREAD_INFO(%rcx)
testl $3,CS-ARGOFFSET(%rsp) // 檢查是否中斷了內核
je retint_kernel // 從中斷返回內核空間
/* Interrupt came from user space */
/*
* Has a correct top of stack, but a partial stack frame
* %rcx: thread info. Interrupts off.
*/
// 用戶空間被中斷,返回用戶空間
retint_with_reschedule:
movl $_TIF_WORK_MASK,%edi
retint_check:
LOCKDEP_SYS_EXIT_IRQ
movl TI_flags(%rcx),%edx
andl %edi,%edx
CFI_REMEMBER_STATE
jnz retint_careful // 需要處理need_resched
retint_swapgs: /* return to user-space */
/*
* The iretq could re-enable interrupts:
*/
DISABLE_INTERRUPTS(CLBR_ANY)
TRACE_IRQS_IRETQ
SWAPGS
jmp restore_args
retint_restore_args: /* return to kernel space */
DISABLE_INTERRUPTS(CLBR_ANY)
/*
* The iretq could re-enable interrupts:
*/
TRACE_IRQS_IRETQ
restore_args:
RESTORE_ARGS 1,8,1
irq_return:
INTERRUPT_RETURN // native_irq進入
ENTRY(native_iret)
/*... 省略 ...*/
/* edi: workmask, edx: work */
retint_careful:
CFI_RESTORE_STATE
bt $TIF_NEED_RESCHED,%edx
jnc retint_signal // 需要處理信號
TRACE_IRQS_ON
ENABLE_INTERRUPTS(CLBR_NONE)
pushq_cfi %rdi
SCHEDULE_USER // 返回用戶空間之前調度schedule
popq_cfi %rdi
GET_THREAD_INFO(%rcx)
DISABLE_INTERRUPTS(CLBR_NONE)
TRACE_IRQS_OFF
jmp retint_check // 再次檢查thread_info flags
retint_signal:
testl $_TIF_DO_NOTIFY_MASK,%edx
jz retint_swapgs
TRACE_IRQS_ON
ENABLE_INTERRUPTS(CLBR_NONE)
SAVE_REST
movq $-1,ORIG_RAX(%rsp)
xorl %esi,%esi # oldset
movq %rsp,%rdi # &pt_regs
call do_notify_resume
RESTORE_REST
DISABLE_INTERRUPTS(CLBR_NONE)
TRACE_IRQS_OFF
GET_THREAD_INFO(%rcx)
jmp retint_with_reschedule // 處理完信號,再次跳轉處理need_resched
//// 注意,如果內核配置支持搶占,則返回內核時使用這個retint_kernel
#ifdef CONFIG_PREEMPT
/* Returning to kernel space. Check if we need preemption */
/* rcx: threadinfo. interrupts off. */
ENTRY(retint_kernel)
// 檢查__preempt_count是否為0
cmpl $0,PER_CPU_VAR(__preempt_count)
jnz retint_restore_args // 不為0,則禁止搶占
bt $9,EFLAGS-ARGOFFSET(%rsp) /* interrupts off? */
jnc retint_restore_args
call preempt_schedule_irq // 可以搶占內核
jmp exit_intr // 再次檢查
#endif
CFI_ENDPROC
END(common_interrupt)