【深入理解Linux锁机制】二、中断屏蔽

【深入理解Linux锁机制】二、中断屏蔽

【深入理解Linux内核锁】二、中断屏蔽

#上一篇了解了内核锁的由来,本篇文章主要来讲一下中断屏蔽的底层实现以及原理。

1、中断屏蔽思想

#中断屏蔽,正如其名,屏蔽掉CPU的中断响应功能,解决并发引起的竞态问题。

在进入临界区前屏蔽中断,这么做有什么好处,以及有什么弊端?

好处在于:

解决了进程与中断之间的并发:保证在执行临界区代码时,不被中断所打断。解决了进程与进程之间调度的并发:系统的进程调度与中断息息相关,同时也限制了系统进程的并发,解决了系统进程并发带来的竞态问题。弊端在于:

各类中断类型较多,一棒子打死影响大:Linux内核中,除了系统进程调度依赖中断,还有一些异步I/O等众多操作都依赖中断,因此长时间屏蔽中断是很危险的,会对系统造成严重影响,因此也要求临界区代码要简短。解决的不够完善:关闭中断能够解决进程调度、中断引发的竞态,但是这些都是单CPU内部的,对于SMP对称多处理器,仍然不可避免的会收到其他CPU的中断。因此,并不能解决SMP多CPU引发的竞态。因此,单独使用中断屏蔽通常不是一种值得推荐的避免竞态的方法

2、Linux内核中断屏蔽的实现

#2.1 Linux内核提供的API接口

#关于中断屏蔽,Linux内核所提供的接口如下:

local_irq_enable() // 使能本CPU的中断

local_irq_disable() // 禁止本CPU的中断

local_irq_save(flags) // 禁止本CPU的中断,并保存CPU中断位的信息

local_irq_restore(flags) // 使能本CPU的中断,并恢复CPU中断位的信息

local_bh_disable(void) // 禁止本CPU底半部中断

local_bh_enable(void) // 使能本CPU底半部中断

文件位置:kernel/include/linux/irqflags.h

local_irq_enable与local_irq_disable:直接打开/关闭本CPU内的中断,包括了顶半部和底半部中断的打开和关闭。local_irq_save与local_irq_restore:直接打开/关闭本CPU中断,并且保存中断屏蔽前的状态,便于后续恢复local_bh_enable与local_bh_disable:直接打开/关闭本CPU内的底半部中断

2.2 API接口实现分析

#因为中断屏蔽与底层芯片架构有关,不同架构处理方式不同,我们以ARM为例

2.2.1 local_irq_enable

##define local_irq_enable() do { raw_local_irq_enable(); } while (0)

#define raw_local_irq_enable() arch_local_irq_enable()

#define arch_local_irq_enable arch_local_irq_enable

static inline void arch_local_irq_enable(void)

{

asm volatile(

" cpsie i @ arch_local_irq_enable"

:

:

: "memory", "cc");

}

函数介绍:local_irq_enable函数用于将CPSR寄存器中的中断使能位设为1,从而使得CPU能够响应中断。

文件位置:kernel/arch/arm/include/asm/irqflags.h

相关实现:

asm:声明一个内联汇编表达式

cpsie i:全称Change Processor State, Interrupts Enabled,主要用来设置CPSR寄存器的I位,来允许本CPU响应中断。

memory:向汇编说明,此处内存发生了更改,类似于内存屏障的作用

cc:表示可能会修改条件码的标志

汇编语言的格式,大家可以自行简单了解

2.2.2 local_irq_disable

##define local_irq_disable() \

do { raw_local_irq_disable(); trace_hardirqs_off(); } while (0)

#define raw_local_irq_disable() arch_local_irq_disable()

#define arch_local_irq_disable arch_local_irq_disable

static inline void arch_local_irq_disable(void)

{

asm volatile(

" cpsid i @ arch_local_irq_disable"

:

:

: "memory", "cc");

}

函数介绍:local_irq_disable函数用于将CPSR寄存器中的中断使能位设为0,从而禁止CPU响应中断。

文件位置:kernel/arch/arm/include/asm/irqflags.h

相关实现:同上

cpsid:全称Change Processor State, Interrupts Disabled,用于清除CPSR寄存器的中断标志,以禁止中断!

这里顺便提及一下,CPSR寄存器为Current Program Status Register,用于存储当前程序的状态信息,包括中断使能状态、处理器模式、条件标志等。

大多数的ARM处理器,都采用CPSR寄存器来管理装填信息,所以ARM处理器可以直接进行操作。

