linux中断实例

原文:http://edsionte.com/techblog/%E5%86%85%E6%A0%B8%E6%96%B0%E6%89%8B%E5%8C%BA


你的第一个中断程序:

本文通过一个简单的中断程序来描述一般中断程序的基本框架。完整代码在这里。

中断程序一般会包含在某个设备的驱动程序中,因此,接下来的程序本质上还是一个内核模块。说到内核模块,你应该知道首先去看什么了吧?对了,就是内核模块加载函数。
view source
print?
01    static int __init myirq_init()
02    {
03        printk("Module is working..\n");
04        if(request_irq(irq,myirq_handler,IRQF_SHARED,devname,&mydev)!=0)
05        {
06            printk("%s request IRQ:%d failed..\n",devname,irq);
07            return -1;
08        }
09        printk("%s rquest IRQ:%d success..\n",devname,irq);
10        return 0;
11    }

在内核加载函数中,我们除了显示一些信息外,最重要的工作就是申请一根中断请求线,也就是注册中断处理程序。很明显,这一动作是通过 request_irq函数来完成的。这个函数的原型如下:
view source
print?
1    static int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev);

第一个参数是中断号,这个中断号对应的就是中断控制器上IRQ线的编号。

第二个参数是一个irq_handler_t类型个函数指针:
view source
print?
1    typedef irqreturn_t (*irq_handler_t)(int, void *);

handler所指向的函数即为中断处理程序,需要具体来实现。

第三个参数为标志位,可以取IRQF_DISABLED、IRQF_SHARED和IRQF_SAMPLE_RANDOM之一。在本实例程序中取 IRQF_SHARED,该标志表示多个设备共享一条IRQ线,因此相应的每个设备都需要各自的中断服务例程。一般某个中断线上的中断服务程序在执行时会屏蔽请求该线的其他中断,如果取IRQF_DISABLED标志,则在执行该中断服务程序时会屏蔽所有其他的中断。取 IRQF_SAMPLE_RANDOM则表示设备可以被看做是事件随见的发生源。

第四个参数是请求中断的设备的名称。可以在/proc/interface中查看到具体设备的名称,与此同时也可以查看到这个设备对应的中断号以及请求次数,甚至中断控制器的名称。

第五个参数为一个指针型变量。注意此参数为void型,也就是说通过强制转换可以转换为任意类型。这个变量在IRQF_SHARED标志时使用,目的是为即将要释放中断处理程序提供唯一标志。因为多个设备共享一条中断线,因此要释放某个中断处理程序时,必须通过此标志来唯一指定这个中断处理程序。习惯上,会给这个参数传递一个与设备驱动程序对应的设备结构体指针。关于中断程序,可参考这里的文章。

以上就是request_irq函数各个参数的意义。

与中断处理程序的注册相对应的是free_irq函数,它会注销相应的中断处理程序,并释放中断线。这个函数一般被在内核模块卸载函数中被调用。
view source
print?
1    static void __exit myirq_exit()
2    {
3        printk("Module is leaving..\n");
4        free_irq(irq,&mydev);
5        printk("%s request IRQ:%d success..\n",devname,irq);
6    }

如果该中断线不是共享的,那么该函数在释放中断处理程序的同时也将禁用此条中断线。如果是共享中断线,只是释放与mydev对应的中断处理程序。除非该中断处理程序恰好为该中断线上的最后一员,此条中断线才会被禁用。在此处,你也可以感受到mydev的重要性。

下面具体分析中断处理函数。该函数的功能很简单,只是显示一些提示信息。
view source
print?
01    static irqreturn_t myirq_handler(int irq,void* dev)
02    {
03        struct myirq mydev;
04        static int count=1;
05        mydev=*(struct myirq*)dev;
06        printk("key: %d..\n",count);
07        printk("devid:%d ISR is working..\n",mydev.devid);
08        printk("ISR is leaving..\n");
09        count++;
10        return IRQ_HANDLED;
11    }

另外,本内核模块在插入时还需要附带参数,下面的语句首先定义两个参数,然后利用宏module_param宏来接受参数。
view source
print?
1    static int irq;
2    static char* devname;
3    
4    module_param(devname,charp,0644);
5    module_param(irq,int,0644);

使用方法:

1.通过cat /proc/interrupts查看中断号,以确定一个即将要共享的中断号。本程序因为是与键盘共享1号中断线,因此irq=1;

2.使用如下命令就可以插入内核:

sudo insmod filename.ko irq=1 devname=myirq

3.再次查看/proc/interrupts文件,可以发现1号中断线对应的的设备名处多了myirq设备名;

4.dmesg查看内核日志文件,可看到在中断处理程序中所显示的信息;

5.卸载内核模块;

可以看到,内核模块加载后,我们所写中断处理程序是被自动调用的,主要是因为该中断线上有键盘所发出的中断请求,因此内核会执行该中断线上的所有中断处理程序,当然就包括我们上述所写的那个中断处理程序。关于中断处理程序的执行,可参考这里的文章。

