Linux内核分析:中断和异常(1)



        分析中断,本质上是一种特殊的电信号,由硬件设备发向处理器,处理器接收到中断后,会马上向操作系统反应此信号的到来,然后就由OS负责处理这些新到来的数据,中断可以随时发生,才不用操心与处理器的时间同步问题。不同的设备对应的中断不同,他们之间的不同从操作系统级来看,差别就在于一个数字标识-----中断号,即中断请求(IRQ)。讨论中断就不得不提及异常,异常和中断不一样,它在产生时必须要考虑与处理器的时钟同步,实际上,异常也常常称为同步中断,在处理器执行到由于编程失误而导致的错误指令的时候,或者是在执行期间出现特殊情况,必须要靠内核来处理的时候,处理器就会产生一个异常。因为许多处理器体系结构处理异常以及处理中断的方式类似,因此,内核对它们的处理也很类似。

1、基本概念

        中断:中断通常指的是异步中断。是由硬件设备依照CPU时钟信号随机产生的。中断是一种电信号,由硬件设备生成,并送入中断控制器的输入引脚中,中断控制器会给CPU发送一个电信号,CPU检测到这个信号,就中断当前的工作转而处理中断。每个中断都通过一个唯一的数字标志。这些中断值称为中断请求(IRQ,Interrupt Request)线。

        异常:异常通常指的是同步中断。是当指令执行时由CPU控制单元产生的,之所以成为同步,是因为只有在一条指令终止后CPU才会发出中断。当CPU执行到由于编程失误而导致的错误指令(比如被0除)的时候,或者在执行期间出现踢输情况(如缺叶)而必须靠内核来处理的时候,处理器就产生一个异常。

        80x86发布了大约20中不同的异常,内核必须为每种异常提供一个专门的异常处理程序,对于某些异常,CPU在执行异常处理程序前会产生一个硬件出错码,并压入内核态堆栈。

        下面列表给出了在80x86处理器中可以找到的异常的向量、名字及其简单描述。

Linux内核分析:中断和异常(1)_第1张图片


2、中断信号的作用

        中断或异常处理程序不是一个进程,而是一个内核控制路径,代表中断发生时正在运行的进程执行。中断处理是内核执行的最敏感的任务之一,必须满足以下约束:

  1. 内核相应中断后的操作分两部分,关键的紧急的部分,内核立即执行,其余的推迟随后执行;
  2. 中断程序必须使内核控制路径能以嵌套的方式执行,当最后一个内核控制路径终止时,内核必须能恢复被中断进程的执行,或者如果中断信号已经导致了重新调度,内核能切换到另外的进程;
  3. 尽管中断可以嵌套,但在临界区中,中断必须禁止。但内核必须尽可能限制这样的临界区,大部分时间应该以开中断的方式运行。

3、中断描述符表

        中断描述符表(Interrupt Descriptor Table,IDT)是一个系统表它与每一个中断或异常向量相联系,每一个向量在表中有相应的中断或异常处理程序的入口地址。内核在允许中断发生前,必须适当地初始化IDT

        IDT表中每一项对应一个中断或异常向量,每个向量由8个字节组成,因此最多需要256×8=2048字节来存放IDT。

        IDT的地址存放在idtr寄存器中(idtr CPU寄存器使IDT可以位于内存的任何地方,它指定IDT的线性基地址及其限制。在允许中断前,必须用lidr汇编指令初始化idtr。)。中断发生时,内核就从IDT中查询相应中断的处理信息。

        IDT包含三种类型的中断描述符,如下图总每种描述符的64位的含义。尤其值得注意的是,子啊40——43为的Type字段的值表示描述符的类型。

Linux内核分析:中断和异常(1)_第2张图片

    这些描述符是:

任务门(task gate):当任务发生时,必须取代当前进程的那个进程的TSS选择符存放在任务门中。

中断门(Interrupt gate):包含段选择符和中断或异常处理程序的段内偏移量。当控制权转移到一个适当的段时,处理器清IF标志,从而关闭将来会发生的可屏蔽中断。

陷阱门(Trap gate):与中断门相似,只是控制权传递到一个适当的段时处理器不修改IF标志。

4、异常处理

    异常处理一般由三个部分组成:

  1. 在内核堆栈中保存大多数寄存器的内容(通过汇编指令实现);
  2. 用高级的C函数处理异常;
  3. 通过:ret_from_exception()函数从异常处理程序退出。

