linux 中断机制的处理过程

时间:2022-08-11 19:33:54
一、中断的概念


中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。Linux中通常分为外部中断(又叫硬件中断)和内部中断(又叫异常)。


在实地址模式中,CPU把内存中从0开始的1KB空间作为一个中断向量表。表中的每一项占4个字节。但是在保护模式中,有这4个字节的表项构成的中断向量表不满足实际需求,于是根据反映模式切换的信息和偏移量的足够使得中断向量表的表项由8个字节组成,而中断向量表也叫做了中断描述符表(IDT)。在CPU中增加了一个用来描述中断描述符表寄存器(IDTR),用来保存中断描述符表的起始地址。


二、中断的请求过程


外部设备当需要操作系统做相关的事情的时候,会产生相应的中断。设备通过相应的中断线向中断控制器发送高电平以产生中断信号,而操作系统则会从中断控制器的状态位取得那根中断线上产生的中断。而且只有在设备在对某一条中断线拥有控制权,才可以向这条中断线上发送信号。也由于现在的外设越来越多,中断线又是很宝贵的资源不可能被一一对应。因此在使用中断线前,就得对相应的中断线进行申请。无论采用共享中断方式还是独占一个中断,申请过程都是先讲所有的中断线进行扫描,得出哪些没有别占用,从其中选择一个作为该设备的IRQ。其次,通过中断申请函数申请相应的IRQ。最后,根据申请结果查看中断是否能够被执行。


中断机制的核心数据结构是 irq_desc, 它完整地描述了一条中断线 (或称为 “中断通道” )。以下程序源码版本为linux-2.6.32.2。


其中irq_desc 结构在 include/linux/irq.h 中定义:


typedef    void (*irq_flow_handler_t)(unsigned int irq, struct irq_desc *desc);


struct irq_desc {


    unsigned int      irq;    


    struct timer_rand_state *timer_rand_state;


    unsigned int            *kstat_irqs;


#ifdef CONFIG_INTR_REMAP


    struct irq_2_iommu      *irq_2_iommu;


#endif


    irq_flow_handler_t   handle_irq; /* 高层次的中断事件处理函数 */


    struct irq_chip      *chip; /* 低层次的硬件操作 */


    struct msi_desc      *msi_desc;


    void          *handler_data; /* chip 方法使用的数据*/


    void          *chip_data; /* chip 私有数据 */


    struct irqaction  *action;   /* 行为链表(action list) */


    unsigned int      status;       /* 状态 */


    unsigned int      depth;     /* 关中断次数 */


    unsigned int      wake_depth;   /* 唤醒次数 */


    unsigned int      irq_count; /* 发生的中断次数 */


    unsigned long     last_unhandled;   /*滞留时间 */


    unsigned int      irqs_unhandled;


    spinlock_t    lock; /*自选锁*/


#ifdef CONFIG_SMP


    cpumask_var_t     affinity;


    unsigned int      node;


#ifdef CONFIG_GENERIC_PENDING_IRQ


    cpumask_var_t     pending_mask;


#endif


#endif


    atomic_t      threads_active;


    wait_queue_head_t   wait_for_threads;


#ifdef CONFIG_PROC_FS


    struct proc_dir_entry    *dir; /* 在 proc 文件系统中的目录 */


#endif


    const char    *name;/*名称*/


} ____cacheline_internodealigned_in_smp;


 


I、Linux中断的申请与释放:在<linux/interrupt.h>, , 实现中断申请接口:


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


函数参数说明


unsigned int irq:所要申请的硬件中断号


irq_handler_t handler:中断服务程序的入口地址,中断发生时,系统调用handler这个函数。irq_handler_t为自定义类型,其原型为:


typedef irqreturn_t (*irq_handler_t)(int, void *);


而irqreturn_t的原型为:typedef enum irqreturn irqreturn_t;


enum irqreturn {


    IRQ_NONE,/*此设备没有产生中断*/


    IRQ_HANDLED,/*中断被处理*/


    IRQ_WAKE_THREAD,/*唤醒中断*/


};


