中断上下文中的preempt count

参考博客:

 Linux上下文切换[转]_thonmin的博客-CSDN博客_linux 上下文切换

中断子系统

1. 背景

中讲到:

 softirq在同一个CPU上是串行的,这点体现在代码的哪里呢?

./kernel/softirq.c

中断上下文中的preempt count_第1张图片

wowo科技的文章中讲了两种场景,其中中断嵌套由于Linux不支持,所以暂时不讨论,只讨论下面的一个场景: softirq中的中断

假设一个中断下半部softirq(随便什么类型的softirq)正在执行,在调用softirq的hander之前,也就是__do_softirq()中一定会调用__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET)然后打开CPU中断local_irq_enable(),最后执行softirq handler

就在执行softirq handler的时候在同一个CPU上发生了外部中断,此时CPU能够响应这个外部中断,并同样走到了irq_exit()的Line408处,由于之前调用了 __local_bh_disable_ip,所以此时if条件为假,invoke_softirq()得不到调用导致新的softirq无法执行

第二个中断结束,退出到被中断的softirq处继续执行,依靠__do_softirq()中的restart标签机制,将没有处理的softirq处理完毕

似乎一切很合理,但是这里有一个隐藏很深的问题: in_interrupt()的实现与preempt count有关系,但是程序在我上述描述的场景中用到的所有preempt count都是在中断上下文,而我们又知道Linux的中断上下文不存在struct task_struct结构,那么这里的preempt count是哪里的?

2. 中断栈

相关资料请见以下博客:

linux kernel中的栈的介绍_代码改变世界ctw的博客-CSDN博客

中断子系统

  • ARM32的IRQ STACK

./arch/arm/kernel/setup.c

中断上下文中的preempt count_第2张图片

 arm32的中断栈很小,只有12byte,在系统初始化的时候,cpu_init()函数会进行中断模式stack的设定,在此不详细介绍

为什么arm32的中断栈如此小呢?这与arm32架构下中断处理的模式有关系,参考如下描述:

中断上下文中的preempt count_第3张图片

简单说来就是在arm32架构下,真正的中断处理都在svc mode,发生中断时,先进入irq mode,再进入svc mode完成大部分工作,而进入svc mode,也伴随着栈切换,也就是说,arm32的irq handler是运行在被中断进程的内核栈上面的

  • ARM64的IRQ STACK

./arch/arm64/kernel/irq.c

中断上下文中的preempt count_第4张图片

arm64架构专门设计了中断栈用于irq handler,可以看到代码中中断栈是基于per-cpu的,中断栈可能是静态分配的,也可能是vmalloc动态分配的,大小为THREAD_SIZE

小总结:

linux kernel arm32中定义的irq栈,其实就在一个static struct stack结构体变量中,大小为12bytes. irq_hander使用svc栈
linux kernel arm64中定义的irq栈,在内存"首地址"处,大小16k. irq_hander使用irq栈

为什么arm64要引入特别的IRQ STACK?

中断上下文中的preempt count_第5张图片

具体可以看看下面这个patch的描述:arm64: Introduce IRQ stack [LWN.net] 

中断上下文中的preempt count_第6张图片 

3. 中断栈的切换

根据上一小节描述,arm32架构下,中断最开始会进入IRQ mode,但是它稍纵即逝,最终进入SVC mode处理具体的中断,所以irq handler是运行在被中断的进程的内核栈上面的。具体栈切换请参考中断子系统的四小节

在arm64架构下,irq handler运行在cpu独立的IRQ stack中,可以参考中断向量表arch/arm64/kernel/entry.S,精简后的大概流程如下,可以看到实际上中断上半部和中断底半步都是在IRQ STACK上完成的

中断上下文中的preempt count_第7张图片

4. 猜想-中断上下文中的preempt count究竟是什么

参考:Linux内核进程栈的两种架构_Andy Pines的博客-CSDN博客

在中断上下文中一定会有这样的代码片段:

中断上下文中的preempt count_第8张图片

最终都是去操作preempt_count这个成员来通过软件的方式去标识运行上下文

  • ARM32架构,thread_info在内核栈中,获取preempt_count的途径如下:

通过sp找到栈底,自然也就找到了thread_info,进而找到preempt_count

在了解了arm32中断栈切换的原理后,由于irq handler是运行在SVC模式的内核栈上的,所以sp就是被中断的内核栈顶指针,preempt_count属于被中断进程

中断上下文中的preempt count_第9张图片

  •  ARM64架构,thread_info不在内核栈中,thread_info在struct task里面,获取preempt_count的途径如下:

arm64架构下,进程描述符是用寄存器来承载的,在了解了arm64中断栈切换原理后,我们并没有发现sp_el0有什么变化,所以sp_el0仍然是被中断进程的进程描述符,所以preempt_count属于被中断进程

中断上下文中的preempt count_第10张图片 5. 思考

综上所述,在中断上下文中操作到的preempt_count实际上是属于被中断进程的(仍然是推测+猜想,无实证),这也就能解释本文最开始提到的问题:如何保证softirq在同一个CPU上的串行执行?这下就很简单了,不管是因为中断嵌套也好,还是softirq被中断也好,前者用于标识运行上下文操作的preempt_count与后者用于检测的preempt_count是同一个变量,也都是被中断进程的那个preempt_count

同样,这里又会引申出另一个深层次的问题: 中断上下文为什么不能睡眠?

通过查找各种资料,大概归于两类原因:

a. Linux中断的设计要求

从中断设计目标看,它应该尽快完成处理返回现场,甚至为此还特意分出了下半部,调用阻塞函数,不可避免得增加了时延,显然违背初衷

b.与进程调度相关

从Linux的设计规则上看,中断上下文不是调度实体,调用阻塞函数使中断上下文参与调度,打破了设计规则,而且有很大程度上可能无法返回原来的现场,如果可以靠的也是运气

关于第二点,<深入Linux设备驱动程序内核机制>有如下描述:

中断上下文中的preempt count_第11张图片

我们再看看引起睡眠的schedule函数,先取得当前cpu上的进程运行队列,再从该队列中取出当前运行的进程,而在中断上下文中的rq->curr就是被中断的进程,进而让被中断的进程参与调度,在arm32架构下,被中断的进程可能在以后的调度中又被中断,导致内核栈被改写,被调度出去的中断下下文就再也回不去了;在arm64架构下,调度器根本就看不到中断栈,所以一旦中断中出现了调度,就有可能再也回不去了。

 中断上下文中的preempt count_第12张图片

你可能感兴趣的:(Linux基础,驱动学习,linux,debian,运维)