Linux内核抢占

主要介绍内核抢占的相关概念和具体实现,以及抢占对内核调度和内核竞态和同步的一些影响。


1. 基本概念

  • 用户抢占和内核抢占

    • 用户抢占发生点

      • 当从系统调用或者中断上下文返回用户态的时候,会检查need_resched标志,如果被设置则会重新选择用户态task执行
    • 内核抢占发生点
      • 当从中断上下文返回内核态的时候,检查need_resched标识以及__preemp_count计数,如果标识被设置,并且可抢占,则会触发调度程序
      • 内核代码由于阻塞等原因直接或间接显示调用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                 => 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触发一次),然后在下一次中断返回时检查,如果设置将触发重新调度
  • __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())

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,%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)

4. 抢占与SMP并发安全

  • 中断嵌套可能导致死锁和竞态,一般中断上下文会关闭本地中断
  • 软中断
  • 一个核上的task访问percpu变量时可能由于内核抢占导致重新调度到另一个核上继续访问另一个核上同名percpu变量,从而可能发生死锁和竞态,所以访问percpu或者共享变量时需要禁止抢占
  • 自旋锁需要同时关闭本地中断和内核抢占

5. 几个问题作为回顾

  • 什么时候可抢占?
  • 什么时候需要抢占重新调度?
  • 自旋锁为什么需要同时关闭中断和抢占?
  • 为什么中断上下文不能睡眠?关闭抢占后能否睡眠?
  • 为什么percpu变量的访问需要禁止抢占?
时间: 2024-10-12 21:01:41

Linux内核抢占的相关文章

Linux内核抢占实现机制分析【转】

Linux内核抢占实现机制分析 转自:http://blog.chinaunix.net/uid-24227137-id-3050754.html [摘要]本文详解了Linux内核抢占实现机制.首先介绍了内核抢占和用户抢占的概念和区别,接着分析了不可抢占内核的特点及实时系统中实现内核抢占的必要性.然后分析了禁止内核抢占的情况和内核抢占的时机,最后介绍了实现抢占内核所做的改动以及何时需要重新调度. [关键字]内核抢占,用户抢占,中断, 实时性,自旋锁,抢占时机,调度时机,schedule,pree

Linux内核抢占实现机制分析

Sailor_forever  [email protected] 转载请注明 http://blog.csdn.net/sailor_8318/archive/2008/09/03/2870184.aspx [摘要]本文详解了Linux内核抢占实现机制.首先介绍了内核抢占和用户抢占的概念和区别,接着分析了不可抢占内核的特点及实时系统中实现内核抢占的必要性.然后分析了禁止内核抢占的情况和内核抢占的时机,最后介绍了实现抢占内核所做的改动以及何时需要重新调度. [关键字]内核抢占,用户抢占,中断, 

Linux内核抢占机制 - 简介

本文首发于 http://oliveryang.net,转载时请包含原文或者作者网站链接. 本文主要围绕 Linux 内核调度器 Preemption 的相关实现进行讨论.其中涉及的一般操作系统和 x86 处理器和硬件概念,可能也适用于其它操作系统. 1. 背景知识 要深入理解 Preemption 必须对操作系统的 Context Switch 做一个全面的梳理.最终可以了解 Preemption 和 Context Switch 概念上的区别与联系. 1.1 Context Switch C

Linux内核抢占机制 - 实现

本文首发于 http://oliveryang.net,转载时请包含原文或者作者网站链接. 本文主要围绕 Linux 内核调度器 Preemption 的相关实现进行讨论.其中涉及的一般操作系统和 x86 处理器和硬件概念,可能也适用于其它操作系统. 1. Scheduler Overview Linux 调度器的实现实际上主要做了两部分事情, 任务上下文切换 在 Preemption Overview 里,我们对任务上下文切换做了简单介绍.可以看到,任务上下文切换有两个层次的实现:公共层和处理

Linux用户抢占和内核抢占详解(概念, 实现和触发时机)--Linux进程的管理与调度(二十)

1 非抢占式和可抢占式内核 为了简化问题,我使用嵌入式实时系统uC/OS作为例子 首先要指出的是,uC/OS只有内核态,没有用户态,这和Linux不一样 多任务系统中, 内核负责管理各个任务, 或者说为每个任务分配CPU时间, 并且负责任务之间的通讯. 内核提供的基本服务是任务切换. 调度(Scheduler),英文还有一词叫dispatcher, 也是调度的意思. 这是内核的主要职责之一, 就是要决定该轮到哪个任务运行了. 多数实时内核是基于优先级调度法的, 每个任务根据其重要程度的不同被赋予

Linux下的内核抢占

2017-03-03 很遗憾之前在介绍进程调度的文章中,虽然涉及到了内核抢占,但是却没有对其进行深入介绍,今天就稍微总结下内核抢占. 内核抢占在一定程度上减少了对某种事件的响应延迟,这也是内核抢占被引入的目的.之前的内核中,除了显示调用系统调度器的某些点,内核其他地方是不允许中断的,如果内核在做一些比较复杂的工作,就会造成某些急于处理的事得不到及时响应.针对内核抢占其实本质上也是对当前进程而言(不知道这么描述是否合适),因为内核是为用户程序提供服务,换言之,其本身不会主动的去执行某个动作.这里内

Linux内核态抢占机制分析

http://blog.sina.com.cn/s/blog_502c8cc401012pxj.html [摘要]本文首先介绍非抢占式内核(Non-Preemptive Kernel)和可抢占式内核(Preemptive Kernel)的区别.接着分析Linux下有两种抢占:用户态抢占(User Preemption).内核态抢占(Kernel Preemption).然后分析了在内核态下:如何判断能否抢占内核(什么是可抢占的条件):何时触发重新调度(何时设置可抢占条件):抢占发生的时机(何时检

Linux内核态抢占机制分析(转)

Linux内核态抢占机制分析  http://blog.sina.com.cn/s/blog_502c8cc401012pxj.html 摘 要]本文首先介绍非抢占式内核(Non-Preemptive Kernel)和可抢占式内核(Preemptive Kernel)的区别.接着分析Linux下有两种抢占:用户态抢占(User Preemption).内核态抢占(Kernel Preemption).然后分析了在内核态下:如何判断能否抢占内核(什么是可抢占的条件):何时触发重新调度(何时设置可抢

[linux内核][linux中断]——内核抢占

1,禁止内核抢占的函数是preempt_disable(),允许内核抢占的方法是preempt_enable() 2,真正的上锁前,为何要调用preempt_disable()来关闭抢占的case http://blog.csdn.net/kasalyn/article/details/11473885 [cpp] view plaincopy static inline void raw_spin_lock(raw_spinlock_t *lock) { preempt_disable();