这样,一个最基本的中断程序就编写完成了!try!

后记:

这个程序调试起来并不难,但是我们并不能仅仅局限在这个程序本身。以它为入口点深入学习中断的基本原理再好不过。下面给出几个学习的入口点。

1.为何我们的中断程序和其他设备共享了一个中断线后会被执行?或者说,共享中断线上的所有中断服务例程是怎么执行的?

2.中断涉及到那些基本的数据结构?这些数据结构之间有什么关系?

3.do_IRQ()函数的大体执行流程是什么?

亲们,要学习的东西还很多,让我们一起加油吧!

中断下半部-tasklet:
tasklet的实现


tasklet(小任务)机制是中断处理下半部分最常用的一种方法,其使用也是非常简单的。正如在前文中你所知道的那样,一个使用tasklet的中断程序首先会通过执行中断处理程序来快速完成上半部分的工作,接着通过调用tasklet使得下半部分的工作得以完成。可以看到,下半部分被上半部分所调用,至于下半部分何时执行则属于内核的工作。对应到我们此刻所说的tasklet就是,在中断处理程序中,除了完成对中断的响应等工作,还要调用 tasklet,如下图示。

tasklet由tasklet_struct结构体来表示,每一个这样的结构体就表示一个tasklet。在<linux/interrupt.h>中可以看到如下的定义:
view source
print?
1    tasklet_struct
2    {
3        struct tasklet_struct *next;
4        unsigned long state;
5        atomic_t count;
6        void (*func)(unsigned long);
7        unsigned long data;
8    };

在这个结构体中,第一个成员代表链表中的下一个tasklet。第二个变量代表此刻tasklet的状态,一般为 TASKLET_STATE_SCHED,表示此tasklet已被调度且正准备运行;此变量还可取TASKLET_STATE_RUN,表示正在运行,但只用在多处理器的情况下。count成员是一个引用计数器,只有当其值为0时候,tasklet才会被激活;否则被禁止,不能被执行。而接下来的 func变量很明显是一个函数指针,它指向tasklet处理函数,这个处理函数的唯一参数为data。



使用tasklet


在使用tasklet前,必须首先创建一个tasklet_struct类型的变量。通常有两种方法:静态创建和动态创建。这样官方的说法仍然使我们不能理解这两种创建到底是怎么一回事。不够透过源码来分析倒是可以搞明白。

在<linux/interrupt.h>中的两个宏:
view source
print?
1    464#define DECLARE_TASKLET(name, func, data) \
2    465struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
3    466
4    467#define DECLARE_TASKLET_DISABLED(name, func, data) \
5    468struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

就是我们进行静态创建tasklet的两种方法。通过第一个宏创建的tasklet处于激活状态,再通过调度函数被挂起尽而被内核执行;而通过第二个宏创建的tasklet处于禁止状态。从两个宏的定义可以看到,所谓的静态创建就是直接定义个一个名为name的tasklet_struct类型的变量,并将宏中各个参数相应的赋值给这个name变量的各个成员。注意,两个宏在功能上差异就在于对name变量count成员的赋值上,具体原因在第一部分已经说明。也许你对ATOMIC_INIT这样的初始化方式感到疑惑,那么看完定义后,你就会一目了然:
view source
print?
1    //在arch/x86/include/asm/atomic.h中
2    15#define ATOMIC_INIT(i)  { (i) }
3    //在linux/types.h中
4    190typedef struct {
5    191        int counter;
6    192} atomic_t;

与静态创建相对的是动态创建,通过给tasklet_init函数传递一个事先定义的指针,来动态创建一个tasklet。这个函数源码如下。
view source
print?
1    470void tasklet_init(struct tasklet_struct *t,
2    471                  void (*func)(unsigned long), unsigned long data)
3    472{
4    473        t->next = NULL;
5    474        t->state = 0;
6    475        atomic_set(&t->count, 0);
7    476        t->func = func;
8    477        t->data = data;
9    478}

相信你在阅读上面的代码是基本上没有什么难以理解的地方,不过这里还是要特别说明一下atomic_set函数:
view source
print?
1    //在arch/x86/include/asm/atomic.h中
2    35static inline void atomic_set(atomic_t *v, int i)
3    36{
4    37        v->counter = i;
5    38}

首先tasklet_init当中,将&t->count传递给了此函数。也就是说将atomic_t类型的成员count的地址传递给了atomic_set函数。而我们在此函数中却要为count变量中的成员counter赋值。如果说我们当前要使用i,那么应该是如下的引用方法:t-》count.i。明白了吗?

ok,通过上述两种方法就可以创建一个tasklet了。同时,你应该注意到不管是上述那种创建方式都有func参数。透过上述分析的源码,我们可以看到func参数是一个函数指针,它指向的是这样的一个函数:
view source
print?
1    void tasklet_handler(unsigned long data);

如同上半部分的中断处理程序一样,这个函数需要我们自己来实现。

创建好之后,我们还要通过如下的方法对tasklet进行调度:
view source
print?
1    tasklet_schedule(&my_tasklet)

