内核抢占
内核抢占是指当进程在内核模式下运行的时候可以被其他进程抢占,编译内核时打开配置宏CONFIG_PREEMPT
则允许内核抢占。
抢占是指当进程在内核模式下运行的时候可以被其他进程抢占,如果优先级更高的进程处于就绪状态,强行剥夺当前进程的处理器使用权。
禁止内核抢占
有时候进程可能在执行一些关键操作,不能被抢占;所以内核设计了抢占计数器。如果抢占计数器为0,表示可以被抢占;如果抢占计数器器不为0,表示不能被抢占。
抢占计数器
每个进程的task_struct
结构体中的thread_info
结构体有一个抢占计数器:preempt_count
,用来表示当前进程是否可以被抢占。arm64
架构下的thread_info
定义如下:
/*
* low level task data that entry.S needs immediate access to.
*/
struct thread_info {
unsigned long flags; /* low level flags */
mm_segment_t addr_limit; /* address limit */
#ifdef CONFIG_ARM64_SW_TTBR0_PAN
u64 ttbr0; /* saved TTBR0_EL1 */
#endif
int preempt_count; /* 0 => preemptable, <0 => bug */
};
preempt_count
是一个32位的整数,内核对它的位进行了划分,不同位域对应不同的场景,如下图:
字段 | 位数 | 用途 |
---|---|---|
不可屏蔽中断标志(NMI,Non Maskable Interrupt) | 1位(20) | 记录不可屏蔽中断 |
硬中断计数 | 4 位(16-19) | 记录硬中断计数 |
禁用软中断计数 | 8 位(8-15) | 记录禁用软中断的计数 |
抢占禁用计数 | 8 位(0-7) | 表示内核中主动禁用抢占的层级(如 preempt_disable() )。 |
相关定义代码如下:
/*
* We put the hardirq and softirq counter into the preemption
* counter. The bitmask has the following meaning:
*
* - bits 0-7 are the preemption count (max preemption depth: 256)
* - bits 8-15 are the softirq count (max # of softirqs: 256)
*
* The hardirq count could in theory be the same as the number of
* interrupts in the system, but we run all interrupt handlers with
* interrupts disabled, so we cannot have nesting interrupts. Though
* there are a few palaeontologic drivers which reenable interrupts in
* the handler, so we need more than one bit here.
*
* PREEMPT_MASK: 0x000000ff
* SOFTIRQ_MASK: 0x0000ff00
* HARDIRQ_MASK: 0x000f0000
* NMI_MASK: 0x00100000
* PREEMPT_NEED_RESCHED: 0x80000000
*/
#define PREEMPT_BITS 8
#define SOFTIRQ_BITS 8
#define HARDIRQ_BITS 4
#define NMI_BITS 1
各种场景分别利用各自的位禁止或开启抢占。
(1)普通场景(PREEMPT_MASK):对应函数preempt_disable()和 preeempt_enable()。
(2)软中断场景(SOFTIRQ_MASK):对应函数local_bh_disable()和local_bh_enable()。
(3)硬中断场景(HARDIRQ_MASK):对应函数__irq_enter()和__irq_exit()。
(4)不可屏蔽中断场景(NMI_MASK):对应函数nmi_enter()和nni_exit()。
反过来,我们可以通过抢占计数器的值判断当前处于什么场景:
#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))
/*
* Are we doing bottom half or hardware interrupt processing?
*
* in_irq() - We're in (hard) IRQ context
* in_softirq() - We have BH disabled, or are processing softirqs
* in_interrupt() - We're in NMI,IRQ,SoftIRQ context or have BH disabled
* in_serving_softirq() - We're in softirq context
* in_nmi() - We're in NMI context
* in_task() - We're in task context
*
* Note: due to the BH disabled confusion: in_softirq(),in_interrupt() really
* should not be used in new code.
*/
#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)
#define in_task() (!(preempt_count() & \
(NMI_MASK | HARDIRQ_MASK | SOFTIRQ_OFFSET)))
- in_irq()表示硬中断场景,也就是正在执行硬中断。
- in_softirq()表示软中断场景,包括禁止软中断和正在执行软中断。
- in_interrupt()表示正在执行不可屏蔽中断、硬中断或软中断,或者禁止软中断。
- in_serving_softirq()表示正在执行软中断。
- in_nmi()表示不可屏蔽中断场景。
- in_task()表示普通场景,也就是进程上下文。
内核自旋锁,申请自旋锁的函数中包含了禁止内核抢占(**preempt_disable**
):
spin_lock() -> raw_spin_lock() -> _raw_spin_lock() -> __raw_spin_lock()
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable();
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
释放自旋锁的函数中包含了开启内核抢占(**preempt_enable**
):
spin_unlock() -> raw_spin_unlock() -> _raw_sspin_unlock() -> __raw_spin_unlock()
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
spin_release(&lock->dep_map, 1, _RET_IP_);
do_raw_spin_unlock(lock);
preempt_enable();
}
进程和软中断互斥
如果进程和软中断可能访问同一个对象,那么进程和软中断需要互斥,进程需要禁止软中断。
如果进程只需要和本处理器的软中断互斥,那么进程只需要要禁止本处理器的软中断;如果进程要和所有处理器的软中断互斥,那么进程需要禁止本处理器的软中断,还要使用自旋锁和其他处理器的软中断互斥。
抢占计数器preempt_count
中,bit 8
~ bit 15
表示软中断计数。
禁止软中断local_bh_disable()
的时候把当前进程的软中断计数加2。
注意:这个接口只能禁止本处理器的软中断,不能禁止其他处理器的软中断。
开启软中断local_bh_enable()
的时候把当前进程的软中断计数减2。如果软中断计数、硬中断计数和不可屏蔽中断计数都是0,并且有软中断需要处理,那么执行软中断。
static __always_inline void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
{
preempt_count_add(cnt);
barrier();
}
// include/linux/preempt.h
/*
#define SOFTIRQ_DISABLE_OFFSET (2 * SOFTIRQ_OFFSET)
#define SOFTIRQ_OFFSET (1UL << SOFTIRQ_SHIFT)
#define SOFTIRQ_SHIFT (PREEMPT_SHIFT + PREEMPT_BITS)
#define PREEMPT_SHIFT 0
#define PREEMPT_BITS 8
*/
static inline void local_bh_disable(void)
{
__local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}
static inline void local_bh_enable_ip(unsigned long ip)
{
__local_bh_enable_ip(ip, SOFTIRQ_DISABLE_OFFSET);
}
static inline void local_bh_enable(void)
{
__local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}
进程和硬中断互斥
如果进程和硬中断可能访问同一个对象,那么进程和硬中断需要要互斥,进程需要禁止硬中断。
如果进程只需要和本处理器的硬中断互斥,那么进程只需要要禁止本处理器的硬中断;如果进程要和所有处理器的硬中断互斥,那么进程需要禁止本处理器的硬中断,还要使用自旋锁和其他处理器的硬中断互斥(有禁用所有处理器中断的方法,但一般不使用)。
禁止硬中断的接口如下。
(1)local_irq_disable()
。
(2)local_irq_save(flags)
:首先把硬中断状态保存在参数flags中,然后禁止硬中断。
这两个接口只能禁止本处理器的硬中断,不能禁止其他处理器的硬中断。禁止硬中断后,处理器不会响应中断请求。
开启硬中断的接口如下。
(1)local_irq_enable()
。
(2)local_irq_restore(flags)
:恢复本处理器的硬中断状态。
local_irq_disable()
和local_irq_enable()
不能嵌套使用,local_irq_save(flags)
和local_irq_restore(flags)
可以嵌套使用。
硬中断如何使用抢占计数器preempt_count
?
单纯的禁用硬中断<font style="color:rgb(64, 64, 64);">local_irq_disable()</font>
不会修改<font style="color:rgb(64, 64, 64);">preempt_count</font>
,它主要通过设置ARM64的DAIF寄存器中的来屏蔽中断。
当中断实际发生时,进入中断处理程序会调用<font style="color:rgb(64, 64, 64);">irq_enter()</font>
,这会修改<font style="color:rgb(64, 64, 64);">preempt_count</font>
的HARDIRQ域。
/*
* Enter an interrupt context.
*/
void irq_enter(void)
{
rcu_irq_enter();
if (is_idle_task(current) && !in_interrupt()) {
/*
* Prevent raise_softirq from needlessly waking up ksoftirqd
* here, as softirq will be serviced on return from interrupt.
*/
local_bh_disable();
tick_irq_enter();
_local_bh_enable();
}
__irq_enter();
}
/*
* It is safe to do non-atomic ops on ->hardirq_context,
* because NMI handlers may not preempt and the ops are
* always balanced, so the interrupted value of ->hardirq_context
* will always be restored.
*/
#define __irq_enter() \
do { \
account_irq_enter_time(current); \
preempt_count_add(HARDIRQ_OFFSET); \
trace_hardirq_enter(); \
} while (0)
参考资料
- Professional Linux Kernel Architecture,Wolfgang Mauerer
- Linux内核深度解析,余华兵
- Linux设备驱动开发详解,宋宝华
- linux kernel 4.12