linux 内核提供的中断_linux 内核 中断函数 内核线程 访问同一个变量 - CSDN
  • Linux 内核中断

    千次阅读 2015-06-23 10:15:21
    1.中断引入?  硬件如果要和cpu进行通信有那么几种方法?(把这个过程比作是老师分苹果的故事)  有程序查询法,中断,DMA,通道等。  程序查询法就是老师每一个小孩子都去问,他们的苹果有没有吃完了


    一、引    子    (    http://blog.csdn.net/ccjjnn19890720/article/details/7284474 )

    1.中断引入?

            硬件如果要和cpu进行通信有那么几种方法?(把这个过程比作是老师分苹果的故事)   

            有程序查询法,中断,DMA,通道等。

          程序查询法就是老师每一个小孩子都去问,他们的苹果有没有吃完了,想想这也是一件比较累的事情。中断就比较智能了,就是当小孩子自己吃完了苹果,如果还想要的话就自己去想老师要。DMA好像是老师把这个权利给了一个孩子,如果有人要苹果的时候,这个时候老师就叫一个小孩子过来,说接下来的事情就交给你了,你帮我分苹果,但是还是必须要和我说一下的,但是苹果是你去分。通道好像就是老师这个职位对于分苹果已经不管了,有一个专门的一个人来管理这个事情。

            不过没有一种方法可以解决所有的问题,因为有的硬件与cpu通信数据量比较大,可以选择是DMA和通道,但是如果硬件本身是一个低速的设备,使用通道和DMA可以说是一种资源的浪费,而这个时候中断也就是最好的解决的方式。你有需要才来告诉我,而不是我(cpu)一直去询问你。这是一种从被动到主动的过程。这样cpu就不用花大量的时间周期去查询你有没有要处理的请求。这样解放了cpu,使cpu有更多的时间去处理它要做的事情。


    2.中断实例

           首先,中断的过程其实很简单,比如键盘这个中断过程就是,当键盘被按下了,那么就会产生一个电信号(可以认为中断就是一个电信号),这个信号会传到中断控制器,然后中断控制器在上报到cpu,cpu 收到这个请求的时候就会中断自己当前的事情来出来出来键盘的输入。其实这是一个很简单的过程。虽然过程很简单,但是操作系统本身要做可没有想象中的那么简单,因为很多东西都是那么容易让人混淆的。



    3.中断基础

    (1)中断的概念:指CPU在执行过程中,出现某些突发事件急待处理,CPU暂停执行当前程序,转去处理突发事件,处理完后CPU又返回原程序被中断的位置继续执行
    (2)中断的分类:内部中断和外部中断
              内部中断:中断源来自CPU内部(软件中断指令、溢出、触发错误等)
              外部中断:中断源来自CPU外部,由外设提出请求

    (3)屏蔽中断和不可屏蔽中断:
              可屏蔽中断:可以通过屏蔽字被屏蔽,屏蔽后,该中断不再得到响应
              不可屏蔽中断:不能被屏蔽

    (4)向量中断和非向量中断:
              向量中断:CPU通常为不同的中断分配不同的中断号,当检测到某中断号的中断到来后,就自动跳转到与该中断号对应的地址执行
              非向量中断:多个中断共享一个入口地址。进入该入口地址后再通过软件判断中断标志来识别具体哪个是中断
               ///也就是说向量中断由软件提供中断服务程序入口地址,非向量中断由软件提供中断入口地址

            /*典型的非向量中断首先会判断中断源,然后调用不同中断源的中断处理程序*/
    irq_handler()
    {
    ...
    int int_src = read_int_status();/*读硬件的中断相关寄存器*/
    switch(int_src){//判断中断标志
    case DEV_A:
    dev_a_handler();
    break;
    case DEV_B:
    dev_b_handler();
    break;
    ...
    default:
    break;
    }
    ...
    }




    二、中断的请求过程

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


    三.中断的处理过程1(http://blog.chinaunix.net/uid-26772535-id-3222508.html

    1、最简单的中断机制

    最简单的中断机制就是像芯片手册上讲的那样,在中断向量表中填入跳转到对应处理函数的指令,然后在处理函数中实现需要的功能。类似下图:

    这种方式在原来的单片机课程中常常用到,一些简单的单片机系统也是这样用。

    它的好处很明显,简单,直接。

     

    2、linux中断处理的下半部概念引入

    中断处理函数所作的第一件事情是什么?答案是屏蔽中断(或者是什么都不做,因为常常是如果不清除IF位,就等于屏蔽中断了),当然只屏蔽同一种中断。之所以要屏蔽中断,是因为新的中断会再次调用中断处理函数,导致原来中断处理现场的破坏。即,破坏了 interrupt context。

    随着系统的不断复杂,中断处理函数要做的事情也越来越多,多到都来不及接收新的中断了。于是发生了中断丢失,这显然不行,于是产生了新的机制:分离中断接收与中断处理过程。中断接收在屏蔽中断的情况下完成;中断处理在时能中断的情况下完成,这部分被称为中断下半部。

    从上图中看,只看int0的处理。Func0为中断接收函数。中断只能简单的触发func0,而func0则能做更多的事情,它与funcA之间可以使用队列等缓存机制。当又有中断发生时,func0被触发,然后发送一个中断请求到缓存队列,然后让funcA去处理。

    由于func0做的事情是很简单的,所以不会影响int0的再次接收。而且在func0返回时就会使能int0,因此funcA执行时间再长也不会影响int0的接收。

    【注意1-上半部分与下班部分】:Linux中断分为两个半部:上半部(tophalf)和下半部(bottom half)。

    上半部的功能是"登记中断",当一个中断发生时,它进行相应地硬件读写后就把中断例程的下半部挂到该设备的下半部执行队列中去。因此,上半部执行的速度就会很快,可以服务更多的中断请求。但是,仅有"登记中断"是远远不够的,因为中断的事件可能很复杂。

     因此,Linux引入了一个下半部,来完成中断事件的绝大多数使命。

     下半部和上半部最大的不同是下半部是可中断的,而上半部是不可中断的,下半部几乎做了中断处理程序所有的事情,而且可以被新的中断打断!下半部则相对来说并不是非常紧急的,通常还是比较耗时的,因此由系统自行安排运行时机,不在中断服务上下文中执行。

    【注意2-查看中断号】:linux 中断号的查看可以使用下面的命令: cat  /proc/interrupts”。

    【注意3-下半部的实现机制比较1】:

     (1)在Linux2.6的内核中存在三种不同形式的下半部实现机制:软中断, tasklet  和 工作队列。

     简单地说,一般的驱动程序的编写者需要做两个选择。 首先,你是不是需要一个可调度的实体来执行需要推后完成的工作――从根本上来说,有休眠的需要吗?

    =====>>>>要是有,工作队列就是你的惟一选择。

    =====>>>>否则最好用tasklet。要是必须专注于性能的提高,那么就考虑softirq。

                                              


       软中断和tasklet都是运行在中断上下文中,它们与任一进程无关,没有支持的进程完成重新调度。所以软中断和tasklet不能睡眠、不能阻塞,它们的代码中不能含有导致睡眠的动作,如减少信号量、从用户空间拷贝数据或手工分配内存等。也正是由于它们运行在中断上下文中,所以它们在同一个CPU上的执行是串行的,这样就不利于实时多媒体任务的优先处理。

          而工作队列用于:如果推后执行的任务需要睡眠,那么就选择工作队列。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。

    (2)下面将比较三种机制的差别与联系。
            软中断:

       软中断支持SMP,同一个softirq可以在不同的CPU上同时运行,softirq必须是可重入的。软中断是在编译期间静态分配的,它不像tasklet那样能被动态的注册或去除。kernel/softirq.c中定义了一个包含32个softirq_action结构体的数组。每个被注册的软中断都占据该数组的一项。因此最多可能有32个软中断。2.6版本的内核中定义了六个软中断:HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、 SCSI_SOFTIRQ、TASKLET_SOFTIRQ。
            一般情况下,在硬件中断处理程序后都会试图调用do_softirq()函数,每个CPU都是通过执行这个函数来执行软中断服务的。由于软中断不能进入硬中断部分,且同一个CPU上软中断的执行是串行的,即不允许嵌套,因此,do_softirq()函数一开始就检查当前CPU是否已经正出在中断服务中,如果是则 do_softirq()函数立即返回。这是由do_softirq()函数中的 if (in_interrupt()) return; 保证的。

     1、软中断是在编译期间静态分配的。
             2、最多可以有32个软中断。
             3、软中断不会抢占另外一个软中断,唯一可以抢占软中断的是中断处理程序。
             4、可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),
                因此也需要使用自旋锁来保护其数据结构。
             5、目前只有两个子系直接使用软中断:网络和SCSI。
             6、执行时间有:从硬件中断代码返回时、在ksoftirqd内核线程中和某些显示检查并执行软中断的代码中。

     tasklet:

     引入tasklet,最主要的是考虑支持SMP,提高SMP多个cpu的利用率;不同的tasklet可以在不同的cpu上运行。tasklet可以理解为softirq的派生,所以它的调度时机和软中断一样。对于内核中需要延迟执行的多数任务都可以用tasklet来完成,由于同类tasklet本身已经进行了同步保护,所以使用tasklet比软中断要简单的多,而且效率也不错。 tasklet把任务延迟到安全时间执行的一种方式,在中断期间运行,即使被调度多次,tasklet也只运行一次,不过tasklet可以在SMP系统上和其他不同的tasklet并行运行。在SMP系统上,tasklet还被确保在第一个调度它的CPU上运行,因为这样可以提供更好的高速缓存行为,从而提高性能。 

        与一般的软中断不同,某一段tasklet代码在某个时刻只能在一个CPU上运行,但不同的tasklet代码在同一时刻可以在多个CPU上并发地执行。 Kernel/softirq.c中用tasklet_trylock()宏试图对当前要执行的tasklet(由指针t所指向)进行加锁,如果加锁成功(当前没有任何其他CPU正在执行这个tasklet),则用原子读函数atomic_read()进一步判断count成员的值。如果count为0,说明这个tasklet是允许执行的。如果tasklet_trylock()宏加锁不成功,或者因为当前tasklet的count值非0而不允许执行时,我们必须将这个tasklet重新放回到当前CPU的tasklet队列中,以留待这个CPU下次服务软中断向量TASKLET_SOFTIRQ时再执行。为此进行这样几步操作:

    (1)先关 CPU中断,以保证下面操作的原子性。

    (2)把这个tasklet重新放回到当前CPU的tasklet队列的首部;

    (3)调用 __cpu_raise_softirq()函数在当前CPU上再触发一次软中断请求TASKLET_SOFTIRQ;

    (4)开中断。
           软中断和tasklet都是运行在中断上下文中,它们与任一进程无关,没有支持的进程完成重新调度。所以软中断和tasklet不能睡眠、不能阻塞,它们的代码中不能含有导致睡眠的动作,如减少信号量、从用户空间拷贝数据或手工分配内存等。也正是由于它们运行在中断上下文中,所以它们在同一个CPU上的执行是串行的,这样就不利于实时多媒体任务的优先处理。

      引入tasklet,最主要的是考虑支持SMP,提高SMP多个cpu的利用率;两个相同的tasklet决不会同时执行。tasklet可以理解为softirq的派生,所以它的调度时机和软中断一样。对于内核中需要延迟执行的多数任务都可以用tasklet来完成,由于同类tasklet本身已经进行了同步保护,所以使用tasklet比软中断要简单的多,而且效率也不错。tasklet把任务延迟到安全时间执行的一种方式,在中断期间运行,即使被调度多次,tasklet也只运行一次,不过tasklet可以在SMP系统上和其他不同的tasklet并行运行。在SMP系统上,tasklet还被确保在第一个调度它的CPU上运行,因为这样可以提供更好的高速缓存行为,从而提高性能。

           tasklet的特性:.不允许两个两个相同类型的tasklet同时执行,即使在不同的处理器上。


         1、tasklet是使用两类软中断实现的:HI_SOFTIRQ和TASKLET_SOFTIRQ。
              2、可以动态增加减少,没有数量限制。
              3、同一类tasklet不能并发执行。
              4、不同类型可以并发执行。
              5、大部分情况使用tasklet。

     工作队列:

            如果推后执行的任务需要睡眠,那么就选择工作队列。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。work queue造成的开销最大,因为它要涉及到内核线程甚至是上下文切换。这并不是说work queue的低效,但每秒钟有数千次中断,就像网络子系统时常经历的那样,那么采用其他的机制可能更合适一些。 尽管如此,针对大部分情况工作队列都能提供足够的支持。

          工作队列特性:
          1).工作队列会在进程上下文中执行!
          2).可以阻塞。(前两种机制是不可以阻塞的)
          3).可以被重新调度。(前两种只可以被中断处理程序打断)
          4).使用工作队列的两种形式:
                 1>缺省工作者线程(works threads)
                 2>自建的工作者线程
          5).在工作队列和内核其他部分之间使用锁机制就像在其他的进程上下文一样。
          6).默认允许响应中断。
          7).默认不持有任何锁。

         1、由内核线程去执行,换句话说总在进程上下文执行。
              2、可以睡眠,阻塞。

    【注意4-下半部的实现机制比较2】:

    tasklet与workqueue的区别和不同应用环境总结2

      什么情况下使用工作队列,什么情况下使用tasklet。

      (1)如果推后执行的任务需要睡眠,那么就选择工作队列。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。

      (2)如果推后执行的任务不需要睡眠,那么就选择tasklet。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。


    注意5-下半部使用方式

               Request_irq 挂的中断函数要尽量简单,只做必须在屏蔽中断情况下要做的事情。中断的其他部分都在下半部中完成。

               软中断的使用原则很简单,永远不用。它甚至都不算是一种正是的中断处理机制,而只是tasklet的实现基础。

    工作队列也要少用,如果不是必须要用到线程才能用的某些机制,就不要使用工作队列。其实对于中断来说,只是对中断进行简单的处理,大部分工作是在驱动程序中完成的。所以有什么必要非使用工作队列呢?

    除了上述情况,就要使用tasklet。

               即使是下半部,也只是作必须在中断中要做的事情,如保存数据等,其他都交给驱动程序去做。


    3、Linux实现下半部的机制--(1)软中断

    下面看看linux中断处理。作为一个操作系统显然不能任由每个中断都各自为政,统一管理是必须的。

    我们不可中断部分的共同部分放在函数do_IRQ中,需要添加中断处理函数时,通过request_irq实现。下半部放在do_softirq中,也就是软中断,通过open_softirq添加对应的处理函数。

     

    4、Linux实现下半部的机制--(2)tasklet

    (Tasklet作为一种新机制,显然可以承担更多的优点。正好这时候SMP越来越火了,因此又在tasklet中加入了SMP机制,保证同种中断只能在一个cpu上执行。在软中断时代,显然没有这种考虑。因此同一种中断可以在两个cpu上同时执行,很可能造成冲突.)

    旧事物跟不上历史的发展时,总会有新事物出现。

    随着中断数的不停增加,软中断不够用了,于是下半部又做了进化。

    软中断用轮询的方式处理。假如正好是最后一种中断,则必须循环完所有的中断类型,才能最终执行对应的处理函数。显然当年开发人员为了保证轮询的效率,于是限制中断个数为32个。

    为了提高中断处理数量,顺道改进处理效率,于是产生了tasklet机制。

    Tasklet采用无差别的队列机制,有中断时才执行,免去了循环查表之苦。

    总结下tasklet的优点:

    (1)无类型数量限制;

    (2)效率高,无需循环查表;

    (3)支持SMP机制;

     

    5、Linux实现下半部的机制--(3)工作队列

     前面的机制不论如何折腾,有一点是不会变的。它们都在中断上下文中。什么意思?说明它们不可挂起。而且由于是串行执行,因此只要有一个处理时间较长,则会导致其他中断响应的延迟。为了完成这些不可能完成的任务,于是出现了工作队列。工作队列说白了就是一组内核线程,作为中断守护线程来使用。多个中断可以放在一个线程中,也可以每个中断分配一个线程。

    工作队列对线程作了封装,使用起来更方便。

    因为工作队列是线程,所以我们可以使用所有可以在线程中使用的方法。

    Tasklet其实也不一定是在中断上下文中执行,它也有可能在线程中执行。

    假如中断数量很多,而且这些中断都是自启动型的(中断处理函数会导致新的中断产生),则有可能cpu一直在这里执行中断处理函数,会导致用户进程永远得不到调度时间。

    为了避免这种情况,linux发现中断数量过多时,会把多余的中断处理放到一个单独的线程中去做,就是ksoftirqd线程。这样又保证了中断不多时的响应速度,又保证了中断过多时不会把用户进程饿死。

    问题是我们不能保证我们的tasklet或软中断处理函数一定会在线程中执行,所以还是不能使用进程才能用的一些方法,如放弃调度、长延时等。 




    三.中断的处理过程2--实现实例(http://blog.csdn.net/dragon101788/article/details/10238415

    1、中断处理的tasklet(小任务)机制

    中断服务程序一般都是在中断请求关闭的条件下执行的,以避免嵌套而使中断控制复杂化。但是,中断是一个随机事件,它随时会到来,如果关中断的时间太长,CPU就不能及时响应其他的中断请求,从而造成中断的丢失。因此,Linux内核的目标就是尽可能快的处理完中断请求,尽其所能把更多的处理向后推迟。例如,假设一个数据块已经达到了网线,当中断控制器接受到这个中断请求信号时,Linux内核只是简单地标志数据到来了,然后让处理器恢复到它以前运行的状态,其余的处理稍后再进行(如把数据移入一个缓冲区,接受数据的进程就可以在缓冲区找到数据)。因此,内核把中断处理分为两部分:上半部(tophalf)和下半部(bottomhalf),上半部(就是中断服务程序)内核立即执行,而下半部(就是一些内核函数)留着稍后处理,

    首先,一个快速的“上半部”来处理硬件发出的请求,它必须在一个新的中断产生之前终止。通常,除了在设备和一些内存缓冲区(如果你的设备用到了DMA,就不止这些)之间移动或传送数据,确定硬件是否处于健全的状态之外,这一部分做的工作很少。

    下半部运行时是允许中断请求的,而上半部运行时是关中断的,这是二者之间的主要区别。

    但是,内核到底什时候执行下半部,以何种方式组织下半部?这就是我们要讨论的下半部实现机制,这种机制在内核的演变过程中不断得到改进,在以前的内核中,这个机制叫做bottomhalf(简称bh),在2.4以后的版本中有了新的发展和改进,改进的目标使下半部可以在多处理机上并行执行,并有助于驱动程序的开发者进行驱动程序的开发。下面主要介绍常用的小任务(Tasklet)机制及2.6内核中的工作队列机制。


    小任务机制

    这里的小任务是指对要推迟执行的函数进行组织的一种机制。其数据结构为tasklet_struct,每个结构代表一个独立的小任务,其定义如下:

    <span style="font-family:SimSun;font-size:18px;">struct tasklet_struct {
    struct tasklet_struct *next; /*指向链表中的下一个结构*/
    unsignedlong state; /* 小任务的状态*/
    atomic_tcount; /* 引用计数器*/
    void(*func) (unsigned long); /* 要调用的函数*/
    unsignedlong data; /* 传递给函数的参数*/
    };</span>

    结构中的func域就是下半部中要推迟执行的函数,data是它唯一的参数。
    State域的取值为TASKLET_STATE_SCHED或TASKLET_STATE_RUN。TASKLET_STATE_SCHED表示小任务已被调度,正准备投入运行,TASKLET_STATE_RUN表示小任务正在运行。         TASKLET_STATE_RUN只有在多处理器系统上才使用,单处理器系统什么时候都清楚一个小任务是不是正在运行(它要么就是当前正在执行的代码,要么不是)。
    Count域是小任务的引用计数器。如果它不为0,则小任务被禁止,不允许执行;只有当它为零,小任务才被激活,并且在被设置为挂起时,小任务才能够执行。
    1. 声明和使用小任务大多数情况下,为了控制一个寻常的硬件设备,小任务机制是实现下半部的最佳选择。小任务可以动态创建,使用方便,执行起来也比较快。
    我们既可以静态地创建小任务,也可以动态地创建它。选择那种方式取决于到底是想要对小任务进行直接引用还是一个间接引用。如果准备静态地创建一个小任务(也就是对它直接引用),使用下面两个宏中的一个:
    DECLARE_TASKLET(name,func, data)
    DECLARE_TASKLET_DISABLED(name,func, data)
    这两个宏都能根据给定的名字静态地创建一个tasklet_struct结构。当该小任务被调度以后,给定的函数func会被执行,它的参数由data给出。这两个宏之间的区别在于引用计数器的初始值设置不同。第一个宏把创建的小任务的引用计数器设置为0,因此,该小任务处于激活状态。另一个把引用计数器设置为1,所以该小任务处于禁止状态。例如:
    DECLARE_TASKLET(my_tasklet,my_tasklet_handler, dev);
    这行代码其实等价于
    structtasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0),
    tasklet_handler,dev};
    这样就创建了一个名为my_tasklet的小任务,其处理程序为tasklet_handler,并且已被激活。当处理程序被调用的时候,dev就会被传递给它。
    2. 编写自己的小任务处理程序小任务处理程序必须符合如下的函数类型:
    voidtasklet_handler(unsigned long data)
    由于小任务不能睡眠,因此不能在小任务中使用信号量或者其它产生阻塞的函数。但是小任务运行时可以响应中断。
    3. 调度自己的小任务通过调用tasklet_schedule()函数并传递给它相应的tasklt_struct指针,该小任务就会被调度以便适当的时候执行:
    tasklet_schedule(&my_tasklet); /*把my_tasklet标记为挂起 */
    在小任务被调度以后,只要有机会它就会尽可能早的运行。在它还没有得到运行机会之前,如果一个相同的小任务又被调度了,那么它仍然只会运行一次。
    可以调用tasklet_disable()函数来禁止某个指定的小任务。如果该小任务当前正在执行,这个函数会等到它执行完毕再返回。调用tasklet_enable()函数可以激活一个小任务,如果希望把以DECLARE_TASKLET_DISABLED()创建的小任务激活,也得调用这个函数,如:
    tasklet_disable(&my_tasklet); /*小任务现在被禁止,这个小任务不能运行*/
    tasklet_enable(&my_tasklet); /* 小任务现在被激活*/
    也可以调用tasklet_kill()函数从挂起的队列中去掉一个小任务。该函数的参数是一个指向某个小任务的tasklet_struct的长指针。在小任务重新调度它自身的时候,从挂起的队列中移去已调度的小任务会很有用。这个函数首先等待该小任务执行完毕,然后再将它移去。
    4.tasklet的简单用法
    下面是tasklet的一个简单应用,以模块的形成加载。

    <span style="font-family:SimSun;font-size:18px;">#include <linux/module.h>
    #include<linux/init.h>
    #include<linux/fs.h>
    #include<linux/kdev_t.h>
    #include <linux/cdev.h>
    #include <linux/kernel.h>
    #include<linux/interrupt.h>
    
    static struct t asklet_struct my_tasklet;
    
    static void tasklet_handler (unsigned longd ata)
    {
    printk(KERN_ALERT,"tasklet_handler is running./n");
    }
    
    staticint __init test_init(void)
    {
    tasklet_init(&my_tasklet,tasklet_handler,0);
    tasklet_schedule(&my_tasklet);
    return0;
    }
    
    static void __exit test_exit(void)
    {
    tasklet_kill(&tasklet);
    printk(KERN_ALERT,"test_exit is running./n");
    }
    MODULE_LICENSE("GPL");
    
    module_init(test_init);
    module_exit(test_exit);</span>

    从这个例子可以看出,所谓的小任务机制是为下半部函数的执行提供了一种执行机制,也就是说,推迟处理的事情是由tasklet_handler实现,何时执行,经由小任务机制封装后交给内核去处理。

    二、中断处理的工作队列机制

    工作队列(work queue)是另外一种将工作推后执行的形式,它和前面讨论的tasklet有所不同。工作队列可以把工作推后,交由一个内核线程去执行,也就是说,这个下半部分可以在进程上下文中执行。这样,通过工作队列执行的代码能占尽进程上下文的所有优势。最重要的就是工作队列允许被重新调度甚至是睡眠。

    那么,什么情况下使用工作队列,什么情况下使用tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。


    1.工作、工作队列和工作者线程

          如前所述,我们把推后执行的任务叫做工作(work),描述它的数据结构为work_struct,这些工作以队列结构组织成工作队列(workqueue),其数据结构为workqueue_struct,而工作线程就是负责执行工作队列中的工作。系统默认的工作者线程为events,自己也可以创建自己的工作者线程。


    2.表示工作的数据结构

    工作用<linux/workqueue.h>中定义的work_struct结构表示:


    struct work_struct{
    unsigned long pending; /* 这个工作正在等待处理吗?*/
    struct list_head entry; /* 连接所有工作的链表 */
    void (*func) (void *); /* 要执行的函数 */
    void *data; /* 传递给函数的参数 */
    void *wq_data; /* 内部使用 */
    struct timer_list timer; /* 延迟的工作队列所用到的定时器 */
    };

    这些结构被连接成链表。当一个工作者线程被唤醒时,它会执行它的链表上的所有工作。工作被执行完毕,它就将相应的work_struct对象从链表上移去。当链表上不再有对象的时候,它就会继续休眠。

    3. 创建推后的工作

    要使用工作队列,首先要做的是创建一些需要推后完成的工作。可以通过DECLARE_WORK在编译时静态地建该结构:

    DECLARE_WORK(name, void (*func) (void *), void *data);

    这样就会静态地创建一个名为name,待执行函数为func,参数为data的work_struct结构。

    同样,也可以在运行时通过指针创建一个工作:

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

    这会动态地初始化一个由work指向的工作。

    4. 工作队列中待执行的函数

    工作队列待执行的函数原型是:

    void work_handler(void *data)

    这个函数会由一个工作者线程执行,因此,函数会运行在进程上下文中。默认情况下,允许响应中断,并且不持有任何锁。如果需要,函数可以睡眠。需要注意的是,尽管该函数运行在进程上下文中,但它不能访问用户空间,因为内核线程在用户空间没有相关的内存映射。通常在系统调用发生时,内核会代表用户空间的进程运行,此时它才能访问用户空间,也只有在此时它才会映射用户空间的内存。

    5. 对工作进行调度

    现在工作已经被创建,我们可以调度它了。想要把给定工作的待处理函数提交给缺省的events工作线程,只需调用

    schedule_work(&work);

    work马上就会被调度,一旦其所在的处理器上的工作者线程被唤醒,它就会被执行。

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

    schedule_delayed_work(&work, delay);

    这时,&work指向的work_struct直到delay指定的时钟节拍用完以后才会执行。

    6. 工作队列的简单应用

    #include<linux/module.h>
    #include<linux/init.h>
    #include<linux/workqueue.h>
    
    staticstruct workqueue_struct *queue =NULL;
    staticstruct work_struct work;
    
    staticvoid work_handler(struct work_struct*data)
    {
    printk(KERN_ALERT"work handler function./n");
    }
    
    staticint __init test_init(void)
    {
    queue= create_singlethread_workqueue("helloworld"); /*创建一个单线程的工作队列*/
    if(!queue)
    goto err;
    
    INIT_WORK(&work, work_handler);
    schedule_work(&work);/*schedule_work是添加到系统的events workqueue, 要添加到自己的workqueue, 应该使用queue_work, 故此处有误*/
    
    return 0;
    err:
    return-1;
    }
    
    staticvoid __exit test_exit(void)
    {
    destroy_workqueue(queue);
    }
    MODULE_LICENSE("GPL");
    module_init(test_init);
    module_exit(test_exit);








    展开全文
  • Linux内核中断嵌套

    千次阅读 2016-10-31 20:07:14
    为了支持中断的嵌套执行,Linux内核在进入中断服务程序之前会将硬中断开启,运行完中断服务程序之后再将硬中断关闭,在这期间硬件中断时可以被抢占的,而软中断执行过程中硬件中断始终是开启的。如果没

    假设读者对Linux内核的中断一定的理解,下面来介绍Linux内核的中断嵌套和抢占规则。
    1 中断运行过程中的开关情况
    在进入硬中断的时候,CPU会自动将硬中断功能关闭;进入软件中断的时候不会关闭中断功能。为了支持中断的嵌套执行,Linux内核在进入中断服务程序之前会将硬中断开启,运行完中断服务程序之后再将硬中断关闭,在这期间硬件中断是可以被抢占的,而软中断执行过程中硬件中断始终是开启的。如果没有特殊说明,下面的中断都指硬中断。关闭中断是硬件自动完成的,为了实现中断的嵌套,需要人为的打开中断。中断发生后会进行多次函数跳转,最终会进入到真正的中断处理函数handle_IRQ_event(),在该函数里执行各设备的中断服务程序。函数定义如下:

    irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
    {
        irqreturn_t ret, retval = IRQ_NONE;
        unsigned int status = 0;
    
        handle_dynamic_tick(action);
    
        if (!(action->flags & IRQF_DISABLED))
            local_irq_enable_in_hardirq();//开中断
    
        do {
            ret = action->handler(irq, action->dev_id);
            if (ret == IRQ_HANDLED)
                status |= action->flags;
            retval |= ret;
            action = action->next;
        } while (action);
    
        if (status & IRQF_SAMPLE_RANDOM)
            add_interrupt_randomness(irq);
        local_irq_disable();//关中断
    
        return retval;
    }

    while循环是依次执行中断向量上的所有设备的中断服务程序,由各设备中断服务程序对中断源进行识别。可以看到在执行中断服务程序之前执行local_irq_enable_in_hardirq()将中断打开,在执行完服务程序之后执行local_irq_disable()将中断再次关闭。也就是说在执行具体设备的中断服务程序时是允许中断发生的,也就是中断嵌套。
    2 中断的抢占规则
    硬件中断可以抢占软件中断,硬件中断可以被更高优先级的硬件中断抢占,软件中断不能抢占别的中断。内核保证中断和异常执行的过程中不会发生新的异常,实际上内核态能触发的异常只有缺页异常,但是中断处理程序不执行会引发缺页异常的操作。
    3 中断的嵌套
    中断的抢占是可以嵌套的,导致内核控制路径的嵌套执行。中断执行过程中不允许睡眠和进程切换(一般情况下睡眠都会导致进程切换),而且对中断的抢占只能发生在中断服务程序执行期间,只有这期间中断服务才是开启的。同时,同一中断通道不允许嵌套执行,即同一中断向量上的中断服务程序至多只能有一个出现在所有CPU的中断控制路径中。
    4 软中断,tasklet和工作队列
    软中断不同于软件中断,软中断发生的时机是从中断、调用或者异常返回用户空间之前,按照软中断在结构数组中定义的顺序依次执行。同一个软中断可以在不同CPU上并发执行。软中断执行过程中也不允许睡眠和进程切换。
    tasklet基于软中断实现,在软中断结构数组中占用两项。当软中断执行到这两项时就会跳转到tasklet函数入口处,依次执行队列中的tasklet函数。同一个tasklet不能在不同CPU上并发执行,但是不同tasklet可以在不同CPU上执行。tasklet始终运行在被初始提交的同一处理器上。
    工作队列实际上是使用内核线程来实现的,工作队列允许睡眠,可以当做普通的内核线程来看待,他有自己的上下文环境。

    展开全文
  • 细说Linux内核中断机制(详)

    千次阅读 2018-07-21 09:59:40
    本文着重介绍Linux内核中断处理的始末流程,因此对一些基本的概念应该有所了解。 2.硬件支持 我们知道,CPU有一个INTR引脚,用于接收中断请求信号。 而中断控制器用于提供中断向量,即第几号中断。 3.内核需要...

    在技术面前,多问为什么总是好的,知其然不如知其所以然。

    为什么要有中断?

    1.前言

    本文尽量以设计者的角度去介绍中断。

    本文着重介绍Linux内核中中断处理的始末流程,因此对一些基本的概念应该有所了解。

    2.硬件支持

    我们知道,CPU有一个INTR引脚,用于接收中断请求信号。

    而中断控制器用于提供中断向量,即第几号中断。

    3.内核需要做哪些工作?

    3.1需要一张表

    首先,中断可能来源于外部设备,而外部设备多种多样,也可能来源于CPU,无论如何我们需要区分到底是具体哪种中断,哪个设备产生的。因此,我们首先需要一个表,表中每项表示一种中断,内容可以是指向具体中断服务程序的函数指针,这是最简单的中断向量表,这样,当中断发生时,将中断向量作为中断向量表的下表,就可以直接找到中断服务程序。如图1所示。

    图1-简要中断向量表标题

     

    但是操作系统考虑的更多,比如中断优先级、中断类型标识等。于是就产生了如下图所示中断向量表项。

    中断向量表项结构

    3.2对于用于外设的通用中断,需要一个队列

    但是这样还不行,一方面中断向量有硬件中断控制器产生,因此中断向量数目受限于硬件,无法弹性增长;另一方面,作为通用操作系统,可能存在许多不同的外部设备,也会操作中断向量不够用。因此,解决办法就是共用中断向量。系统为每个中断向量设置一个队列,根据中断源所使用的中断向量,将其挂入到相应的队列中去。其中队列的队列头是irq_desc_t结构体数组。

    typedef struct {
    	unsigned int status;		/* IRQ status */
    	hw_irq_controller *handler;
    	struct irqaction *action;	/* IRQ action list */
    	unsigned int depth;		/* nested irq disables */
    	spinlock_t lock;
    } ____cacheline_aligned irq_desc_t;
    
    extern irq_desc_t irq_desc [NR_IRQS];

    其中,通过其结构体内部的结构体 irqaction 形成一个队列。irqaction结构体可以理解为挂在当前中断向量队列中的特定的设备中断服务程序,如此,同一中断向量队列,就能够区分是哪个中断服务程序属于哪个设备了。

    struct irqaction {
    	void (*handler)(int, void *, struct pt_regs *);    //设备具体中断服务程序
    	unsigned long flags;
    	unsigned long mask;
    	const char *name;
    	void *dev_id;                                     //设备的id表示
    	struct irqaction *next;                           //队列中下一个此结构体
    };
    

    最终形成的中断服务结构图如下所示。

    中断服务结构图

    4.中断向量表的设置

    首先,中断向量表内容从0~0x20均为CPU内部产生的中断,包括除0、页面错等。从0x20开始均为用于外部设备的通用中断(包括中断请求队列),但是0x80系统调用除外。这些表项的内容都是在中断向量表初始化的时候进行设置。

    4.1内部中断的向量表项设置

    对于0~19个内部中断设置由下面函数设置。

    void __init trap_init(void)
    {
    #ifdef CONFIG_EISA
    	if (isa_readl(0x0FFFD9) == 'E'+('I'<<8)+('S'<<16)+('A'<<24))
    		EISA_bus = 1;
    #endif
    
    	set_trap_gate(0,&divide_error);
    	set_trap_gate(1,&debug);
    	set_intr_gate(2,&nmi);
    	set_system_gate(3,&int3);	/* int3-5 can be called from all */
    	set_system_gate(4,&overflow);
    	set_system_gate(5,&bounds);
    	set_trap_gate(6,&invalid_op);
    	set_trap_gate(7,&device_not_available);
    	set_trap_gate(8,&double_fault);
    	set_trap_gate(9,&coprocessor_segment_overrun);
    	set_trap_gate(10,&invalid_TSS);
    	set_trap_gate(11,&segment_not_present);
    	set_trap_gate(12,&stack_segment);
    	set_trap_gate(13,&general_protection);
    	set_trap_gate(14,&page_fault);
    	set_trap_gate(15,&spurious_interrupt_bug);
    	set_trap_gate(16,&coprocessor_error);
    	set_trap_gate(17,&alignment_check);
    	set_trap_gate(18,&machine_check);
    	set_trap_gate(19,&simd_coprocessor_error);
    
    	set_system_gate(SYSCALL_VECTOR,&system_call);
    
    	/*
    	 * default LDT is a single-entry callgate to lcall7 for iBCS
    	 * and a callgate to lcall27 for Solaris/x86 binaries
    	 */
    	set_call_gate(&default_ldt[0],lcall7);
    	set_call_gate(&default_ldt[4],lcall27);
    
    	/*
    	 * Should be a barrier for any external CPU state.
    	 */
    	cpu_init();
    
    #ifdef CONFIG_X86_VISWS_APIC
    	superio_init();
    	lithium_init();
    	cobalt_init();
    #endif
    }
    

    4.2通用中断向量表项设置

    中断类型为中断门的中断向量表设置具体函数如下。

    void set_intr_gate(unsigned int n, void *addr)
    {
            //设置中断向量表项
            //n表示第几项
            //addr表示中断服务的入口程序地址
    	_set_gate(idt_table+n,14,0,addr);
    }
    

    事实上,上面函数参数 void *addr即为上面中断服务结构图中的一系列IRQ0x00_interrupt(),这类函数也是中断服务程序入口函数,即进入中断最先执行的一小段程序,这段程序非常重要,读者可以先猜测一下其功能。

    5.通用中断,中断请求队列初始化

    之前提到,用于外设的通用中断,多个中断源可以共用一个中断向量,因此就有了上文中断服务结构图中的中断请求队列,为了清晰,这里再贴出其结构图。每一个中断请求队列可以想象成一个中断通道,里面容纳了一系列具体设备的中断服务程序。

    ​​​​通用中断请求队列图

    在4.2中我们仅仅是设置了中断向量表项的内容,但是对于通用中断来说,那时仅仅设置了一系列IRQ0x00_interrupt()通用中断入口函数,并没有将各个设备的具体中断服务程序通过结构体irqaction挂到相应的中断请求队列中。

    所以,真正的中断服务要到具体设备初始化程序将中断服务程序通过request_irq()向系统“登记”,挂入某个中断请求队列以后才会发生,下面的函数根据函数参数设置一个irqaction结构体,并根据下标irq挂入相应的irq_desc[irq]所表示的中断请求队列中。

    int request_irq(unsigned int irq, 
    		void (*handler)(int, void *, struct pt_regs *),
    		unsigned long irqflags, 
    		const char * devname,
    		void *dev_id)
    {
    	int retval;
    	struct irqaction * action;
    
    #if 1
    	/*
    	 * Sanity-check: shared interrupts should REALLY 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).
    	 */
    	if (irqflags & SA_SHIRQ) {
    		if (!dev_id)
    			printk("Bad boy: %s (at 0x%x) called us without a dev_id!\n", devname, (&irq)[-1]);
    	}
    #endif
    
    	if (irq >= NR_IRQS)
    		return -EINVAL;
    	if (!handler)
    		return -EINVAL;
    
    	action = (struct irqaction *)
    			kmalloc(sizeof(struct irqaction), GFP_KERNEL);
    	if (!action)
    		return -ENOMEM;
            //irqaction具体参数的设置
    	action->handler = handler;            
    	action->flags = irqflags;
    	action->mask = 0;
    	action->name = devname;
    	action->next = NULL;
    	action->dev_id = dev_id;
    
    	retval = setup_irq(irq, action);        //根据irq,将其挂入某个中断请求队列中
    	if (retval)
    		kfree(action);
    	return retval;
    }

    6.中断服务程序的响应

    上述工作已经完成中断机制所需要的所有环境,对于0x20以上的通用中断,外部设备也已经将中断服务程序挂入到相应的中断请求队列中。

    我们现在假设外部程序产生了一次中断请求,该请求通过中断控制器到达了CPU的中断请求引线INTR,并且中断开着,所以当CPU执行完当前指令后就来响应此才中断请求。

    在介绍前,我们首先需要明白中断的执行意味着什么?

    一般外设产生的中断属于突发状况,也就是需要离开正在执行的程序,转向执行中断服务。那么这就面临着几个问题:

    • 当前程序的运行级别和中断程序的运行级别是否相同?这非常重要,会引起堆栈的切换
    • 如何保存当前程序的执行状态,以便中断服务结束后能恢复执行?

    带着这两个问题,我们接着往下看。

    6.1获取中断向量,执行准备工作

    CPU从中断控制器取得中断向量后,就从中断向量表查找该中断向量所指的那一项,还记得​​​​通用中断请求队列图 里面的IRQ0x0x_interrupt()函数吗?它是通用中断向量表项中的一部分,即该通用中断通道的总服务程序入口函数。

    另外,当中断在用户空间发生,当前运行级别为3,而中断服务程序属于内核,其运行级别在中断向量表项中用DPL标识为0,因此,需要切换堆栈:即从用户空间堆栈切换成当前进程的系统空间堆栈。正在运行的堆栈指针存放在寄存器TR所指向的TSS中(这不是重点),因此CPU从TSS中取出系统堆栈指针,完成用户堆栈到系统堆栈的切换。完成堆栈切换后,会将EFLAGS的内容及中断返回地址压入系统堆栈。注意这些压栈操作是由中断指令INT本身发出的,这是还未进入中断通道的总服务程序入口函数IRQ0x0x_interrupt()。

    而至于切换堆栈,本人暂时没弄清楚由哪些代码完成,还请不吝赐教!

    相反,如果中断发生在系统空间,那就无需切换堆栈了,两者差别如下图所示。

    中断发生在用户空间或系统空间的区别

    6.2执行中断总入口函数IRQ0x0YY_interrupt()

    由于此过程非常关键,在此单独叙述。

    以IRQ0x03_interrupt()为例,将函数具体内容列出。

    __asm__ ( \
    "\n" \
    "IRQ0x03_interrupt: \n\t" \
    "pushl $0x03 - 256 \n\t" \        //中断向量号-256,压栈
    "jmp common_interrupt");          //跳转
    
    
    ----------------------------------------------------
    #define BUILD_COMMON_IRQ() \
    asmlinkage void call_do_IRQ(void); \
    __asm__( \
    	"\n" __ALIGN_STR"\n" \
    	"common_interrupt:\n\t" \
    	SAVE_ALL \                                //保存现场,中断前夕所有寄存器内容压栈
    	"pushl $ret_from_intr\n\t" \              //将一个函数地址压栈
    	SYMBOL_NAME_STR(call_do_IRQ)":\n\t" \    
    	"jmp "SYMBOL_NAME_STR(do_IRQ));           //跳转到do_IRQ函数

    首先执行IRQ0x03_interrupt()

    然后,又跳转到common_interrupt

    最后又跳转到do_IRQ函数。

    其中,保存现场的SAVE_ALL操作:

    #define SAVE_ALL \
    	cld; \
    	pushl %es; \
    	pushl %ds; \
    	pushl %eax; \
    	pushl %ebp; \
    	pushl %edi; \
    	pushl %esi; \
    	pushl %edx; \
    	pushl %ecx; \
    	pushl %ebx; \
    	movl $(__KERNEL_DS),%edx; \
    	movl %edx,%ds; \
    	movl %edx,%es;

    因此,在跳转到do_IRQ时,系统堆栈应该如下图所示,结合上面代码,可以说非常清晰了。

    图6.2 进入中断服务程序时,系统堆栈示意图

    堆栈中所保存的这些内容,用于将来恢复进入中断前的程序的执行。

    6.3 do_IRQ函数

    接下来就是执行do_IRQ函数,即要执行具体的中断服务函数了,那么需要具备哪些条件呢?

    • 需要获取中断调用号,即上面的0x03,注意不是中断向量,为什么?
    • 好像没了。。。。

    那么中断调用号从哪获取呢?先看一下do_IRQ函数原型。

    unsigned int do_IRQ(struct pt_regs regs);
    
    struct pt_regs {
    	long ebx;
    	long ecx;
    	long edx;
    	long esi;
    	long edi;
    	long ebp;
    	long eax;
    	int  xds;
    	int  xes;
    	long orig_eax;
    	long eip;
    	int  xcs;
    	long eflags;
    	long esp;
    	int  xss;
    };
    

    请关注其参数 regs,然后再对照图6.2系统堆栈,你会发现什么?

    如何没有想到,再提醒一下函数调用在栈中的构造过程是什么?

    没错,当前所构造出系统堆栈的内容,其实是做了do_IRQ函数的参数,而在调用do_IRQ前,最后压入的ret_from_intr则是do_IRQ的返回地址。

    这样中断服务程序do_IRQ所需的中断调用号有了,返回地址也设置妥了,接下来真的就要进入do_IRQ了。

    结合前面所述,再看中断请求队列,可以想象一下do_IRQ函数具体做了什么?

    • 根据中断请求号,作为数组队列头irq_desc的下标,获取相应的中断请求队列
    • 然后,依次处理队列中具体设备的中断服务程序

    限于篇幅,这里不再贴出代码,具体可以查看arch/i386/kernel/irq.c中的代码。

    在函数的末尾,可能会执行软中断服务程序 do_softirq(),关于软中断的由来请参考其他资料。在函数执行完,就会按照之前精心设置的返回地址ret_from_intr进行返回了。

    7.中断返回

    do_IRQ()函数通过返回地址ret_from_intr会到达entry.S中标号ret_from_intr处:

    ENTRY(ret_from_intr)
    	GET_CURRENT(%ebx)
    	movl EFLAGS(%esp),%eax		# mix EFLAGS and CS
    	movb CS(%esp),%al
    	testl $(VM_MASK | 3),%eax	# return to VM86 mode or non-supervisor?
    	jne ret_with_reschedule
    	jmp restore_all

    上面的操作主要是检查中断前夕,cpu运行于用户空间还是系统空间,若发生于用户空间,转移到ret_with_reschedule,然后最终还是会到达restore_all处。

    ret_with_reschedule:
    	cmpl $0,need_resched(%ebx)
    	jne reschedule
    	cmpl $0,sigpending(%ebx)
    	jne signal_return
    restore_all:
    	RESTORE_ALL
    
    	ALIGN
    signal_return:
    	sti				# we can get here from an interrupt handler
    	testl $(VM_MASK),EFLAGS(%esp)
    	movl %esp,%eax
    	jne v86_signal_return
    	xorl %edx,%edx
    	call SYMBOL_NAME(do_signal)
    	jmp restore_all
    
    
    reschedule:
    	call SYMBOL_NAME(schedule)    # test
    	jmp ret_from_sys_call
    
    
    

    首先,在ret_with_reschedule中判断是否需要进行一次进程调度,需要这转移到reschedule处,接着会转移到ret_from_sys_call,但是从ret_from_sys_call最终还是会到达restore_all处。

    而restore_all操作如下:

    #define RESTORE_ALL	\
    	popl %ebx;	\
    	popl %ecx;	\
    	popl %edx;	\
    	popl %esi;	\
    	popl %edi;	\
    	popl %ebp;	\
    	popl %eax;	\
    1:	popl %ds;	\
    2:	popl %es;	\
    	addl $4,%esp;	\
    3:	iret;	

    这与之前的SAVE_ALL遥相呼应

    #define SAVE_ALL \
    	cld; \
    	pushl %es; \
    	pushl %ds; \
    	pushl %eax; \
    	pushl %ebp; \
    	pushl %edi; \
    	pushl %esi; \
    	pushl %edx; \
    	pushl %ecx; \
    	pushl %ebx; \
    	movl $(__KERNEL_DS),%edx; \
    	movl %edx,%ds; \
    	movl %edx,%es;

    这样,当到达RESTORE_ALL的iret时,iret使CPU从中断返回,和进入中断时对应,如果是从系统态返回到用户态就会将堆栈切换到用户堆栈。

    结束

    这就是Linux内核2.4.0版本的中断机制的内容,当然这里省略了软中断的内容,需要的读者可以参考其他资料。

    技术是为了解决问题的,技术的门槛往往在于不了解技术本身,一旦清楚其过程,也就没有了门槛,但是这对于设计一项技术还远远不够,这就要求我们在了解技术的过程中,多问为什么,知其然不如知其所以然。

    参考资料:

    《Linux内核情景分析》毛德操,胡希明

    展开全文
  • Linux实现中断处理 内核是怎么知道应用程序要调用系统调用的呢?或者说应用程序怎么通知系统内核自己需要执行一个系统调用,这是通过软中断实现的,通过引发一个异常来促使系统切换到内核态去执行异常处理程序 ...
    • Linux实现中断处理

    内核是怎么知道应用程序要调用系统调用的呢?或者说应用程序怎么通知系统内核自己需要执行一个系统调用,这是通过软中断实现的,通过引发一个异常来促使系统切换到内核态去执行异常处理程序

    PS:什么时候会从用户态切换到内核态呢?1.中断;2.陷阱;3.系统调用

    中断分为两种,硬中断和软中断;

    在许多处理器体系结构处理异常和处理中断的方式类似,为了助于理解,可以把异常想象成软中断,我们通常说的中断是硬中断,硬中断是由硬件引起而不是软件引起的

    每一种中断都有一个对应的中断处理程序,如果一种中断设备可以产生多种中断,那这个中断设备就有多个中断处理程序对应,而一个中断处理程序就是该设备对应的设备驱动程序中的一部分

    我们想让中断处理程序运行的快,还想让中断处理程序多干活,显然是有冲突的,所以我们把中断程序分为两个部分,中断处理程序是上半部分,能够被允许稍后完成的工作会推迟到下半部去(注意!在执行中断上半部分的时候,是不允许产生其他中断的,所以设置标志位IRQF_DISABLED

    举个栗子拿我们可爱的网卡来说事情吧,例如网卡从网上收集了很多数据包,于是我们的朋友网卡君就通知内核,嘿,我这来了一批新货,要看看么,于是内核屁颠屁颠的去处理了(调用中断处理程序)中断开始执行,通知硬件开始拷贝数据(从网卡到内存),这件事是很紧迫的,因为网卡有一定的缓存,超过缓存这家伙就不干活了(丢弃数据包),为了压榨网卡(让他好好干活),内核必须尽快处理这件事情,而且这件事情是硬件相关的(就是和网卡脱不了干系),没了网卡干不了,不能延后,拷贝完这些数据包之后的事情就可以放到下部分了,于是内核忙完了,又去处理刚才被中断的进程了

    • 中断处理程序标志

    IRQF_DISABLED——在执行当前中断处理程序的时候,禁止所有其他的中断(野蛮!!尽量给想要尽快运行的轻量级中断使用)

    IRQF_SAMPLE_RANDOM——表明这个设备的中断对内核熵池有贡献

      PS:内核熵池负责提供从各种随机事件导出真正的随机数,就是说中断啥时候来(随机的),有个中断产生速率(自然也随机了),就拿去扔池      子里产生随机数用了。。。和中断关系似乎不是很大

    IRQF_TIMER——专门为系统定时器的中断处理而准备的

    IRQF_SHARED——共享中断线,中断线可以理解为中断单独对着的一个号,同时一个号可以对应多个终端

    • 中断和锁之间不得不说的那些事儿

    这里简单的把锁分为两类,自旋锁和睡眠锁(当然对应着很多具体的锁),分为两类是因为,自旋锁是可以用于中断(争用该锁会导致忙循环)的,但是拥有睡眠性质的锁是不能用于中断(争用该锁会导致睡眠),只能用于进程。我们之前说了,中断处理程序的上半部分是很紧迫的,这么紧迫的事情你怎么可以去睡眠偷懒呢?!哪怕现在轮不到你,你也得给我等着!

    为什么不能让中断睡眠?因为睡眠是为了进程调度存在的,但是中断处理程序一旦睡眠不仅没有事件可以唤醒中断处理程序,而且无法调度,因为进程有个进程号,但是中断没有,一旦当前的中断处理睡眠了,没有其他的东西会获得处理器,而且中断处理永远不会被唤醒,那么系统就会瘫痪

    一定要在获取锁之前,禁止本地中断!!!为啥呢?见下图

     

    欲知下半部分如何,且听下回分解

    展开全文
  • Linux内核——中断机制

    千次阅读 2014-08-07 08:54:04
    中断机制 为什么需要中断? 如果让内核定期对设备进行轮询,以便处理设备,那会做很多无用功,因为外设的处理速度...中断处理程序与其他内核函数的区别在于,中断处理程序是被内核调用来响应中断的,而它们运行于我们称
  • Linux内核中断系统处理机制-详细分析

    万次阅读 多人点赞 2019-04-14 17:04:08
    Linux内核中断 一、中断概述 中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再...
  • Linux 内核中软中断机制

    千次阅读 2013-10-31 00:53:42
    中断实现的原理图:    软件中断机制的构成核心元素包括:软中断状态、软中断向量表、软中断守护内核线程。...软中断守护内核线程:内核建立一个内核线程ksoftirqd来轮询软中断状态,调用软中断向量表中的软中断
  • Linux 内核中断(softirq)执行分析

    千次阅读 2009-07-14 15:32:00
    Linux 内核中断(softirq)执行分析 Author: sinisterEmail: sinister@whitecell.orgHomepage:http://www.whitecell.org Date: 2007-01-11本文对 Linux 内核中断的执行流程进行了分析,并尽可能的结合当前运行...
  • Linux中断机制分析

    千次阅读 2018-09-12 13:41:05
    上次更新博客到了linux内核中断子系统。这次总结一下软中断,也就是softirq。之后还会总结一些tasklet、工作队列机制。 1. 为什么要软中断  编写驱动的时候,一个中断产生之后,内核在中断处理函数中可能需要...
  • Linux内核中断、软中断、tasklet

    千次阅读 2015-10-10 19:55:23
    在之前我所写的Linux驱动程序中,会经常使用到中断机制,像CC1100高频驱动、倒车雷达驱动等等。但所用到的中断机制都基本上是用到中断的顶半部,即:编写中断处理函数,通过request_irq函数申请中断,这样当中断来临...
  • Linux内核PHY中断调试

    千次阅读 2012-09-16 09:53:30
    Linux内核PHY中断调试   分类:  UBOOTLinux驱动Linux 内核和文件系统PowerPC体系结构2010-11-01 20:492098人阅读评论(5)收藏举报 今天解决了一个PHY中断的问题,现在越来越觉得我以前的想法是错误的。...
  • Linux内核中断机制(一):中断注册方法

    千次阅读 2016-12-27 22:00:29
    今天在网上看到一份不错的讲解内核中断原理的文章,分享给大家! 1.中断注册方法 在 linux 内核中用于申请中断的函数是 request_irq(),函数原型在Kernel/irq/manage.c 中定义: int request_irq(unsigned ...
  • linux内核中断开关

    2013-01-16 16:25:59
    在2.6内核中,可以通过下面两个函数中的其中任何一个关闭当前处理器上的所有中断处理,这两个函数定义在 中:  void local_irq_save(unsigned long flags);  void local_irq_disable(void);  对 local_irq_save...
  • linux中断--内核中断编程

    千次阅读 2014-04-14 19:56:51
    在前面分析了中断的基本原理后,就可以写一个内核中断程序来体验以下,也可以借此程序继续深入来了解内核中断的执行过程 一.内核中断程序: 我们还是来看一看成程序: 在看程序之前,要熟悉如何进行模块编程,和了解...
  • Linux内核:关于中断你需要知道的

    千次阅读 2014-07-21 15:28:48
    1、中断处理程序与其他内核函数真正的区别在于,中断处理程序是被内核调用来相应中断的,而它们运行于中断上下文(原子上下文)中,在该上下文中执行的代码不可阻塞。中断就是由硬件打断操作系统。 2、异常与中断...
  • Linux内核中断路径中不能睡眠,为什么?这里就行了很深入的讨论,值得一看:http://bbs2.chinaunix.net/viewthread.php?tid=1618430但是,他们的讨论最后没有得出一个明确的结论。其中,cskyrain在8楼 的思考触及到...
  • Linux内核之禁止中断和禁止内核抢占

    千次阅读 2017-04-02 22:59:42
    禁止中断指的是Linux内核停工了一组接口用于操作机器上的中断状态。这些接口为我们提供了能够禁止当前处理器的中断系统,或者屏蔽掉整个机器的一条中断线的能力。通过禁止中断,可以确保某个中断处理程序不会抢占...
  • Linux内核中断和异常分析(上)

    千次阅读 2016-03-03 23:03:07
    中断,通常被定义为一个事件。打个比方,你烧热水,水沸腾了,这时候你要去关掉烧热水的电磁炉,然后再去办之前手中停不下来的事情。那么热水沸腾就是打断你正常工作的一个信号机制。当然,还有其它的情况,我们以后...
  • 引言软中断、tasklet和工作队列并不是Linux内核中一直存在的机制,而是由更早版本的内核中的“下半部”(bottom half)演变而来。下半部的机制实际上包括五种,但2.6版本的内核中,下半部和任务队列的函数都消失了,...
  • linux内核分析--中断处理流程

    千次阅读 2014-04-18 19:05:57
    linux内核中断处理, 里面由始至终都贯穿着"重要的事马上做, 不重要的事推后做"的异步处理思想. 于是整理一下~ 第一阶段--获取中断号 每个CPU都有响应中断的能力, 每个CPU响应中断时都走相同的流程. 这个流程...
1 2 3 4 5 ... 20
收藏数 137,650
精华内容 55,060
关键字:

linux 内核提供的中断