文章目录
Linux中断编程之顶半部和底半部机制
概述
前面已经简单了解并实现了基于中断机制的任务处理在内核中,但是实际上Linux的中断机制远远不是只要这么简单的。
在Linux内核中,任务处理类型细分的话分为三类:进程、软中断、硬件中断。而其中软中断和硬件中断都是属于中断方式(打断当前任务获取CPU资源处理触发事件在还原当前执行任务)。
前面也曾提到过“优先级”一词,“优先级”是衡量一个任务获取CPU资源的一种能力评价,优先级越高那么获取CPU资源的能力越强,它投入运行的时间就能提前。Linux内核任务优先级划分:
- 硬件中断的优先级高于软中断。
- 软中断的优先级高于进程。
- 进程之间也存在优先级,高优先级的进程和低优先级的进程。
- 软中断同样具有优先级之分,但是硬件中断并没有优先级之分,先到先处理。
当高优先级任务占用CPU资源的适合,其他进程会进入“休眠”,但是中断的世界里是没有休眠的,并且中断不参与进程的调度。中断的世界没有休眠,那么对中断的使用要求就很高了,越是高优先级的中断任务,那么对其处理时间就越要求短,如果高优先级任务长期占有CPU资源自然会导致延误很多其他中断或者进程的处理,所以就有了Linux中断编程中的顶半部和底半部机制了。
Linux内核对中断处理函数(isr_handler)的要求
注意:Linux内核要求中断处理函数执行的速度越快越好,更不能进行休眠操作,如果中断处理函数长时间占用CPU资源,而本身它的优先级最高,那么就会导致其他任务无法及时获取到CPU资源,也就无法及时运行处理,从而影响了并发和响应能力。
那么Linux内核中有些硬件的中断处理函数无法满足Linux内核的这个要求呢,比如在中断处理中需要等待外设的一些数据进行读取呢,那么对于这种不能满足Linux内核的要求的中断处理,势必会影响到Linux内核的并发和响应能力,对于这种情况,我们就需要使用Linux内核的顶半部和底半部机制来对中断处理任务进行优化了。
Linux内核中断编程之顶半部特点
顶半部就是做原先中断处理函数中比较紧急、耗时较短的内容,一旦硬件中断触发,CPU首先执行顶半部,执行时间非常快,执行完会立即释放CPU资源给其他任务使用,CPU在执行期间不允许发生CPU资源的切换,也就能踏踏实实的执行顶半部任务,顶半部本质就是中断处理函数,只是将原先中断处理中的最紧要、耗时最短的内容留下了而已。
Linux中断编程之底半部特点
底半部做原先中断处理函数中比较耗时且不紧急的内容,CPU会在“适当”的适合去执行底半部的任务(当CPU执行底半部的时候,底半部的优先级势必是当前最高的),但是CPU在执行期间如果再次遇到一个高优先级的任务到来,那么势必会抢走底半部的CPU资源,等到高优先级的任务执行完毕后再将CPU资源还给底半部,底半部继续进行处理,底半部本质就是延后执行的一种手段而已。
如果仅仅单纯将某个事件延后执行,可以利用底半部机制来实现,底半部实现方法有三种:
- tasklet
- 工作队列
- 软中断
底半部实现机制之tasklet
tasklet特点: tasklet本身是基于软中断实现,优先级是高于进程而低于硬件中断,所以tasklet对应的延后处理函数不能进行休眠操作,tasklet对应的延后处理函数执行原先中断处理函数中毕竟耗时和不紧急的内容。
- Linux内核描述tasklet的数据结构:
struct tasklet_struct {
void (*func)(unsigned long data);
unsigned long data;
...
};
成员说明:
- func:tasklet的延后处理函数,做原先中断处理函数中不紧急或者耗时较长的内容,由于基于软中断实现,所以里面不能进行休眠操作,形参data是保存传递进来的参数。
- data:给tasklet延后处理函数传递的参数。
利用tasklet实现底半部延后执行的编程步骤:
- 定义初始化一个tasklet对象
int g_data = 0x55;
DECLARE_TASKLET(btn_tasklet, //对象名
btn_tasklet_func,//tasklet的延后处理函数
(unsigned long)&g_data);//给延后处理函数传递的参数
//结果:
btn_tasklet.func = btn_tasklet_func;
btn_tasklet.data = (unsigned long)&g_data;
- 编写tasklet延后处理函数(例如上面的btn_tasklet_func)
//data = (unsigned long)&g_data;
void btn_tasklet_func(unsigned long data)
{
//做原先中断处理函数中耗时不紧急的内容
}
- 在顶半部中断处理函数中向内核“登记”(而不是调用),一旦登记完成,内核会在“适当”的时候去调用tasklet的延后处理函数,登记的函数为:
tasklet_schedule(&btn_tasklet);
案例实现(使用tasklet在按键后处理执行打印信息)
- btn_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/input.h> //标准按键值:KEY_*
//声明描述按键信息相关的数据结构
struct btn_resource {
int gpio; //按键对应的GPIO编号
char *name; //按键名称
int code; //按键值
};
//定义初始化按键硬件信息对象
static struct btn_resource btn_info[] = {
{
.gpio = PAD_GPIO_A+28,
.name = "KEY_UP",
.code = KEY_UP
},
{
.gpio = PAD_GPIO_B+9,
.name = "KEY_DOWN",
.code = KEY_DOWN
},
};
//记录按键的硬件信息
static struct btn_resource *pdata;
//tasklet延后处理函数
//适当的时候被内核调用
//基于软中断实现
//不能进行休眠操作
//data = (unsigned long)&g_data;
static void btn_tasket_function(unsigned long data)
{
int state;
//1.获取按键对应GPIO的电平状态,获取按键的操作状态
state = gpio_get_value(pdata->gpio);
//2.打印按键信息
printk("底半部:%s:按键值[%d]按键状态[%s]\n",
__func__, pdata->code, state?"松开":"按下");
}
static int g_data = 0x5555;
//定义初始化一个tasklet对象
static DECLARE_TASKLET(btn_tasklet, //定义对象
btn_tasket_function,//指定延后处理函数
(unsigned long)&g_data);//传参
//顶半部:中断处理函数
//一旦中断到来,先执行顶半部
//不允许CPU资源切换
static irqreturn_t button_isr(int irq, void *dev)
{
//1.获取对应的当前触发的硬件中断的硬件信息
pdata = (struct btn_resource *)dev;
//2.向内核登记底半部tasklet延后处理函数,一旦登记
//完成,内核将来在适当的时候调用延后处理函数
tasklet_schedule(&btn_tasklet);
//3.验证顶半部的执行先于延后处理函数
printk("顶半部:%s\n", __func__);
return IRQ_HANDLED; //执行成功
}
static int btn_init(void)
{
int i;
//1.申请GPIO资源
//配置为输入功能由内核已经帮你实现
//2.申请中断资源,注册中断处理函数
//注意:多个按键共享一个中断处理函数
for (i = 0; i < ARRAY_SIZE(btn_info); i++) {
//由GPIO编号求中断号
int irq = gpio_to_irq(btn_info[i].gpio);
gpio_request(btn_info[i].gpio,
btn_info[i].name);
request_irq(irq, //中断号
button_isr,//注册的中断处理函数
IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
btn_info[i].name,//中断名称
&btn_info[i] //将每个按键对应的硬件信息传 递给中断处理函数
);
}
return 0;
}
static void btn_exit(void)
{
int i;
//1.释放中断资源删除中断处理函数
//2.释放GPIO资源
for(i = 0; i < ARRAY_SIZE(btn_info); i++) {\
int irq = gpio_to_irq(btn_info[i].gpio);
gpio_free(btn_info[i].gpio);
free_irq(irq, &btn_info[i]);
}
}
module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");
- Makefile
obj-m += btn_drv.o
all:
make -C /opt/kernel SUBDIRS=$(PWD) modules
clean:
make -C /opt/kernel SUBDIRS=$(PWD) clean
- 执行实现:
底半部实现机制之工作队列
工作队列特点: 工作队列诞生的本质就是解决tasklet的延后处理函数不能进行休眠操作的问题,因为在某些场合需要在延后的处理流程中进行休眠操作,对于这样的情况必须使用工作队列,所以工作队列是基于进程实现的,所以自然工作队列的延后处理函数是可以进行休眠操作,但是它的优先级低于软中断,低于硬件中断,工作队列延后处理函数中依旧还是去处理中断处理遗留下来的耗时较长或者不紧急的内容。
- Linux内核描述工作队列的数据结构:
struct work_struct {
work_func_t func;
...
};
成员说明:
- func:工作队列的延后处理函数,里面可以进行休眠操作。
配套函数:
//给工作队列添加一个延后处理函数
INIT_WORK(&工作队列对象, 工作队列的延后处理函数);
//向内核登记工作队列的延后处理函数,一旦登记完成
//将来内核在适当的时候调用其延后处理函数
schedule_work(&工作队列对象);
利用工作队列实现延后执行的编程步骤:
- 定义初始化工作队列对象。
struct work_struct btn_work; //定义对象
INIT_WORK(&btn_work, btn_work_function);
- 编写工作队列延后处理,此函数可以进行休眠操作。
//work指针就是指向自己定义初始化的对象
//work=&btn_work
void btn_work_function(struct work_struct *work)
{
//不紧急耗时较长的内容
}
- 向内核登记工作队列的延后处理函数,一旦登记完成内核在适当的时候调用延后处理函数。
schedule_work(&btn_work);
案例实现(使用工作队列在按键后处理执行打印信息)
- btn_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/input.h> //标准按键值:KEY_*
//声明描述按键信息相关的数据结构
struct btn_resource {
int gpio; //按键对应的GPIO编号
char *name; //按键名称
int code; //按键值
};
//定义初始化按键硬件信息对象
static struct btn_resource btn_info[] = {
{
.gpio = PAD_GPIO_A+28,
.name = "KEY_UP",
.code = KEY_UP
},
{
.gpio = PAD_GPIO_B+9,
.name = "KEY_DOWN",
.code = KEY_DOWN
},
};
//记录按键的硬件信息
static struct btn_resource *pdata;
//工作队列延后处理函数
//适当的时候被内核调用
//基于进程实现
//能进行休眠操作
//work=&btn_work
static void btn_work_function(struct work_struct *work)
{
int state;
//1.获取按键对应GPIO的电平状态,获取按键的操作状态
state = gpio_get_value(pdata->gpio);
//2.打印按键信息
printk("底半部:%s:按键值[%d]按键状态[%s]\n",
__func__, pdata->code, state?"松开":"按下");
}
//定义一个工作队列对象
static struct work_struct btn_work;
//顶半部:中断处理函数
//一旦中断到来,先执行顶半部
//不允许CPU资源切换
static irqreturn_t button_isr(int irq, void *dev)
{
//1.获取对应的当前触发的硬件中断的硬件信息
pdata = (struct btn_resource *)dev;
//2.向内核登记底半部延后处理函数,一旦登记
//完成,内核将来在适当的时候调用延后处理函数
schedule_work(&btn_work);
//3.验证顶半部的执行先于延后处理函数
printk("顶半部:%s\n", __func__);
return IRQ_HANDLED; //执行成功
}
static int btn_init(void)
{
int i;
//1.申请GPIO资源
//配置为输入功能由内核已经帮你实现
//2.申请中断资源,注册中断处理函数
//注意:多个按键共享一个中断处理函数
for (i = 0; i < ARRAY_SIZE(btn_info); i++) {
//由GPIO编号求中断号
int irq = gpio_to_irq(btn_info[i].gpio);
gpio_request(btn_info[i].gpio,
btn_info[i].name);
request_irq(irq, //中断号
button_isr,//注册的中断处理函数
IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING,
btn_info[i].name,//中断名称
&btn_info[i] //将每个按键对应的硬件信息传 递给中断处理函数
);
}
//2.初始化工作队列指定延后处理函数
INIT_WORK(&btn_work, btn_work_function);
return 0;
}
static void btn_exit(void)
{
int i;
//1.释放中断资源删除中断处理函数
//2.释放GPIO资源
for(i = 0; i < ARRAY_SIZE(btn_info); i++) {\
int irq = gpio_to_irq(btn_info[i].gpio);
gpio_free(btn_info[i].gpio);
free_irq(irq, &btn_info[i]);
}
}
module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");
- Makefile
obj-m += btn_drv.o
all:
make -C /opt/kernel SUBDIRS=$(PWD) modules
clean:
make -C /opt/kernel SUBDIRS=$(PWD) clean
- 执行实现
底半部实现机制之软中断
软中断特点: tasklet本身是基于软中断,软中断的延后处理函数同样不能进行休眠操作,软中断的延后处理函数可以同时运行在多个CPU核上,而tasklet的延后处理函数同一时刻只能运行在一个CPU核上,所以软中断的延后处理函数在设计的时候务必考虑可重入性,软中断编程实现上不能以insmod/rmmod形式动态的安装和写在,必须和uImage编译在一起,这样软件设计相对比较繁琐,所以就有了tasklet的产生。tasklet本质就是解决软中断设计的繁琐问题(软中断的替代)。
- 基于以上特点,所以这里就暂时不详细描述软中断的底半部实现了。
总结:
- 如果延后执行的内容中,没有休眠操作,用tasklet或者工作队列都可以
- 如果延后执行的内容中,有休眠操作,只能用工作队列
- 如果延后执行的内容中,没有休眠操作,并且考虑效率问题,使用tasklet