Linux 中断

中断简介

根据中断的来源,中断可分为内部中断和外部中断。内部中断的中断源来自 CPU 的内部(软件中断、溢出、除法错误等);外部中断的中断源来自 CPU 外部,由外设提出请求。

根据中断是否可以屏蔽,中断分为可屏蔽中断和不可屏蔽中断(NMI,Not Masked Interrupt)。

根据中断入口跳转方法的不同,中断可分为向量中断和非向量中断。向量中断由硬件提供中断服务程序入口地址,非向量中断由软件提供中断服务程序入口地址。


Linux 中断处理程序架构

Linux 将中断处理程序分解为两个半部:顶半部(Top Half)和底半部(Bottom Half)。

顶半部用于完成尽量少的比较紧急的功能,它往往只是简单地读取寄存器中的中断状态,并在清除中断标志后就进行“登记中断”的工作。“登记中断”意味着将底半部处理程序挂到该设备的底半部执行队列中去。这样,顶半部执行的速度就会很快,从而可以服务更多的中断请求。

小提示:在 Linux 中查看 /proc/interrupts 文件可以获得系统中中断的统计信息。


Linux 申请和释放中断

1. 申请irq

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, 

               const char *name, void *dev)

irq:要申请的硬件中断号

handler:向系统登记的中断处理函数(顶半部),是一个回调函数。中断发生时,系统调用这个函数,dev参数将被传递给它。

irqflags:中断处理属性,可以指定中断的触发方式及处理方式。

name:中断名字

dev:要传递给中断服务程序的私有数据,一般设置为这个设备的设备结构体或者 NULL。

返回值:0 表示成功;-EINVAL 表示中断号无效或处理函数指针为 NULL,-EBUSY 表示中断已经被占用且不能共享。

int devm_request_irq(struct device *dev, unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id)

此函数与 request_irq() 的区别是 devm_ 开头的 API 申请的是内核 “managed”的资源,一般不需要在出错处理和 remove() 接口里再显式的释放。

2. 释放 irq

与 request_irq() 相对应的函数为 free_irq(),free_irq() 的原型为:

void free_irq(unsigned int irq, void *dev_id)。


使能和屏蔽中断

1. 使能和屏蔽一个中断源

屏蔽一个中断源的函数如下:

void disable_irq(int irq);

void disable_irq_nosync(int irq);

disable_irq_nosync() 和 disable_irq() 的区别在于前者立即返回,而后者等待目前的中断处理完成。

由于 disable_irq() 会等待指定的中断被处理完,因此如果在 n 号中断的顶半部调用 disable_irq(n),会引起系统的死锁,这种情况下,只能调用 disable_irq_nosync(n)。

使能一个中断源的函数如下:

void enable_irq(int irq);

2. 使能和屏蔽本 CPU 内的所有中断

屏蔽本 CPU 内的所有中断的函数(或宏)如下:

#define local_irq_save(flags) ...

void local_irq_disable(void);

前者会将目前的中断状态保留在 flags 中(注意 flags 为 unsigned long 类型,被直接传递,而不是通过指针),后者直接禁止中断而不是保存状态。

与上述两个禁止中断对应的恢复中断的函数(或宏)为:

#define local_irq_restore(flags) ...

void local_irq_enable(void);

注:以 local_ 开头的方法的作用范围是本 CPU 内。


底半部机制

Linux 实现底半部的机制主要有 tasklet、工作队列、软中断和线程化 irq。

1. tasklet

tasklet 的使用较简单,它的执行上下文是软中断,执行时机通常是顶半部返回的时候。

使用步骤如下:

第1步:定义 tasklet 及其处理函数,并将两者关联

void xxx_do_tasklet(unsigned long data);

DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, data);

第2步:调用 tasklet

使用函数 tasklet_schedule(&xxx_tasklet) 调用即可。

使用模板如下:

/* 定义 tasklet 和底半部函数并将它们关联 */
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx tasklet, xxx_do_tasklet, 0);

/* 中断处理底半部 */
void xxx_do_tasklet(unsigned long)
{
	...
}

/* 中断处理顶半部 */
irqreturn xxx_interrupt(int irq, void *dev_id)
{
	...
	tasklet_schedule(&xxx_tasklet);
	...
}

/* 设备驱动模板加载函数 */
int __init xxx_init(void)
{
	...
	/* 申请中断 */
	result = request_irq(xxx_irq, xxx_interrupt, 0, "xxx", NULL);
	...
	return IRQ_HANDLED;
}

/* 设备驱动模板卸载函数 */
void __exit xxx_exit(void)
{
	...
	/* 释放中断 */
	free_irq(xxx_irq, xxx_interrupt);
	...
}

2. 工作队列

工作队列的使用方法和 tasklet 非常类似,但是工作队列的执行上下文是内核线程,因此可以调度和睡眠。