2.2.3 local_irq_save

##define IRQMASK_REG_NAME_R "primask"

#define local_irq_save(flags) \

do { \

raw_local_irq_save(flags); \

trace_hardirqs_off(); \

} while (0)

#define raw_local_irq_save(flags) \

do { \

typecheck(unsigned long, flags); \

flags = arch_local_irq_save(); \

} while (0)

static inline unsigned long arch_local_irq_save(void)

{

unsigned long flags;

asm volatile(

" mrs %0, " IRQMASK_REG_NAME_R " @ arch_local_irq_save\n"

" cpsid i"

: "=r" (flags) : : "memory", "cc");

return flags;

}

函数介绍:arch_local_irq_save函数,用于保存当前中断状态并禁用中断。

文件位置:kernel/arch/arm/include/asm/irqflags.h

相关实现:

mrs %0 IRQMASK_REG_NAME_R:这行代码使用mrs指令将中断屏蔽寄存器的值读取到通用寄存器%0中。IRQMASK_REG_NAME_R是一个占位符,表示要读取的中断屏蔽寄存器的名称,实际的中断屏蔽寄存器为primask。通过这行代码,中断屏蔽寄存器的值被保存到了%0寄存器中。

: "=r" (flags) : : "memory", "cc": 这是一个约束部分,用于指定寄存器和内存的使用约束。"=r" (flags)表示将%0寄存器的值保存到flags变量中。"memory"和"cc"表示这段代码可能会修改内存和条件码寄存器。

总的来说,这段代码主要实现了:

保存中断屏蔽寄存器的值到flags变量中,并返回关闭本CPU的中断

2.2.4 local_irq_restore

##define IRQMASK_REG_NAME_W "primask"

#define local_irq_restore(flags) \

do { \

if (raw_irqs_disabled_flags(flags)) { \

raw_local_irq_restore(flags); \

trace_hardirqs_off(); \

} else { \

trace_hardirqs_on(); \

raw_local_irq_restore(flags); \

} \

} while (0)

#define raw_local_irq_restore(flags) \

do { \

typecheck(unsigned long, flags); \

arch_local_irq_restore(flags); \

} while (0)

/*

* restore saved IRQ & FIQ state

*/

static inline void arch_local_irq_restore(unsigned long flags)

{

asm volatile(

" msr " IRQMASK_REG_NAME_W ", %0 @ local_irq_restore"

:

: "r" (flags)

: "memory", "cc");

}

函数介绍:arch_local_irq_restore函数,用于恢复当前中断状态并打开中断。

相关实现:同上

2.2.5 local_bh_enable

#static inline void local_bh_enable(void)

{

__local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);

}

void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)

{

WARN_ON_ONCE(in_irq());

lockdep_assert_irqs_enabled();

#ifdef CONFIG_TRACE_IRQFLAGS

local_irq_disable();

#endif

/*

* Are softirqs going to be turned on now:

*/

if (softirq_count() == SOFTIRQ_DISABLE_OFFSET)

trace_softirqs_on(ip);

/*

* Keep preemption disabled until we are done with

* softirq processing:

*/

preempt_count_sub(cnt - 1);

if (unlikely(!in_interrupt() && local_softirq_pending())) {

/*

* Run softirq if any pending. And do it in its own stack

* as we may be calling this deep in a task call stack already.

*/

do_softirq();

}

preempt_count_dec();

#ifdef CONFIG_TRACE_IRQFLAGS

local_irq_enable();

#endif

preempt_check_resched();

}

EXPORT_SYMBOL(__local_bh_enable_ip);

asmlinkage __visible void do_softirq(void)

{

__u32 pending;

unsigned long flags;

if (in_interrupt())

return;

local_irq_save(flags);

pending = local_softirq_pending();

if (pending && !ksoftirqd_running(pending))

do_softirq_own_stack();

local_irq_restore(flags);

}

函数介绍:local_bh_enable函数,启用本地的底半部bottom half处理,当中断来临时,底半部处理可以被执行。

文件位置:kernel/include/linux/bottom_half.h

相关实现:

调用__local_bh_enable_ip传入两个参数,这两个参数的作用是:_THIS_IP_:是一个宏定义,用于获取当前的指令地址,也就是调用 local_bh_enable 函数的地方。SOFTIRQ_DISABLE_OFFSET:是一个常量,用于指定软中断禁用的偏移量。__local_bh_enable_ip其主要作用是:处理完软中断softirq后重新启用本地底半部bottom half处理,并检查是否需要进行进程调度。WARN_ON_ONCE(in_irq()):判断是否处于硬件中断上下文中,如果是,则打印警告信息lockdep_assert_irqs_enabled():这是一个锁依赖性检查宏,用于确保在调用此函数时中断是被启用的。if (softirq_count() == SOFTIRQ_DISABLE_OFFSET) lockdep_softirqs_on(ip):如果当前软中断计数等于SOFTIRQ_DISABLE_OFFSET,则启用软中断。__preempt_count_sub(cnt - 1):减少抢占计数,这是为了防止在处理软中断时发生抢占。do_softirq:这里表示如果有待处理的软中断,那么就调用do_softirq()函数来处理这些软中断。in_interrupt():判断是否处于硬中断中,如果是,则直接返回local_irq_save:它保存并关闭本地中断,以防止在处理软中断时被其他硬中断打断。local_softirq_pending:它获取当前待处理的软中断。如果存在待处理的软中断,并且软中断处理线程(ksoftirqd)没有在运行,那么就在当前的栈上处理软中断。local_irq_restore:恢复本地中断。

这里有一个疑问,大家不妨思考一下:

中断上半部和下半部的机制,就是为了让那些紧急处理的事情放在下半部,不那么紧急或者时间较长的任务放到下半部处理,来保证系统的实时性。

那么在这里,使能中断底半部之后,仍然执行了local_irq_save和local_irq_restore,来关闭本地硬中断,这么做是为了什么?

我的猜想:local_bh_disable和local_bh_enable是成对出现的,当我们关闭掉了底半部中断时,也有可能硬中断引发了多个软中断触发,在此打开的时候,此时已经就已经挂起了其他的软中断处理程序;

如果不关闭硬中断,这时候就有可能发生嵌套,导致堆栈溢出。

大家不妨可以一起讨论下。

2.2.6 local_bh_disable

#static inline void local_bh_disable(void)

{

__local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);

}

static __always_inline void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)

{

preempt_count_add(cnt);

barrier();

}

函数介绍:local_bh_disable函数,增加当前进程的抢占计数,从而阻止内核抢占当前进程。

文件位置:kernel/include/linux/bottom_half.h

相关实现:

preempt_count_add:增加当前进程的抢占计数barrier:执行内存屏障,以确保抢占计数的增加在所有其他内存操作之前完成。

2.2.7 抢占计数机制

#在local_bh_eable和local_bh_disable中,有一些抢占计数的操作,如:preempt_count_add、preempt_count_dec、preempt_count_dec等,这些作用是什么呢?

抢占计数(preempt_count)在Linux内核中起着非常重要的作用。它主要用于防止内核抢占。

在Linux内核中,当一个进程正在执行内核代码时,如果发生了中断或者有更高优先级的进程需要运行,那么当前进程可能会被抢占,即暂停当前进程,转而去执行中断处理程序或者更高优先级的进程。这种机制被称为内核抢占。

然而,有些情况下,我们不希望当前进程被抢占。例如,当一个进程正在修改一些全局数据结构时,如果这个进程被抢占,那么其他进程可能会看到这些数据结构处于不一致的状态。为了防止这种情况发生,我们可以通过增加抢占计数来禁止内核抢占。

当抢占计数大于0时,内核抢占被禁止。

当抢占计数等于0时,内核抢占被允许。

因此,我们可以通过调用preempt_count_add函数来增加抢占计数,从而禁止内核抢占,通过调用preempt_count_dec和preempt_count_dec函数来减少抢占计数,从而允许内核抢占。

总的来说,抢占计数的作用就是用于控制内核抢占的开启和关闭,以保证内核代码的正确执行。

关于中断底半部机制,内容较为复杂,放在后面单独拆解!

3、总结

#该篇文章,主要了解以下几点:

中断屏蔽的思想中断屏蔽的好处与不足Linux内核提供的中断屏蔽接口中断屏蔽的底层操作的实现方式

欢迎关注【嵌入式艺术】,董哥原创!

相关推荐

13部先婚后爱的甜宠古装剧,你看过多少部?
365bet365官网

13部先婚后爱的甜宠古装剧,你看过多少部?

📅 07-26 👁️ 9647
成功励志微信公众号 有哪些励志的微信公众号?
第365用英语怎么说

成功励志微信公众号 有哪些励志的微信公众号?

📅 07-17 👁️ 3048
世界杯10大难忘发型:托尼、威廉、凯文各老师含泪转发