Linux irq

linux irq

GIC控制器

Generic Interrupt Controller。ARM提供的通用中断控制器。接受硬件中断信号,并经过一定处理后,分发给对应的CPU进行处理。分为V1-V4,

GICV2

GIC是联系外设中断和CPU的桥梁,也是各CPU之间中断互联的通道,负责检测,管理,分发中断。

image-20250714085007704

1
2
3
4
5
6
主要负责
使能/禁止中断。
把中断分组到group0还是group1,0作为安全模式使用连接FIQ,1作为非安全模式,连接IRQ.
多核系统中将中断分配到不同处理器上。
设置电平的触发方式。
虚拟化扩展。

ARM CPU对外的连接只有2个中断,IRQ & FIQ,相对应的处理模式分别是 IRQ 一般中断处理模式 和 FIQ 快速中断处理模式,所以GIC最后要把中断汇集成2条线,与CPU对接。

在GICV2中,gic由两个大模块distributor和interface组成。

distributor

主要负责中断源的管理,优先级,中断使能,中断屏蔽等。中断分发,对于PPI,SGI是各个core独有的中断,不参与目的core的仲裁,SPI 是所有 core 共享的,根据配置决定中断发往的core。中断优先级的处理,将最高优先级的中断发送给cpu interface。寄存器使用GICD作为前缀,一个gic中,只有一个GICD。

主要的作用是检测各个中断源的状态,控制各个中断源的行为,分发各个中断源产生的中断事件到指定的一个/多个CPU接口上。虽然分发器可以管理多个中断源,但是它总是把优先级最高的那个中断请求送往CPU接口。分发器对中断的控制包括:

1
2
3
4
5
6
7
8
打开或关闭每个中断,Distributor对中断的控制分成两个级别,一个是全局中断的控制(GIC_DIST_CTRL),一旦关闭了全局中断,那么任何的中断源产生的中断事件都不会被传递到cpu interface。另一个级别是针对各个中断源进行控制,(GIC_DIST_ENABLE_CLEAR),关闭一个中断源会导致该中断事件不会分发到CPU interface,但不影响其他中断源产生中断事件的分发。
控制将当前优先级最高的中断事件分发到一个或者一组CPU interface,当一个中断事件分发到多个CPU interface的时候,GIC的内部逻辑应该只保证assert一个CPU。
优先级控制
interrupt属性设定。电平触发,边缘触发等等。
interrupt group设定。设置每个中断的group。
将SGI中断分发到目标CPU上。
每个中断状态可见。
提供软件机制来设置和清楚外设终端的pending状态。

cpu interface

用于连接器,与处理器进行交互,将GICD发送的中断信息,通过IRQ,FIQ等管脚,传输给core。寄存器使用GICC作为前缀,每一个core,有一个cpu interface。

1
2
3
4
5
6
打开或关闭cpu interface 向连接的CPU assert中断事件,对于arm,cpu interface和cpu之间的中断信号线是nIRQCPU 和 nFIQCPU, 如果关闭了中断,即便是distributor分发了一个中断事件到CPU interface,也不会assert指定的IRQ或者FIQ通知core。
中断的确认。core会向cpu interface应答中断,应答当前优先级最高的那个中断,中断一旦被应答,distributor就会把该中断的状态从pending修改为active。ack了之后,cpu就会deassert nirqcpu和nfiqcpu信号线。
中断处理完毕的通知。当interruput handler处理完了一个中断的时候,会向写CPU interface的寄存器通知GIC CPU已经处理完该中断,做这个动作一方面是通知 Distributor 将中断状态修改为 deactive,另外一方面,CPU interface 会 priority drop,从而允许其他的 pending 的中断向 CPU 提交。
为 CPU 设置中断优先级掩码。通过 priority mask,可以 mask 掉一些优先级比较低的中断,这些中断不会通知到 CPU。
设置中断抢占策略。
在多个中断同时到来的时候,选择一个优先级最高的通知CPU。

virtual cpu interface

将GICD发送的虚拟中断信息,通过VIRQ,VFIQ管脚,传输给core,每一个core,有一个virtual cpu interface,而在这个virtual cpu interface中,又包含以下两个组件,virtual interface control,virtual cpu interface。

gic中断类别

gicv2,将中断,分成了group0,安全,FIQ 和group1,非安全,IRQ。

支持三种类型的中断。

1
2
3
4
5
6
7
8
9
GICV2:

SGI software generated interrupt。软件触发的中断,件可以通过写 GICD_SGIR 寄存器来触发一个中断事件,一般用于核间通信,内核中的 IPI:inter-processor interrupts 就是基于 SGI。
PPI private peripheral interrupt。私有外设中断,是每个核心私有的中断,PPI会送达到指定的cpu上,应用场景有CPU的本地时钟。
SPI Shared peripheral interrupt。公用的外部设备中断,也定义为共享中断。中断产生后,可以分发到某一CPU上,中断号ID32 - ID1019用于SPI,ID1020 - ID1023保留用于特殊用途;

GICV3:

SGI,SPI, LPI(locality spicific peripheral interrupt)GICV3中引入,是基于消息的中断,他们的配置保存在表中而不是寄存器。

GICV3的组成部分,GICV3中,主要由Distributor,cpu interface,redistributor,its,GICV3中,将cpu interface从GIC中抽离,放入到了cpu中,cpu interface通过AXI Stream,与gic进行通信。 当GIC要发送中断,GIC通过AXI stream接口,给cpu interface发送中断命令,cpu interface收到中断命令后,根据中断线映射配置,决定是通过IRQ还是FIQ管脚,向cpu发送中断。

image-20250715092053150

image-20250714093922084

1
2
3
4
5
6
7
8
root@root:~# cat /proc/interrupts 

虚拟中断号 硬件中断号
CPU0 CPU1 CPU2 CPU3
10: 1 0 0 0 GICv2 84 Level CC_IRQ
12: 356258 361084 352728 352728 GICv2 30 Level arch_timer (本地时钟)
15: 0 0 0 0 GICv2 225 Level clocksource@2,f0106000
16: 0 0 0 0 GICv2 340 Level arm-pmu

gic中断处理流程

1
2
3
4
5
6
7
GIC决定每个中断的 使能 状态,不使能的中断,是不能发送中断的
如果某个中断的中断源有效,GIC将该中断的状态设置为pending状态,然后判断该中断的目标core
对于每一个core,GIC将当前处于pending状态的优先级最高的中断,发送给该core的cpu interface
cpu interface接收GIC发送的中断请求,判断优先级是否满足要求,如果满足,就将中断通过nFIQ或nIRQ管脚,发送给core。
core响应该中断,通过读取 GICC_IAR 寄存器,来认可该中断。读取该寄存器,如果是软中断,返回源处理器ID,否则返回中断号。
当core认可该中断后,GIC将该中断的状态,修改为active状态
当core完成该中断后,通过写 EOIR (end of interrupt register)来实现优先级重置,写 GICC_DIR 寄存器,来无效该中断。

gic中断优先级

gicv2,支持最小16个,最大256个中断优先级。

中断状态和处理流程

image-20250715092647205

1
2
3
4
5
6
每个中断都维护了一个状态机。

inactive: 无中断状态,没有pending也没有active。
pending:硬件或软件触发了中断,该中断事件已经通过硬件信号通知到了GIC,等待GIC分配的CPU进行处理,在电平触发模式下,产生中断的同时保持pengding状态。
Active:cpu已经应答该中断请求,并且正在处理中。
Active and pending:当一个中断源处于Active时,同一中断源又触发了中断,进入pending状态,挂起来状态。

软件框架

1
2
3
4
5
6
主要分为四部分:

1.硬件无关代码
2.cpu架构相关的中断处理
3.中断控制器代码
4.普通其他驱动
1
2
3
4
5
常见术语

irq number 软件定义,和硬件无关,CPU需要为每一个外设中断编号,
irq domain,irq域,将某一类资源划分成不同的领域,相同的域下共享一些共同的属性。irq domain负责GIC中hwirq到 虚拟irq的映射。
中断上半部/下半部:中断上半部处理简单的紧急的功能,清楚中断处理标志。大部分任务放到下半部处理。

中断设备树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
gic: interrupt-controller@fd400000 {
compatible = "arm,gic-v3";
#interrupt-cells = <3>; // 参数个数
#address-cells = <2>;
#size-cells = <2>;
ranges;
interrupt-controller;

reg = <0x0 0xfd400000 0 0x10000>, /* GICD */
<0x0 0xfd460000 0 0xc0000>; /* GICR */
interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_HIGH>;
its: interrupt-controller@fd440000 { // 在gic设备节点下,有一个子设备节点its,ITS设备用于将消息信号中断(MSI)路由到cpu
compatible = "arm,gic-v3-its";
msi-controller;
#msi-cells = <1>; // MSI设备的DeviceID
reg = <0x0 0xfd440000 0x0 0x20000>; /*ITS寄存器的物理地址*/
};
};

中断控制器code