在枚举类型irqreturn定义在include/linux/irqreturn.h文件中。


 


unsigned long flags:中断处理的属性,与中断管理有关的位掩码选项,有一下几组值:


#define IRQF_DISABLED       0x00000020    /*中断禁止*/


#define IRQF_SAMPLE_RANDOM  0x00000040    /*供系统产生随机数使用*/


#define IRQF_SHARED      0x00000080 /*在设备之间可共享*/


#define IRQF_PROBE_SHARED   0x00000100/*探测共享中断*/


#define IRQF_TIMER       0x00000200/*专用于时钟中断*/


#define IRQF_PERCPU      0x00000400/*每CPU周期执行中断*/


#define IRQF_NOBALANCING 0x00000800/*复位中断*/


#define IRQF_IRQPOLL     0x00001000/*共享中断中根据注册时间判断*/


#define IRQF_ONESHOT     0x00002000/*硬件中断处理完后触发*/


#define IRQF_TRIGGER_NONE   0x00000000/*无触发中断*/


#define IRQF_TRIGGER_RISING 0x00000001/*指定中断触发类型:上升沿有效*/


#define IRQF_TRIGGER_FALLING 0x00000002/*中断触发类型:下降沿有效*/


#define IRQF_TRIGGER_HIGH   0x00000004/*指定中断触发类型:高电平有效*/


#define IRQF_TRIGGER_LOW 0x00000008/*指定中断触发类型:低电平有效*/


#define IRQF_TRIGGER_MASK   (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \


               IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)


#define IRQF_TRIGGER_PROBE  0x00000010/*触发式检测中断*/


 


const char *dev_name:设备描述,表示那一个设备在使用这个中断。


 


void *dev_id:用作共享中断线的指针.。一般设置为这个设备的设备结构体或者NULL。它是一个独特的标识, 用在当释放中断线时以及可能还被驱动用来指向它自己的私有数据区,来标识哪个设备在中断 。这个参数在真正的驱动程序中一般是指向设备数据结构的指针.在调用中断处理程序的时候它就会传递给中断处理程序的void *dev_id。如果中断没有被共享, dev_id 可以设置为 NULL。


II、释放IRQ


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


III、中断线共享的数据结构


   struct irqaction {


    irq_handler_t handler; /* 具体的中断处理程序 */


    unsigned long flags;/*中断处理属性*/


    const char *name; /* 名称,会显示在/proc/interreupts 中 */


    void *dev_id; /* 设备ID,用于区分共享一条中断线的多个处理程序 */


    struct irqaction *next; /* 指向下一个irq_action 结构 */


    int irq;  /* 中断通道号 */


    struct proc_dir_entry *dir; /* 指向proc/irq/NN/name 的入口*/


    irq_handler_t thread_fn;/*线程中断处理函数*/


    struct task_struct *thread;/*线程中断指针*/


    unsigned long thread_flags;/*与线程有关的中断标记属性*/


};


thread_flags参见枚举型


enum {


    IRQTF_RUNTHREAD,/*线程中断处理*/


    IRQTF_DIED,/*线程中断死亡*/


    IRQTF_WARNED,/*警告信息*/


    IRQTF_AFFINITY,/*调整线程中断的关系*/


};


多个中断处理程序可以共享同一条中断线,irqaction 结构中的 next 成员用来把共享同一条中断线的所有中断处理程序组成一个单向链表,dev_id 成员用于区分各个中断处理程序。


根据以上内容可以得出中断机制各个数据结构之间的联系如下图所示:

linux 中断机制的处理过程 



 三.中断的处理过程


Linux中断分为两个半部:上半部(tophalf)和下半部(bottom half)。上半部的功能是"登记中断",当一个中断发生时,它进行相应地硬件读写后就把中断例程的下半部挂到该设备的下半部执行队列中去。因此,上半部执行的速度就会很快,可以服务更多的中断请求。但是,仅有"登记中断"是远远不够的,因为中断的事件可能很复杂。因此,Linux引入了一个下半部,来完成中断事件的绝大多数使命。下半部和上半部最大的不同是下半部是可中断的,而上半部是不可中断的,下半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断!下半部则相对来说并不是非常紧急的,通常还是比较耗时的,因此由系统自行安排运行时机,不在中断服务上下文中执行。


