1 中断向量表

Cpu通过查询PIC可以获取上报中断对应的中断号irq,这部分是体系结构相关的汇编代码实现。
中断向量表:cpu通过中断号查询中断向量表获取中断处理函数,处理中断异常。

硬件设计中断电路的时候,选择中断触发方式,比如电平触发—》cpu的中断pin上默认低电平—》外部器件触发中断,将cpu-pin拉成高电平—》PIC根据软件注册中断,可以知道这个外部设备对应的中断号—》然后上报到cpu—》通过中断向量表查找对应的中断处理函数;

2 中断处理机制

分为HARDIRQ和SOFTIRQ,hardirq是在关中断的情况下执行,softirq执行期间可以响应硬中断,屏蔽SOFTIRQ;

3 中断描述符Struct irq_desc

3.1 Struct irq_desc

结构体有几个重要的成员,如下
Struct irq_data irq_data
保存中断号irq和chip信息等;

struct irq_chip chip是对PIC的一个软件抽象,对硬件PIC的封装,屏蔽掉不同体系结构,PIC不同的问题;chip内部包含了对当前中断的操作函数,屏蔽、打开中断,设定中断触发信号等操作函数。

unsigned int __percpu
kstat_irqs
Per-cpu变量,用来统计系统的中断计数;

irq_flow_handler_t handle_irq
指向一个跟当前中断触发信号相关的操作函数,针对中断线的操作。与irq一一对应,这条中断线上可能会挂接多个设备(共享中断);

struct irqaction action
针对具体设备的中断处理抽象,具体设备的中断行为描述符

const char
name
handle_irq对应的名称,在/proc/interrupts文件中显示。

3.2 每个设备的中断行为描述符 struct irqaction *action

irq_handler_t handler
针对设备的中断处理函数,也称中断服务例程ISR;

void dev_id
共享中断时,通过dev_id来查询在这个共享中断线上具体哪个设备;在共享中断注册时,通常把设备信息结构体赋值给dev_id;

irq_handler_t thread_fn
struct task_struct
thread
当通过request_threaded_irq函数注册中断时才会用到,这种方式中断处理函数是在线程上下文。不常用。

3.3 handle_irq与action 关系

irq_flow_handler_t handle_irq与struct irqaction *action 的关系如图:
image.png
handle_irq针对 irq line 的整体操作,action是挂接在这条中断线的一个设备的中断行为描述符,action->handler是通过request_irq注册中断是挂接的处理函数。也成为中断服务例程ISR;一条中断线上可能会挂接多个设备,这些设备共享一个中断号irq,每个设备的中断描述符以链表形式链接在一起。

4 do_IRQ

分析do_IRQ之前,先简单说下系统初始化时,通过irq_set_handler初始化 irq_desc->handle_irq挂接处理函数。
在do_IRQ (arm体系中是asm_do_IRQ)函数中,调用irq_enter认为是hardirq处理部分,irq_exit是softirq的处理部分;
do_IRQ
|—- 保存被中断打断的执行现场
|—- irq_enter
HARDIRQ处理开始,Preempt_count变量加HARDIRQ_OFFSET,表示处于hardirq上下文,在hardirq处理完成后,irq_exit会将此变量减HARDIRQ_OFFSET;
|—- check_stack_overflow()
检查栈是否会存在溢出;栈溢出会打印栈的相关信息。
如果中断处理过程中打开对外部中断的响应,外部中断就会打断正在执行的中断,形成中断嵌套;中断嵌套越深,栈就可能会溢出。
|—- generic_handle_irq
HARDIRQ的执行函数,通过irq获取对应的中断描述符struct irq_desc,此中断描述符包含了中断处理过程中所需要的信息。调用中断处理函数desc->handle_irq()处理分为两部分:
第一部分在系统初始化时挂接的,操作中断线相关;
第二部分调用desc->action->handler,程序员注册中断时挂接的中断处理函数,这个函数是处理具体触发中断设备的函数;
来个图,简单明了
image.png

  1. |---irq_exit<br /> Preempt_count___1_)变量减HARDIRQ_OFFSET,表示HARDIRQ处理结束;<br /> 在执行invoke_softirqsoftirq真正的执行函数)之前,需要确认此时没有运行在中断上下文,且本地SOFTIRQ有挂起。<br />invoke_softing<br /> |--- 如果是线程方式,会唤醒中断线程,执行中断处理<br /> |--- __do_softing<br /> 1 函数开始对中断的处理,屏蔽SOFTIRQ响应,开启硬中断响应;即可以被硬中断打断,不能被SOFTIRQ打断。
  2. - __local_bh_disable,标识preempt_countSOFTIRQ域来告诉当前处于softirq的上下文;每次执行SOFTIRQ前会判断是否在中断上下文,如果是,则不执行本次SOFTIRQ
  3. - local_irq_enable,开启硬中断;
  4. 2**)获取所有挂起的SOFTIRQ类型对应的bit位——pending**,**从pending的最低位(左边)开始找到置1bit值**,bit值-1后作为索引找到softirq_vec[]中对应的softirq类型的action,执行action函数处理SOFTIRQ。<br /> 3)在SOFTIRQ执行期间,查看是否有新的SOFTIRQ挂起,如果有就继续执行新挂起的SOFTIRQ;**如果有大量SOFTIRQ挂起,linxu默认最多执行10次,或者执行时间小于2ms;后续挂起的SOFTIRQ将由唤醒ksoftirqd线程来处理**。防止cpu长时间无法响应其他任务。<br />_ _<br />_1preempt_countthread_info结构体内部成员int preempt_count,记数器,内核抢占等发生的次数;preempt_count按照bit划分成多个域,PREEMPT:bit[7:0], SOFTIRQ:bit[15:8], HARDIRQ:[19:16], nmi:bit20, PREEMPT_NEED_RESCHED:bit31;_<br />_2pending,是标志位,不记录软中断次数。_

