这里来给大家演示一下Linux中的简单的中断程序
一、查看中断
中断常见的应用是在驱动程序中,例如我们的键盘鼠标,每次我们的点按都会产生一个中断,从而让计算机识别。该实验是自己虚拟出一个设备,将该设备注册到系统中,之后用自己编写的中断服务例程来使用中断,首先看一下当前系统中的中断。
输入
cat /proc/interrupts
这是我们系统中当前的中断
第一列:IRQ序号
CPU0/CPU1/CPU3/CPU4分别是当前CPU上发生中断的次数
下一列:中断控制器名称,比如IO-APIC
最后一列:设备名称,比如timer
二、编写中断程序
由中断的知识我们可以知道,我们要写一个中断程序,就必须要申请一根中断线,一个中断线又对应一个IRQ号,,这里我们选择使用1号中断线和i8042共用一条中断线。
这里我们来看一下我们完整的中断程序interrupt.c
# include <linux/kernel.h>
# include <linux/init.h>
# include <linux/module.h>
# include <linux/interrupt.h>
static int irq; //irq号
static char * devname; //设备名称
//这两个是用来让我们在命令行传入参数
module_param(irq,int,0644);
module_param(devname,charp,0644); //这里charp相当于char*,是字符指针
struct myirq
{
int devid; //这个主要用在共享irq中
};
struct myirq mydev={1119};
//中断处理函数
static irqreturn_t myirq_handler(int irq,void * dev)
{
struct myirq mydev;
static int count=1;
mydev = *(struct myirq*)dev;
printk("key: %d..\n",count);
printk("devid:%d ISR is working..\n",mydev.devid);
printk("ISR is leaving......\n");
count++;
return IRQ_HANDLED;
}
//内核模块初始化函数
static int __init myirq_init(void) //最重要的工作是注册中断线,并将自己写的中断服务例程注册进去,用request_irq完成
{
printk("Module is working...\n");
if(request_irq(irq,myirq_handler,IRQF_SHARED,devname,&mydev)!=0)
{
printk("%s request IRQ:%d failed..\n",devname,irq);
return -1;
}
printk("%s request IRQ:%d success...\n",devname,irq);
return 0;
}
//内核模块退出函数
static void __exit myirq_exit(void)
{
printk("Module is leaving...\n");
free_irq(irq,&mydev); //注销函数
printk("Free the irq:%d..\n",irq);
}
MODULE_LICENSE("GPL");
module_init(myirq_init);
module_exit(myirq_exit);
接下来解读一下
request_irq在内核源码 include/linux/interrupt.h中
request_irq的第一个参数irq就是中断号,对应的就是中断控制器上IRQ的编号
第二个参数 handler,即我们要注册的中断服务例程,需要我们自己去实现,它有一个特定类型的返回值irqreturn_t, 这个类型可以在irqreturn.h文件中找到
下一个参数flags,它指定了快速中断或者中断共享中中断的处理属性,在这里对flags的取值是IRQ_SHARED,即允许共享。
参数name:它是设备名称,即我们刚刚中断中使用的cat /proc/interrupt命令来查看的最后一列
参数dev,指dev_id,主要用于共享中断线。注意该参数的类型是void,即可以使用强制转换为任意类型,可以作为共享中断时中断区别的一个参数。
进入request_irq这个函数,实际上这个函数调用了request_threaded_irq函数。这个函数在kerenl/irq/manage.c中。
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
{
struct irqaction *action;
struct irq_desc *desc;
int retval;
if (irq == IRQ_NOTCONNECTED)
return -ENOTCONN;
/*
* Sanity-check: shared interrupts must pass in a real dev-ID,
* otherwise we'll have trouble later trying to figure out
* which interrupt is which (messes up the interrupt freeing
* logic etc).
*
* Also IRQF_COND_SUSPEND only makes sense for shared interrupts and
* it cannot be set along with IRQF_NO_SUSPEND.
*/
if (((irqflags & IRQF_SHARED) && !dev_id) ||
(!(irqflags & IRQF_SHARED) && (irqflags & IRQF_COND_SUSPEND)) ||
((irqflags & IRQF_NO_SUSPEND) && (irqflags & IRQF_COND_SUSPEND)))
return -EINVAL;
desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
if (!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return -EINVAL;
if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
retval = irq_chip_pm_get(&desc->irq_data);
if (retval < 0) {
kfree(action);
return retval;
}
retval = __setup_irq(irq, desc, action);
if (retval) {
irq_chip_pm_put(&desc->irq_data);
kfree(action->secondary);
kfree(action);
}
#ifdef CONFIG_DEBUG_SHIRQ_FIXME
if (!retval && (irqflags & IRQF_SHARED)) {
/*
* It's a shared IRQ -- the driver ought to be prepared for it
* to happen immediately, so let's make sure....
* We disable the irq to make sure that a 'real' IRQ doesn't
* run in parallel with our fake.
*/
unsigned long flags;
disable_irq(irq);
local_irq_save(flags);
handler(irq, dev_id);
local_irq_restore(flags);
enable_irq(irq);
}
#endif
return retval;
}
irqreturn_t在内核源码 include/linux/irqreturn.h中
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef _LINUX_IRQRETURN_H
#define _LINUX_IRQRETURN_H
/**
* enum irqreturn
* @IRQ_NONE interrupt was not from this device or was not handled
* @IRQ_HANDLED interrupt was handled by this device
* @IRQ_WAKE_THREAD handler requests to wake the handler thread
*/
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;
#define IRQ_RETVAL(x) ((x) ? IRQ_HANDLED : IRQ_NONE)
#endif
request_threaded_irq函数。这个函数在kerenl/irq/manage.c
这里首先定义了两个非常重要的结构体,irqaction *action 与 irq_desc,接下来进行一系列的判断,之后会执行“desc = irq_to_desc(irq);”, 这一行是根据中断号irq,在irq_desc数组中,返回一个具体的irq_desc,实际上我们的注册就是要将我们传进来的参数生成一个个的action,再添加到irq_desc上,具体是通过“retval = __setup_irq(irq, desc, action);"这个函数进行的。
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
既然有注册,就会有对应的注销函数,在interrupt.c中,我们将注销函数放在了退出函数中,free_irq(irq,&mydev);
free_irq函数还是在manage.c文件中,主要进行的是action = __free_irq(desc, dev_id);,之后又释放action,kfree(action);
const void *free_irq(unsigned int irq, void *dev_id)
{
struct irq_desc *desc = irq_to_desc(irq);
struct irqaction *action;
const char *devname;
if (!desc || WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return NULL;
#ifdef CONFIG_SMP
if (WARN_ON(desc->affinity_notify))
desc->affinity_notify = NULL;
#endif
action = __free_irq(desc, dev_id);
if (!action)
return NULL;
devname = action->name;
kfree(action);
return devname;
}
接下来看中断服务例程。
//中断处理函数
static irqreturn_t myirq_handler(int irq,void * dev)
{
struct myirq mydev;
static int count=1;
mydev = *(struct myirq*)dev;
printk("key: %d..\n",count);
printk("devid:%d ISR is working..\n",mydev.devid);
printk("ISR is leaving......\n");
count++;
return IRQ_HANDLED;
}
interrupt.c中,给中断服务例程起名为myirq_handler,对应的名字要与注册中使用的名字一致。在myirq_handler中实际上也就是一个计数,即进入中断一次,count自增1,之后返回的是IRQ_HANDLER。前面说到这里返回的值代表接收到了准确的中断信号并且做了相应的正确处理,到此为止,模块就写完了。
三、插入模块
之后就是插入模块,只要1号中断线上发生了中断,不管谁发出的,都会执行我们的中断服务例程。
Makefile文件如下:
book@100ask:~/Mooc/CH05$ cat Makefile
# Makefile文件注意:加入前面的.c文件起名为first.c,那么这里的Makefile文件中的.o文件就要起名first.o
# # 只有root用户才能加载和卸载模块
obj-m:=interrupt.o # 产生first模块的目标文件
# #目标文件 文件 要与模块名字相同
CURRENT_PATH:=$(shell pwd)
LINUX_KERNEL:=$(shell uname -r)
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
#
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules # 编译模块
# #[TAB] 内核的路径 当前目录编译完放哪里? 表明编译的是内核模块
#
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean # 清理模块
先make编译
插入模块,并且给irq,devname赋值
sudo insmod interrupt.ko irq=1 devname=myirq
查看中断情况
cat /proc/interrupts
发现,我们已经将设备myirq挂到了1号中断线上
我们来查看一下日志信息dmesg
可以看到程序正确的运行了,每进入一次中断就会显示一些信息
退出模块
sudo rmmod interrupt
模块已经正确退出了
四、分析内核源代码
此实验只是简单体验了一下中断的程序,实际上可以从源代码中验证我们在理论中学到的东西。从中断的发生到真正执行到我们的中断服务例程,中间的过程是怎么样的.
这实际上是一个非常复杂的过程,如果要追根溯源的话,我们可以从内核的初始化开始,其实这个过程是非常有意思的,我们可以找到我们内核中的main函数,从初始化我们的中断到真正执行我们的中断服务例程,我们在这里就从do_IRQ函数来看起。
我们每次进入do_IRQ函数都是从汇编的代码中跳入,可以说发生了中断之后,do_IRQ是我们执行的第一个C语言函数,我们就从这里开始分析。这里的代码与体系结构息息相关,我们的do_IRQ函数位于根目录下的arch/x86/kernel/irq.c
这个函数最核心的部分是执行handle_irq函数,从handle_irq进入,执行完后返回到exiting_irq(),
/*
* do_IRQ handles all normal device IRQ's (the special //do_IRQ处理所有普通设备的中断
* SMP cross-CPU interrupts have their own specific
* handlers).
*/
__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
struct irq_desc * desc;
/* high bit used in ret_from_ code */
unsigned vector = ~regs->orig_ax;
entering_irq();
/* entering_irq() tells RCU that we're not quiescent. Check it. */
RCU_LOCKDEP_WARN(!rcu_is_watching(), "IRQ failed to wake up RCU");
desc = __this_cpu_read(vector_irq[vector]);
if (!handle_irq(desc, regs)) {
ack_APIC_irq();
if (desc != VECTOR_RETRIGGERED && desc != VECTOR_SHUTDOWN) {
pr_emerg_ratelimited("%s: %d.%d No irq handler for vector\n",
__func__, smp_processor_id(),
vector);
} else {
__this_cpu_write(vector_irq[vector], VECTOR_UNUSED);
}
}
exiting_irq();
set_irq_regs(old_regs);
return 1;
}
进入 arch/x86/kernel/irq_64.c, 发现在handle_irq函数中,最后还是执行了generic_handle_irq_desc()函数,传入的参数是desc,即struct irq_desc类型的参数,
bool handle_irq(struct irq_desc *desc, struct pt_regs *regs)
{
if (IS_ERR_OR_NULL(desc))
return false;
generic_handle_irq_desc(desc);
return true;
}
进入/include/linux/irqdesc.h, 发现generic_handle_irq_desc 最后还是要执行desc下的handel_irq函数,
static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
desc->handle_irq(desc);
}
这里需要说明一下,这里的函数是handel_irq函数,它是在irq_desc结构中,
hadle_irq也是一根回调函数,负责处理底层的细节,比如中断确认,边沿触发以及电平触发的处理。最后再由这个函数去调用irqaction中的handler,
实际上,handler才是最后我们想要的中断服务例程。
在这里,就以APIC的注册为例,来看一下handle_irq中回调的是哪个函数。
我们进入/arch/x86/kernel/apic/io_apic.c, 在这里找到handle_irq的挂载过程。
在如下函数的最后两行可以看到其实在这里(hdl = fasteoi ? handle_fasteoi_irq : handle_edge_irq;)是进行一个选择,是哪种方式。后面将会按照handle_edge_irq的方式,即边沿触发的方式来分析。
实际上执行的是__irq_set_handler这个函数,我们来进入这个函数
static void mp_register_handler(unsigned int irq, unsigned long trigger)
{
irq_flow_handler_t hdl;
bool fasteoi;
if (trigger) {
irq_set_status_flags(irq, IRQ_LEVEL);
fasteoi = true;
} else {
irq_clear_status_flags(irq, IRQ_LEVEL);
fasteoi = false;
}
hdl = fasteoi ? handle_fasteoi_irq : handle_edge_irq;
__irq_set_handler(irq, hdl, 0, fasteoi ? "fasteoi" : "edge");
}
/kernel/irq/chip.c
void
__irq_set_handler(unsigned int irq, irq_flow_handler_t handle, int is_chained,
const char *name)
{
unsigned long flags;
struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, 0);
if (!desc)
return;
__irq_do_set_handler(desc, handle, is_chained, name);
irq_put_desc_busunlock(desc, flags);
}
EXPORT_SYMBOL_GPL(__irq_set_handler);
接下来会进入__irq_do_set_handler函数。这个函数比较长,它会有一系列的判断,最关键的语句是”desc->handle_irq = handle“,即将handle挂载到了handle_irq上。
/kernel/irq/chip.c
static void
__irq_do_set_handler(struct irq_desc *desc, irq_flow_handler_t handle,
int is_chained, const char *name)
{
if (!handle) {
handle = handle_bad_irq;
} else {
struct irq_data *irq_data = &desc->irq_data;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
/*
* With hierarchical domains we might run into a
* situation where the outermost chip is not yet set
* up, but the inner chips are there. Instead of
* bailing we install the handler, but obviously we
* cannot enable/startup the interrupt at this point.
*/
while (irq_data) {
if (irq_data->chip != &no_irq_chip)
break;
/*
* Bail out if the outer chip is not set up
* and the interrupt supposed to be started
* right away.
*/
if (WARN_ON(is_chained))
return;
/* Try the parent */
irq_data = irq_data->parent_data;
}
#endif
if (WARN_ON(!irq_data || irq_data->chip == &no_irq_chip))
return;
}
/* Uninstall? */
if (handle == handle_bad_irq) {
if (desc->irq_data.chip != &no_irq_chip)
mask_ack_irq(desc);
irq_state_set_disabled(desc);
if (is_chained)
desc->action = NULL;
desc->depth = 1;
}
desc->handle_irq = handle;
desc->name = name;
if (handle != handle_bad_irq && is_chained) {
unsigned int type = irqd_get_trigger_type(&desc->irq_data);
/*
* We're about to start this interrupt immediately,
* hence the need to set the trigger configuration.
* But the .set_type callback may have overridden the
* flow handler, ignoring that we're dealing with a
* chained interrupt. Reset it immediately because we
* do know better.
*/
if (type != IRQ_TYPE_NONE) {
__irq_set_trigger(desc, type);
desc->handle_irq = handle;
}
irq_settings_set_noprobe(desc);
irq_settings_set_norequest(desc);
irq_settings_set_nothread(desc);
desc->action = &chained_action;
irq_activate_and_startup(desc, IRQ_RESEND);
}
}
这里以handle_edge_irq为例来分析,
/kernel/irq/chip.c
在该函数中,会找到一个非常重要的函数handle_irq_event(),
void handle_edge_irq(struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
if (!irq_may_run(desc)) {
desc->istate |= IRQS_PENDING;
mask_ack_irq(desc);
goto out_unlock;
}
/*
* If its disabled or no action available then mask it and get
* out of here.
*/
if (irqd_irq_disabled(&desc->irq_data) || !desc->action) {
desc->istate |= IRQS_PENDING;
mask_ack_irq(desc);
goto out_unlock;
}
kstat_incr_irqs_this_cpu(desc);
/* Start handling the irq */
desc->irq_data.chip->irq_ack(&desc->irq_data);
do {
if (unlikely(!desc->action)) {
mask_irq(desc);
goto out_unlock;
}
/*
* When another irq arrived while we were handling
* one, we could have masked the irq.
* Renable it, if it was not disabled in meantime.
*/
if (unlikely(desc->istate & IRQS_PENDING)) {
if (!irqd_irq_disabled(&desc->irq_data) &&
irqd_irq_masked(&desc->irq_data))
unmask_irq(desc);
}
handle_irq_event(desc);
} while ((desc->istate & IRQS_PENDING) &&
!irqd_irq_disabled(&desc->irq_data));
out_unlock:
raw_spin_unlock(&desc->lock);
}
/kernel/irq/handle.c
该函数执行的最主要的语句是handle_irq_event_percpu,
irqreturn_t handle_irq_event(struct irq_desc *desc)
{
irqreturn_t ret;
desc->istate &= ~IRQS_PENDING;
irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
raw_spin_unlock(&desc->lock);
ret = handle_irq_event_percpu(desc);
raw_spin_lock(&desc->lock);
irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
return ret;
}
进入handle_irq_event_percpu这个函数,该函数就实现了依次执行这条中断线上的所有中断服务例程,通过for_each_action_of_desc 这个宏来实现。 首先执行中断服务例程的是”res = action->handler(irq, action->dev_id);“这一句,将对应的返回值给了res,在后面可以在switch语句中可以看到,res的两个参数是“IRQ_WAKE_THREAD:” 和”IRQ_HANDLED“, 我们看一下for_each_action_of_desc 这个宏
/kernel/irq/handle.c
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
irqreturn_t retval = IRQ_NONE;
unsigned int irq = desc->irq_data.irq;
struct irqaction *action;
record_irq_time(desc);
for_each_action_of_desc(desc, action) {
irqreturn_t res;
trace_irq_handler_entry(irq, action);
res = action->handler(irq, action->dev_id);
trace_irq_handler_exit(irq, action, res);
if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pS enabled interrupts\n",
irq, action->handler))
local_irq_disable();
switch (res) {
case IRQ_WAKE_THREAD:
/*
* Catch drivers which return WAKE_THREAD but
* did not set up a thread function
*/
if (unlikely(!action->thread_fn)) {
warn_no_thread(irq, action);
break;
}
__irq_wake_thread(desc, action);
/* Fall through - to add to randomness */
case IRQ_HANDLED:
*flags |= action->flags;
break;
default:
break;
}
retval |= res;
}
return retval;
}
for_each_action_of_desc 这个宏位于/kernel/irq/internals.h中,可以清晰的看到,这其实是一个for循环进行遍历,desc中的action,action的下一个
#define for_each_action_of_desc(desc, act) \
for (act = desc->action; act; act = act->next)
将刚刚所说的过程进行一个小结:
首先在汇编代码中进入do_IRQ函数(call do_IRQ)
如下对应32位系统
如下对应64位系统
接下来执行do_IRQ, handle_irq
最后执行这个函数的结果,是执行handle_irq,也就是对应下面的handle_irq函数
之后这里的函数会回调上一个函数,这里以handle_edge_irq为例,那么经过一层一层的调用,调用了最后才执行了中断服务例程。
这就是我们整体的一个过程
**如果以上内容对您有用的话,麻烦点赞、关注或者收藏哦~!**