5、中断处理

       中断处理一般由四部分组成:

  1. 在内核态堆栈中保存IRQ的值和寄存器的内容;
  2. 为正在给IRQ线服务的PIC发送一个应答,这将允许PIC进一步发出中断;
  3. 执行共享这个IRQ的所有设备的中断服务例程ISR;
  4. 跳到:ret_from_intr()的地址来从中断处理程序退出。
        在相应一个特定中断时,内核会执行一个函数,这个函数就叫做中断处理程序(interrupt handler),或者叫做中断服务例程(interrupt service routine,ISR)。中断处理程序运行在中断上下文中,该上下文中的代码不可以阻塞。要注意,中断处理序执行的代码不是一个进程,中断处理程序比一个进程要“轻”

        每个中断和异常都会引起一个内核控制路径,而内核控制路径是可以任意嵌套的。也就是说,一个中断处理程序可以被另一个中断处理程序“中断”。为了允许这样的嵌套,中断处理程序就必须永不阻塞,换句话说,进程被中断,在中断程序运行期间,不能发生进程切换。这是因为,一个中断产生时,内核会把当前寄存器的内容保存在内核态堆栈中,这个内核态堆栈属于当前进程,嵌套中断时,上一个中断执行程序产生的寄存器内容同样也会保存在该内核态堆栈,然后从嵌套的下一个中断恢复时,又从内核态堆栈中取出来放进寄存器中。

6、IRQ数据结构

        每个能够发出中断请求的硬件设备控制器都有一条名为IRQ(Interrupt Request)的输出线。所有现有的IRQ线(IRQ line)都与一个名为可编程中断控制器(Programmable Interrupt Controller,PIC)的硬件电路的输入引脚相连。PIC执行以下动作:

  1. 监视IRQ线,检查产生的信号(raised signal)。如果有两条或两条以上的IRQ线产生信号,就选择引脚编号较小的IRQ线;
  2. 如果一个引发信号出现在IRQ线上:
    1. 把接收到的信号转换成对应的向量;
    2. 把这个向量存放在中断控制器的一个I/O端口,从而允许CPU通过数据总线读此向量;
    3. 把引发信号发送到处理器的INTR引脚,即产生一个中断;
    4. 等待,知道CPU通过把这个中断信号写进可编程中断控制器的一个I/O端口来确认它;当这种情况发生时,清INTR线。
  3. 返回到第1步。
        IRQ线从0开始顺序编号,第一条IRQ线IRQ0,与IRQn关联的Intel缺省向量是n+32.通过PIC可以修改IRQ和向量之间的映射。可以通过修改PIC禁止IRQ,但禁止的中断是不会丢失的,它们一旦被激活,PIC就又把它们发送到CPU,这样CPU可以一次处理同一类型的IRQ,PIC禁止IRQ不同于屏蔽中断,屏蔽中断是被忽略的。

        传统的PIC是由两片8259A芯片级联成的,可以支持15个IRQ线。

        每个IRQ都有自己的描述符:irq_desc_t,描述符中有字段指向PIC对象,有字段指向ISR的链表(因为每个IRQ线上可以注册多个中断处理程序)。所有的irq_desc_t合起来组成irq_desc数组。irq_desc数组是Linux内核中用于维护IRQ资源的管理单元,它存储了某IRQ号对应的哪些处理函数,属于哪个PIC管理、来自哪个设备、IRQ自身的属性、资源等,是内核中断子系统的一个核心数组。

        有时候中断处理需要做的工作很多,而中断处理程序的性质要求它必须在尽量短的时 间内处理完毕,所以中断处理的过程可以分为两部分或者两半(half)。中断处理程序属 于“上半部(top half)”——接受到一个中断,立刻开始执行,但只做有严格时限的工作。 能够被允许稍微晚一点完成的工作会放到“下半部(bottom half)中去,下半部不会马上 执行,而是等到一个合适的时机调度执行。也就是说,关键而紧急的部分,内核立即执行,属于上半部;其余推迟的部分,内核随后执行,属于下半部。

        比如说当网卡接收到数据包时,会产生一个中断,中断处理程序首要进行的工作是通知硬件拷贝最新的网络数据包到内存,然后读取网卡更多的数据包。这样网卡缓存就不会溢出 。至于对数据包的处理和其他随后工作,则放到下半部进行。关于下半部的细节,我们后面会讨论。