5 SOFTIRQ的触发时机和处理时机

5.1 softirq 触发时机

触发softirq后,softirq会处于挂起状态;
5.1.1 触发SOFTIRQ
raise_softirq
|—- 关中断,保证preempt_count原子性
|—- raise_softirq_irqoff
|—- raise_softirq_irqoff
—-or_softirq_pending 设置softirq挂起标志位(具体看5.1.3)
|—-如果不在中断上下文中,会唤醒softirqd线程执行SOFTIRQ;
调用raises_softriq,或者调用raise_sofit_irqoff(需要关中断)的地方很多,tasklet,timer等不列举;
5.1.2 __softirq_pending 标识挂起的软中断类型
local_softirq_pending()
#define local_softirq_pengding() (irq_stat[smp_processor_id()].softirq_pengding)
irq_stat是个irq_cpustat_t结构体类型的数组,数组索引是cpu号,它的成员
softirq_pending是个unsigned int型,linxu中的每一类softirq在__softirq_pending内占用一位,linux默认工10种SOFTIRQ类型;
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, / Unused, but kept as tools rely on the numbering. Sigh! /
RCU_SOFTIRQ, / Preferable RCU should always be the last softirq /
NR_SOFTIRQS
};

5.13 softirq_vec,记录每类SOFTIRQ的处理函数,数组索引是SOFTIRQ的类型;

kernel/softirq.c
static struct softirq_action softirq_vec[NR_SOFTIRQS ] 每个cpu都有自己的一份,记录当前软中断状态,数组索引为softirq的类型,如下枚举;
enum
{
HI_SOFTIRQ=0,—————-数字越低优先级越高,所以最优先处理,代表高优先级的tasklet
TIMER_SOFTIRQ,—————时钟中断相关的tasklet
NET_TX_SOFTIRQ,————-把数据包传送到网卡
NET_RX_SOFTIRQ,————-从网卡接收数据包
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,————常规tasklet
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, / Unused, but kept as tools rely on the numbering. Sigh! /
RCU_SOFTIRQ, / Preferable RCU should always be the last softirq /
NR_SOFTIRQS
};

5.1.4 设置挂起SOFTIRQ的类型
宏函数:or_softirq_pending
#define or_softirq_pending(x) (local_softirq_pending() |= (x))

5.2 linux检查softirq挂起状态有四种情况

1)硬件中断代码返回的时候
硬件中断上报后,linux系统会调用do_IRQ执行中断处理流程,do_IRQ函数的前半部分是处理HARDIRQ操作,后半部分是处理SOFTIRQ部分;先查询是否有SOFTIRQ挂起,然后会执行SOFTIRQ处理。
do_IRQ->irq_exit()->do_softirq

2)开启中断底半部时
local_bh_enable->
local_bh_enable_ip->当前没有运行在中断上下文且有softirq挂起的情况下,会执行do_softirq

3)ksoftirqd内核服务线程运行的时候
static int run_ksoftirq(void __bind_cpu),这个是linux自动创建的线程,专门运行softirq;两种情况会用到这个线程处理SOFTIRQ;
情况1:注册中断是通过request_threaded_irq,且第三个参数irq_handler_t thread_fn挂接我们要执行的中断函数;
情况2: 满足_do_softirq中唤醒ksoftirqd线程的条件,即执行时间超过2ms或者softirq处理超过10次(不是10个SOFTIRQ)或者检查到cpu需要调度时;
if(time_before(jiffies,end) && !need_resched() && —max_restart)) //max_restart默认10
goto restart; //再次执行SOFTIRQ
wakeup_softirqd(); //唤醒SOFTIRQ处理线程

4)在一些内核子系统中显示的去检查挂起的SOFTIRQ
例如
int netif_ni(struct sk_buff
skb)

6 中断注册

6.1 request_irq
request_irq注册中断,会调用request_threaded_irq申请中断,request_threaded_irq的第三个参数 irq_handler_t thread_fn默认为NULL ;如果需要通过线程的方式执行中断处理函数,可以直接调用request_threaded_irq,第三个参数 irq_handler_t thread_fn挂接我们要执行的中断函数即可。