中断号的查看可以使用下面的命令:“cat /proc/interrupts”。


Linux实现下半部的机制主要有tasklet和工作队列。


小任务tasklet的实现


其数据结构为struct tasklet_struct,每一个结构体代表一个独立的小任务,定义如下


 


struct tasklet_struct


{


    struct tasklet_struct *next;/*指向下一个链表结构*/


    unsigned long state;/*小任务状态*/


    atomic_t count;/*引用计数器*/


    void (*func)(unsigned long);/*小任务的处理函数*/


    unsigned long data;/*传递小任务函数的参数*/


};


 


state的取值参照下边的枚举型:


enum


{


    TASKLET_STATE_SCHED,    /* 小任务已被调用执行*/


    TASKLET_STATE_RUN   /*仅在多处理器上使用*/


};


count域是小任务的引用计数器。只有当它的值为0的时候才能被激活,并其被设置为挂起状态时,才能够被执行,否则为禁止状态。


I、声明和使用小任务tasklet


静态的创建一个小任务的宏有一下两个:


#define DECLARE_TASKLET(name, func, data)  \


struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }


 


#define DECLARE_TASKLET_DISABLED(name, func, data) \


struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }


这两个宏的区别在于计数器设置的初始值不同,前者可以看出为0,后者为1。为0的表示激活状态,为1的表示禁止状态。其中ATOMIC_INIT宏为:


#define ATOMIC_INIT(i)   { (i) }


即可看出就是设置的数字。此宏在include/asm-generic/atomic.h中定义。这样就创建了一个名为name的小任务,其处理函数为func。当该函数被调用的时候,data参数就被传递给它。


II、小任务处理函数程序


    处理函数的的形式为:void my_tasklet_func(unsigned long data)。这样DECLARE_TASKLET(my_tasklet, my_tasklet_func, data)实现了小任务名和处理函数的绑定,而data就是函数参数。


III、调度编写的tasklet


调度小任务时引用tasklet_schedule(&my_tasklet)函数就能使系统在合适的时候进行调度。函数原型为:


static inline void tasklet_schedule(struct tasklet_struct *t)


{


    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))


       __tasklet_schedule(t);


}


这个调度函数放在中断处理的上半部处理函数中,这样中断申请的时候调用处理函数(即irq_handler_t handler)后,转去执行下半部的小任务。


如果希望使用DECLARE_TASKLET_DISABLED(name,function,data)创建小任务,那么在激活的时候也得调用相应的函数被使能


tasklet_enable(struct tasklet_struct *); //使能tasklet


tasklet_disble(struct tasklet_struct *); //禁用tasklet


tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long);


当然也可以调用tasklet_kill(struct tasklet_struct *)从挂起队列中删除一个小任务。清除指定tasklet的可调度位,即不允许调度该tasklet 。


使用tasklet作为下半部的处理中断的设备驱动程序模板如下:


/*定义tasklet和下半部函数并关联*/


void my_do_tasklet(unsigned long);


DECLARE_TASKLET(my_tasklet, my_tasklet_func, 0);


/*中断处理下半部*/


void my_do_tasklet(unsigned long)


{


  ……/*编写自己的处理事件内容*/


}


/*中断处理上半部*/


irpreturn_t my_interrupt(unsigned int irq,void *dev_id)


{


 ……


 tasklet_schedule(&my_tasklet)/*调度my_tasklet函数,根据声明将去执行my_tasklet_func函数*/


 ……


}


/*设备驱动的加载函数*/


int __init xxx_init(void)


{


 ……


 /*申请中断, 转去执行my_interrupt函数并传入参数*/


 result=request_irq(my_irq,my_interrupt,IRQF_DISABLED,"xxx",NULL);


 ……


}


/*设备驱动模块的卸载函数*/


void __exit xxx_exit(void)


{


……


/*释放中断*/


free_irq(my_irq,my_interrupt);


……


}


工作队列的实现