1
2
3
4
5
6
7
8
9
10
11
12
IRQCHIP_DECLARE(gic_v3, "arm,gic-v3", gicv3_of_init);  //初始化一个struct of_device_id的静态常量,并放置在__irqchip_of_table中

drivers/irqchip/irq-gic.c
IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init);
--> gic_of_init


init/main.c asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
--> early_irq_init(); init_IRQ();
--> arch/arm64/kernel/irq.c --> irqchip_init();
--> driver/irqchip/irqchip.c --> of_irq_init(__irqchip_of_table);
--> drivers/of/irq.c ????
1
https://doc.embedfire.com/linux/rk356x/driver/zh/latest/linux_driver/subsystem_interrupt.html

中断上下部分的处理手段

上下文

上下文信息,既包含虚拟内存,栈,全局变量等用户态的资源,也包括内核堆栈,寄存器资源,不同类型的上下文切换,会涉及到不同类型的资源切换。

临界

临界资源

临界资源是一次仅允许一个进程使用的共享资源。各进程采取互斥的方式,实现共享的资源称作临界资源。

临界区

每个进程中访问临界资源的那段代码称为临界区(criticalsection),每次只允许一个进程进入临界区,进入后,不允许其他进程进入。显然,若能保证进程互斥地进入自己的临界区,便可实现诸进程对临界资源的互斥访问。

上下文类型

进程上下文切换
1
2
3
4
5
某个进程时间片耗尽,会被系统挂起,切换到其他等待 CPU 的进程。
进程所需系统资源不足,需要等到资源满足时才可运行,此时会被挂起,其他进程会被调度。
进程通过 sleep 方法主动挂起,其他进程就有机会被调度。
有更高优先级的进程,当前进程会被挂起,高优先级进程会被调度。
硬件中断时,CPU 上的进程会被中断挂起,转而执行内核中的中断服务程序。
线程上下文切换
1
2
3
同一进程内线程切换:由于线程共享进程的虚拟内存和大部分资源,调度器只需切换线程私有的寄存器、内核栈和调度信息,而不需要切换虚拟内存,因此开销较小、

不同进程间切换:由于不共享虚拟内存和资源,切换时除了保存/恢复寄存器等线程上下文,还需要切换虚拟内存(mm_struct/CR3),这就是完整的进程上下文切换。
中断上下文切换
1
2
中断上下文切换指的是为了响应硬件的事件,中断处理会打断进程的正常调度和执行,转而调用中断处理程序,响应设备事件。而在打断其他进程时,就需要将当前的状态保存下来。这样在中断结束后,进程仍然可以从原来的状态恢复运行。
中断上下文切换,并不需要保存和恢复进程的虚拟内存等用户态资源,只需要处理 CPU 寄存器、内核堆栈等内核态的资源即可。

运行在进程上下文的内核代码是可抢占的,但中断上下文会一直运行到结束,不会被抢占。所以中断处理程序代码要受到一些限制。

1
2
3
4
5
中断代码不能:
睡眠/放弃CPU,因为内核在进入中断前会 关闭进程调度,一旦睡眠/放弃CPU,这时内核无法调度别的进程来执行,系统就会死掉。
尝试获得信号量,如果获得不到信号量,代码就会睡眠,导致如上的结果。
执行耗时的任务,中断处理应该尽可能快,如果一个处理程序是IRQF_DISABLE类型,它执行的时候会禁止所有中断。
访问用户空间的虚拟地址,因为中断允许在内核空间。

抢占计数

Linux配置打开了CONFIG_PREEMPT表示允许高优先级的任务抢占低优先级任务,但是在spin lock,中断/软中断上下文中依旧不允许抢占的。在linux系统中使用了一个Per-CPU的32位变量来标识一些特殊场景,如下

image-20250908

softirq

1
2
3
4
5
6
7
8
9
10
11
12
13
enum {
HI_SOFTIRQ = 0, /* 优先级高的tasklets */
TIMER_SOFTIRQ, /* 定时器的下半部 */
NET_TX_SOFTIRQ, /* 发送网络数据包 */
NET_RX_SOFTIRQ, /* 接收网络数据包 */
BLOCK_SOFTIRQ, /* BLOCK装置 */
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ, /* 正常优先级的tasklets */
SCHED_SOFTIRQ, /* 调度程序 */
HRTIMER_SOFTIRQ, /* 高分辨率定时器 */
RCU_SOFTIRQ, /* RCU锁定 */
NR_SOFTIRQS /* 10 */
};
  • 一个软中断不会抢占另一个软中断
  • 唯一可以抢占软中断的是中断处理程序
  • 不同软中断可以在不同处理器同时执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// raise_softirq 触发软中断
