中断处理

转自:http://blog.chinaunix.net/uid-25445243-id-4052877.html

一、中断申请和释放函数
1.1、申请函数

        在linux内核中用于申请中断的函数是request_irq(),函数原型在Kernel/irq/manage.c中定义:

        int request_irq(unsigned int irq, irq_handler_t handler,
                                     unsigned long irqflags, const char *devname, void *dev_id)

        功能:在发生对应于第 1个参数 irq 的中断时,则调用第2个参数handler 为要注册的中断服务函数(也就是把handler()
中断服务函数注册到内核中)。

        参数说明:

        irq是要申请的硬件中断号。

 

        handler是向系统注册的中断处理函数,是一个回调函数,中断发生时,系统调用这个函数,dev_id参数将被传递给它。        

        irqflags指定了快速中断或中断共享等中断处理属性。
            IRQF_DISABLED:表示中断处理程序是快速处理程序,快速处理程序被调用时屏蔽所有中断,慢速处理程序不屏蔽。
            IRQF_SHARED:表示多个设备共享中断。

            IRQF_SAMPLE_RANDOM:表示对系统熵有贡献,对系统获取随机数有好处。

        这几个flag是可以通过或的方式同时使用的。

        devname设置中断名称,通常是设备驱动程序的名称  在cat /proc/interrupts中可以看到此名称。

        dev_id在中断共享时会用到,一般设置为这个设备的设备结构体或者NULL。建议将设备结构指针作为dev_id参数。中
断共享注册时的注册函数中的dev_id参数是必不可少的,并且dev_id的值必须唯一。
        返回值:

        request_irq()返回0表示成功,返回-INVAL表示中断号无效或处理函数指针为NULL,返回-EBUSY表示中断已经被占用

且不能共享。

        中断处理例程限制:

        处理例程不能向用户空间发送或者接收数据,因为它不是在任何进程的上下文中执行的,处理例程也不能做任何可能发生

休眠的操作,例如调用wait_event、使用不带GFP_ATOMIC标志的内存分配操作、或者锁住一个信号量等等,最后处理例程不

能调用schdule函数。

        中断处理例程的一个典型任务:如果中断通知进程所等待的事件已经发生,比如新数据到达,就会唤醒在该设备上休眠的

进程。比如在读每一个数据之前,read调用都是阻塞的,每当新的数据到达时,中断处理例程就会唤醒此进程。
        中断处理例程的返回值:如果发现其设备的确需要处理,应该返回IRQ_HANDLED,否则,返回值应该是IRQ_NONE。

1.2、注销函数

        注销函数定义在Kernel/irq/manage.c中定义: 

         void free_irq(unsigned int irq, void *dev_id)

        返回值:

        函数运行正常时返回0,否则返回对应错误的负值。

1.3、共享中断

        共享中断的不同设备的iqraction结构体都会添加进该中断号对应的 irq_desc结构体的action成员所指向的irqaction链表

内。当内核发生中断时,它会依次调用该链表内所有的handler函数。因此若驱动程序需要使用共享中断机制,其中断处理函数

必须有能力识别是否是自己的硬件产生了中断。通常是通过读取该硬件设备提供的中断flag标志位进行判断。        
        共享中断通过request_irq安装,注意两点:

        1、请求中断时,必须指定flags参数中的SA_SHIRQ位。

        2、dev_id参数必须是唯一的,任何指向模块地址空间的指针都可以使用,但dev_id不能设置成NULL。

        当请求一个共享中断时,如果满足下面条件之一,那么request_irq就会成功:

        1、中断信号线空闲。

        2、任何已经注册了该中断信号线的处理例程也标识了IRQ是共享的。