使用步骤如下:

第1步:定义工作队列和处理函数

struct work_struct my_wq;

void my_wq_func(struct work_struct *work);

第2步:初始化工作并将工作队列与处理函数绑定

INIT_WORK(&my_wq, my_wq_func);

第3步:调度工作队列

与 tasklet 调度类似,直接调用函数 schecule_work(&my_wq); 即可。

使用模板如下:

/* 定义工作队列和关联函数 */
struct work_struct xxx_wq;
void xxx_do_work(struct work_struct *work);

/* 中断处理底半部 */
void xxx_do_work(unsigned long)
{
	...
}

/* 中断处理顶半部 */
irqreturn xxx_interrupt(int irq, void *dev_id)
{
	...
	schedule_work(&xxx_wq);
	...
}

/* 设备驱动模板加载函数 */
int __init xxx_init(void)
{
	...
	/* 申请中断 */
	result = request_irq(xxx_irq, xxx_interrupt, 0, "xxx", NULL);
	...
	/* 初始化工作队列 */
	INIT_WORK(&xxx_wq, &xxx_do_work);
}

/* 设备驱动模板卸载函数 */
void __exit xxx_exit(void)
{
	...
	/* 释放中断 */
	free_irq(xxx_irq, xxx_interrupt);
	...
}
3. 软中断

软中断(Softirq)也是一种传统的底半部处理机制,它的执行时机通常是顶半部返回的时候,tasklet是基于软中断实现的,因此也运行于软中断上下文。

在 Linux 内核中,用 softirq_action 结构体表征了一个软中断,这个结构体包含软中断处理函数指针和传递给该函数的参数。使用 open_softirq() 函数可以注册软中断对应的处理函数,而 raise_softirq() 函数可以出发一个软中断。

4. threaded_irq

在内核中,除了可以通过 request_irq()、devm_request_irq() 申请中断以外,还可以通过 request_threaded_irq() 和 devm_request_threaded_irq() 申请,函数原型如下:

int request_threaded_irq(unsigned int irq, irq_handler_t handler,
    irq_handler_t thread_fn,
    unsigned long flags, const char *name, void *dev);

int devm_request_threaded_irq(struct device *dev, unsigned int irq,
 irq_handler_t handler, irq_handler_t thread_fn,
 unsigned long irqflags, const char *devname,
 void *dev_id);

它们比 request_irq()、devm_request_irq() 多了一个参数 thread_fn。用这两个 API 申请中断的时候,内核会为相应的中断分配一个对应的内核线程。

参数 handler 对应的函数执行于中断上下文,参数 thread_fn 对应的函数执行于内核线程。如果 handler 结束的时候,返回值是 IRQ_WAKE_THREAD,内核会调度对应线程执行 thread_fn 对应的函数。

中断共享

多个设备共享一根硬件中断线的情况在实际的硬件系统中广泛存在,Linux 支持这种中断共享。中断共享的使用方法如下:

(1)共享中断的多个设备在申请中断时,都应该使用 IRQF_SHARED 标志,而且一个设备以 IRQF_SHARED 申请某中断成功的前提是该中断未被申请,或该中断虽然被申请了,但是之前申请该中断的所有设备也都以 IRQF_SHARED 标志申请该中断。

(2)尽管内核模块可访问的全部地址都可以作为 request_irq(..., void *dev_id) 的最后一个参数 dev_id,当时设备结构体指针显然是可传入的最佳参数。

(3)在中断到来时,会遍历执行共享此中断的所有中断处理程序,知道某一个函数返回 IRQ_HANDLED。在中断处理程序顶半部中,应根据硬件寄存器中的信息比照传入的 dev_id 参数迅速判断是否为本设备的中断,若不是,应迅速返回 IRQF_NONE。

使用模板如下:

/* 中断处理顶半部 */
irqreturn xxx_interrupt(int irq, void *dev_id)
{
	...
	int status = read_int_status();						/* 获知中断源 */
	if (!is_myint(dev_id, status))						/* 判断是否为本设备中断 */
		return IRQ_NONE;								/* 不是本设备中断,立即返回 */
	...	
	/* 是本设备中断,进行处理 */
	...
	return IRQ_HANDLED;									/* 返回 IRQ_HANDLED 表明中断已被处理 */
}

/* 设备驱动模板加载函数 */
int __init xxx_init(void)
{
	...
	/* 申请共享中断 */
	result = request_irq(xxx_irq, xxx_interrupt, IRQF_SHARED, "xxx", NULL);
	...
	return IRQ_HANDLED;
}

/* 设备驱动模板卸载函数 */
void __exit xxx_exit(void)
{
	...
	/* 释放中断 */
	free_irq(xxx_irq, xxx_interrupt);
	...
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值