通过此函数的调用,我们的tasklet就会被挂起,等待机会被执行



一个举例


在此只分析上下两部分的调用关系,完整代码在这里查看。
view source
print?
01    //define a argument of tasklet struct
02    static struct tasklet_struct mytasklet;
03    
04    static void mytasklet_handler(unsigned long data)
05    {
06        printk("This is tasklet handler..\n");
07    }
08    
09    static irqreturn_t myirq_handler(int irq,void* dev)
10    {
11        static int count=0;
12        if(count<10)
13        {
14            printk("-----------%d start--------------------------\n",count+1);
15                    printk("The interrupt handeler is working..\n");
16                    printk("The most of interrupt work will be done by following tasklet..\n");
17                    tasklet_init(&mytasklet,mytasklet_handler,0);
18                tasklet_schedule(&mytasklet);
19                    printk("The top half has been done and bottom half will be processed..\n");
20        }
21        count++;
22            return IRQ_HANDLED;
23    }

从代码中可以看到,在上半部中通过调用tasklet,使得对时间要求宽松的那部分中断程序推后执行。

中断下半部-工作队列:
为什么还需要工作队列?


工作队列(work queue)是另外一种将中断的部分工作推后的一种方式,它可以实现一些tasklet不能实现的工作,比如工作队列机制可以睡眠。这种差异的本质原因是,在工作队列机制中,将推后的工作交给一个称之为工作者线程(worker thread)的内核线程去完成(单核下一般会交给默认的线程events/0)。因此,在该机制中,当内核在执行中断的剩余工作时就处在进程上下文(process context)中。也就是说由工作队列所执行的中断代码会表现出进程的一些特性,最典型的就是可以重新调度甚至睡眠。

对于tasklet机制(中断处理程序也是如此),内核在执行时处于中断上下文(interrupt context)中。而中断上下文与进程毫无瓜葛,所以在中断上下文中就不能睡眠。

因此,选择tasklet还是工作队列来完成下半部分应该不难选择。当推后的那部分中断程序需要睡眠时,工作队列毫无疑问是你的最佳选择;否则,还是用tasklet吧。

中断上下文


在了解中断上下文时,先来回顾另一个熟悉概念:进程上下文(这个中文翻译真的不是很好理解,用“环境”比它好很多)。一般的进程运行在用户态,如果这个进程进行了系统调用,那么此时用户空间中的程序就进入了内核空间,并且称内核代表该进程运行于内核空间中。由于用户空间和内核空间具有不同的地址映射,并且用户空间的进程要传递很多变量、参数给内核,内核也要保存用户进程的一些寄存器、变量等,以便系统调用结束后回到用户空间继续执行。这样就产生了进程上下文。

所谓的进程上下文,就是一个进程在执行的时候,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容。当内核需要切换到另一个进程时(上下文切换),它需要保存当前进程的所有状态,即保存当前进程的进程上下文,以便再次执行该进程时,能够恢复切换时的状态继续执行。上述所说的工作队列所要做的工作都交给工作者线程来处理,因此它可以表现出进程的一些特性,比如说可以睡眠等。

对于中断而言,是硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理,中断上下文就可以理解为硬件传递过来的这些参数和内核需要保存的一些环境,主要是被中断的进程的环境。因此处于中断上下文的tasklet 不会有睡眠这样的特性。

工作队列的使用


内核中通过下述结构体来表示一个具体的工作:
view source
print?
1    struct work_struct
2    {
3        unsigned long pending;//这个工作是否正在等待处理
4        struct list_head entry;//链接所有工作的链表,形成工作队列
5        void (*func)(void *);//处理函数
6        void *data;//传递给处理函数的参数
7        void *wq_data;//内部使用数据
8        struct timer_list timer;//延迟的工作队列所用到的定时器
9    };

而这些工作(结构体)链接成的链表就是所谓的工作队列。工作者线程会在被唤醒时执行链表上的所有工作,当一个工作被执行完毕后,相应的 work_struct结构体也会被删除。当这个工作链表上没有工作时,工作线程就会休眠。

通过如下宏可以创建一个要推后的完成的工作:
view source
print?
1    DECLARE_WORK(name,void(*func)(void*),void *data);

也可以通过下述宏动态创建一个工作:
view source
print?
1    INIT_WORK(struct work_struct *work,void(*func)(void*),void *data);

与tasklet类似,每个工作都有具体的工作队列处理函数,原型如下:
view source
print?
1    void work_handler(void *data)

将工作队列机制对应到具体的中断程序中,即那些被推后的工作将会在func所指向的那个工作队列处理函数中被执行。

实现了工作队列处理函数后,就需要schedule_work函数对这个工作进行调度,就像这样:
view source
print?
1    schedule_work(&work);

这样work会马上就被调度,一旦工作线程被唤醒,这个工作就会被执行(因为其所在工作队列会被执行)。


附一个中断实例的代码:http://download.csdn.net/detail/lucien_cc/4216092


  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值