1.4、常用宏

        1、中断类型: 在request_irq(irq, handler, flags, devname, dev_id)中使用

        #define SA_SHIRQ            共享中断(旧版本的,2.6.19之前的内核)
        #define IRQF_SHARED         共享中断(新版本的)
        #define SA_INTERRUPT        快速中断(旧版本的)
        #define IRQF_DISABLED       快速中断(新版本的)
        #define IRQF_SAMPLE_RANDOM  表示本中断源可以用作随机数生成器的熵池
        2、中断的触发类型: 在set_irq_type(irq, type)中使用
        #define IRQ_TYPE_NONE           0x00000000     未指明类型
        #define IRQ_TYPE_EDGE_RISING    0x00000001     上升沿触发
        #define IRQ_TYPE_EDGE_FALLING   0x00000002     下降沿触发
        #define IRQ_TYPE_EDGE_BOTH      (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
        #define IRQ_TYPE_LEVEL_HIGH     0x00000004     高电平触发
        #define IRQ_TYPE_LEVEL_LOW      0x00000008     低电平触发
        #define IRQ_TYPE_SENSE_MASK     0x0000000f     /* Mask of the above */
        #define IRQ_TYPE_PROBE          0x00000010     /* Probing in progress */
二、顶半部和底半部

        定义:Linux通过将中断处理例程分成两个部分来解决这个问题。称为顶半部的部分,是实际响应中断的例程,也就是request_irq

注册的中断例程。而所谓的底半部是一个被顶半部调度,并在稍后更安全的时间内执行的例程。顶半部和底半部处理例程之间的

最大不同,就是当底半部处理例程执行时,所有的中断都是打开的----这就是所谓的在更安全时间内运行。

        典型的情况是顶半部保存设备的数据到一个设备特定的缓冲区并调度它的底半部,然后退出。然后底半部执行其他的必要工

作,例如唤醒进程、启动另外的I/O操作等等。这种方式允许在底半部工作期间,顶半部还可以继续为新的中断服务。

        Linux内核有两种不同的机制可以用来实现底半部处理,tasklet通常是底半部处理的优选机制,因为这种机制非常快,但是

所有的tasklet代码必须是原子的。除了tasklet之外,还可以选择工作队列,它可以具有更高的延迟,但允许休眠。

2.1、tasklet

        在编写设备驱动时, tasklet 机制是一种比较常见的机制。
        tasklet是一个可以在由系统决定的安全时刻,在软件中断上下文被调度运行的特殊函数。通常用于减少中断处理的时间,将
本应该是在中断服务程序中完成的任务转化成软中断完成。为了最大程度的避免中断处理时间过长而导致中断丢失,有时候我们
需要把一些在中断处理中不是非常紧急的任务放在后面执行,而让中断处理程序尽快返回。它们可以被多次调度运行,但tasklet
的调度并不会累积,也就是说,实际只会运行一次。
        但是tasklet可以与其他的tasklet并行地运行在对称多处理器(SMP)系统上,这样,如果驱动程序有多个tasklet,它们必须
使用某种锁机制来避免彼此间的冲突。tasklet可确保和第一次调度它们的函数运行在同样的CPU上。tasklet和中断处理例程之
间的锁是必须的。

        tasklet的使用比较简单,只需要定义tasklet及其处理函数并将两者关联即可,在定义时可以采用两种形式。

        例:
        struct tasklet_struct my_tasklet;

        Void my_tasklet_func(unsigned long);
        第一种:

        DECLARE_TASKLET(my_tasklet, my_tasklet_func, data)

        代码DECLARE_TASKLET实现了定义名称为my_tasklet的tasklet并将其与my_tasklet_func这个函数绑定,而传入这个函
数的参数为data。
        第二种:
        tasklet_init(&my_tasklet, my_tasklet_func, data);

        需要调度tasklet的时候引用一个tasklet_schedule()函数就能使系统在适当的时候进行调度,如下所示

        tasklet_schedule(&my_tasklet)

        下面给出驱动模板

