文章目录
- linux中断原理
- 中断控制器GIC
- 中断源分类
- 中断ID、中断线(中断号/中断源)
- 中断ID
- 中断线(中断号/中断源)
- MX6U中断源
- 上半部与下半部(顶半部和底半部)
- 下半部实现机制
- linux软中断
- 注册软中断
- 触发软中断
- 初始化软中断
- tasklet
- asklet_init:初始化tasklet
- tasklet_schedule:调度tasklet
- Example
- 工作队列
- 创建工作
- schedule_work:调度工作
- Example
linux中断原理
中断控制器GIC
GIC 是 ARM 公司给 Cortex-A/R 内核提供的一个中断控制器,类似 Cortex-M 内核中的NVIC。
当 GIC 接收到外部中断信号以后就会报给 ARM 内核,但是ARM 内核只提供了四个信号给 GIC 来汇报中断情况:VFIQ、VIRQ、FIQ 和 IRQ,他们之间的关系如下:
GIC 接收众多的外部中断,然后对其进行处理,最终就只通过四个信号报给 ARM 内核,这四个信号的含义如下:
信号 | 含义 |
---|---|
VFIQ | 虚拟快速 FIQ。 |
VIRQ | 虚拟外部 IRQ。 |
FIQ | 快速中断 IRQ。 |
IRQ | 外部中断 IRQ。 |
中断源分类
GIC 将众多的中断源分为分为三类:
-
SPI(Shared Peripheral Interrupt),共享中断,顾名思义,所有 Core 共享的中断,这个是最常见的,那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。
比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。 -
PPI(Private Peripheral Interrupt),私有中断,我们说了 GIC 是支持多核的,每个核肯定有自己独有的中断。
这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。 -
SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,
通过向寄存器GICD_SGIR 写入数据来触发,系统会使用 SGI 中断来完成多核之间的通信。
中断ID、中断线(中断号/中断源)
中断ID
每一个 CPU 最多支持 1020 个中断 ID,中断 ID 号为 ID0~ID1019,这 1020 个 ID 包含了 PPI、SPI 和 SGI。
ID0~ID15:这 16 个 ID 分配给 SGI。
ID16~ID31:这 16 个 ID 分配给 PPI。
ID32~ID1019:这 988 个 ID 分配给 SPI,具体某个 ID 对应哪个中断由半导体厂商根据实际情况定义。
中断线(中断号/中断源)
I.MX6U 的总共使用了 128 个中断 ID,加上前面属于 PPI 和 SGI 的 32 个 ID,I.MX6U 的中断源共有 128+32=160个;
这 128 个中断 ID 对应的中断在《I.MX6ULL 参考手册》的“3.2 Cortex A7 interrupts”小节。
所以MX6ULL的中断线IRQ0–128对应的中断ID为ID32–ID160。
MX6U中断源
上半部与下半部(顶半部和底半部)
在使用request_irq 申请中断的时候注册的中断服务函数属于中断处理的上半部
上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
Linux 内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出;
那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部;剩下的所有工作都可以放到下半部去执行;
比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。
上半部与下半部的的划分:
-
如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
-
如果要处理的任务对时间敏感,可以放到上半部。
-
如果要处理的任务与硬件有关,可以放到上半部。
-
除了上述三点以外的其他任务,优先考虑放到下半部。
下半部实现机制
一开始 Linux 内核提供了“bottom half”机制来实现下半部,简称“BH”。
从 2.5 版本的 Linux内核开始 BH 已经被抛弃了,引入了软中断和tasklet来替代“BH”机制。
linux软中断
Linux 内核使用结构体 softirq_action 表示软中断, softirq_action结构体定义在文件 include/linux/interrupt.h 中。
struct softirq_action
{
void (*action)(struct softirq_action *);
};
在 kernel/softirq.c 文件中一共定义了 10 个软中断。
软中断枚举类型
static struct softirq_action softirq_vec[NR_SOFTIRQS];
/* NR_SOFTIRQS 是枚举类型,定义在文件 include/linux/interrupt.h 中,定义如下 */
enum
{
HI_SOFTIRQ = 0, /* 高优先级软中断*/
TIMER_SOFTIRQ, /* 定时器软中断*/
NET_TX_SOFTIRQ, /* 网络数据发送软中断*/
NET_RX_SOFTIRQ, /* 网络数据接收软中断*/
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ, /* tasklet 软中断*/
SCHED_SOFTIRQ, /* 调度软中断*/
HRTIMER_SOFTIRQ, /* 高精度定时器软中断*/
RCU_SOFTIRQ, /* RCU 软中断*/
NR_SOFTIRQS
};
一共有 10 个软中断,因此 NR_SOFTIRQS 为 10,因此数组 softirq_vec 有 10 个元素。
softirq_action 结构体中的 action 成员变量就是软中断的服务函数,数组 softirq_vec 是个全局数组,因此所有的 CPU(对于SMP系统而言)都可以访问到。
SMP:对称多处理(Symmetric multiprocessing)
每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数是相同的,都是数组softirq_vec中定义的action函数。
注册软中断
要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数,软中断必须在编译的时候静态注册!
void open_softirq(int nr, void (*action)(struct softirq_action *))
nr:要开启的软中断,在上面的(软中断枚举类型)中选一个。
action:软中断对应的处理函数。
返回值:没有返回值。
触发软中断
注册好软中断以后需要通过 raise_softirq 函数触发
void raise_softirq(unsigned int nr)
nr:要触发的软中断,在上面的(软中断枚举类型)中选一个。
返回值:没有返回值。
初始化软中断
linux内核自动初始化软中断,Linux 内核使用softirq_init
函数初始化软中断,softirq_init
函数定义在kernel/softirq.c
文件里面。
void __init softirq_init(void)
{
int cpu;
for_each_possible_cpu(cpu)
{
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
}
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
softirq_init 函数默认会打开TASKLET_SOFTIRQ
和HI_SOFTIRQ
。
tasklet
tasklet 是利用软中断来实现的另外一种下半部机制,在软中断和 tasklet 之间,建议使用 tasklet。
Linux 内核使用 tasklet_struct 结构体来表示 tasklet。
struct tasklet_struct
{
struct tasklet_struct *next; /* 下一个 tasklet */
unsigned long state; /* tasklet 状态 */
atomic_t count; /* 计数器,记录对 tasklet 的引用数 */
void (*func)(unsigned long); /* tasklet 执行的函数 */
unsigned long data; /* 函数 func 的参数 */
};
func 函数就是 tasklet 要执行的处理函数,用户定义函数内容,相当于中断处理函数。
asklet_init:初始化tasklet
如果要使用 tasklet,必须先定义一个 tasklet,然后使用 tasklet_init 函数初始化 tasklet。
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long),
unsigned long data);
t:要初始化的 tasklet
func:tasklet 的处理函数。
data:要传递给 func 函数的参数
返回值:没有返回值。
可以使用宏DECLARE_TASKLET
来一次性完成tasklet的定义和初始化,DECLARE_TASKLET
定义在include/linux/interrupt.h
文件中
DECLARE_TASKLET(name, func, data)
name 为要定义的 tasklet 名字,这个名字就是一个 tasklet_struct 类型的时候变量;
func就是 tasklet 的处理函数;
data 是传递给 func 函数的参数;
tasklet_schedule:调度tasklet
在上半部,也就是中断处理函数中调用 tasklet_schedule 函数就能使 tasklet 在合适的时间运行
void tasklet_schedule(struct tasklet_struct *t)
t:要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。
返回值:没有返回值。
Example
/* 定义 taselet */
struct tasklet_struct testtasklet;
/* tasklet 处理函数 */
void testtasklet_func(unsigned long data)
{
/* tasklet 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 tasklet */
tasklet_schedule(&testtasklet);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 tasklet */
tasklet_init(&testtasklet, testtasklet_func, data);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}
工作队列
工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。
如果要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或 tasklet。
Linux 内核使用work_struct结构体表示一个工作。
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; /* 工作队列处理函数 */
};
这些工作组织成工作队列,工作队列使用workqueue_struct结构体表示。
Linux 内核使用工作者线程(worker thread)来处理工作队列中的各个工作,Linux 内核使用worker结构体表示工作者线程
每个 worker 都有一个工作队列,工作者线程处理自己工作队列中的所有工作。
在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。
创建工作
定义一个 work_struct 结构体,使用 INIT_WORK 宏来初始化工作;
#define INIT_WORK(_work, _func)
_work 表示要初始化的工作,_func 是工作对应的处理函数。
也可以使用 DECLARE_WORK 宏一次性完成工作的创建和初始化;
#define DECLARE_WORK(n, f)
n 表示定义的工作(work_struct),f 表示工作对应的处理函数。
schedule_work:调度工作
工作也是需要调度才能运行。
bool schedule_work(struct work_struct *work)
work:要调度的工作。
返回值:0 成功,其他值 失败。
Example
/* 定义工作(work) */
struct work_struct testwork;
/* work 处理函数 */
void testwork_func_t(struct work_struct *work);
{
/* work 具体处理内容 */
}
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 work */
schedule_work(&testwork);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 初始化 work */
INIT_WORK(&testwork, testwork_func_t);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}
dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。