工作队列work_struct结构体,位于/include/linux/workqueue.h


 


typedef void (*work_func_t)(struct work_struct *work);


struct work_struct {


      atomic_long_t data; /*传递给处理函数的参数*/


#define WORK_STRUCT_PENDING 0/*这个工作是否正在等待处理标志*/             


#define WORK_STRUCT_FLAG_MASK (3UL)


#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)


      struct list_head entry;  /* 连接所有工作的链表*/


      work_func_t func; /* 要执行的函数*/


#ifdef CONFIG_LOCKDEP


      struct lockdep_map lockdep_map;


#endif


};


这些结构被连接成链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。可以通过DECLARE_WORK在编译时静态地创建该结构,以完成推后的工作。


#define DECLARE_WORK(n, f)                                 \


      struct work_struct n = __WORK_INITIALIZER(n, f)


而后边这个宏为一下内容:


#define __WORK_INITIALIZER(n, f) {                      \


      .data = WORK_DATA_INIT(),                            \


      .entry      = { &(n).entry, &(n).entry },                    \


      .func = (f),                                        \


      __WORK_INIT_LOCKDEP_MAP(#n, &(n))                   \


      }


其为参数data赋值的宏定义为:


#define WORK_DATA_INIT()       ATOMIC_LONG_INIT(0)


这样就会静态地创建一个名为n,待执行函数为f,参数为data的work_struct结构。同样,也可以在运行时通过指针创建一个工作:


INIT_WORK(struct work_struct *work, void(*func) (void *));


这会动态地初始化一个由work指向的工作队列,并将其与处理函数绑定。宏原型为:


#define INIT_WORK(_work, _func)                                        \


      do {                                                        \


             static struct lock_class_key __key;                 \


                                                              \


             (_work)->data = (atomic_long_t) WORK_DATA_INIT();  \


             lockdep_init_map(&(_work)->lockdep_map, #_work, &__key, 0);\


             INIT_LIST_HEAD(&(_work)->entry);                 \


             PREPARE_WORK((_work), (_func));                         \


      } while (0)


在需要调度的时候引用类似tasklet_schedule()函数的相应调度工作队列执行的函数schedule_work(),如:


schedule_work(&work);/*调度工作队列执行*/


如果有时候并不希望工作马上就被执行,而是希望它经过一段延迟以后再执行。在这种情况下,可以调度指定的时间后执行函数:


schedule_delayed_work(&work,delay);函数原型为:


int schedule_delayed_work(struct delayed_work *work, unsigned long delay);


其中是以delayed_work为结构体的指针,而这个结构体的定义是在work_struct结构体的基础上增加了一项timer_list结构体。


struct delayed_work {


    struct work_struct work;


    struct timer_list timer; /* 延迟的工作队列所用到的定时器,当不需要延迟时初始化为NULL*/


};


这样,便使预设的工作队列直到delay指定的时钟节拍用完以后才会执行。


使用工作队列处理中断下半部的设备驱动程序模板如下:


/*定义工作队列和下半部函数并关联*/


struct work_struct my_wq;


void my_do_work(unsigned long);


/*中断处理下半部*/


void my_do_work(unsigned long)


{


  ……/*编写自己的处理事件内容*/


}


/*中断处理上半部*/


irpreturn_t my_interrupt(unsigned int irq,void *dev_id)


{


 ……


 schedule_work(&my_wq)/*调度my_wq函数,根据工作队列初始化函数将去执行my_do_work函数*/


 ……


}


/*设备驱动的加载函数*/


int __init xxx_init(void)


{


 ……


 /*申请中断,转去执行my_interrupt函数并传入参数*/


 result=request_irq(my_irq,my_interrupt,IRQF_DISABLED,"xxx",NULL);


 ……


 /*初始化工作队列函数,并与自定义处理函数关联*/


 INIT_WORK(&my_irq,(void (*)(void *))my_do_work);


 ……


}


/*设备驱动模块的卸载函数*/


void __exit xxx_exit(void)


{


……


/*释放中断*/


free_irq(my_irq,my_interrupt);


……


}