点击(此处)折叠或打开

  1. void xxx_do_tasklet(unsigned long);
  2. DECLARE_TASKLET(xxx_tasklet,xxx_do_tasklet,0); 
  3. void xxx_do_tasklet(unsigned long)
  4. {
  5.       ……
  6. }
  7. irqreturn_t xxx_interrupt(int irq,void *dev_id,struct pt_regs *regs)
  8. {
  9.       ……
  10.       tasklet_schedule(&xxx_tasklet);
  11.       ……
  12. int _init xxx_init(void)
  13. {
  14.       ……
  15.    result=request_irq(xxx_irq,xxx_interrupt,SA_INTERRUPT,”xxx”,NULL)
  16.       ……
  17. void _exit xxx_exit(void)
  18. {
  19.       ……
  20.       free_irq(xxx_irq,xxx_irq_interrupt);
  21.       ……
  22. }

        tasklet函数详解

点击(此处)折叠或打开

  1. #include <linux/interrupt.h>
  2. struct tasklet_struct {
  3.     struct tasklet_struct *next; 
  4.     unsigned long state; 
  5.     atomic_t count; 
  6.     void (*func)(unsigned long); 
  7.     unsigned long data; 
  8. }; 
  9. void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data); 
  10. #define DECLARE_TASKLET(name, func, data)  struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
  11. #define DECLARE_TASKLET_DISABLED(name, func, data)  struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data } 

        1) void tasklet_disable(struct tasklet_struct *t);

            函数暂时禁止给定的 tasklet被 tasklet_schedule 调度,直到这个 tasklet 再次被enable;若这个 tasklet 当前在运行,

这个函数忙等待直到这个tasklet退出。

        2) void tasklet_disable_nosync(struct tasklet_struct *t);

            和tasklet_disable类似,但是tasklet可能仍然运行在另一个 CPU。

        3) void tasklet_enable(struct tasklet_struct *t);

            使能一个之前被disable的 tasklet。若这个 tasklet 已经被调度, 它会很快运行。 tasklet_enable和tasklet_disable必须

匹配调用, 因为内核跟踪每个 tasklet 的"禁止次数"。

        4) void tasklet_schedule(struct tasklet_struct *t);

            调度 tasklet 执行,如果tasklet在运行中被调度, 它在完成后会再次运行; 这保证了在其他事件被处理当中发生的事件受到应

有的注意. 这个做法也允许一个 tasklet 重新调度它自己。

        5) void tasklet_hi_schedule(struct tasklet_struct *t);

            和tasklet_schedule类似,只是在更高优先级执行。当软中断处理运行时, 它处理高优先级 tasklet。在其他软中断之前,只

有具有低响应周期要求的驱动才应使用这个函数, 可避免其他软件中断处理引入的附加周期。

        6) void tasklet_kill(struct tasklet_struct *t);

            确保了 tasklet 不会被再次调度来运行,通常当一个设备正被关闭或者模块卸载时被调用。如果 tasklet正在运行,这个函数

等待直到它执行完毕。若tasklet重新调度它自己,则必须阻止在调用 tasklet_kill前它重新调度它自己,如同使用 del_timer_sync。

2.2、工作队列

        定义:工作队列会在将来的某个时间、在某个特殊的工作者进程上下文中调用一个函数。因为工作队列函数运行在进程上下文中,
因此可在必要时休眠。但是不能从工作队列向用户空间复制数据,工作者进程无法访问其他任何进程的地址空间。

        Linux中的Workqueue机制就是为了简化内核线程的创建。通过调用workqueue的接口就能创建内核线程。并且可以根据当前

系统CPU的个数创建线程的数量,使得线程处理的事务能够并行化。workqueue是内核中实现简单而有效的机制,他显然简化了内核

daemon的创建,方便了用户的编程。

2.2.1、Workqueue机制的实现

        Workqueue机制中定义了两个重要的数据结构,分析如下:

        1、cpu_workqueue_struct 结构。
        该结构将CPU和内核线程进行了绑定。在创建workqueue的过程中,Linux根据当前系统CPU的个数创建 cpu_workqueue_struct。
该结构主要维护了一个任务队列,以及内核线程需要睡眠的等待队列,另外还维护了一个任务上下文,即 task_struct。

        2、work_struct结构是对任务的抽象。在该结构中需要维护具体的任务方法,需要处理的数据,以及任务处理的时间。该结构定义

如下:

点击(此处)折叠或打开

  1. struct work_struct {
  2.     unsigned long pending;
  3.     struct list_head entry; 
  4.     void (*func)(void *); 
  5.     void *data; 
  6.     void *wq_data; 
  7.     strut timer_list timer; 
  8. };      

        当用户调用workqueue的初始化接口create_workqueue或者create_singlethread_workqueue对 workqueue队列进行初始化

时,内核就开始为用户分配一个workqueue对象,并且将其链到一个全局的workqueue队列中。然后 Linux根据当前CPU的情况,为

workqueue对象分配与CPU个数相同的cpu_workqueue_struct对象,每个 cpu_workqueue_struct对象都会存在一条任务队列。紧接

着,Linux为每个cpu_workqueue_struct对象分配一个内核thread,即内核daemon去处理每个队列中的任务。至此,用户调用初始化

接口将workqueue初始化完毕,返回workqueue的指针。

       在初始化workqueue过程中,内核需要初始化内核线程,注册的内核线程工作比较简单,就是不断的扫描对应cpu_workqueue_struct

中的任务队列,从中获取一个有效任务,然后执行该任务。所以如果任务队列为空,那么内核daemon就在cpu_workqueue_struct中的等

待队列上睡眠,直到有人唤醒daemon去处理任务队列。

       Workqueue初始化完毕之后,将任务运行的上下文环境构建起来了,但是具体还没有可执行的任务,所以,需要定义具体的work_struct
对象。然后将work_struct加入到任务队列中,Linux会唤醒daemon去处理任务。

      上述描述的workqueue内核实现原理可以描述如下:

   

        在Workqueue机制中,提供了一个系统默认的workqueue队列——keventd_wq,这个队列是Linux系统在初始化的时候就创建的。
用户可以直接初始化一个work_struct对象,然后在该队列中进行调度,使用更加方便。

2.2.2、Workqueue编程接口

序号

接口函数

说明

1

create_workqueue

用于创建一个workqueue队列,为系统中的每个CPU都创建一个内核线程。输入参数:

@name:workqueue的名称

2

create_singlethread_workqueue

用于创建workqueue,只创建一个内核线程。输入参数:

@name:workqueue名称

3

destroy_workqueue

释放workqueue队列。输入参数:

@ workqueue_struct:需要释放的workqueue队列指针

4

schedule_work

调度执行一个具体的任务,执行的任务将会被挂入Linux系统提供的workqueue——keventd_wq输入参数:

@ work_struct:具体任务对象指针

5

schedule_delayed_work

延迟一定时间去执行一个具体的任务,功能与schedule_work类似,多了一个延迟时间,输入参数:

@work_struct:具体任务对象指针

@delay:延迟时间

6

queue_work

调度执行一个指定workqueue中的任务。输入参数:

@ workqueue_struct:指定的workqueue指针

@work_struct:具体任务对象指针

7

queue_delayed_work

延迟调度执行一个指定workqueue中的任务,功能与queue_work类似,输入参数多了一个delay。

 
        例子:
        定义一个工作队列:

        struct work_struct my_wq;

        void my_wq_func(unsigned long);

        通过INIT_WORK可以初始化这个工作队列并将工作队列与处理函数绑定

        INIT_WORK(&my_wq, my_wq_func):

        调度工作队列:

        schedule_work(&my_wq)

2.3、Workqueue与tasklet的联系

        工作队列类似 tasklets,允许内核代码请求在将来某个时间调用一个函数,不同在于:

        (1)tasklet 在软件中断上下文中运行,所以 tasklet 代码必须是原子的。而工作队列函数在一个特殊内核进程上下文运行,有更
多的灵活性,且能够休眠。

        (2)tasklet只能在最初被提交的处理器上运行,这只是工作队列默认工作方式。

        (3)内核代码可以请求工作队列函数被延后一个给定的时间间隔。

        (4)tasklet 执行的很快, 短时期, 并且在原子态, 而工作队列函数可能是长周期且不需要是原子的,两个机制有它适合的情形。

 
原文地址:https://www.cnblogs.com/dirt2/p/5622822.html