中断上下文及抢占标志位的检查——基于调度及锁举例
一、背景
这篇文章作为“内核中断相关”专栏的第一篇是因为中断上下文及抢占标志位的检查贯穿着整个内核硬中断软中断处理流程,标志位的开启与关闭及如何检查标志位如能大致摸清,就能帮助我们对中断处理流程有一定的印象,这对我们理解linux整体底层机制,以及各个子模块如调度子模块等都是大有帮助的,同时也对我们驱动或内核开发时,对系统状态的监测及关键逻辑时的状态判断及逻辑是否会重入等事情来说算是打了一个基础。
之所以以调度相关场景来举例,是因为调度模块的时钟中断是一个系统里必不可少的一个关键硬中断,它是一定存在的,另外,该时钟中断的处理及相关的前后的逻辑在中断上下文的标志位的检查上也有不少细节。梳理完这些不仅对中断上下文有一个清晰的概念,对调度子系统的理解也更加深刻,对调度子系统的相关主要函数逻辑,如sched_switch/sched_stat_runtime等的调度链也能有一定的印象。
本文先在第二章里介绍中断上下文的概念,比如有哪些中断上下文,各个中断上下文的状态是如何标志和检查的,这里面还需要强调的一点是,是否在处理硬中断或者软中断,及硬中断或者软中断是否被禁用是两个概念也是分开的两个标志位来判断和检查的,另外,硬中断是否被禁用的检查与体系结构相关,其标志位的检查并不像软中断禁用的检查那么方便,这些都会在这一章里讲到。
有了第二章的基础以后,我们在第三章里,分析几个调度相关的场景,通过ftrace抓到的trace来看标志位状态和调用链并找出是哪里置上的,分析的这几个调度相关的场景其实在 内核调度抢占模式——voluntary和full对比-CSDN博客 里也有提及和做过堆栈展示,这篇博客里会进一步对这几个调度相关的场景的中断相关的细节进一步展开。另外,在第三章里,我们还会编写一个测试用的内核模块,做各种常用锁的使用,看中断上下文标志位的变化并结合ftrace一起来观察。
二、中断上下文及其标志位
中断上下文做底层开发的同学都应该比较清楚,通俗意义上的理解,是cpu在执行硬中断或者软中断或者nmi中断的过程中,狭义上来说,这确实是对的;但是,从广义上来说,应该还要包括禁用硬中断或者禁用软中断期间。为什么这么说,因为从下面的include/linux/preempt.h里的in_interrupt的定义就可以看到,它包含了softirq_count,而softirq_count在后面会讲到,它是表示local_bh_disable的次数,也就是禁用软中断的次数,所以,从源码上来阐述这个“中断上下文”的含义,应该要从广义上至少要包含禁用软中断次数,对于硬中断而言,相对比较特殊,在 2.2.2 一节里会展开:
内核里in_interrupt的使用非常广泛,相比in_irq和in_softirq而言使用更加广泛:
2.1 关于中断上下文有关的标志位的判断,preempt_count变量
在上面我们讲到了in_interrupt,从下图的实现可以看到in_interrupt判断的内容包含硬中断、软中断、NMI中断,那么具体是怎么判断的呢?
2.1.1 preempt_count变量
上面讲到的include/linux/preempt.h里的这些宏的函数最终都是用的preempt_count变量来计算和判断的。
preempt_count是一个体系结构相关的变量,x86架构下是per_cpu变量:
而arm64下是与线程相关的结构体struct thread_info里:
preempt_count用到的bit位,见下面这张图
从上图中可以看到:
preempt_count是一个cpu的标志位统计,可以覆盖的标记的事项有:
1)当前cpu是否正在处理硬中断
2)当前cpu是否正在处理软中断
3)当前cpu禁用软中断的次数
4)当前cpu禁用抢占的次数
细心的同学可能已经发现,为什么有当前cpu禁用软中断的次数,但是没有当前cpu禁用硬中断的次数呢?另外,当前cpu禁用硬中断的次数如何获取呢?这点会在下面 2.2.2 里介绍。
2.1.2 x86里的__preempt_count的need_resched标志位
这一节的标题虽然限制在x86,但是下面的大部分逻辑还是能适用于arm64等平台的。
上面的preempt_count的介绍,其实都是读的per_cpu变量__preempt_count的去掉PREEMPT_NEED_RESCHED的bits位,如下逻辑:
而__preempt_count这个per_cpu变量还有一个对调度系统非常关键的标志位,也就是need_resched标志位,这个标志位如果是1表示之前的时钟中断进来后的调度算法的逻辑监测到当前系统需要进行上下文切换,但是这时候可能受制于一些其他原因不行能进行上下文切换,这里说的其他原因也就是上面提到的preempt_count,如果preempt_count大于0,就有导致不能上下文切换的原因,比如正在softirqoff,preempt被关了等等。
事实上,should_resched函数就是调度系统用于判断当前是否需要进行上下文切换的核心函数:
而在should_resched的使用上,比如在启用CONFIG_PREEMPT_DYNAMIC模式(关于CONFIG_PREEMPT_DYNAMIC的细节见 内核调度抢占模式——voluntary和full对比-CSDN博客 博文)时,有下面直接传入0作为should_resched的参数
其实,__preempt_count里的PREEMPT_NEED_RESCHED标志位是取反保存和检查的:
这样,__preempt_count是0就表示系统需要进行上下文切换调度。
2.2 重点强调一下在执行中断期间和禁用中断的区别,及在标志位上的体现
preempt_count是一个内核里维护的一个软件的变量,所以,并没有那个芯片上的寄存器有记录这些信息,不像硬中断相关的flags标志位是一个硬件相关或者架构相关的变量(会被记录到芯片的特殊寄存器里或者通过芯片特殊寄存器维护)。执行中断期间就是指当前cpu正在响应执行某个中断;禁用中断,则是指接下来的当前cpu上要执行的行为不期望被中断打断。详细的细节还得区分是硬中断还是软中断。
2.2.1 软中断执行期间和禁用软中断计数
对于是否在执行软中断的判断,内核里在inclue/linux/preempt.h里有如下定义:
in_serving_softirq函数在内核里也有不少使用,比如在统计cpu的top统计相关的usr/sys/hardirq/softirq运行时间的逻辑 account_system_time 里,就有下面的逻辑
就是在统计的这一刻如果在运行软中断,就会把这次采样的周期时间算做软中断执行时间
in_serving_softirq的判断是用preempt_count的bit8,in_serving_softirq表示正在处理软中断,比如正在处理net的软中断等
对于禁用软中断的计数,用的是preempt_count的bit9~15,如果调用了local_bh_disable,所在核的preempt_count的bit9~15就会被+1。软中断计数有两个用途,一是为了保护在软中断执行期间,再被硬中断打断而导致硬中断退出后再执行软中断而造成的软中断嵌套,有这个计数,在硬中断退出后检查当前这个禁用软中断计数是否大于0,如果大于0,则不执行软中断;二是为了保护一些驱动或者内核逻辑不期望被刚才说的硬中断进来后退出后执行的软中断的逻辑干扰。
另外有个关软中断的函数__local_bh_enable_ip在实时内核里有自己的实现,在普通内核里,在开启CONFIG_TRACE_IRQFLAGS功能也就是监测软硬中断开关行为的功能后,有如下实现:
在lockdep_softirqs_off里会把当前的ip也就是返回地址记下来
另外,调用local_irq_disable后,preempt_count的低0~7bits并不会+1,这样做也挺好,因为已经有irq单独的off的count统计在preempt_count里了,再增加低0~7bits会导致细节上不够明确。
这一点会在 3.2.1 里做实验
顺便提及一下,spin_lock_irqsave则会在preempt_count的低0~7bits里+1,因为spin_lock_irqsave属于spin_lock系列,spin_lock系列的接口都会在preempt_count里+1的,另外,spin_lock_irqsave虽然有关硬中断的功能,但是关硬中断的状态记录属于arch层面的东西,不在内核的软件变量维护里,这一点也会在 2.2.2 里介绍,所以,如果没有这个preempt_count的低0~7bits做记录,preempt_count里就没有体现这个关中断行为的任何标记了,这是不行的。
2.2.2 硬中断执行期间和禁用硬中断
硬中断关闭的状态是一个非常危险的状态,或者说是一个“瘫痪”状态,首先,关闭硬中断的这个核肯定不能响应时钟中断等其他中断了,由于没有tick时钟中断,调度系统肯定也就不工作了,由于没有机会再执行硬中断,也自然没有机会在硬中断执行之后去执行软中断,所以软中断这时候也没法再执行了。除此以外,printk会被block不再输出,ipi(核间通讯中断)任何向关闭硬中断的核发送ipi的上下文会陷入死循环。
所以,我们需要尽可能地降低关闭硬中断的场景,以及要减少关闭硬中断时执行的操作。
何为关闭硬中断,其实有两种场景:
1)正在执行硬中断处理函数
2)使用spin_lock_irqsave或者local_irq_disable关闭硬中断
这两种场景对应于我们标志位来说也是不一样的,场景1)我们可以通过上面讲到的preempt_count里的HARDIRQ_MASK相关的bit位来拿到,但是对于场景2)我们甚至都无法通过preempt_count来拿到这个状态,而是通过arch相关的函数通过特殊寄存器来拿到。
事实上,其实如果是硬中断关闭的状态时,就算能通过preempt_count这种软件的数值能拿到这个关硬中断状态又如何呢?这时候刚才也说了其实系统处于“瘫痪”状态,就算拿到这个状态,其实你也啥也干不了。当然有时候我们如果及时知道存在这样的状态,或者能监测到出现这样的状态对我们整体系统的稳定性也有非常大的作用的。在后面的内核中断子系统栏目里的博文里会讲到这样的一些手段。
关于上面说的这两种场景的细节,我们分两节来介绍。
2.2.2.1 执行硬中断的处理函数
与硬中断处理相关的有两个宏非常重要,irq_enter和irq_exit
irq_enter如下实现
调用irq_enter_rcu再调用了__irq_enter_raw
下图中的0x10000就是bit16,preempt_count里的硬中断mask位置
irq_exit如下实现
调用__irq_exit_rcu继而调用preempt_count_sub(HARDIRQ_OFFSET);
对于调度子系统的tracepoint点,sched_stat_runtime而言,时钟中断进来出发的sched_stat_runtime就是在硬中断处理期间的,也就是上面说的irq_enter和irq_exit期间,这时候判断in_interrupt是true的,这个会在3.1.1里做实验
2.2.2.2 spin_lock_irqsave或者local_irq_disable关闭硬中断
spin_lock_irqsave和local_irq_disable的共同点是都会关闭硬中断,spin_lock_irqsave会另外关闭抢占,也就是preempt_count的低0~7bits会加1。
再次强调,关闭硬中断的行为并不会改变preempt_count的值,我们会在 3.2.2 里做实验
2.3 编写一个内核函数来获取到中断上下文各个标志位及是否需要调度及irq的flags
在做第三章的实验之前,我们先得准备一个函数,用来获取并打印当前cpu上的preempt_count及需要调度状态及硬中断关闭状态这些与这一篇博客相关的一些状态值。
其中,preempt_count里的各种标志位我们比较清楚了,但是对于硬中断关闭状态如何获取呢?
2.3.1 参考ftrace里的标志位的实现,反推如何获取硬中断关闭状态
我们看到下面的截图,下图是用ftrace打开sched抓到的trace的一小部分(关于ftrace的抓取,在 内核tracepoint的注册回调及添加的方法-CSDN博客 里的 2.1.1.1 一节里有个例子脚本):
可以从上图的左边的红色小框看到d,d表示的是硬中断关闭状态
在kernel/trace/trace.c里获取上图中的中间那些状态值:
在include/linux/trace_events.h里
在tracing_gen_ctx_flags函数里有irqs_disabled_flags函数通过传入local_save_flags得到的irqflags来获取是否当前是否处于硬中断关闭状态
要注意的是,它这里用的local_save_flags,请不要用local_irq_save函数,local_irq_save函数会禁用中断并保存当前中断状态,而我们只想获取当前中断状态,而不需要禁用中断
2.3.2 函数实现
获取这一篇博客所涉及的所有的标志位的状态的函数和打印函数:
struct osmon_irq_preempt_items {
u32 cpu;
u32 bneed_resched; // 0 or 1
u32 preempt_count;
u32 preempt_count_lowbits;
u32 hardirq_count;
u32 hardirq_off; // 0 or 1
u32 softirq_serving; // 0 or 1
u32 softirq_offcount;
u32 flags;
};
void get_preempt_count_items_and_hardirqoff(struct osmon_irq_preempt_items* o_pitems)
{
u64 flags;
local_save_flags(flags);
o_pitems->cpu = smp_processor_id();
o_pitems->bneed_resched = (test_preempt_need_resched() ? 1 : 0);
o_pitems->preempt_count = preempt_count();
o_pitems->flags = (u32)flags;
o_pitems->preempt_count_lowbits = (o_pitems->preempt_count & 0xff);
o_pitems->hardirq_count = ((o_pitems->preempt_count & 0xF0000) >> 16);
o_pitems->hardirq_off = (irqs_disabled_flags(flags)?1:0);
o_pitems->softirq_offcount = ((o_pitems->preempt_count & 0xFE00));
o_pitems->softirq_serving = (o_pitems->preempt_count & 0x100);
}
void printk_osmon_irq_preempt_items(struct osmon_irq_preempt_items* o_pitems)
{
printk("cpu[%d] need_resched[%d] preempt_count[0x%lx] flags[0x%lx] preempt_count_lowbits[%d] "
"hardirq_count[%d] hardirq_off[%d] softirq_serving[%d] softirq_offcount[%d]\n",
o_pitems->cpu, o_pitems->bneed_resched, o_pitems->preempt_count, o_pitems->flags, o_pitems->preempt_count_lowbits,
o_pitems->hardirq_count, o_pitems->hardirq_off, o_pitems->softirq_serving, o_pitems->softirq_offcount);
}
void printk_curr_osmon_irq_preempt(void)
{
struct osmon_irq_preempt_items items;
get_preempt_count_items_and_hardirqoff(&items);
printk_osmon_irq_preempt_items(&items);
}
其实我们还可以获取别的cpu上的除了irq的flags以外的其他状态值:
void get_preempt_count_items(int i_cpu, struct osmon_irq_preempt_items* o_pitems)
{
//u32 pc = per_cpu(__preempt_count, i_cpu);
u32 pc = (u32)(*per_cpu_ptr(&__preempt_count, i_cpu));
o_pitems->cpu = i_cpu;
o_pitems->bneed_resched = ((pc & PREEMPT_NEED_RESCHED) ? 0 : 1);
o_pitems->flags = 0;
o_pitems->hardirq_off = 0;
o_pitems->preempt_count = (pc & ~PREEMPT_NEED_RESCHED);
o_pitems->preempt_count_lowbits = (o_pitems->preempt_count & 0xff);
o_pitems->hardirq_count = ((o_pitems->preempt_count & 0xF0000) >> 16);
o_pitems->softirq_offcount = ((o_pitems->preempt_count & 0xFE00));
o_pitems->softirq_serving = (o_pitems->preempt_count & 0x100);
}
三、基于调度场景及锁的使用场景,分析中断上下文标志位
选择调度相关的场景一方面在之前的博客 内核调度抢占模式——voluntary和full对比-CSDN博客 中有过相关例子,可以形成知识点上的连接,关联后能强化记忆。另外一方面,时钟中断相对比较好抓,周期固定,方便分析。
3.1 基于调度场景来分析
这一节里,我们并不改动调度相关的内核代码,而是借助tracepoint抓取几个调度的tracepoint点的中断上下文信息,并配合perf来看调用堆栈,分析标志位相关的行为。
关于tracepoint的注册回调及添加方法,参考之前的博客 内核tracepoint的注册回调及添加的方法-CSDN博客 。
关于perf的原理,参考之前的博客 perf原理介绍-CSDN博客 。
关于perf的调度相关的抓取方法,在 内核调度抢占模式——voluntary和full对比-CSDN博客 里的第三章也有例子。
3.1.1 sched_stat_runtime的tracepoint点的中断上下文标志位
这里,我们通过注册tracepoint的回调借助dump_stack函数来看调用堆栈,并通过 2.3.2 里说的获取方式并打印到dmesg里
注册自定义的sched_stat_runtime的tracepoint回调函数如下:
static void cb_sched_stat_runtime(void *i_data, struct task_struct *i_curr,
u64 i_runtime, u64 i_vruntime)
{
if (in_interrupt() && !blog) {
blog = true;
dump_stack();
printk("in_interrupt=%d\n", in_interrupt());
printk_curr_osmon_irq_preempt();
}
if (!in_interrupt() && !blog1) {
blog1 = true;
dump_stack();
printk("not_in_interrupt=%d\n", in_interrupt());
printk_curr_osmon_irq_preempt();
}
}
sched_stat_runtime有在中断处理过程中和不在中断处理过程中两种状态,前者就是时钟中断进来以后的线程运行信息状态更新时调用到的逻辑,后者是线程切换或唤醒等触发的场景,是不在时钟中断处理过程中的。再次强调,这里说的不在时钟中断处理过程中是指不在irq_enter和irq_exit期间的意思。
在硬中断处理期间的情况:
可以看到hardirq_count是1,表示在硬中断处理期间,这时候in_interrupt是65536
下面是sched_stat_runtime在非硬中断处理期间:
我们发现,无论是哪种情况,hardirq_off都是1,对应于ftrace里的d,对于tick里的时候,可以看到:
__hrtimer_run_queues的行为是被raw_spin_lock_irqsave保护起来的。
对于刚才实验的不在硬中断处理里的case,它是在__schedule里执行pick_next_task_fair执行线程切换逻辑时触发的,如下是调用local_irq_disable关了硬中断的
从__schedule的退出前的实现部分就可以得知,上图中的irq关闭状态维持到最后:
context_switch最后调用了finish_task_switch
而finish_task_switch里的注释也表示这是在irq关闭期间的
3.1.2 sched_switch的tracepoint点的中断上下文标志位
与3.1.1一样的方法,抓到的打印:
与sched_stat_runtime里的非硬中断处理流程里的例子基本一致。
3.2 基于锁的使用场景来分析
3.2.1 local_bh_disable的实验
下面的程序是编写了一个内核模块,在初始化时调用了两次local_bh_disable,并在local_bh_enable前死循环持续了10秒,每一秒打印一次 2.3.2 里的函数
抓到的打印如下(local_bh_enable并不会增加preempt_count的0~7bits的count):
softirq_offcount是1024,也就是bit10,bit8是softirq_serving,bit9开始是local_bh_disable计数,1024也就是计数了2次,与预期一致。
另外,可以看到need_resched也与预期一致的,在经过了1秒以后,就变成了need_resched状态了。
完成的程序代码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/spinlock.h>
#include <linux/delay.h>
#include <linux/jiffies.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhaoxin");
MODULE_DESCRIPTION("A simple Linux kernel module using spinlock running 10 second.");
MODULE_VERSION("1.0");
static DEFINE_SPINLOCK(my_spinlock);
static DEFINE_SPINLOCK(my_spinlock1);
int my_cb(void) {
dump_stack();
return 1;
}
struct osmon_irq_preempt_items {
u32 cpu;
u32 bneed_resched; // 0 or 1
u32 preempt_count;
u32 preempt_count_lowbits;
u32 hardirq_count;
u32 hardirq_off; // 0 or 1
u32 softirq_serving; // 0 or 1
u32 softirq_offcount;
u32 flags;
};
void get_preempt_count_items_and_hardirqoff(struct osmon_irq_preempt_items* o_pitems)
{
u64 flags;
local_save_flags(flags);
o_pitems->cpu = smp_processor_id();
o_pitems->bneed_resched = (test_preempt_need_resched() ? 1 : 0);
o_pitems->preempt_count = preempt_count();
o_pitems->flags = (u32)flags;
o_pitems->preempt_count_lowbits = (o_pitems->preempt_count & 0xff);
o_pitems->hardirq_count = ((o_pitems->preempt_count & 0xF0000) >> 16);
o_pitems->hardirq_off = (irqs_disabled_flags(flags)?1:0);
o_pitems->softirq_offcount = ((o_pitems->preempt_count & 0xFE00));
o_pitems->softirq_serving = (o_pitems->preempt_count & 0x100);
}
void printk_osmon_irq_preempt_items(struct osmon_irq_preempt_items* o_pitems)
{
printk("cpu[%d] need_resched[%d] preempt_count[0x%x] flags[0x%x] preempt_count_lowbits[%d] "
"hardirq_count[%d] hardirq_off[%d] softirq_serving[%d] softirq_offcount[%d]\n",
o_pitems->cpu, o_pitems->bneed_resched, o_pitems->preempt_count, o_pitems->flags, o_pitems->preempt_count_lowbits,
o_pitems->hardirq_count, o_pitems->hardirq_off, o_pitems->softirq_serving, o_pitems->softirq_offcount);
}
void get_preempt_count_items(int i_cpu, struct osmon_irq_preempt_items* o_pitems)
{
u32 pc = per_cpu(__preempt_count, i_cpu);
o_pitems->cpu = i_cpu;
o_pitems->bneed_resched = ((pc & PREEMPT_NEED_RESCHED) ? 0 : 1);
o_pitems->flags = 0;
o_pitems->hardirq_off = 0;
o_pitems->preempt_count = (pc & ~PREEMPT_NEED_RESCHED);
o_pitems->preempt_count_lowbits = (o_pitems->preempt_count & 0xff);
o_pitems->hardirq_count = ((o_pitems->preempt_count & 0xF0000) >> 16);
o_pitems->softirq_offcount = ((o_pitems->preempt_count & 0xFE00));
o_pitems->softirq_serving = (o_pitems->preempt_count & 0x100);
}
static void printk_preempt_count_by_cpu(int i_cpu)
{
struct osmon_irq_preempt_items items;
get_preempt_count_items(i_cpu, &items);
printk_osmon_irq_preempt_items(&items);
}
static void printk_preempt_count(void *data) {
struct osmon_irq_preempt_items items;
get_preempt_count_items_and_hardirqoff(&items);
printk_osmon_irq_preempt_items(&items);
}
static int __init testspinlock_init(void) {
unsigned long flags, oldflags;
unsigned long start_time, end_time;
printk(KERN_INFO "testspinlock: Module loaded.\n");
//trace_printk("before spin_lock preempt_count=%d, dump_stack_cb=%d\n", (int)preempt_count(), my_cb());
trace_printk("beofore spin_lock in_interrupt()=%d\n", in_interrupt() ? 1 : 0);
// 加锁
local_bh_disable();
local_bh_disable();
//spin_lock_irqsave(&my_spinlock, oldflags);
printk(KERN_INFO "testspinlock: Spinlock acquired, waiting for 10 second...\n");
local_save_flags(flags);
printk("preempt_count=%d, flags=%llx, irqoff=%d\n", preempt_count(), flags, irqs_disabled_flags(flags)?1:0);
// 获取当前时间
start_time = jiffies;
{
u64 flags;
local_save_flags(flags);
printk("cpu[%d]preempt_count=%d, flags=%llx, irqoff=%d\n", smp_processor_id(), preempt_count(), flags, irqs_disabled_flags(flags)?1:0);
}
// 死循环,直到时间超过 10 秒
#if 1
while (time_before(jiffies, start_time + HZ * 10)) {
u64 start_time1 = jiffies;
{
printk_preempt_count(NULL);
}
while (time_before(jiffies, start_time1 + HZ * 1)) {
}
}
#endif
// 解锁
//spin_unlock_irqrestore(&my_spinlock, oldflags);
local_bh_enable();
local_bh_enable();
printk(KERN_INFO "testspinlock: Spinlock released.\n");
return 0; // 0 indicates successful loading
}
static void __exit testspinlock_exit(void) {
printk(KERN_INFO "testspinlock: Module unloaded.\n");
}
module_init(testspinlock_init);
module_exit(testspinlock_exit);
3.2.2 spin_lock_irqsave的实验
将 3.2.1 里稍微改一下,增加了spin_unlock_irqrestore:
抓到了打印如下(如果用dmesg -w观察的话会发现这种硬中断关闭的情况,dmesg输出要等到insmod结束也就是10秒以后才会吐出打印):
上图中可以看到:
spin_lock_irqsave会增加preempt_count的0~7bits的count,并不会增加preempt_count里的hardirq部分,会改变irq的flags,另外,关了硬中断,时钟中断也进不来,自然无法更新need_resched标志位