// 触发事件 --> 硬中断代码返回处 ksofirq内核线程中 执行软中断处理代码中,eg network。

// do_sofrirq 执行软中断
do_softirq --> if (in_interrupt()) return;
如果检测到有 (HARDIRQ_MASK | SOFTIRQ_MASK | NMI_MASK)在这些中断中。会直接返回。不执行。
PREEMPT_MASK (0x000000ff): 表示抢占计数,用于跟踪内核抢占状态
SOFTIRQ_MASK (0x0000ff00): 表示软中断计数,用于跟踪软中断执行状态
HARDIRQ_MASK (0x000f0000): 表示硬中断计数,用于跟踪硬中断处理状态
NMI_MASK (0x00f00000): 表示不可屏蔽中断(NMI)计数

/*
硬中断发生的时候,cpu会自动禁用中孤单。并跳转到中断处理程序。
硬件中断本身不会禁用软中断,但会通过增加 preempt_count 中的 HARDIRQ_MASK 位来标记当前处于硬中断上下文中。
硬中断可以被另一个优先级比自己高的硬中断 中断,不能被同/低 级的硬中断中断。更不能被软中断中断。
软中断可以被硬中断中断,但不会被另一个软中断中断,在一个CPU上。软中断总是串行执行的。
*/

触发softirq

image-20250909090703297

执行softirq

image-20250909093354426

软中断执行时机分为三种,中断退出时检测是否有软中断执行,进程上下文中主动执行,spin_unlock_bh后执行。

image-20250909094325638

进程被中断打断后进入到中断上下文中,进入中断后cpu硬件会自动关闭cpu本地中断响应,处理中断完成后在执行irq_exit中断退出时,当检测到有软中断pending时执行软中断;如果软中断是在中断上下文执行时,在软中断处理中会调用local_irq_enable打开CPU本地中断响应再处理软中断程序,如果是触发的软中断线程,硬中断已经完成退出也会使能本地中断。因此在软中断执行过程中打开了中断响应,所以可能会再次进入硬中断上下文。

image-20250909102119846

在一个task中处理一个变量此时被硬件中断打断进行中断处理函数,在中断处理快结束时如果有软中断pending将会先处理软中断,如果软中断中也访问了该变量,那么就出现竞态异常,因此为了处理进程和软中断的竞态,调用spin_lock_bh和spin_unlock_bh进行保护,在硬件中断处理完要进入软中断将会被禁止,硬件中断会被直接退出,继而task可以继续运行,当task再执行spin_unlock_bh时会触发执行软中断。另外如果软中断处理函数中的竞态可能在多核直接发生,为了保护多核的临界处理在软中断中只需要调用spin_lock和spin_unlock即可,不需要调用spin_lock_bh和spin_unlock_bh,因为每个cpu上只有一个软中断可以运行不需要做软中断之间的临界保护。总结就是在进程上下文中要避免软中断和多核的竞态保护就调用spin_lock_bh和spin_unlock_bh,软中断中避免多核的竞态保护就调用spin_lock和spin_unlock即可。

tasklet

tasklet(小任务机制)是利用软中断实现的下半部机制。是softirq的特殊实现。Tasklet则是自身串行化的,同一时间只能有一个特定的tasklet在运行。

1
2
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
tasklet_action_common中Tasklet确保同一时间只有一个特定的tasklet实例在运行,避免了竞争条件:

软中断和tasklet都是运行在中断上下文中,它们与任一进程无关,没有支持的进程完成重新调度。所以软中断和tasklet不能睡眠、不能阻塞,它们的代码中不能含有导致睡眠的动作,如减少信号量、从用户空间拷贝数据或手工分配内存等。也正是由于它们运行在中断上下文中,所以它们在同一个CPU上的执行是串行的,这样就不利于实时多媒体任务的优先处理。

workqueue

将下半部工作推迟,给一个内核线程去执行 ——work 总是运行于进程上下文.可以睡眠。允许被重新调度。

api

普通work
init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
声明且初始化
DECLARE_WORK(name , void (*function)(struct work_struct *));
DECLARE_DELAYED_WORK(name, void(*function)(struct work_struct *));
EG:
void my_work_handler(struct work_struct *work);
DECLARE_WORK(my_work, my_work_handler);

初始化一个已经声明的工作队列
INIT_WORK(struct work_struct *work, void(*function)(struct work_struct *));
INIT_DELAYED_WORK(struct delayed_work *work, void(*function)(struct work_struct *));
EG:
void my_work_handler(struct work_struct * work);
struct work_struct my_work;
INIT_WORK(&my_work, my_work_handler);
schedule
1
2
3
4
schedule_work(struct work_struct *work);
schedule_delayed_work(struct delayed_work *work, unsigned long delay);

