内核抢占

xiaoxiao2021-02-28  82

1. 基本概念

用户抢占和内核抢占  用户抢占发生点  当从系统调用或者中断上下文返回用户态的时候,会检查need_resched标志,如果被设置则会重新选择用户态task执行 内核抢占发生点  当从中断上下文返回内核态的时候,检查need_resched标识以及__preemp_count计数,如果标识被设置,并且可抢占,则会触发调度程序preempt_schedule_irq()内核代码由于阻塞等原因直接或间接显示调用schedule,比如preemp_disable时可能会触发preempt_schedule() 本质上内核态中的task是共享一个内核地址空间,在同一个core上,从中断返回的task很可能执行和被抢占的task相同的代码,并且两者同时等待各自的资源释放,也可能两者修改同一共享变量,所以会造成死锁或者竞态等;而对于用户态抢占来说,由于每个用户态进程都有独立的地址空间,所以在从内核代码(系统调用或者中断)返回用户态时,由于是不同地址空间的锁或者共享变量,所以不会出现不同地址空间之间的死锁或者竞态,也就没必要检查__preempt_count,是安全的。__preempt_count主要负责内核抢占计数。

2. 内核抢占的实现

percpu变量__preempt_count 抢占计数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 123456 123456

__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; } 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980 __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()) 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103

3. 系统调用和中断处理流程的实现以及抢占的影响

(arch/x86/kernel/entry_64.S)

系统调用入口基本流程

保存当前rsp, 并指向内核栈,保存寄存器状态用中断号调用系统调用函数表中对应的处理函数返回时检查thread_info的flags,处理信号以及need_resched  如果没信号和need_resched,直接恢复寄存器返回用户空间如果有信号处理信号,并再次检查如果有need_resched,重新调度,返回再次检查

中断入口基本流程

保存寄存器状态call do_IRQ中断返回,恢复栈,检查是中断了内核上下文还是用户上下文  如果是用户上下文,检查thread_info flags是否需要处理信号和need_resched,如果需要,则处理信号和need_resched,再次检查; 否则,直接中断返回用户空间如果是内核上下文,检查是否需要need_resched,如果需要,检查__preempt_count是否为0(能否抢占),如果为0,则call preempt_schedule_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,
转载请注明原文地址: https://www.6miu.com/read-78864.html

最新回复(0)