美文网首页
中断处理与下半部的故事

中断处理与下半部的故事

作者: Gitlusen | 来源:发表于2018-05-03 20:43 被阅读0次

    说几句废fu之言,前几天没有接着写进程调度记录的文章,当然现在也不会写,如题,从现在开始记录linux内核基础知识。


    中断处理程序原则:接受到一个中断,便立即开始执行应答或复位硬件,前提是在所有中断被禁止的额情况下完成。

    中断和异常相关概念:

    中断——异步的:

    由硬件随机产生,在程序执行的任何时候可能出现

    异常——同步的:

    在(特殊的或出错的)指令执行时由CPU控制单元产生

    中断处理程序重要函数:

    First job.

    1.注册、分配中断号irq、激活中断处理程序handler、设置标志掩码、署名设备ASCII名称、设置共享中断线:

    kernel/irq/manage.c

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

    注:request_irq可能睡眠,不能在中断上下文或不允许阻塞的代码中调用。

    换句话可以知道,中断上下文不允许睡眠,而该函数可能引起阻塞。


    2.注销、中断处理程序,释放中断线

    kernel/irq/manage.c

    void free_irq(unsigned int irq,void* dev_id)                       

    通常在驱动卸载时使用,必须在进程上下文中调用。

    3.启用中断

    kernel/irq/manage.c 中的 enable_irq:

       static void __enable_irq(struct irq_desc *desc, unsigned int irq)

    内部调用了 __enable_irq,首先上自旋锁,找到 irq_desc 结构体指针,判断嵌套深度,刷新 IRQ 状态,释放自旋锁。

    参数desc: 指向 irq_desc 结构体的指针irq: 中断通道号

    4.关闭中断

    kernel/irq/manage.c 中的 disable_irq:

     void disable_irq(unsigned int irq)

    参数irq: 中断通道号

    5.关闭中断 (无等待)

    disable_irq 会保证存在的 IRQ handler 完成操作,而 disable_irq_nosync 立即关中断并返回。事实上,disable_irq 首先调用 disable_irq_nosync,然后调用 synchronize_irq 同步。

      void disable_irq_nosync(unsigned int irq)

    6.同步中断 (多处理器)

      void synchronize_irq(unsigned int irq)

    7.设置 IRQ 芯片

    kernel/irq/chip.c:

    set_irq_chip()

    int set_irq_chip(unsigned int irq, struct irq_chip *chip)

    8.设置 IRQ 类型

    kernel/irq/chip.c: set_irq_type()

    int set_irq_type(unsigned int irq, unsigned int type)

    9.设置 IRQ 数据

    kernel/irq/chip.c: set_irq_data()

    150 int set_irq_data(unsigned int irq, void *data)

    10.设置 IRQ 芯片数据

    kernel/irq/chip.c: set_irq_chip_data()

      int set_irq_chip_data(unsigned int irq, void *data)


    重头戏

    中断处理流程:

    一、监视 IRQ 线,检查产生的信号。如果有两条以上的 IRQ 线上产生信号,就选择引脚编号较小的 IRQ 线。

    二、如果一个引发信号出现在 IRQ 线上:

               1.把接收到的引发信号转换成对应的向量号

               2.把这个向量存放在中断控制器的一个 I/O 端口(0x20、0x21),从而允许 CPU 通过数据总线读此向量。

               3.把引发信号发送到处理器的 INTR 引脚,即产生一个中断。

               4.等待,直到 CPU 通过把这个中断信号写进可编程中断控制器的一个 I/O 端口来确认它;当这种情况发生时,清 INTR 线。

    三、继续监控IRQ线。

    处理完中断信号后,控制单元所执行的指令就是被选中处理程序的第一条指令。中断或异常被处理完后,相应的处理程序必须产生一条iret指令,把控制权转交给被中断的进程。

    中断信息的保存

    紧急的事情马上做,其他事情往后推。

    中断处理程序首先要做:

    1、将中断号压入栈中,以便找到对应的中断服务程序

    2、将当前寄存器信息压入栈中,以便中断退出时恢复上下文

    显然, 这两步都是不可重入的。因此在进入中断服务程序时,CPU 已经自动禁止了本 CPU 上的中断响应。

    引为 n 的元素中存放着下列指令的地址:

    pushl n-256

    jmp common_interrupt

    执行结果是将中断号 - 256 保存在栈中,这样栈中的中断都是负数,而正数用来表示系统调用。这样,系统调用和中断可以用一个有符号整数统一表示。

    common_interrupt 的定义:

    // arch/x86/kernel/entry_32.S

    613 common_interrupt:

    614        SAVE_ALL

    615        TRACE_IRQS_OFF

    616        movl %esp,%eax # 将栈顶地址放入 eax,这样 do_IRQ 返回时控制转到 ret_from_intr()

    617        call do_IRQ # 核心中断处理函数

    618        jmp ret_from_intr # 跳转到 ret_from_intr()

    其中 SAVE_ALL 宏将被展开成:

    cld

    push %es # 保存除 eflags、cs、eip、ss、esp (已被 CPU 自动保存) 外的其他寄存器

    push %ds

    pushl %eax

    pushl %ebp

    pushl %edi

    pushl %edx

    pushl %ecx

    pushl %ebx

    movl $ _ _USER_DS, %edx

    movl %edx, %ds # 将用户数据段选择符载入 ds、es

    movl %edx, %es

    处理中断

    前面汇编代码的实质是,以中断发生时寄存器的信息为参数,调用 arch/x86/kernel/irq32.c 中的 do_IRQ 函数。

    我们注意到 unlikely 和 unlikely 宏定义,它们的含义是

    #define likely(x)      __builtin_expect((x),1)

    #define unlikely(x)    __builtin_expect((x),0)

    __builtin_expect 是 GCC 的内部机制,意思是告诉编译器哪个分支条件更有可能发生。这使得编译器把更可能发生的分支条件与前面的代码顺序串接起来,更有效地利用 CPU 的指令流水线。

    do_IRQ 函数流程:

               1、保存寄存器上下文

               2、调用 irq_enter:// kernel/softirq.c

    void irq_enter(void)

    282 {

    283 #if def CONFIG_NO_HZ

    // 无滴答内核,它将在需要调度新任务时执行计算并在这个时间设置一个时钟中断,允许处理器在更长的时间内(几秒钟)保持在最低功耗状态,从而减少了电能消耗。

    284        int cpu = smp_processor_id();

    285        if (idle_cpu(cpu) && !in_interrupt())

    286                tick_nohz_stop_idle(cpu); // 如果空闲且不在中断中,则停止空闲,开始工作

    287 #end if

    288        __irq_enter();

    289 #if def CONFIG_NO_HZ

    290        if (idle_cpu(cpu))

    291                tick_nohz_update_jiffies(); // 更新 jiffies

    292 #end if

    293 }

    // include/linux/hardirq.h

    135 #define __irq_enter()                                  \

    /* 在宏定义函数中,do { ... } while(0) 结构可以把语句块作为一个整体,就像函数调用,避免宏展开后出现问题 */

    136        do {                                            \ 

    137                rcu_irq_enter();                        \

    138                account_system_vtime(current);          \

    139                add_preempt_count(HARDIRQ_OFFSET);      \ /* 程序嵌套数量计数器递增1 */

    140                trace_hardirq_enter();                  \

    141        } while (0)

               3、如果可用空间不足 1KB,可能会引发栈溢出,输出内核错误信息

               4、如果 thread_union 是 4KB 的,进行一些特殊处理

               5、调用 desc->handle_irq(irq, desc),调用 __do_IRQ() (kernel/irq/handle.c)

    5.1 取得中断号,获取对应的 irq_desc

    5.2 如果是 CPU 内部中断,不需要上锁,简单处理完就返回了

    5.3 上自旋锁

    5.4 应答中断芯片,这样中断芯片就能开始接受新的中断了。

    5.5 更新中断状态。

            IRQ_REPLAY:如果被禁止的中断管脚上产生了中断,这个中断是不会被处理的。当这个中断号被允许产生中断时,会将这个未被处理的中断转为 IRQ_REPLAY。

            IRQ_WAITING:探测用,探测时会将所有没有中断处理函数的中断号设为 IRQ_WAITING,只要这个中断管脚上有中断产生,就把这个状态去掉,从而知道哪些中断管脚上产生过中断。

             IRQ_PENDING、IRQ_INPROGRESS 是为了确保同一个中断号的处理程序不能重入,且不能丢失这个中断的下一个处理程序。具体地说,当内核在运行某个中断号对应的处理程序时,状态会设置成IRQ_INPROGRESS。如果发现已经有另一实例在运行了,就将这下一个中断标注为 IRQ_PENDING 并返回。这个已在运行的实例结束的时候,会查看是否期间有同一中断发生了,是则再次执行一遍。

    5.6 如果链表上没有中断处理程序,或者中断被禁止,或者已经有另一实例在运行,则进行收尾工作。

    5.7  循环:

    释放自旋锁

    执行函数链:handle_IRQ_event()。其中主要是一个循环,依次执行中断处理程序链表上的函数,并根据返回值更新中断状态。如果愿意,可以参与随机数采样。中断处理程序执行期间,打开本地中断。

    上自旋锁

    如果当前中断已经处理完,则退出;不然取消中断的 PENDING 标志,继续循环。

    5.8 取消中断的 INPROGRESS 标志

    5.9 收尾工作:有的中断在处理过程中被关闭了,->end() 处理这种情况;释放自旋锁。

               6、执行 irq_exit(),在 kernel/softirq.c 中

    6.1 递减中断计数器

    6.2 检查是否有软中断在等待执行,若有则执行软中断。

    6.3 如果使用了无滴答内核看是不是该休息了。

               7、恢复寄存器上下文,跳转到 ret_from_intr (跳转点早在 common_interrupt 中就被指定了)

    在中断处理过程中,我们反复看到对自旋锁的操作。在单处理器系统上,spinlock是没有作用的;在多处理器系统上,由于同种类型的中断可能连续产生,同时被几个 CPU处理(注意,应答中断芯片是紧接着获得自旋锁后,位于整个中断处理流程的前部,因此在中断处理流程的其余部分,中断芯片可以触发新的中断并被另一个CPU 开始处理),如果没有自旋锁,多个 CPU 可能同时访问 IRQ 描述符,造成混乱。因此在访问 IRQ 描述符的过程中需要有spinlock 保护。

    下半部的事

    三足鼎力:软中断、tasklet、工作队列

    软中断:

    软中断作为下半部机制的代表,是随着SMP(share memory processor)的出现应运而生的,它也是tasklet实现的基础(tasklet实际上只是在软中断的基础上添加了一定的机制)。

    软中断一般是“可延迟函数”的总称,有时候也包括了tasklet(请读者在遇到的时候根据上下文推断是否包含tasklet)。它的出现就是因为要满足上面所提出的上半部和下半部的区别,使得对时间不敏感的任务延后执行,而且可以在多个CPU上并行执行,使得总的系统效率可以更高。

    它的特性包括:

    a)产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不能被自己打断,只能被硬件中断打断(上半部)。

    b)可以并发运行在多个CPU上(即使同一类型的也可以)。所以软中断必须设计为可重入的函数(允许多个CPU同时操作),因此也需要使用自旋锁来保护其数据结构。

    tasklet:

    由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet。

    tasklet是由软中断引出的,是IO驱动程序实现可延迟函数的首选方法。 内核定义了两个软中断掩码HI_SOFTIRQ和TASKLET_SOFTIRQ(两者优先级不同), 这两个掩码对应的软中断处理函数作为入口, 进入tasklet处理过程.

    它具有以下特性:

    a)一种特定类型的tasklet只能运行在一个CPU上,不能并行,只能串行执行。

    b)多个不同类型的tasklet可以并行在多个CPU上。

    c)软中断是静态分配的,在内核编译好之后,就不能改变。但tasklet就灵活许多,可以在运行时改变(比如添加模块时)。

    tasklet是在两种软中断类型的基础上实现的,因此如果不需要软中断的并行特性,tasklet就是最好的选择

    工作队列

    软中断不能睡眠、不能阻塞。由于中断上下文出于内核态,没有进程切换,所以如果软中断一旦睡眠或者阻塞,将无法退出这种状态,导致内核会整个僵死。但可阻塞函数不能用在中断上下文中实现,必须要运行在进程上下文中,例如访问磁盘数据块的函数。因此,可阻塞函数不能用软中断来实现。但是它们往往又具有可延迟.

    工作队列有着自己的处理线程, 这些work被推迟到这些线程中去处理. 处理过程只可能发生在这些工作线程中, 所以这里可以睡眠.

    内核默认启动了一个工作队列, 对应一组工作线程events/n(n代表处理器编号, 这样的线程有n个). 驱动程序可以直接向这个工作队列添加任务. 某些驱动程序还可能会创建并使用属于自己的工作队列.

    因此在2.6版的内核中出现了在内核态运行的工作队列(替代了2.4内核中的任务队列)。它也具有一些可延迟函数的特点(需要被激活和延后执行),但是能够能够在不同的进程间切换,即可在进程上下文中完成任务,这就是工作队列的关键。

    (未完)

    中断处理程序编写

    (1)他运行在中断上下文,所以不能使用可能引起阻塞或者调度的函数。否则实时性得不到满足。

    (2)一开始判断是否产生中断

    (3)清除中中断标志

    (4)硬件相关操作

    相关文章

      网友评论

          本文标题:中断处理与下半部的故事

          本文链接:https://www.haomeiwen.com/subject/nlvlrftx.html