在面试的时候我们常常问或者被问一个问题:几种中断下半部机制softirq、tasklet、workqueue有什么区别?linux为什么要设计这几种机制?真正能够回答清楚的人还是少数的。下面我们就详细分析一下这其中的区别。
本文的代码分析基于linux kernel 3.18.22和arm64架构,最好的学习方法还是”RTFSC”
1. linux中断
arm64和其他所有cpu架构的中断处理流程都是一样:正常执行流程被打断进入中断服务程序,保护现场、处理中断、恢复现场:
[^ARMPG]
在整个中断处理过程中,arm64的cpu全局中断是自动disable的(PSTATE寄存器中的interrupt bit被masks)。如果用户想支持interrupt nested,需要自己在中断服务程序中使能中断。linux现在是不使用中断嵌套的。
[^ARMPG]
1.1 cpu中断打开/关闭
arm64关闭和打开本地cpu的全局中断的方法,是操作SPSR(Saved Process Status Register)寄存器IRQ mask bit。
[^ARMPG]
linux中arm64关闭和打开本地cpu中断的函数实现。
- arch/arm64/include/asm/irqflags.h:
- local_irq_disable() -> raw_local_irq_disable() -> arch_local_irq_disable()
- local_irq_enable() -> raw_local_irq_enable() -> arch_local_irq_enable()
1 |
|
1.2 中断控制器GIC
上面描述了cpu对全局中断的处理,但是还有一个工作需要有人去做:就是把外部中断、内部中断、cpu间中断等各种中断按照优先级、亲和力、私有性等发送给多个cpu。负责这个工作的就是中断控制器GIC(Generic Interrupt Controller)。
[^GICANALY]
从软件角度上看,GIC可以分成两个功能模块:[^ARMPG]
- Distributor。负责连接系统中所有的中断源,通过寄存器可以独立的配置每个中断的属性:priority、state、security、outing information、enable status。定义哪些中断可以转发到cpu core。
- CPU Interface。cpu core用来接收中断,寄存器主要提供的功能:mask、 identify 、control states of interrupts forwarded to that core。每个cpu core拥有自己的cpu interface。
对GIC来说,中断可以分成以下几种类型:[^ARMPG]
- SGI(Software Generated Interrupt),Interrupt IDs 0-15。系统一般用其来实现IPI中断。
- PPI(Private Peripheral Interrupt),Interrupt IDs16-31。私有中断,这种中断对每个cpu都是独立一份的,比如per-core timer中断。
- SPI(Shared Peripheral Interrupt),Interrupt numbers 32-1020。最常用的外设中断,中断可以发给一个或者多个cpu。
- LPI(Locality-specific Peripheral Interrupt)。基于message的中断,GICv2和GICv1中不支持。
GIC从原理上理解并不难,但是如果涉及到级联等技术细节,整个初始化过程还是比较复杂。大家可以自行下载GIC手册:GIC-400、GIC-500学习,GIC代码分析也是一篇很不错的分析文章。
一款GIC相关的操作函数都会集中到irq_chip数据结构中,以GIC-400为例,它的相关操作函数如下:
- drivers/irqchip/irq-gic.c:
1 | static struct irq_chip gic_chip = { |
1.3 linux中断处理流程
从代码上看linux中断的处理流程大概是这样的:
从处理流程上看,对于gic的每个中断源,linux系统分配一个irq_desc数据结构与之对应。irq_desc结构中有两个中断处理函数desc->handle_irq()和desc->action->handler(),这两个函数代表中断处理的两个层级:
desc->handle_irq()。第一层次的中断处理函数,这个是系统在初始化时根据中断源的特征统一分配的,不同类型的中断源的gic操作是不一样的,把这些通用gic操作提取出来就是第一层次的操作函数。具体实现包括:
handle_fasteoi_irq();
handle_simple_irq();
handle_edge_irq();
handle_level_irq();
handle_percpu_irq();
handle_percpu_devid_irq();desc->action->handler()。第二层次的中断处理函数,由用户注册实现具体设备的驱动服务程序,都是和GIC操作无关的代码。同时一个中断源可以多个设备共享,所以一个desc可以挂载多个action,由链表结构组织起来。
1.4 中断服务注册
从上一节的中断二层结构中可以看到第二层的中断处理函数desc->action->handler是由用户来注册的,下面我们来分析具体注册过程:
- kernel/irq/manage.c:
- request_irq() -> request_threaded_irq() -> __setup_irq()
1 | static inline int __must_check |
1.5 中断线程化
从上一节可以看到,使用request_irq()注册的是传统中断,而直接使用request_threaded_irq()注册的是线程化中断。线程化中断的主要目的把中断上下文的任务迁移到线程中,减少系统关中断的时间,增强系统的实时性。
中断对应的线程命名规则为:
1 | t = kthread_create(irq_thread, new, "irq/%d-%s", irq, |
我们通过ps命令查看系统中的中断线程,注意这些线程是实时线程SCHED_FIFO:
1 | root@:/ |
线程化中断的创建和处理任务流程如下:
线程和action是一一对应的,即用户注册一个中断处理程序对应一个中断线程。
1.6 外设中断打开/关闭
前面的章节讲述了本地cpu全局中断的enable/disable。如果要操作单个中断源的enable/disable,使用enable_irq()/disable_irq()函数。最后调用主要是GIC chip相关的函数:
- kernel/irq/manage.c:
- enable_irq() -> __enable_irq() -> irq_enable()
1 | void enable_irq(unsigned int irq) |
- kernel/irq/manage.c:
- enable_irq() -> __enable_irq() -> irq_enable()
1 | void disable_irq(unsigned int irq) |
1.7 中断亲和力
同样基于GIC chip提供的能力,我们能配置中断源对cpu的亲和力。
- kernel/irq/manage.c:
- enable_irq() -> __enable_irq() -> irq_enable()
1 | static inline int |
2. linux中断下半部
接下来就是大名鼎鼎的中断下半部了,包括:softirq、tasklet、workqueue。中断下半部的主要目的就是减少系统关中断的时间,把少关键代码放在中断中做,大部分处理代码放到不用关中断的空间去做。
上面有最激进的方法中断线程化,但是大部分时候还是需要用到中断上、下半部的方法。
workqueue在另外文章中已经有详细解析,本处只解析softirq、tasklet。
2.1 preempt_count
1 | static __always_inline int preempt_count(void) |
开始之前先了解一下preempt_count这个背景知识,preempt_count是thread_info结构中的一个字段,用来表示当前进程能否被抢占。
所谓的抢占:是指在进程在内核空间运行,如果主动不释放cpu,在时间片用完或者高优先级任务就绪的情况下,会被强行剥夺掉cpu的使用权。
但是进程可能在做一些关键操作,不能被抢占,被抢占后系统会出错。所以linux设计了preempt_count字段,=0可以被抢占,>0不能被抢占。
进程在中断返回内核态时,做是否可抢占的检查:
- arch/arm64/kernel/entry.s:
- el1_irq() -> __enable_irq() -> irq_enable()
1 | .align 6 |
虽然preempt_count>0就是禁止抢占,linux进一步按照各种场景对preempt_count bit进行了资源划分:
reserved bits | bit21 | bit20 | bit19-bit16 | bit15-bit8 | bit7-bit0 |
---|---|---|---|---|---|
PREEMPT_ACTIVE | NMI | HARDIRQ | SOFTIRQ | PREEMPT |
1 | /* |
各场景分别利用各自的bit来disable/enable抢占:
- 普通场景(PREEMPT_MASK)。对应函数preempt_disable()、preempt_enable()。
- 软中断场景(PREEMPT_MASK)。对应函数local_bh_disable()、local_bh_enable()。
- 普通中断场景(HARDIRQ_MASK)。对应函数irq_enter()、irq_exit()。
- NMI中断场景(NMI_MASK)。对应函数nmi_enter()、nmi_exit()。
所以反过来,我们也可以通过preempt_count的值来判断当前在什么场景:
1 | #define in_irq() (hardirq_count()) |
2.2 softirq
回到中断上下半部的架构,linux系统虽然将大部分工作移出了中断上下文,不关闭中断。但是它也希望移出的工作能够很快的得到执行,软中断为了保证自己能很快执行,使用__local_bh_disable_ip()禁止抢占。
softirq的具体实现机制如下:
- 系统支持固定的几种软中断,softirq_vec数组用来记录这些软中断执行函数:
1 | enum |
- 使用irq_stat[cpu].__softirq_pending来记录每个cpu上所有softirq的pending状态,raise_softirq()用来置位一个softirq pending:
1 | void raise_softirq(unsigned int nr) |
- softirq的执行有两个时刻:在退出中断irq_exit()时或者在softirqd线程当中:
软中断使用smpboot_register_percpu_thread()函数,给每个cpu上创建了对应的softirqd线程:
1 | root@:/ # ps | grep softirq |
软中断优先在irq_exit()中执行,如果超过时间等条件转为softirqd线程中执行。满足以下任一条件软中断在softirqd线程中执行:
- 在irq_exit()->__do_softirq()中运行,时间超过2ms。
- 在irq_exit()->__do_softirq()中运行,轮询软中断超过10次。
- 在irq_exit()->__do_softirq()中运行,本线程需要被调度。
- 调用raise_softirq()唤醒软中断时,不在中断环境中。
我们也看到,软中断处理是按照优先级逐个调用softirq_vec[]数组中的软中断处理函数,所以前面的软中断是可以阻塞后面的软中断的。这个在我们写程序的时候需要注意。
2.3 tasklet
linux已经有了softirq机制,为什么还需要tasklet机制?最主要的原因是softirq是多cpu执行的,可能碰到很多重入的问题,而tasklet同一时刻只能在一个cpu上执行,不需要处理重入互斥问题。另外linux也不建议用户去添加新的软中断。
下面我们来具体分析一下tasklet的实现机制:
- per-cpu变量tasklet_vec/tasklet_hi_vec以链表的形式记录了当前cpu需要处理的tasklet任务:
1 | void __init softirq_init(void) |
- push一个tasklet任务:
1 | static inline void tasklet_schedule(struct tasklet_struct *t) |
- 处理一个tasklet任务:
1 | static void tasklet_action(struct softirq_action *a) |
参考资料
[^ARMPG]: ARM Cortex-A Series Programmer’s Guide for ARMv8-A
[^GICANALY]: GIC代码分析