request_irq
|—- request_threaded_irq
1) 如果注册的是共享中断,dev_id通常中断设备相关的结构体,用来辨识设备。
2) irq_to_desc(irq),根据irq作为索引获取中断描述符desc;
3) 以GFP_KERNEL 标志kzalloc申请内存存放irqaction,可能导致睡眠。
4) __setup_irq,实现冗余,核心是把新中断(irqaction)与中断描述符desc->action挂接上;共享中断时,新irqaction的flag必须与共享的中断线上的其他irqaction相同,否则会报-EBUSY;

8 tasklet

tasklet基于SOFTIRQ实现的;

8.1 TASKLET_SOFTIRQ对应的执行函数

系统初始化SOFTIRQ时,安装了TASKLET_SOFTIRQ执行函数,流程如下
softirq_init
|—- open_softirq(TASKLET_SOFTIRQ,tasklet_action) /open_softirq(HI_SOFTIRQ,tasklet_hi_action)
|—-softirq_vec[nr].action = action; 安装执行函数

8.2 struct tasklet_struct

struct tasklet_struct
{
struct tasklet_struct next; //将多个tasklet链接成单向循环链表
unsigned long state; //TASKLET_STATE_SCHED or TASKLET_STATE_RUN
atomic_t count; //0:激活tasklet 非0:禁用tasklet
void (
func)(unsigned long); //用户自定义函数
unsigned long data; //函数入参
};
重要参数说明
unsigned long state,设置成TASKLET_STATE_SCHED防止重复提交,设置成TASKLET_STATE_RUN防止在不同cpu上同时执行tasklet的处理函数,这样走可以保证tasklet函数不用考虑并发情况,不会有资源竞争问题;

void (*func)(unsigned long),程序员初始化tasklet时注册的处理函数;

8.3 向系统提交tasklet

调用tasklet_schedule,可以将tasklet提交,所谓提交就是讲tasklet对象加入到tasklet_vec链表上。
8.3.1 tasklet_vec
kernel/softirq.c
struct tasklet_head
{
struct tasklet_struct head;
struct tasklet_struct tail;
}

static DEFINE _PER_CPU(struct tasklet_head, tasklet_vec)
tasklet_vec是个per-cpu的链表,通过tasklet_schedule提交的tasklet都会添加到这个链表上。

8.3.2 提交tasklet*

tasklet_schedule
|—- 设置tasklet的状态,即tasklet_struct的state成员置TASKLET_STATE_SCHED,然后将这个tasklet加入到tasklet_vec链表中;
|—- raise_softirq_irqoff(TASKLET_SOFTIRQ), 提交tasklet类型的SOFTIRQ(raise_softirq_irqoff分析详见5.1.1)

8.4 TASKLET_SOFTIRQ对应的执行函数tasklet_action

检查到irq_stat[smp_processor_id()].__softirq_pengding置位,就会执行softirq_vec[nr].action ,也就是 tasklet_action(详见8.1);
|—-局部变量list获取tasklet_vec链表,并将tasklet_vec链表清空,也就是说提交的tasklet执行完成后,不会再出现在tasklet链表;提交一次,调度运行一次。
|—- tasklet_trylock 获取tasklet的状态,也就是tasklet结构体成员state是否置TASKLET_STATE_RUN;

  • 如果处于运行状态,则不执行这个tasklet,并且将这个tasklet重启挂接到tasklet_vec链表上;然后通过调用__raise_sofirq_irqoff,挂起一个TASKLET_SOFTIRQ类型的软中断。这样处理可以保证多core系统时,tasklet的串行化执行,不用考虑并发问题。
  • 如果不是运行状态,则清除tasklet的TASKLET_STATE_SCHED标志,此时这个tasklet可以再次提交到其他cpu的tasklet_vec链表上了。

|—-执行程序员在tasklet_init时注册的处理函数。

9 核间中断 IPI

多核多线程由PIC统一控制,PIC允许一个硬件线程中断其他的硬件线程,这种方式称核间中断。

IPI 全称为Inter-Processor Interrupt,即处理中间的中断,需要可编程中断控制器PIC or APIC的支持;
32位核间中断寄存器IPIBase,该寄存器包含目的线程的编号、中断向量、即中断类型。核间中断通过向这个寄存器中写入需要的值实现中断其他硬件线程。

实例:硬件线程A中断硬件线程B

  • 线程A向IPIBase写入B的thread ID 、中断向量、中断类型
  • PIC通知B所在的内核挂起当前执行任务,并根据中断向量跳转中断服务例程ISR入口。

    9.1 通过IPI进行核间通信

    使用 IPI 进行核间通信的关键在于要利用中断服务例程 ISR 去读取一个事先约好的共享内存区域。
    1)发起方首先将消息写到一块共享内存中,然后发起核间中断。
    2)被中断的硬件线程在中断服务例程中读取该内存,以获得发起方通知的消息。
    3)为防止多核间的竞争导致消息被改写,使用这种方式必须利用锁机制来确保消息的完整性