一、linux中断处理机制
1、最简单的中断机制
最简单的中断机制就是像芯片手册上讲的那样,在中断向量表中填入跳转到对应处理函数的指令,然后在处理函数中实现需要的功能。类似下图:
这种方式在原来的单片机课程中常常用到,一些简单的单片机系统也是这样用。它的好处很明显,简单,直接。
2、上下半部机制
中断处理函数所作的第一件事情是什么?答案是屏蔽中断(或者是什么都不做,因为常常是如果不清除IF位,就等于屏蔽中断了),当然只屏蔽同一种中断。之所以要屏蔽中断,是因为新的中断会再次调用中断处理函数,导致原来中断处理现场的破坏。即,破坏了 interrupt context。
随着系统的不断复杂,中断处理函数要做的事情也越来越多,多到都来不及接收新的中断了。于是发生了中断丢失,这显然不行,于是产生了新的机制:分离中断接收与中断处理过程。中断接收在屏蔽中断的情况下完成;中断处理在时能中断的情况下完成,这部分被称为中断下半部。
从上图中看,只看int0的处理。Func0为中断接收函数。中断只能简单的触发func0,而func0则能做更多的事情,它与funcA之间可以使用队列等缓存机制。当又有中断发生时,func0被触发,然后发送一个中断请求到缓存队列,然后让funcA去处理。
由于func0做的事情是很简单的,所以不会影响int0的再次接收。而且在func0返回时就会使能int0,因此funcA执行时间再长也不会影响int0的接收。
二、常用的下半部处理机制
1、软中断(Softirq)
是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候,运行于软中断上下文。
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];
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
};//定义在文件 include/linux/interrupt.h 中
要使用软中断,必须先使用 open_softirq 函数注册对应的软中断处理函数,open_softirq 函数原型如下
void open_softirq(int nr, void (*action)(struct softirq_action *));
注册好软中断以后需要通过 raise_softirq 函数触发,raise_softirq 函数原型如下
void raise_softirq(unsigned int nr);
软中断必须在编译的时候静态注册!Linux 内核使用 softirq_init 函数初始化软中断, softirq_init 函数 定义在 kernel/softirq.c 文件里面,softirq_init 函数默认会打开 TASKLET_SOFTIRQ 和 HI_SOFTIRQ。
2、tasklet
基本原理
tasklet 是通过软中断实现的,所以它本身也是软中断。软中断用轮询的方式处理,假如正好是最后一种中断,则必须循环完所有的中断类型,才能最终执行对应的处理函数。显然当年开发人员为了保证轮询的效率,于是限制中断个数为32个。
为了提高中断处理数量,顺道改进处理效率,于是产生了tasklet机制。
- Tasklet采用无差别的队列机制,有中断时才执行,免去了循环查表之苦。
-
无类型数量限制,效率高,无需循环查表,支持 SMP 机制,
-
一种特定类型的 tasklet只能运行在一个 CPU 上,不能并行,只能串行执行。 多个不同类型的 tasklet 可以并行在多个 CPU 上。
-
软中 断是静态分配的,在内核编译好之后,就不能改变。但 tasklet 就灵活许多,可以在运行时改变(比如添加模块时)。
使用方法
如果要使用 tasklet,必须先定义一个 tasklet结构体,然后使用 tasklet_init 函数初始化 tasklet,taskled_init 函数原型如下:
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 的参数 */
}
void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
t :要初始化的 taskletfunc : 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 的时候引用一个 tasklet_schedule()函数就能使系统在适当的时候进行调度运行:
void tasklet_schedule(struct tasklet_struct *t)
t:要调度的 tasklet,也就是 DECLARE_TASKLET 宏里面的 name。
使用示例
/* 定义 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);
......
}
/* 驱动出口函数 */
static int __exit xxxx_exit(void)
{
......
/* 释放中断资源 */
free_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
/* 释放 tasklet */
tasklet_kill(&testtasklet);
......
}
3、工作队列
基本原理
前面的机制不论如何折腾,有一点是不会变的。它们都在中断上下文中。什么意思?说明它们不可挂起。也就是说软中断不能睡眠、不能阻塞,原因是由于中断上下文处于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。
而且由于是串行执行,因此只要有一个处理时间较长,则会导致其他中断响应的延迟。为了完成这些不可能完成的任务,于是出现了工作队列。工作队列说白了就是一组内核线程,作为中断守护线程来使用。多个中断可以放在一个线程中,也可以每个中断分配一个线程。工作队列对线程作了封装,使用起来更方便。因为工作队列是线程,所以我们可以使用所有可以在线程中使用的方法。
工作队列结构原理
work_struct, workqueue_struct, cpu_workqueue_struct三者之间关系如下:
-
内核启动时会为每一个CPU创建一个cpu_workqueue_struct结构,同时还会创建名为
kworker/u:x
(x
是0
开始的整数,表示CPU
编号)工作者内核线程,该线程创建之后处于sleep
状态,这个线程创建好后处于睡眠状态,等待用户加入工作,来唤醒线程去调度工作结构体 work_struct 中的工作函数。 -
工作work_struct 是通过链表连接在cpu_workqueue_struct上(
delayed_works成员
),后面其他work_struct连接在前一个后面,组成一个队列 -
工作者线程被唤醒后,会去自己负责的工作队列上依次执行上面struct_work结构中的工作函数,执行完成后就会把 work_struct 从链表上删除。
-
如果想使用工作队列来延后执行一段代码,必须先创建work_struct -> cpu_workqueue_struct,然后把工作节点work_struct加入到workqueue_struct工作队列中,加入后工作者线程就会被唤醒,在适当的时机就会执行工作函数。
Linux 内核使用 work_struct 结构体表示一个工作,内容如下
workqueue.h - include/linux/workqueue.h - Linux source code (v3.3) - Bootlin
struct work_struct {
atomic_long_t data;
struct list_head entry; //链表指针 把每个工作连接在一个链表上组成一个双向链表
work_func_t func; //处理函数
};
typedef void (*work_func_t)(void *work);
struct list_head {
struct list_head *next, *prev;
};
这些工作组织成工作队列,工作队列使用 workqueue_struct 结构体表示,内容如下:
struct workqueue_struct {
unsigned int flags; /* W: WQ_* flags */
union {
struct cpu_workqueue_struct __percpu *pcpu;
struct cpu_workqueue_struct *single;
unsigned long v;
} cpu_wq; /* I: cwq's */
struct list_head list; /* W: list of all workqueues */
struct mutex flush_mutex; /* protects wq flushing */
int work_color; /* F: current work color */
int flush_color; /* F: current flush color */
atomic_t nr_cwqs_to_flush; /* flush in progress */
struct wq_flusher *first_flusher; /* F: first flusher */
struct list_head flusher_queue; /* F: flush waiters */
struct list_head flusher_overflow; /* F: flush overflow list */
mayday_mask_t mayday_mask; /* cpus requesting rescue */
struct worker *rescuer; /* I: rescue worker */
int nr_drainers; /* W: drain in progress */
int saved_max_active; /* W: saved cwq max_active */
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
char name[]; /* I: workqueue name */
};
workqueue_struct.list变量就是来组织work_struct串联起来的!
struct cpu_workqueue_struct {
struct global_cwq *gcwq; /* I: the associated gcwq */
struct workqueue_struct *wq; /* I: the owning workqueue */
int work_color; /* L: current color */
int flush_color; /* L: flushing color */
int nr_in_flight[WORK_NR_COLORS];
/* L: nr of in_flight works */
int nr_active; /* L: nr of active works */
int max_active; /* L: max active works */
struct list_head delayed_works; /* L: delayed works */
};
内核通过delayed_works
成员把第一个 work_struct
连接起来,后面work_struct
通过本身的entry
成员把自己连接在链表上。
工作队列分类
内核工作队列分成共享工作队列和自定义工作队列两种:
共享工作队列:系统在启动时候自动创建一个工作队列,驱动开发者如果想使用这个队列,则不需要自己创建工作队列,只需要把自己的work添加到这个工作队列上即可。使用schedule_work这个函数可以把work_struct添加到工作队列中
自定义工作队列:由于共享工作队列是大家共同使用的,如果上面的工作函数有存在睡眠的情况,阻塞了,则会影响到后面挂接上去的工作执行时间,当你的动作需要尽快执行,不想受其它工作函数的影响,则自己创建一个工作队列,然后把自己的工作添加到这个自定义工作队列上去。
使用自定义工作队列分为两步:
创建工作队列:使用creat_workqueue(name)创建一个名为name的工作队列
把工作添加到上面创建的工作队列上:使用queue_work函数把一个工作结构work_struc添加到指定的工作队列上
共享工作队列使用示例:
使用共享工作队列很简单,直接定义一个 work_struct 结构体变量即可,然后使用 INIT_WORK 宏来初始化工作,最后通过schedule_work函数将work_struct添加到工作队列,并启动调度。
/* 定义工作(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);
......
}
自定义工作队列使用示例:
/* 定义工作(work) */
struct work_struct testwork;
//创建一个自定义工作队列指针
struct workqueue_struct *mywq;
/* work 处理函数 */
void testwork_func_t(struct work_struct *work)
{ /* work 具体处理内容 */ }
/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
......
/* 调度 work */
queue_work(mywq,&testwork);
......
}
/* 驱动入口函数 */
static int __init xxxx_init(void)
{
......
/* 创建自定义工作队列 */
mywq = create_workqueue("mywq");
/* 初始化 work */
INIT_WORK(&testwork, testwork_func_t);
/* 注册中断处理函数 */
request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
......
}
三、调试手段
1、查看软中断和内核线程
- /proc/softirqs 提供了软中断的运行情况;
- /proc/interrupts 提供了硬中断的运行情况。
查看软中断在CPU上累计次数:
// Linux 中的软中断包括网络收发、定时、调度、RCU 锁等各种类型,可以通过查看 /proc/softirqs 来观察软中断的运行情况。
// TIMER(定时中断)、NET_RX(网络接收)、SCHED(内核调度)、RCU(RCU 锁)
[root@k8s /proc]# cat softirqs
CPU0 CPU1
HI: 5 1
TIMER: 444492709 271957759
NET_TX: 18937 15860
NET_RX: 34769092 430587974
BLOCK: 12265925 0
BLOCK_IOPOLL: 0 0
TASKLET: 853 592
SCHED: 4489427 66716813
HRTIMER: 0 0
RCU: 151213683 128619479
每个 CPU 都对应一个软中断内核线程,这个软中断内核线程就叫做 ksoftirqd/CPU 编号。
// 查看软中断线程运行情况
[root@k8s /proc]# ps aux | grep softirq
root 6 0.0 0.0 0 0 ? S Apr02 0:06 [ksoftirqd/0]
root 14 0.0 0.0 0 0 ? S Apr02 2:06 [ksoftirqd/1]
ref:
linux内核工作队列_年纪青青的博客-CSDN博客_linux 工作队列
linux工作队列 - workqueue总览_鸭蛋西红柿的博客-CSDN博客_alloc_ordered_workqueue