将work提交到系统默认的global workqueue,true 成功入队,false 已经在队列中。不需要自己控制 workqueue 的亲和性
例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
普通 work
static void my_work_func(struct work_struct *work)
{
pr_info("work executed\n");
}

static DECLARE_WORK(my_work, my_work_func);

void test(void)
{
schedule_work(&my_work); // 提交到系统 workqueue
}

延迟 work
static void my_delayed_work_func(struct work_struct *work)
{
struct delayed_work *dwork = to_delayed_work(work);
pr_info("delayed work executed\n");
}

static DECLARE_DELAYED_WORK(my_dwork, my_delayed_work_func);

void test(void)
{
schedule_delayed_work(&my_dwork, msecs_to_jiffies(1000)); // 1s 后执行
}
自定义work
cancle
1
2
3
// 取消work 模块卸载退出时使用
int cancel_work_sync(struct delayed_work *work);
int cancel_delayed_work_sync(struct delayed_work *work);
flushed
1
2
3
4
// 等待队列执行完成。常用于 模块退出时,确保没有遗留 work 在运行。
void flush_scheduled_work(void);
int schedule_delayed_work_on(int cpu, struct delayed_work *work, unsigned long delay);
int schedule_on_each_cpu(void(*function)(struct work_struct *));

不能在中断上下文里 flush/cancel work,否则可能死锁

create
1
2
3
4
5
6
7
8
9
// 自定义workqueue
// uses one thread for each processor in the system;
struct workqueue_struct *create_workqueue(const char *name);
// uses a single thread.
struct workqueue_struct *create_singlethread_workqueue(const char *name);

WQ_UNBOUND 不绑定CPU
WQ_HIGHPRI 高优先级
WQ_MEM_RECLAIM 允许内存回收路径
add
1
2
3
4
// 提交work到自定义的队列 区别于 schedule_work:可选择具体的 workqueue。
int queue_work(struct workqueue_struct * queue, struct work_struct *work);
int queue_delayed_work(struct workqueue_struct *queue,
struct delayed_work * work , unsigned long delay);
例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static struct workqueue_struct *my_wq;
static DECLARE_WORK(my_work, my_work_func);

int init_module(void)
{
my_wq = alloc_workqueue("my_wq", WQ_UNBOUND | WQ_MEM_RECLAIM, 1);
queue_work(my_wq, &my_work);
return 0;
}

void cleanup_module(void)
{
flush_workqueue(my_wq);
destroy_workqueue(my_wq);
}

只有当无法向系统工作队列提交新的工作项时,才去创建额外的工作队列。因为每个新的工作队列都会花费可观的内存占用。如果新工作队列中的工作项无法与系统工作队列中已存在的工作项共存时,可以调整新的工作队列。例如,新的工作项执行了阻塞操作导致其它系统工作队列被延迟到一个不可接受的程序。

如何选择

1
2
3
如果推后执行的任务需要睡眠,那么就选择工作队列;如果推后执行的任务不需要睡眠,那么就选择tasklet。
另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。
  一般,不要轻易的去使用工作队列,因为每当创建一条工作队列,内核就会为这条工作队列创建一条内核线程。工作队列位于进程上下文,与软中断,tasklet有所区别,工作队列里允许延时,睡眠操作,而软中断,tasklet位于中断上下文,不允许睡眠和延时操作。

为什么linux不是实时操作系统

进入中断处理时,cpu就关闭了本地中断响应,没法再响应其他中断,即linux的中断是没法嵌套的,即使有在高优先级的中断也是没法处理。另外软中断的处理要比任何进程优先级高,因为软中断也是可以在中断上下文中运行。除了中断,软中断外,spinlock在处理过程中是关闭抢占调度的,所以在spinlock期间也是没法调度的。

image-20250908161618848

  • T0时刻 normal task执行系统调用进入内核
  • T1时刻 获取到spin lock,进入临界区保护阶段
  • T2时刻 产生了IRQ1中断,进而进行处理IRQ1中断
  • T3时刻 唤醒了高优先级的RT task,但此时系统处于中断中无法进行调度
  • T4时刻 IRQ1中断处理结束 但接着又触发了IRQ2中断,进入IRQ2中断处理。
  • T5时刻 IRQ2中断处理结束 但仍处于spin lock临界区 无法调度RT task
  • T6时刻 spin lock释放,高优先级的RT task得到调度运行
  • T7时刻 RT task运行结束 normal task继续运行
  • T8时刻 从内核态返回用户态