7、注册中断处理程序

     驱动程序通过request_irq()函数注册一个中断处理程序:

        /* 定义在中 */
        typedef irqreturn_t (*irq_handler_t)(int, void *);
        int request_irq(ussigned int irq,
            irq_handler_t handler,
            unsigned long flags,
            const char *name,
            void *dev  );

    参数解释如下:

        irq           //要分配的中断号

        handler       //是指向中断处理程序的指针

        flags         //设置中断处理程序的一些属性,可能的值如下:
        IRQF_DISABLED            在本次中断处理程序本身期间,禁止所有其他中断。
        IRQF_SAMPLE_RANDOM       这个中断对内核的随机数产生源有贡献。
        IRQF_TIMER               该标志是特别为系统定时器的中断处理准备的。
        IRQF_SHARED              表明多个中断处理程序可以共享这条中断线。也就是说这条中断线上可以注册多个中断处理程序,当中断发生时,所有注册到这条中断线上的handler都会被调用。
        name:                    是与中断相关设备的ASCII文本表示
        dev: 类似于一个cookie,内核每次调用中断处理程序时,都会把这个指针传递给它, 指针的值用来表明到底是什么设备产生了这个中断,当中断线共享时,这条中断线上 的handler们就可以通过dev来判断自己是否需要处理。

8、释放中断处理程序

        通过:free_irq函数注销相应的中断处理程序:

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

        参数和request_irq的参数类似。当一条中断线上注册了多个中断处理程序时,就需要
        dev来说明想要注销的是哪一个handler。

9、中断和异常的硬件处理

        现在介绍CPU控制单元如何处理中断和异常。假定内核已被初始化,因此,CPU在保护模式下运行。

        当执行一条指令后,cs和eip这对寄存器包含下一条将要执行的指令的逻辑地址。在处理这条指令之前,控制单元会检查在运行前一条指令是是否已经发生了一个中断或异常。如果发生,那么控制单元执行以下操作:

  1. 确定与中断或异常关联的向量i(0<=i<=255);
  2. 读由idtr寄存器指向的IDT表中的第i项;
  3. 从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的选择符所标识的段描述符,这个描述符指定中断或异常处理程序所在段的基地址;
  4. 确信中断是由授权的中断发生源发出的,中断处理程序的特权级不能低于引起中断的程序的特权。若是异常,进一步检查DPL,CPL这可以避免用户程序访问特殊的陷阱门或中断们;
  5. 检查是否发生了特权级的变化,也就是检查CPL是否不同于所选择的段描述符的DPL。若不同必须使用心得特权级相关的栈,通过执行以下步骤做到这点:
    1. 读tr寄存器,以访问允许进程的TSS段;
    2. 用与新特权级别相关的栈段和栈指针的正确值装载ss和esp寄存器;
    3. 在新的栈中保存ss和esp以前的值,这些值定义了与旧特权级相关的栈的逻辑地址。
  6. 如果故障已发生,用引起异常的指令地址装载cs和eip寄存器,从而使得这条指令能再次被执行;
  7. 在栈中保存flags、cs及eip的内容;
  8. 如果异常产生了一个硬件出错码,则将它保存在栈中;
  9. 装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。这些值给出了中断或者异常处理程序的第一条指令的逻辑地址。
        控制单元所执行的最后一步就是跳转到中断或异常处理程序。换句话说,处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。

        控制单元所执行的最后一步就是跳转到中断或异常处理程序,相应的处理程序必须产生一条iret指令,把控制权交给被中断的进程,这将迫使控制单元。

  1. 用保存在栈中的值装载cs、eip或eflags寄存器,如果一个硬件出错码曾被压入栈中,并且在eip内容的上面,那么,执行iret指令前必须先弹出这个硬件出错码;
  2. 检查处理程序的CPL是否等于cs中的最低两位,如果是,iret终止执行,否则转入下一步;
  3. 从栈中装载ss和esp寄存器,因此,返回到与旧特权级别相关的栈;
  4. 检查ds、es、fs及gs段寄存器的内容,如果其中一个寄存器包含的选择符是一个段描述符,并且其DPL值小于CPL,那么清相应段寄存器,这是为了禁止用户态程序(CPL=3)利用内核以前所用的段寄存器(DPL=0),如果不清这些寄存器,恶意用户程序可以利用来访问内核地址空间。

          下节开始介绍中断和异常处理程序的嵌套执行:Linux内核分析——中断与异常(2)




你可能感兴趣的:(Linux内核)